diff --git a/Makefile b/Makefile index 2c48019a..f268cf24 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,6 @@ tests/objc/librubiconharness.dylib: $(OBJ_FILES) clean: rm -rf tests/objc/*.o tests/objc/*.d tests/objc/librubiconharness.dylib -%.o: %.m +%.o: %.m tests/objc/*.h clang -x objective-c -I./tests/objc -c $(EXTRA_FLAGS) $< -o $@ diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 692beb16..1d8a8a10 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -6,7 +6,7 @@ ) from .runtime import ( # noqa: F401 IMP, SEL, Block, Class, Ivar, Method, NSObject, ObjCBlock, ObjCClass, - ObjCInstance, ObjCMetaClass, objc_classmethod, objc_const, objc_id, + ObjCInstance, ObjCMetaClass, ObjCProtocol, objc_classmethod, objc_const, objc_id, objc_ivar, objc_method, objc_property, objc_property_t, objc_rawmethod, send_message, send_super, ) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index 7c3a50a5..dcd26edc 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -40,6 +40,7 @@ 'ObjCMutableDictInstance', 'ObjCMutableListInstance', 'ObjCPartialMethod', + 'ObjCProtocol', 'SEL', 'add_ivar', 'add_method', @@ -466,7 +467,7 @@ class objc_method_description(Structure): libobjc.protocol_copyPropertyList.argtypes = [objc_id, POINTER(c_uint)] # Protocol **protocol_copyProtocolList(Protocol *proto, unsigned int *outCount) -libobjc.protocol_copyProtocolList = POINTER(objc_id) +libobjc.protocol_copyProtocolList.restype = POINTER(objc_id) libobjc.protocol_copyProtocolList.argtypes = [objc_id, POINTER(c_uint)] # struct objc_method_description protocol_getMethodDescription( @@ -1104,6 +1105,10 @@ def _select_mixin(cls, object_ptr): if send_message(object_ptr, 'isKindOfClass:', nsdictionary): return ObjCDictInstance + protocol = libobjc.objc_getClass(b'Protocol') + if send_message(object_ptr, 'isKindOfClass:', protocol, restype=c_bool, argtypes=[c_void_p]): + return ObjCProtocol + return cls def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None): @@ -1499,12 +1504,22 @@ class ObjCClass(ObjCInstance, type): @property def superclass(self): + """The superclass of this class, or None if this is a root class (such as NSObject).""" + super_ptr = libobjc.class_getSuperclass(self) if super_ptr.value is None: return None else: return ObjCClass(super_ptr) + @property + def protocols(self): + """The protocols adopted by this class.""" + + out_count = c_uint() + protocols_ptr = libobjc.class_copyProtocolList(self, byref(out_count)) + return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) + def __new__(cls, *args, **kwargs): """Create a new ObjCClass instance or return a previously created instance for the given Objective-C class. The argument may be either @@ -1893,3 +1908,51 @@ def __init__(self, func, restype=NOTHING, *arg_types): def wrapper(self, instance, *args): return self.func(*args) + + +Protocol = ObjCClass('Protocol') + + +class ObjCProtocol(ObjCInstance): + """Python wrapper for an Objective-C protocol.""" + + @property + def name(self): + """The name of this protocol.""" + + return libobjc.protocol_getName(self).decode('utf-8') + + @property + def protocols(self): + """The superprotocols of this protocol.""" + + out_count = c_uint() + protocols_ptr = libobjc.protocol_copyProtocolList(self, byref(out_count)) + return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) + + def __new__(cls, name_or_ptr, bases=None, ns=None): + if bases is not None or ns is not None: + raise NotImplementedError('Creating Objective-C protocols from Python is not yet supported') + + if isinstance(name_or_ptr, (bytes, str)): + name = ensure_bytes(name_or_ptr) + ptr = libobjc.objc_getProtocol(name) + if ptr.value is None: + raise NameError('Objective-C protocol {} not found'.format(name)) + else: + ptr = cast(name_or_ptr, objc_id) + if ptr.value is None: + raise ValueError('Cannot create ObjCProtocol for nil pointer') + elif not send_message(ptr, 'isKindOfClass:', Protocol, restype=c_bool, argtypes=[objc_id]): + raise ValueError('Pointer {} ({:#x}) does not refer to a protocol'.format(ptr, ptr.value)) + + return super().__new__(cls, ptr) + + def __repr__(self): + return '<{cls.__module__}.{cls.__qualname__}: {self.name} at {self.ptr.value:#x}>'.format( + cls=type(self), self=self) + + +# Need to use a different name to avoid conflict with the NSObject class. +# NSObjectProtocol is also the name that Swift uses when importing the NSObject protocol. +NSObjectProtocol = ObjCProtocol('NSObject') diff --git a/tests/objc/BaseExample.h b/tests/objc/BaseExample.h index e40c815c..8ccc4970 100644 --- a/tests/objc/BaseExample.h +++ b/tests/objc/BaseExample.h @@ -1,6 +1,8 @@ #import -@interface BaseExample : NSObject { +#import "Protocols.h" + +@interface BaseExample : NSObject { int _baseIntField; } diff --git a/tests/objc/Protocols.h b/tests/objc/Protocols.h new file mode 100644 index 00000000..481686e3 --- /dev/null +++ b/tests/objc/Protocols.h @@ -0,0 +1,9 @@ +#import + +@protocol ExampleProtocol @end + +@protocol BaseProtocolOne @end + +@protocol BaseProtocolTwo @end + +@protocol DerivedProtocol @end diff --git a/tests/test_core.py b/tests/test_core.py index 728d4a0f..7ffe9d3f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -11,7 +11,7 @@ from rubicon.objc import ( SEL, NSEdgeInsets, NSEdgeInsetsMake, NSObject, NSRange, NSUInteger, - ObjCClass, ObjCInstance, ObjCMetaClass, core_foundation, objc_classmethod, + ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, core_foundation, objc_classmethod, objc_const, objc_method, objc_property, send_message, types, ) from rubicon.objc.runtime import ObjCBoundMethod, libobjc @@ -106,6 +106,33 @@ def test_metametaclass(self): self.assertIsInstance(ExampleMetaMeta, ObjCMetaClass) self.assertEqual(ExampleMetaMeta, NSObject.objc_class) + def test_protocol_by_name(self): + """An Objective-C protocol can be looked up by name.""" + + ExampleProtocol = ObjCProtocol('ExampleProtocol') + self.assertEqual(ExampleProtocol.name, 'ExampleProtocol') + + def test_protocol_caching(self): + """ObjCProtocol instances are cached.""" + + ExampleProtocol1 = ObjCProtocol('ExampleProtocol') + ExampleProtocol2 = ObjCProtocol('ExampleProtocol') + + self.assertIs(ExampleProtocol1, ExampleProtocol2) + + def test_protocol_by_pointer(self): + """An Objective-C protocol can be created from a pointer.""" + + example_protocol_ptr = libobjc.objc_getProtocol(b'ExampleProtocol') + ExampleProtocol = ObjCProtocol(example_protocol_ptr) + self.assertEqual(ExampleProtocol, ObjCProtocol('ExampleProtocol')) + + def test_nonexistant_protocol(self): + """A NameError is raised if a protocol doesn't exist.""" + + with self.assertRaises(NameError): + ObjCProtocol('DoesNotExist') + def test_objcinstance_can_produce_objcclass(self): """Creating an ObjCInstance for a class pointer gives an ObjCClass.""" @@ -130,6 +157,14 @@ def test_objcclass_can_produce_objcmetaclass(self): self.assertEqual(ExampleMeta, ObjCMetaClass("Example")) self.assertIsInstance(ExampleMeta, ObjCMetaClass) + def test_objcinstance_can_produce_objcprotocol(self): + """Creating an ObjCInstance for a protocol pointer gives an ObjCProtocol.""" + + example_protocol_ptr = libobjc.objc_getProtocol(b'ExampleProtocol') + ExampleProtocol = ObjCInstance(example_protocol_ptr) + self.assertEqual(ExampleProtocol, ObjCProtocol('ExampleProtocol')) + self.assertIsInstance(ExampleProtocol, ObjCProtocol) + def test_objcclass_requires_class(self): """ObjCClass only accepts class pointers.""" @@ -149,7 +184,17 @@ def test_objcmetaclass_requires_metaclass(self): with self.assertRaises(ValueError): ObjCMetaClass(NSObject.ptr) + def test_objcprotocol_requires_protocol(self): + """ObjCProtocol only accepts protocol pointers.""" + + random_obj = NSObject.alloc().init() + with self.assertRaises(ValueError): + ObjCProtocol(random_obj.ptr) + random_obj.release() + def test_objcclass_superclass(self): + """An ObjCClass's superclass can be looked up.""" + Example = ObjCClass("Example") BaseExample = ObjCClass("BaseExample") @@ -158,6 +203,8 @@ def test_objcclass_superclass(self): self.assertIsNone(NSObject.superclass) def test_objcmetaclass_superclass(self): + """An ObjCMetaClass's superclass can be looked up.""" + Example = ObjCClass("Example") BaseExample = ObjCClass("BaseExample") @@ -165,6 +212,24 @@ def test_objcmetaclass_superclass(self): self.assertEqual(BaseExample.objc_class.superclass, NSObject.objc_class) self.assertEqual(NSObject.objc_class.superclass, NSObject) + def test_objcclass_protocols(self): + """An ObjCClass's protocols can be looked up.""" + + BaseExample = ObjCClass('BaseExample') + ExampleProtocol = ObjCProtocol('ExampleProtocol') + DerivedProtocol = ObjCProtocol('DerivedProtocol') + + self.assertEqual(BaseExample.protocols, (ExampleProtocol, DerivedProtocol)) + + def test_objcprotocol_protocols(self): + """An ObjCProtocol's protocols can be looked up.""" + + DerivedProtocol = ObjCProtocol('DerivedProtocol') + BaseProtocolOne = ObjCProtocol('BaseProtocolOne') + BaseProtocolTwo = ObjCProtocol('BaseProtocolTwo') + + self.assertEqual(DerivedProtocol.protocols, (BaseProtocolOne, BaseProtocolTwo)) + def test_field(self): "A field on an instance can be accessed and mutated"