Skip to content

Commit

Permalink
Allow declaring methods and properties in ObjCProtocols (beeware#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgelessus committed Oct 18, 2017
1 parent 74c5b5a commit 38d1719
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 17 deletions.
131 changes: 114 additions & 17 deletions rubicon/objc/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,14 @@ def object_isClass(obj):

######################################################################


class objc_property_attribute_t(Structure):
_fields_ = [
('name', c_char_p),
('value', c_char_p),
]


# const char *property_getAttributes(objc_property_t property)
libobjc.property_getAttributes.restype = c_char_p
libobjc.property_getAttributes.argtypes = [objc_property_t]
Expand All @@ -445,16 +453,41 @@ def object_isClass(obj):
libobjc.property_getName.restype = c_char_p
libobjc.property_getName.argtypes = [objc_property_t]

######################################################################
# objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)
libobjc.property_copyAttributeList.restype = POINTER(objc_property_attribute_t)
libobjc.property_copyAttributeList.argtypes = [objc_property_t, POINTER(c_uint)]

# BOOL protocol_conformsToProtocol(Protocol *proto, Protocol *other)
libobjc.protocol_conformsToProtocol.restype = c_bool
libobjc.protocol_conformsToProtocol.argtypes = [objc_id, objc_id]
######################################################################


class objc_method_description(Structure):
_fields_ = [("name", SEL), ("types", c_char_p)]
_fields_ = [
('name', SEL),
('types', c_char_p),
]


# void protocol_addMethodDescription(Protocol *proto, SEL name, const char *types,
# BOOL isRequiredMethod, BOOL isInstanceMethod)
libobjc.protocol_addMethodDescription.restype = None
libobjc.protocol_addMethodDescription.argtypes = [objc_id, SEL, c_char_p, c_bool, c_bool]

# void protocol_addProtocol(Protocol *proto, Protocol *addition)
libobjc.protocol_addProtocol.restype = None
libobjc.protocol_addProtocol.argtypes = [objc_id, objc_id]

# void protocol_addProperty(Protocol *proto, const char *name, const objc_property_attribute_t *attributes,
# unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty)
libobjc.protocol_addProperty.restype = None
libobjc.protocol_addProperty.argtypes = [objc_id, c_char_p, POINTER(objc_property_attribute_t), c_uint, c_bool, c_bool]

# Protocol *objc_allocateProtocol(const char *name)
libobjc.objc_allocateProtocol.restype = objc_id
libobjc.objc_allocateProtocol.argtypes = [c_char_p]

# BOOL protocol_conformsToProtocol(Protocol *proto, Protocol *other)
libobjc.protocol_conformsToProtocol.restype = c_bool
libobjc.protocol_conformsToProtocol.argtypes = [objc_id, objc_id]

# struct objc_method_description *protocol_copyMethodDescriptionList(
# Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount)
Expand All @@ -479,6 +512,10 @@ class objc_method_description(Structure):
libobjc.protocol_getName.restype = c_char_p
libobjc.protocol_getName.argtypes = [objc_id]

# void objc_registerProtocol(Protocol *proto)
libobjc.objc_registerProtocol.restype = None
libobjc.objc_registerProtocol.argtypes = [objc_id]

######################################################################

# const char* sel_getName(SEL aSelector)
Expand Down Expand Up @@ -976,7 +1013,13 @@ def register(cls, attr):
name = attr.replace("_", ":")
cls.imp_keep_alive_table[name] = add_method(cls, name, _objc_method, encoding)

def protocol_register(proto, attr):
name = attr.replace('_', ':')
types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding)
libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, True)

_objc_method.register = register
_objc_method.protocol_register = protocol_register

return _objc_method

Expand All @@ -1001,7 +1044,13 @@ def register(cls, attr):
name = attr.replace("_", ":")
cls.imp_keep_alive_table[name] = add_method(cls.objc_class, name, _objc_classmethod, encoding)

def protocol_register(proto, attr):
name = attr.replace('_', ':')
types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding)
libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, False)

_objc_classmethod.register = register
_objc_classmethod.protocol_register = protocol_register

return _objc_classmethod

Expand All @@ -1018,6 +1067,9 @@ def __init__(self, vartype):
def pre_register(self, ptr, attr):
return add_ivar(ptr, attr, self.vartype)

def protocol_register(self, proto, attr):
raise TypeError('Objective-C protocols cannot have ivars')


class objc_property(object):
def __init__(self):
Expand Down Expand Up @@ -1065,14 +1117,27 @@ def _objc_setter(objc_self, objc_cmd, name):
cls.imp_keep_alive_table[attr] = add_method(cls.ptr, attr, _objc_getter, getter_encoding)
cls.imp_keep_alive_table[setter_name] = add_method(cls.ptr, setter_name, _objc_setter, setter_encoding)

def protocol_register(self, proto, attr):
attrs = (objc_property_attribute_t * 2)(
objc_property_attribute_t(b'T', b'@'), # Type: id
objc_property_attribute_t(b'&', b''), # retain
)
libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, 2, True, True)


def objc_rawmethod(f):
encoding = encoding_from_annotation(f, offset=2)

def register(cls, attr):
name = attr.replace("_", ":")
cls.imp_keep_alive_table[name] = add_method(cls, name, f, encoding)

def protocol_register(proto, attr):
raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod')

f.register = register
f.protocol_register = protocol_register

return f


Expand Down Expand Up @@ -1752,20 +1817,52 @@ def protocols(self):
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 (bases is None) ^ (ns is None):
raise TypeError('ObjCProtocol arguments 2 and 3 must be given together')

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))
if bases is None and ns is None:
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))
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))
name = ensure_bytes(name_or_ptr)

if libobjc.objc_getProtocol(name).value is not None:
raise RuntimeError('An Objective-C protocol named {!r} already exists'.format(name))

# Check that all bases are protocols.
for base in bases:
if not isinstance(base, ObjCProtocol):
raise TypeError(
'An Objective-C protocol can only extend ObjCProtocol objects, '
'not {cls.__module__}.{cls.__qualname__}'
.format(cls=type(base))
)

# Allocate the protocol object.
ptr = libobjc.objc_allocateProtocol(name)
if ptr is None:
raise RuntimeError('Protocol allocation failed')

# Adopt all the protocols.
for proto in bases:
libobjc.protocol_addProtocol(ptr, proto)

# Register all methods and properties.
for attr, obj in ns.items():
if hasattr(obj, 'protocol_register'):
obj.protocol_register(ptr, attr)

# Register the protocol object
libobjc.objc_registerProtocol(ptr)

return super().__new__(cls, ptr)

Expand Down
38 changes: 38 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,44 @@ def subtractOne_(cls, num: c_int) -> c_int:
self.assertEqual(simplemath.addOne_(254), 255)
self.assertEqual(SimpleMath.subtractOne_(75), 74)

def test_protocol_def_empty(self):
"""An empty ObjCProtocol can be defined."""

class EmptyProtocol(metaclass=ObjCProtocol):
pass

def test_protocol_def_methods(self):
"""An ObjCProtocol with method definitions can be defined."""

class ProtocolWithSomeMethods(metaclass=ObjCProtocol):
@objc_classmethod
def class_method(self, param) -> c_int:
pass

@objc_method
def instance_method(self, param) -> c_int:
pass

# TODO Test that the methods are actually defined

def test_protocol_def_property(self):
"""An ObjCProtocol with a property definition can be defined."""

class ProtocolWithAProperty(metaclass=ObjCProtocol):
prop = objc_property()

# TODO Test that the property is actually defined

def test_protocol_def_extends(self):
"""An ObjCProtocol that extends other protocols can be defined."""

ExampleProtocol = ObjCProtocol('ExampleProtocol')

class ProtocolExtendsProtocols(NSObjectProtocol, ExampleProtocol):
pass

self.assertSequenceEqual(ProtocolExtendsProtocols.protocols, [NSObjectProtocol, ExampleProtocol])

def test_function_NSEdgeInsetsMake(self):
"Python can invoke NSEdgeInsetsMake to create NSEdgeInsets."

Expand Down

0 comments on commit 38d1719

Please sign in to comment.