Skip to content

Commit

Permalink
Add basic ObjCProtocol class (beeware#69)
Browse files Browse the repository at this point in the history
Currently this only supports looking up and inspecting protocols, not
creating new ones.
  • Loading branch information
dgelessus committed Oct 6, 2017
1 parent f1502cb commit 55e7b87
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 $@

2 changes: 1 addition & 1 deletion rubicon/objc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
65 changes: 64 additions & 1 deletion rubicon/objc/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'ObjCMutableDictInstance',
'ObjCMutableListInstance',
'ObjCPartialMethod',
'ObjCProtocol',
'SEL',
'add_ivar',
'add_method',
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
4 changes: 3 additions & 1 deletion tests/objc/BaseExample.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import <Foundation/Foundation.h>

@interface BaseExample : NSObject {
#import "Protocols.h"

@interface BaseExample : NSObject <ExampleProtocol, DerivedProtocol> {
int _baseIntField;
}

Expand Down
9 changes: 9 additions & 0 deletions tests/objc/Protocols.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>

@protocol ExampleProtocol @end

@protocol BaseProtocolOne @end

@protocol BaseProtocolTwo @end

@protocol DerivedProtocol <BaseProtocolOne, BaseProtocolTwo> @end
67 changes: 66 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""

Expand All @@ -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."""

Expand All @@ -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")

Expand All @@ -158,13 +203,33 @@ 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")

self.assertEqual(Example.objc_class.superclass, BaseExample.objc_class)
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"

Expand Down

0 comments on commit 55e7b87

Please sign in to comment.