Forum Archive

How to Support Multipeer Connectivity ?

wolf71

Pythonista can support iOS Multipeer Connectivity?
who can show some sample code?

Webmaster4o

@wolf71 You'll have to elaborate a bit more on exactly what you want for those of us who aren't familiar

ccc

https://developer.apple.com/reference/multipeerconnectivity

wolf71

MCSession=ObjCClass('MCSession')

Error:
no objective-C class named 'MCSession' found

lukaskollmer

@wolf71 You first need to load the MultipeerConnectivity Framework:

NSBundle.bundle(Path="/System/Library/Frameworks/MultipeerConnectivity.framework").load()
wolf71

Thanks @lukaskollmer, It's work.

wolf71

how to set the MCNearByServiceBrowser Delegate,so can using pythonista handle callback ? I Try this,but not work.
!!crash!!

Multipeer Connectivity Ref: https://developer.apple.com/library/ios/documentation/MultipeerConnectivity/Reference/MultipeerConnectivityFramework/index.html#//apple_ref/doc/uid/TP40013328

from objc_util import *
import ctypes,time,os

NSBundle.bundle(Path="/System/Library/Frameworks/MultipeerConnectivity.framework").load()
MCPeerID=ObjCClass('MCPeerID')
MCSession=ObjCClass('MCSession')
MCNearbyServiceAdvertiser=ObjCClass('MCNearbyServiceAdvertiser')
MCNearbyServiceBrowser=ObjCClass('MCNearbyServiceBrowser')

def browser_foundPeer_withDiscoverInfo_(_self, _cmd, _browser, _peerID, _info):
    print '!!!!!!!!!!'
    #print 'Service found:', ObjCInstance(_info)

BrowserDelegate = create_objc_class('BrowserDelegate',methods=[browser_foundPeer_withDiscoverInfo_],protocols=['MCNearbyServiceBrowserDelegate'])

myID = MCPeerID.alloc().initWithDisplayName('Wolf_Audio')
#mySession = MCSession.alloc().initWithPeer_(myID)

#aSrv = MCNearbyServiceAdvertiser.alloc().initWithPeer_discoveryInfo_serviceType_(myID,None,'AudioSrv')
# set delegate
#aSrv.setDelegate_(MCDelegate.alloc().init())
#aSrv.startAdvertisingPeer()

aBr = MCNearbyServiceBrowser.alloc().initWithPeer_serviceType_(myID,'AudioSrv')
#aBr.setDelegate_(BrowserDelegate.alloc().init())
aBr.startBrowsingForPeers()

time.sleep(10)

aBr.stopBrowsingForPeers()
#aSrv.stopAdvertisingPeer()

lukaskollmer

Are you using Python 2 or Python 3?

JonB

It is probably a bad idea to pass in alloc().init() into an objc method in the argument directly. objc_util does not retain arguments by default, so what happens is the ObjCInstance is created, a pointer to the class is sent to the objc side of the world, then the object is promptly destroyed in python as it falls out of scope. It sort of depends whether the reference counter gets incremented on the objc side, but basically it is a good idea to separately create a variable and pass it in.

So, just use mydelegate=BrowserDelegate.alloc().init(), and pass that into your other methods.

aBr.setDelegate_(mydelegate)

I could not get your code to crash after fixing this, but I also don't think I have a peer around to trigger the callback. There could be other issuex. If you have pythonista 3, you should install dgelessus's pythonista_startup's enable_fault_handler to tell you where the crash is occuring.

If you get this working, please do report back as others may find this useful.

dgelessus

@JonB I'm no Objective-C expert, but shouldn't the result of XYSomeClass.alloc().init() start out retained? I thought the Objective-C convention was that objects returned from alloc..., copy..., mutableCopy... or new... methods don't need to be retained by hand.

Also objc_util.ObjCInstance automatically retains and releases the underlying object in the Python __new__ and __del__ methods, so as long as the ObjCInstance is alive in Python, the Objective-C instance is too. And Python functions keep their arguments alive for the duration of the function call (except in some VERY specially written function code), so an ObjCInstance should never be garbage-collected during a method call.

By the way, if you only want to install the faulthandler and Objective-C exception handler, you can copy the code for that from this Gist. The rest of my pythonista_startup code can be found in the main repo.

wolf71

@JonB Thanks
using your method chage the program. but crash again.
I think it maybe this class delegate var problem.

=============================
delegate
Property
The delegate object that handles browser-related events.

Declaration
SWIFT
weak var delegate: MCNearbyServiceBrowserDelegate?
OBJECTIVE-C
@property(weak, nonatomic) id< MCNearbyServiceBrowserDelegate > delegate
=====================
Fatal Python error: Aborted

Thread 0x000000016e247000 (most recent call first):


Objective-C exception details:

NSInvalidArgumentException: Invalid serviceType passed to MCNearbyServiceBrowser

Stack trace:

0 CoreFoundation 0x0000000181f02dc8 + 148
1 libobjc.A.dylib 0x0000000181567f80 objc_exception_throw + 56
2 CoreFoundation 0x0000000181f02cf8 + 0
3 MultipeerConnectivity 0x000000019466a6fc + 424
4 PythonistaKit 0x0000000101aa7044 ffi_call_SYSV + 68
5 PythonistaKit 0x0000000101aaca78 ffi_call_int + 1160
6 PythonistaKit 0x0000000101aac5e4 ffi_call + 56
7 PythonistaKit 0x00000001018af4a0 _ctypes_callproc + 820
8 PythonistaKit 0x00000001018b4374 PyCFuncPtr_call + 1048
9 PythonistaKit 0x0000000101419c04 PyObject_Call + 124
10 PythonistaKit 0x0000000101650304 PyEval_EvalFrameEx + 2032
11 PythonistaKit 0x000000010164f8c4 PyEval_EvalCodeEx + 1604
12 PythonistaKit 0x00000001013eb9f0 function_call + 152
13 PythonistaKit 0x0000000101419c04 PyObject_Call + 124
14 PythonistaKit 0x00000001014082c8 instancemethod_call + 180
15 PythonistaKit 0x0000000101419c04 PyObject_Call + 124
16 PythonistaKit 0x00000001013945f0 slot_tp_call + 68
17 PythonistaKit 0x0000000101419c04 PyObject_Call + 124
18 PythonistaKit 0x00000001016507b4 PyEval_EvalFrameEx + 3232
19 PythonistaKit 0x000000010164f8c4 PyEval_EvalCodeEx + 1604
20 PythonistaKit 0x000000010164f274 PyEval_EvalCode + 44
21 PythonistaKit 0x00000001016e02e8 run_mod + 60
22 PythonistaKit 0x00000001016e03bc PyRun_FileExFlags + 148
23 PythonistaKit 0x00000001016dfe34 PyRun_SimpleFileExFlags + 696
24 PythonistaKit 0x00000001016270c4 -[PythonInterpreter runWithOptions:] + 2008
25 PythonistaKit 0x00000001016275c4 -[PythonInterpreter doDispatchBlockOnInterpreterThread:] + 108
26 Foundation 0x00000001828d802c + 340
27 CoreFoundation 0x0000000181eb909c + 24
28 CoreFoundation 0x0000000181eb8b30 + 540
29 CoreFoundation 0x0000000181eb6830 + 724
30 CoreFoundation 0x0000000181de0c50 CFRunLoopRunSpecific + 384
31 Foundation 0x00000001827f0cfc + 308
32 Foundation 0x0000000182811ad8 + 96
33 Foundation 0x00000001828d7e4c + 1000
34 libsystem_pthread.dylib 0x0000000181b67b28 + 156
35 libsystem_pthread.dylib 0x0000000181b67a8c + 0
36 libsystem_pthread.dylib 0x0000000181b65028 thread_start + 4

End of exception details.

dgelessus

@wolf71

NSInvalidArgumentException: Invalid serviceType passed to MCNearbyServiceBrowser

Your service type "AudioSrv" is not valid - the documentation for -[MCNearbyServiceBrowser initWithPeer:serviceType:] says the following about serviceType:

  • Must be 1-15 characters long
  • Can contain only ASCII lowercase letters, numbers and hyphens.

This means that "audiosrv" or "audio-srv" would be valid service types, but "AudioSrv" is not, because it contains an uppercase A and S.

wolf71

@dgelessus I change it to 'audio-srv' ,it's also crash.

Objective-C exception details:

NSInvalidArgumentException: -[BrowserDelegate_2 browser:foundPeer:withDiscoveryInfo:]: unrecognized selector sent to instance 0x1499625a0

Stack trace:

0 CoreFoundation 0x0000000181f02dc8 + 148
1 libobjc.A.dylib 0x0000000181567f80 objc_exception_throw + 56
2 CoreFoundation 0x0000000181f09c4c + 0
3 CoreFoundation 0x0000000181f06bec + 872
4 CoreFoundation 0x0000000181e04c5c _CF_forwarding_prep_0 + 92
5 libdispatch.dylib 0x000000018194d4bc + 24
6 libdispatch.dylib 0x000000018194d47c + 16
7 libdispatch.dylib 0x0000000181952b84 _dispatch_main_queue_callback_4CF + 1844
8 CoreFoundation 0x0000000181eb8d50 + 12
9 CoreFoundation 0x0000000181eb6bb8 + 1628
10 CoreFoundation 0x0000000181de0c50 CFRunLoopRunSpecific + 384
11 GraphicsServices 0x00000001836c8088 GSEventRunModal + 180
12 UIKit 0x00000001870c6088 UIApplicationMain + 204
13 Pythonista3 0x00000001000a81c8 Pythonista3 + 180680
14 libdyld.dylib 0x000000018197e8b8 + 4

End of exception details.

dgelessus

OK, it's a different error, we are making progress :)

Now it's saying that it can't find the method browser:foundPeer:withDiscoveryInfo: on your delegate. It looks like you mistyped the name in your code (you wrote withDiscoverInfo, it should be withDiscoveryInfo, note the y in Discovery).

wolf71

@dgelessus Thank you very much.

edit the bug, but can you tell me how to process the browser:lostPeer: delegate?

Ref: https://developer.apple.com/library/ios/documentation/MultipeerConnectivity/Reference/MCNearbyServiceBrowserDelegateRef/index.html#//apple_ref/occ/intfm/MCNearbyServiceBrowserDelegate/browser:lostPeer:

======================================
Fatal Python error: Aborted

Thread 0x000000016e2d3000 (most recent call first):


Objective-C exception details:

NSInvalidArgumentException: -[BrowserDelegate_6 browser:lostPeer:]: unrecognized selector sent to instance 0x134eb9c40

Stack trace:

0 CoreFoundation 0x0000000181f02dc8 + 148
1 libobjc.A.dylib 0x0000000181567f80 objc_exception_throw + 56
2 CoreFoundation 0x0000000181f09c4c + 0
3 CoreFoundation 0x0000000181f06bec + 872
4 CoreFoundation 0x0000000181e04c5c _CF_forwarding_prep_0 + 92
5 libdispatch.dylib 0x000000018194d4bc + 24
6 libdispatch.dylib 0x000000018194d47c + 16
7 libdispatch.dylib 0x0000000181952b84 _dispatch_main_queue_callback_4CF + 1844
8 CoreFoundation 0x0000000181eb8d50 + 12
9 CoreFoundation 0x0000000181eb6bb8 + 1628
10 CoreFoundation 0x0000000181de0c50 CFRunLoopRunSpecific + 384
11 GraphicsServices 0x00000001836c8088 GSEventRunModal + 180
12 UIKit 0x00000001870c6088 UIApplicationMain + 204
13 Pythonista3 0x00000001000b01c8 Pythonista3 + 180680
14 libdyld.dylib 0x000000018197e8b8 + 4

End of exception details.

JonB

That bug is saying that it tried to call broswe:lostPeer:, but your object did not implement it.
The protocol reference shows those as required.

Simply create a method

def browser_lostPeer_(_self, _cmd, browser, peer):
   print ('lost peer')

and when you create the class pass both methods in the methods argument

... methods=[browser_lostPeer_, browser_foundPeer_withDiscoveryInfo_], ...
wolf71

@JonB Thank you very much.
It's work !!!

wolf71

@JonB can you tell me how write this type delegate.

invitationHandler:(void (^)(BOOL accept,
MCSession *session))invitationHandler

def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,_advertiser,_peerID,_context,_invitationHandler):
    print 'ppppppp'

OBJECTIVE-C
- (void)advertiser:(MCNearbyServiceAdvertiser )advertiser
didReceiveInvitationFromPeer:(MCPeerID
)peerID
withContext:(NSData )context
invitationHandler:(void (^)(BOOL accept,
MCSession
session))invitationHandler

JonB

What you have looks right. Add it to the methods argument of your delegate, and also add the right protocol to your delegate class. MCNearbyServiceAdvertiserDelegate

You are requires to call the handler block with a boolean accept argument, and a MCSession initalized from the peer. StackOverflow gives me:

accept=true
session = ObjCClass('MCSession').alloc().initWithPeer(peerID)
session.delegate = YOURSERVICEADVERTISERDELEGATE
ObjCInstance(handler).invoke(accept, session)

Disclaimer... I don't have anything to test out, you need o find a good end to end example written in ObjC or swift to port over... some of these frameworks are tricky to work with just from the docs alone.

wolf71

@JonB you can using this test code.
Server side:

# -*- coding: utf-8 -*-

from objc_util import *
import ctypes,time,os

NSBundle.bundle(Path="/System/Library/Frameworks/MultipeerConnectivity.framework").load()
MCPeerID=ObjCClass('MCPeerID')
MCSession=ObjCClass('MCSession')
MCNearbyServiceAdvertiser=ObjCClass('MCNearbyServiceAdvertiser')
MCNearbyServiceBrowser=ObjCClass('MCNearbyServiceBrowser')

# Session Delegate 
def session_peer_didChangeState_(_self,_cmd,_session,_peerID,_state):
    print 'session change',_peerID,_session,_state

def session_didReceiveData_fromPeer_(_self,_cmd,_session,_data,_peerID):
    print 'Received Data',_data

def session_didReceiveStream_withName_fromPeer_(_self,_cmd,_session,_stream,_streamName,_peerID):
    print 'Received Stream....',_streamName 

SessionDelegate = create_objc_class('SessionDelegate',methods=[session_peer_didChangeState_, session_didReceiveData_fromPeer_, session_didReceiveStream_withName_fromPeer_],protocols=['MCSessionDelegate'])
SDelegate = SessionDelegate.alloc().init()

# Advertiser Delegate 
def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,advertiser,peerID,context,invitationHandler):
    print 'ppppppp'
    #ObjCInstance(invitationHandler).invoke(True,mySession)

AdvertiserDelegate = create_objc_class('AdvertiserDelegate',methods=[advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_],protocols=['MCNearbyServiceAdvertiserDelegate'])
ADelegate = AdvertiserDelegate.alloc().init()       


# init PeerID
myID = MCPeerID.alloc().initWithDisplayName('wolf_srv')
# init Session and delegate
mySession = MCSession.alloc().initWithPeer_(myID)
mySession.setDelegate_(SDelegate)


'''
    Server 
'''
# Create Server and set delegate
aSrv = MCNearbyServiceAdvertiser.alloc().initWithPeer_discoveryInfo_serviceType_(myID,ns({'player.name':'apple'}),'audio-srv1')
aSrv.setDelegate_(ADelegate)

# Start Server
aSrv.startAdvertisingPeer()
print 'Server start, ID is : ',myID

try:
    while 1:
        time.sleep(0.1)
except KeyboardInterrupt:
    print 'Server Stop...'
    aSrv.stopAdvertisingPeer()
except:            
    raise

=================================
Browser side

# -*- coding: utf-8 -*-

from objc_util import *
import ctypes,time,os

cnt = 0

NSBundle.bundle(Path="/System/Library/Frameworks/MultipeerConnectivity.framework").load()
MCPeerID=ObjCClass('MCPeerID')
MCSession=ObjCClass('MCSession')
MCNearbyServiceAdvertiser=ObjCClass('MCNearbyServiceAdvertiser')
MCNearbyServiceBrowser=ObjCClass('MCNearbyServiceBrowser')

def session_peer_didChangeState_(_self,_cmd,_session,_peerID,_state):
    print 'session change',ObjCInstance(_peerID),_state

def session_didReceiveData_fromPeer_(_self,_cmd,_session,_data,_peerID):
    print 'Received Data',_data

def session_didReceiveStream_withName_fromPeer_(_self,_cmd,_session,_stream,_streamName,_peerID):
    print 'Received Stream....',_streamName 

SessionDelegate = create_objc_class('SessionDelegate',methods=[session_peer_didChangeState_, session_didReceiveData_fromPeer_, session_didReceiveStream_withName_fromPeer_],protocols=['MCSessionDelegate'])
SDelegate = SessionDelegate.alloc().init()

def browser_didNotStartBrowsingForPeers_(_self,_cmd,_browser,_err):
    print ('ERROR!!!')

def browser_foundPeer_withDiscoveryInfo_(_self, _cmd, _browser, _peerID, _info):
    global aBr,mySession

    peerID = ObjCInstance(_peerID)
    aBr.invitePeer_toSession_withContext_timeout_(peerID,mySession,None,0)
    #mySession.connectPeer_withNearbyConnectionData_(peerID,None)
    print '#',peerID,ObjCInstance(_info)

def browser_lostPeer_(_self, _cmd, browser, peer):
    print ('lost peer')

BrowserDelegate = create_objc_class('BrowserDelegate',methods=[browser_foundPeer_withDiscoveryInfo_, browser_lostPeer_, browser_didNotStartBrowsingForPeers_],protocols=['MCNearbyServiceBrowserDelegate'])
Bdelegate = BrowserDelegate.alloc().init()

myID = MCPeerID.alloc().initWithDisplayName('wolf_client')
mySession = MCSession.alloc().initWithPeer_(myID)
mySession.setDelegate_(SDelegate)

print 'my ID',myID
aBr = MCNearbyServiceBrowser.alloc().initWithPeer_serviceType_(myID,'audio-srv1')
aBr.setDelegate_(Bdelegate)
aBr.startBrowsingForPeers()

try:
    while 1:
        time.sleep(0.1)
except KeyboardInterrupt:
    print 'Close...'
    aBr.stopBrowsingForPeers()  
except:            
    raise

wolf71

when delegate trip, Show Error box

Type Error
================
cannot build parament 
globals == locals


===================
def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,advertiser,peerID,context,invitationHandler):

@omz

JonB

Can you paste the actual traceback (if the debugger pops up, click the button in the corner to expand it, then tap Print Traceback). I seem to remember encountering a similar problem in the past. This comes from ctypes, but we need to know which argument is causing problepms.

wolf71

@JonB not trackback info.

maybe the delegate define problem.

:(void(^)(BOOL accept, MCSession *session))invitationHandler

JonB

The error comes from ctypes callback.c. I suspect the issue is the ObjCBlock, the c code cannot convert the pointer it gets.
I know I encountered and solved this elsewhere, but can't find the solution, you might try

def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,advertiser,peerID,context,invitationHandler):
    print('invitation received')
f= advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_ 
f.argtypes  =[c_void_p]*4
f.restype = None
f.encoding=b'v@:@@@@?'
# also, try f.encoding=b'v@:@@@@'
AdvertiserDelegate = create_objc_class('AdvertiserDelegate',methods=[advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_])
wolf71

@JonB It's work. :-)

wolf71

@JonB other problem.

ObjCInstance(invitationHandler).invoke(True,mySession)

No method found for selector "invoke"


print ObjCInstance(invitationHandler)

< NSStackBlock >

Traceback (most recent call last):
File "ctypes/callbacks.c", line 314, in 'calling callback function'
File "/private/var/mobile/Containers/Shared/AppGroup/A2331A94-5917-4220-83BB-A1CC1ED4E5A7/Pythonista3/Documents/Research/MCSrv.py", line 29, in advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler

ObjCInstance(invitationHandler).invoke(True,mySession)
File "/var/containers/Bundle/Application/C653EC7F-B29A-4BAD-8AA1-1B562C926512/Pythonista3.app/Frameworks/PythonistaKit.framework/pylib/site-packages/objc_util.py", line 636, in getattr
cached_method = ObjCInstanceMethod(self, attr)
File "/var/containers/Bundle/Application/C653EC7F-B29A-4BAD-8AA1-1B562C926512/Pythonista3.app/Frameworks/PythonistaKit.framework/pylib/site-packages/objc_util.py", line 860, in init
raise AttributeError('No method found for selector "%s"' % (self.sel_name))
AttributeError: No method found for selector "invoke:"

JonB

It is possible you might need to do something like this

class _block_descriptor (Structure):
   _fields_ = [('reserved', c_ulong), ('size', c_ulong), ('copy_helper', c_void_p), ('dispose_helper', c_void_p), ('signature', c_char_p)]
InvokeFuncType = ctypes.CFUNCTYPE(None, *[c_void_p,ctypes.cbool,c_void_p])
class block_literal(Structure):
    _fields_ = [('isa', c_void_p), ('flags', c_int), ('reserved', c_int), ('invoke', InvokeFuncType), ('descriptor', _block_descriptor)]

then,

blk=block_literal.from_address(invitationHandler)
blk.invoke(ObjCInstance(invitationHandler),True, ObjCInstance(mySession))

It seems as though the ObjCInstance does not handle invoke properly, since invoke is sort of special. The code abive creates the ctypes struct corresponding to the signature, i think, along with the hidden block argument, then calls it.

wolf71

@JonB Thank you very much. I will try it later.

The other question.
I need call this objc function:
- (NSInteger)write:(const uint8_t *)buffer
maxLength:(NSUInteger)length

I write this code:
buf=bytearray('sldfjksadklkadflksadjlsadkjsldfksadlf;sajflksddfjkl')
blen=len(buf)
outstream.write_maxLength_(id(buf),blen)

=============
Traceback (most recent call last):
File "ctypes/callbacks.c", line 314, in 'calling callback function'
File "/private/var/mobile/Containers/Shared/AppGroup/A2331A94-5917-4220-83BB-A1CC1ED4E5A7/Pythonista3/Documents/Research/MCTest.py", line 40, in session_peer_didChangeState

outstream.write_maxLength_(id(buf),blen)
File "/var/containers/Bundle/Application/C653EC7F-B29A-4BAD-8AA1-1B562C926512/Pythonista3.app/Frameworks/PythonistaKit.framework/pylib/site-packages/objc_util.py", line 897, in call
res = objc_msgSend(obj.ptr, sel(self.sel_name), *args)
ArgumentError: argument 3: : wrong type

JonB

@wolf71
A way to check what objc_util thinks you need:

restype,argtypes,encoding=objc_util.parse_types(outstream.write_maxLength_.encoding)

which gives

argtypes=[<class 'ctypes.c_void_p'>, <class 'ctypes.c_void_p'>, <class 'ctypes.c_char_p'>, <class 'ctypes.c_ulong'>]

the first two are the hidden self/selector, so the arguments need to be convertible to a char_p and a long.

write_maxlength(b'hello',5)

works (python3), or probably just the string without bytesarray in py2.

buf=ctypes.create_string_buffer(10)
buf.value=b'hello'

is another way.

dgelessus

@JonB The b"stuff" syntax is valid in Python 2 as well, and returns a normal str (since Python 2 str is 8-bit anyway).

wolf71

@JonB Thank you very much.

It's work !!!

robertiii

I know this is an old thread, but I am desperately wanting to do this in my app. I have copied the code from what was given and it got the two to print seeing each other. After the big section of server and browser side with the correction what needs to be done to get this working and communicating in an app?

mikael

I am trying to get this to work, but the following just crashes Pythonista (latest beta). All fine if you uncomment the last line. Any ideas?

#coding: utf-8
from objc_util import *

NSBundle.bundle(Path="/System/Library/Frameworks/MultipeerConnectivity.framework").load()

MCPeerID = ObjCClass('MCPeerID')
MCNearbyServiceAdvertiser = ObjCClass('MCNearbyServiceAdvertiser')

myID = MCPeerID.alloc().initWithDisplayName('peer')

advertiser = MCNearbyServiceAdvertiser.alloc().initWithPeer_discoveryInfo_serviceType_(myID, None, 'dev_srv')
mikael

Ah, enabled the fault handler and noticed that it was just the service type with an underscore... Nothing to see here, please move along.

mikael

I have put up the two pieces of code above on Github:
Advertiser
Browser

They work fine on first run, but the advertiser/server always crashes on the second run.

It only crashes if there was a successful connection on the first run, otherwise it runs fine.

Fault handler provides only a semi-useful error:

Fatal Python error: Segmentation fault

Current thread 0x000000016fffb000 (most recent call first):
  File "/var/containers/Bundle/Application/01E95FCB-DD81-45FB-B878-47D3C0FF9E35/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/objc_util.py", line 682 in __del__

... which points to the last line in this objc_util code:

def __del__(self):
  # Release the ObjC object's memory:
  objc_msgSend = c['objc_msgSend']
  objc_msgSend.argtypes = [c_void_p, c_void_p]
  objc_msgSend.restype = None
  objc_msgSend(self.ptr, sel('release'))

Changes I have tried to the code in this thread, with no effect:

  • Added session disconnect() to the end
  • Added try/except to only create the delegate classes when they do not already exist

Any help debugging this would be much appreciated.

JonB

You could try to figure out which object is crashing, by manually del'ing them, one at a time. (my guess - services are getting del'd before delegates). Then, add an extra retain() for the offending objects.

Alternatively, maybe try setDelegate_(None) on everything that takes a delegate, to disentangle things.

mikael

@JonB, thanks.

Trying to run incrementally more code after the crash to find the culprit failed, as Pythonista will crash if I execute just an empty file.

Setting delegates to None has not changed things.

Manually deleting either the session object or the advertiser object both cause a crash after a successful connect, but do not cause a crash if I do the deletes after running the server code without a connection.

mikael

@JonB, as this is the code that gets run on an invite, which seems to be the crucial point causing the crashes, and as I understand next to nothing about it, I have to ask if there is anything you see that messes things up?

class _block_descriptor (Structure):
   _fields_ = [('reserved', c_ulong), ('size', c_ulong), ('copy_helper', c_void_p), ('dispose_helper', c_void_p), ('signature', c_char_p)]
InvokeFuncType = ctypes.CFUNCTYPE(None, *[c_void_p,ctypes.c_bool,c_void_p])
class block_literal(Structure):
    _fields_ = [('isa', c_void_p), ('flags', c_int), ('reserved', c_int), ('invoke', InvokeFuncType), ('descriptor', _block_descriptor)]

# Advertiser Delegate
def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,advertiser,peerID,context,invitationHandler):
  print('invitation',peerID)
  blk=block_literal.from_address(invitationHandler)
  blk.invoke(ObjCInstance(invitationHandler),True, ObjCInstance(mySession))
JonB

@mikael said:

advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_

Hmm, okay:
can you print (console) .retainCount() for the advertiser and session objects, for the case when no invite happens, then after the invite occurs?

What I am wondering is if something is getting released by the invitationHandler -- that bit of code is taking the block that is passed to the delegate method, turning it into an actual ObjCBlock, then invoking it.

One thing you could do is override the __del__ method of the suspect ObjCInstances -- maybe set up logging to a file in your script, then insert a logger.debug(self), that way we can see which object is the culprit.

Next, you could try first calling retain() on those instances -- that may cause a memory leak, but probably not a crash.

mikael

Thank you.

Before a connection, both advertiser and session report a retainCount of 2.

After a connection has been established and closed, calling retainCount on either object crashes Pythonista.

Printing counts within the callback, before the invitation invoke gives these:

  • Advertiser: 5
  • Session: 2

And immediately after:

  • Advertiser: 6
  • Session: 4
JonB

Have you tried calling retain, then del the object?

Alternatively -- just monkey patch __del__ so it is a pass only.

mikael

@JonB, thanks.

Tried mySession.retain() on the console after a connection —> crash.

Tried retain() on both session and advertiser immediately after creation, then del’ing them on close. Next run still crashes.

Tried setting __del__ of both objects to a dummy. Next run still crashes in objc_util.__del__, likewise if I try a del mySession on console, so I am probably not monkeypatching properly.

mithrendal

Hi Mikael,

had the same problem. The crash was caused by the garbage collection which is triggered automatically somewhen when python wants to delete the invitationHandler.

My approach/workaround was not to get it deleted by remembering its reference. That way the garbage collector will not call delete on it.

instead of

blk.invoke(ObjCInstance(invitationHandler),True, ObjCInstance(mySession))

try

inviHandler = ObjCInstance(invitationHandler)
someglobalscopeobject.remember_this_thing=inviHandler
blk.invoke(inviHandler,True, ObjCInstance(mySession))

my guess was that something in the MPC framework still holds the reference and uses this invitation handler but the pythonista garbage collector does not know this because it only knows about references in your python programm. Thus when the python gc deletes it, and after that something in the apple mpc framework wants to call it, it will inevitably crash.

EDIT:
instead of someglobalscopedobject you could also use something like that to make it able to be reentrant

import builtins

...
inviHandlerObj = ObjCInstance(invitationHandler)

if (builtins.retainCache == NONE):
   builtins.retainCache = [ ]
builtins.retainCache.append(inviHandlerObj)

blk.invoke(inviHandlerObj,True, ObjCInstance(mySession))
...

mikael

@mithrendal, thanks! @JonB was right, but I just missed the mark.

I used the objc_util.retain_global, and updated the repository with the new version. Next I will package this into a Pythonic API that does not have client-server roles but only peers.

# Advertiser Delegate
def advertiser_didReceiveInvitationFromPeer_withContext_invitationHandler_(_self,_cmd,advertiser,peerID,context,invitationHandler):
  global mySession
  invitation_handler = ObjCInstance(invitationHandler)
  retain_global(invitation_handler)
  blk=block_literal.from_address(invitationHandler)
  blk.invoke(invitation_handler,True, mySession)
mithrendal

Hi @mikael,

Yes that looks promising. I just uploaded my work on this for you at github. It doesn't crash and is completely reentrant. The idea was to make an simple API which games and other apps can import. But I did not work on it recently. When I saw your post that you too want to build an API I thought you might want to have a look. https://github.com/mithrendal/pythonista_mpc

Btw I am a big fan of your Gestures.py API 😀

robertiii

How do I send data from client to server and vise versa during game?

mikael

@robertiii, check examples in @mithrendal’s code, linked in his post above. Or hold for my Grand Pythonic Simplification.

mithrendal

@robertiii be aware, my code on github is still unfinished. I did this some months ago to proof that MPC is really working in Pythonista . Thats what the code does well. I uploaded it then for @mikael as an working example. But it does not yet serve as an solid and simple API, a lot of polishing and refactoring is still needed here. I am still at it, but my time for it is restricted so don't expect a finished API very soon. Of course you decide, but I would wait for @mikael 's Grand Python Simplyfication !!! That is what I am also looking forward to ;-) because he has already done some great APIs for pythonista (see here https://github.com/mikaelho).

mikael

Ok, with expectations set unreasonably high by @mithrendal, please check the repo for the first working version of multipeer.py.

Here's a minimal usage example, a line-based chat:

import multipeer

my_name = input('Name: ')

mc = multipeer.MultipeerConnectivity(
  display_name=my_name,
  service_type='chat'
)

try:
  while True:
    chat_message = input('Message: ')
    mc.send(chat_message)
finally:
  mc.end_all()

This is a functional chat, even though the prompts and incoming messages tend to get messily mixed up.

You can also run the multipeer.py file to try out a cleaner Pythonista UI version of the chat, which demonstrates sending dicts as the message content, and acts as the best 'how-to guide' at the moment.

Docs still need some significant work. All testing and comments highly welcome.

mikael

Noted that shutting and restarting the UI chat client leads to duplicate messages being received, at least with 3 peers, and general instability. I will try including the checks to not re-create classes when already available.

Also, need to implement proper error handling and exceptions if possible.

JonB

btw, those try/except to not recreate the classes would not have worked,

e.g.
SessionDelegate.alloc().init()
would fail because SessionDelegate was cleared.
You might first try
SessionDelegate=ObjCClass('SessionDelegate')

though, note that the objc class name can change when using "debug" mode.

mikael

@JonB, thanks. I would like to say that I understood completely what you said... But no, I didn’t. For clarity, I understood that the thing below would be sufficient, barring debug on/off people?

try:
  AdvertiserDelegate = ObjCClass('AdvertiserDelegate')
  ADelegate = AdvertiserDelegate.alloc().init()
except:
  # ...
mikael

Eh, the issue was with the UI chat demo code, not the MC framework. Seems to work pretty well now, even tolerates and recovers from being backgrounded without any additional effort from the developer.

Next I will take a look at capturing and managing errors from send, mainly.

Would be good to hear if someone has a use case for the streaming or resource-send options, which are much more tricky.

JonB

create_objc_class will create a new class name every time it is run (at least, with the default debug attrib set to 1, ) -- so 'AdvertiserDelegate' might really become 'AdvertiserDelegate1'.

Only really an issue for development, but if you see strange things where a change you just made doesn't seem to be working, thats the reason.

mithrendal

@mikael very super 👍🏿 cool indeed. I do really love your approach with this simple API. Just tried it and it works excellent. Will have a look at the source later...

Edit: made a minor bugfix pull request to avoid an exception of being thrown

robertiii

Great work! Its incredible! Why is it limited to only 8 devices?

mikael

@robertiii, seems to be an Apple-defined limit, that may be driven by Bluetooth LE limits. It is now hard to find concrete current information in Apple docs, but I wanted to make that limit visible so that you get warned before embarking on an ambitious project targeting hundreds of peers in one session.