Hello BeeWare,
Thanks for sharing rubicon-objc. I very much like the light approach at playing with Mac OS APIs with it, instead of PyObjc -- while perfectly confortable with Python, C and generic low-level systems/networking code, I confess myself as a complete Objective-C + Mac OS newbie.
(a bit of context: I've been exploring bluetooth programming on various platforms. I started with Linux, which was surprisingly nice and while waiting for some PRs to be reviewed and hopefully merged, I decided to move on to the Mac; this issue arises from my first very simple attempt).
The facts
Exploring the Apple docs on bluetooth, in particular https://developer.apple.com/reference/iobluetooth/iobluetoothdevice?language=objc, I came up with the following script to enumerate the names and bluetooth addresses of paired devices:
from ctypes import cdll, util
from rubicon.objc import ObjCClass
if __name__ == '__main__':
cdll.LoadLibrary(util.find_library('IOBluetooth'))
IOBluetoothDevice = ObjCClass('IOBluetoothDevice')
paired_devices = IOBluetoothDevice.pairedDevices()
num_devices = paired_devices.count()
for i in range(num_devices):
device = paired_devices.objectAtIndex_(i)
name = device.name
address = device.getAddress()
print('name=%r address=%r' % (name, address))
When run rubicon-objc complains, printing out:
No restype encoding for b'getAddress' (b'r^{BluetoothDeviceAddress=[6C]}')
With this, the names of the devices are printed out nicely, but the addresses show up as None
.
Browsing the code, I quickly figured out the culprit in the ObjCMethod
class. I went digging and learned a bit about Objective C type encodings (at https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html) and quickly matched the b'r^{BluetoothDeviceAddress=[6C]}'
to the following declaration in /System/Library/Frameworks/IOBluetooth.framework/Versions/A/Headers/Bluetooth.h
:
typedef struct BluetoothDeviceAddress BluetoothDeviceAddress;
struct BluetoothDeviceAddress
{
uint8_t data[ 6 ];
};
(this is under 10.9.5, but per docs should be present in all the more recent OS releases)
The way forward
Ideally, the ObjCMethod
class, and in particular ctype_for_encoding
would know how to handle arbitrarily complex Objective C types, including structs and arrays (I see there is some special handling for some types in the code, such as for NSPoint, NSSize and friends -- not sure why, but probably because they vary depending on the OS/CPU architecture).
A generic approach would parse an encoding like the one I got -- b'r^{BluetoothDeviceAddress=[6C]}'
-- and dynamically create a ctypes Structure derived class such that the rubicon-objc caller would get something workable. At first sight, while not terribly complex, the parsing would need to be more sophisticated so as to handle sequences of encodings like they're specified for structures (more, regarding structures: the field name would be gone, of course, in such automatic approach).
Another, simpler, approach that worked for me was a small hack that at least returned a raw ctypes c_void_p
when ctype_for_encoding
fails at its task: this at least does not throw away data and allows the calling code (at the callers responsibility / knowledge) to cast that to the appropriate type and access the underlying data (or make a mistake and segfault!). :)
My hack was just replacing the return None
right after the print for No restype encoding for
... with a return c_void_p
. From there, I updated my code to:
from ctypes import cdll, util, Structure, c_ubyte, cast, POINTER
from rubicon.objc import ObjCClass
class BluetoothDeviceAddress(Structure):
_fields_ = [
('data', c_ubyte * 6),
]
if __name__ == '__main__':
cdll.LoadLibrary(util.find_library('IOBluetooth'))
IOBluetoothDevice = ObjCClass('IOBluetoothDevice')
paired_devices = IOBluetoothDevice.pairedDevices()
num_devices = paired_devices.count()
for i in range(num_devices):
device = paired_devices.objectAtIndex_(i)
name = device.name
address = device.getAddress()
btda_ptr = cast(address, POINTER(BluetoothDeviceAddress))
addr_str = ':'.join(map(lambda b: '%x' % b, btda_ptr[0].data))
print('name=%r address=%r addr_str=%r' % (name, address, addr_str))
This happily did its thing and correctly printed out the address strings of the paired bluetooth devices.
What next
Thanks for taking the time in reading this.
I'd love to hear feedback and guidance on how to move forward and I'll be happy to contribute.
PS: By the way, is there a more Pythonic approach at iterating through the elements of an NSArray? :)