# objective-ctypes # # Copyright (c) 2011, Phillip Nguyen # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # Neither the name of objective-ctypes nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import sys import platform import struct from ctypes import * from ctypes import util from .cocoatypes import * __LP64__ = (8 * struct.calcsize("P") == 64) __i386__ = (platform.machine() == 'i386') __arm64__ = (platform.machine() == 'arm64') if sizeof(c_void_p) == 4: c_ptrdiff_t = c_int32 elif sizeof(c_void_p) == 8: c_ptrdiff_t = c_int64 ###################################################################### lib = util.find_library('objc') # Hack for compatibility with macOS > 11.0 if lib is None: lib = '/usr/lib/libobjc.dylib' objc = cdll.LoadLibrary(lib) ###################################################################### # BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types) objc.class_addIvar.restype = c_bool objc.class_addIvar.argtypes = [c_void_p, c_char_p, c_size_t, c_uint8, c_char_p] # BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) objc.class_addMethod.restype = c_bool # BOOL class_addProtocol(Class cls, Protocol *protocol) objc.class_addProtocol.restype = c_bool objc.class_addProtocol.argtypes = [c_void_p, c_void_p] # BOOL class_conformsToProtocol(Class cls, Protocol *protocol) objc.class_conformsToProtocol.restype = c_bool objc.class_conformsToProtocol.argtypes = [c_void_p, c_void_p] # Ivar * class_copyIvarList(Class cls, unsigned int *outCount) # Returns an array of pointers of type Ivar describing instance variables. # The array has *outCount pointers followed by a NULL terminator. # You must free() the returned array. objc.class_copyIvarList.restype = POINTER(c_void_p) objc.class_copyIvarList.argtypes = [c_void_p, POINTER(c_uint)] # Method * class_copyMethodList(Class cls, unsigned int *outCount) # Returns an array of pointers of type Method describing instance methods. # The array has *outCount pointers followed by a NULL terminator. # You must free() the returned array. objc.class_copyMethodList.restype = POINTER(c_void_p) objc.class_copyMethodList.argtypes = [c_void_p, POINTER(c_uint)] # objc_property_t * class_copyPropertyList(Class cls, unsigned int *outCount) # Returns an array of pointers of type objc_property_t describing properties. # The array has *outCount pointers followed by a NULL terminator. # You must free() the returned array. objc.class_copyPropertyList.restype = POINTER(c_void_p) objc.class_copyPropertyList.argtypes = [c_void_p, POINTER(c_uint)] # Protocol ** class_copyProtocolList(Class cls, unsigned int *outCount) # Returns an array of pointers of type Protocol* describing protocols. # The array has *outCount pointers followed by a NULL terminator. # You must free() the returned array. objc.class_copyProtocolList.restype = POINTER(c_void_p) objc.class_copyProtocolList.argtypes = [c_void_p, POINTER(c_uint)] # id class_createInstance(Class cls, size_t extraBytes) objc.class_createInstance.restype = c_void_p objc.class_createInstance.argtypes = [c_void_p, c_size_t] # Method class_getClassMethod(Class aClass, SEL aSelector) # Will also search superclass for implementations. objc.class_getClassMethod.restype = c_void_p objc.class_getClassMethod.argtypes = [c_void_p, c_void_p] # Ivar class_getClassVariable(Class cls, const char* name) objc.class_getClassVariable.restype = c_void_p objc.class_getClassVariable.argtypes = [c_void_p, c_char_p] # Method class_getInstanceMethod(Class aClass, SEL aSelector) # Will also search superclass for implementations. objc.class_getInstanceMethod.restype = c_void_p objc.class_getInstanceMethod.argtypes = [c_void_p, c_void_p] # size_t class_getInstanceSize(Class cls) objc.class_getInstanceSize.restype = c_size_t objc.class_getInstanceSize.argtypes = [c_void_p] # Ivar class_getInstanceVariable(Class cls, const char* name) objc.class_getInstanceVariable.restype = c_void_p objc.class_getInstanceVariable.argtypes = [c_void_p, c_char_p] # const char *class_getIvarLayout(Class cls) objc.class_getIvarLayout.restype = c_char_p objc.class_getIvarLayout.argtypes = [c_void_p] # IMP class_getMethodImplementation(Class cls, SEL name) objc.class_getMethodImplementation.restype = c_void_p objc.class_getMethodImplementation.argtypes = [c_void_p, c_void_p] # The function is marked as OBJC_ARM64_UNAVAILABLE. if not __arm64__: # IMP class_getMethodImplementation_stret(Class cls, SEL name) objc.class_getMethodImplementation_stret.restype = c_void_p objc.class_getMethodImplementation_stret.argtypes = [c_void_p, c_void_p] # const char * class_getName(Class cls) objc.class_getName.restype = c_char_p objc.class_getName.argtypes = [c_void_p] # objc_property_t class_getProperty(Class cls, const char *name) objc.class_getProperty.restype = c_void_p objc.class_getProperty.argtypes = [c_void_p, c_char_p] # Class class_getSuperclass(Class cls) objc.class_getSuperclass.restype = c_void_p objc.class_getSuperclass.argtypes = [c_void_p] # int class_getVersion(Class theClass) objc.class_getVersion.restype = c_int objc.class_getVersion.argtypes = [c_void_p] # const char *class_getWeakIvarLayout(Class cls) objc.class_getWeakIvarLayout.restype = c_char_p objc.class_getWeakIvarLayout.argtypes = [c_void_p] # BOOL class_isMetaClass(Class cls) objc.class_isMetaClass.restype = c_bool objc.class_isMetaClass.argtypes = [c_void_p] # IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) objc.class_replaceMethod.restype = c_void_p objc.class_replaceMethod.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p] # BOOL class_respondsToSelector(Class cls, SEL sel) objc.class_respondsToSelector.restype = c_bool objc.class_respondsToSelector.argtypes = [c_void_p, c_void_p] # void class_setIvarLayout(Class cls, const char *layout) objc.class_setIvarLayout.restype = None objc.class_setIvarLayout.argtypes = [c_void_p, c_char_p] # Class class_setSuperclass(Class cls, Class newSuper) objc.class_setSuperclass.restype = c_void_p objc.class_setSuperclass.argtypes = [c_void_p, c_void_p] # void class_setVersion(Class theClass, int version) objc.class_setVersion.restype = None objc.class_setVersion.argtypes = [c_void_p, c_int] # void class_setWeakIvarLayout(Class cls, const char *layout) objc.class_setWeakIvarLayout.restype = None objc.class_setWeakIvarLayout.argtypes = [c_void_p, c_char_p] ###################################################################### # const char * ivar_getName(Ivar ivar) objc.ivar_getName.restype = c_char_p objc.ivar_getName.argtypes = [c_void_p] # ptrdiff_t ivar_getOffset(Ivar ivar) objc.ivar_getOffset.restype = c_ptrdiff_t objc.ivar_getOffset.argtypes = [c_void_p] # const char * ivar_getTypeEncoding(Ivar ivar) objc.ivar_getTypeEncoding.restype = c_char_p objc.ivar_getTypeEncoding.argtypes = [c_void_p] ###################################################################### # char * method_copyArgumentType(Method method, unsigned int index) # You must free() the returned string. objc.method_copyArgumentType.restype = c_char_p objc.method_copyArgumentType.argtypes = [c_void_p, c_uint] # char * method_copyReturnType(Method method) # You must free() the returned string. objc.method_copyReturnType.restype = c_char_p objc.method_copyReturnType.argtypes = [c_void_p] # void method_exchangeImplementations(Method m1, Method m2) objc.method_exchangeImplementations.restype = None objc.method_exchangeImplementations.argtypes = [c_void_p, c_void_p] # void method_getArgumentType(Method method, unsigned int index, char *dst, size_t dst_len) # Functionally similar to strncpy(dst, parameter_type, dst_len). objc.method_getArgumentType.restype = None objc.method_getArgumentType.argtypes = [c_void_p, c_uint, c_char_p, c_size_t] # IMP method_getImplementation(Method method) objc.method_getImplementation.restype = c_void_p objc.method_getImplementation.argtypes = [c_void_p] # SEL method_getName(Method method) objc.method_getName.restype = c_void_p objc.method_getName.argtypes = [c_void_p] # unsigned method_getNumberOfArguments(Method method) objc.method_getNumberOfArguments.restype = c_uint objc.method_getNumberOfArguments.argtypes = [c_void_p] # void method_getReturnType(Method method, char *dst, size_t dst_len) # Functionally similar to strncpy(dst, return_type, dst_len) objc.method_getReturnType.restype = None objc.method_getReturnType.argtypes = [c_void_p, c_char_p, c_size_t] # const char * method_getTypeEncoding(Method method) objc.method_getTypeEncoding.restype = c_char_p objc.method_getTypeEncoding.argtypes = [c_void_p] # IMP method_setImplementation(Method method, IMP imp) objc.method_setImplementation.restype = c_void_p objc.method_setImplementation.argtypes = [c_void_p, c_void_p] ###################################################################### # Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) objc.objc_allocateClassPair.restype = c_void_p objc.objc_allocateClassPair.argtypes = [c_void_p, c_char_p, c_size_t] # Protocol **objc_copyProtocolList(unsigned int *outCount) # Returns an array of *outcount pointers followed by NULL terminator. # You must free() the array. objc.objc_copyProtocolList.restype = POINTER(c_void_p) objc.objc_copyProtocolList.argtypes = [POINTER(c_int)] # id objc_getAssociatedObject(id object, void *key) objc.objc_getAssociatedObject.restype = c_void_p objc.objc_getAssociatedObject.argtypes = [c_void_p, c_void_p] # id objc_getClass(const char *name) objc.objc_getClass.restype = c_void_p objc.objc_getClass.argtypes = [c_char_p] # int objc_getClassList(Class *buffer, int bufferLen) # Pass None for buffer to obtain just the total number of classes. objc.objc_getClassList.restype = c_int objc.objc_getClassList.argtypes = [c_void_p, c_int] # id objc_getMetaClass(const char *name) objc.objc_getMetaClass.restype = c_void_p objc.objc_getMetaClass.argtypes = [c_char_p] # Protocol *objc_getProtocol(const char *name) objc.objc_getProtocol.restype = c_void_p objc.objc_getProtocol.argtypes = [c_char_p] # You should set return and argument types depending on context. # id objc_msgSend(id theReceiver, SEL theSelector, ...) # id objc_msgSendSuper(struct objc_super *super, SEL op, ...) # The function is marked as OBJC_ARM64_UNAVAILABLE. if not __arm64__: # void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...) objc.objc_msgSendSuper_stret.restype = None # double objc_msgSend_fpret(id self, SEL op, ...) # objc.objc_msgSend_fpret.restype = c_double # The function is marked as OBJC_ARM64_UNAVAILABLE. if not __arm64__: # void objc_msgSend_stret(void * stretAddr, id theReceiver, SEL theSelector, ...) objc.objc_msgSend_stret.restype = None # void objc_registerClassPair(Class cls) objc.objc_registerClassPair.restype = None objc.objc_registerClassPair.argtypes = [c_void_p] # void objc_removeAssociatedObjects(id object) objc.objc_removeAssociatedObjects.restype = None objc.objc_removeAssociatedObjects.argtypes = [c_void_p] # void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) objc.objc_setAssociatedObject.restype = None objc.objc_setAssociatedObject.argtypes = [c_void_p, c_void_p, c_void_p, c_int] ###################################################################### # id object_copy(id obj, size_t size) objc.object_copy.restype = c_void_p objc.object_copy.argtypes = [c_void_p, c_size_t] # id object_dispose(id obj) objc.object_dispose.restype = c_void_p objc.object_dispose.argtypes = [c_void_p] # Class object_getClass(id object) objc.object_getClass.restype = c_void_p objc.object_getClass.argtypes = [c_void_p] # const char *object_getClassName(id obj) objc.object_getClassName.restype = c_char_p objc.object_getClassName.argtypes = [c_void_p] # Ivar object_getInstanceVariable(id obj, const char *name, void **outValue) objc.object_getInstanceVariable.restype = c_void_p objc.object_getInstanceVariable.argtypes = [c_void_p, c_char_p, c_void_p] # id object_getIvar(id object, Ivar ivar) objc.object_getIvar.restype = c_void_p objc.object_getIvar.argtypes = [c_void_p, c_void_p] # Class object_setClass(id object, Class cls) objc.object_setClass.restype = c_void_p objc.object_setClass.argtypes = [c_void_p, c_void_p] # Ivar object_setInstanceVariable(id obj, const char *name, void *value) # Set argtypes based on the data type of the instance variable. objc.object_setInstanceVariable.restype = c_void_p # void object_setIvar(id object, Ivar ivar, id value) objc.object_setIvar.restype = None objc.object_setIvar.argtypes = [c_void_p, c_void_p, c_void_p] ###################################################################### # const char *property_getAttributes(objc_property_t property) objc.property_getAttributes.restype = c_char_p objc.property_getAttributes.argtypes = [c_void_p] # const char *property_getName(objc_property_t property) objc.property_getName.restype = c_char_p objc.property_getName.argtypes = [c_void_p] ###################################################################### # BOOL protocol_conformsToProtocol(Protocol *proto, Protocol *other) objc.protocol_conformsToProtocol.restype = c_bool objc.protocol_conformsToProtocol.argtypes = [c_void_p, c_void_p] class OBJC_METHOD_DESCRIPTION(Structure): _fields_ = [("name", c_void_p), ("types", c_char_p)] # struct objc_method_description *protocol_copyMethodDescriptionList(Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount) # You must free() the returned array. objc.protocol_copyMethodDescriptionList.restype = POINTER(OBJC_METHOD_DESCRIPTION) objc.protocol_copyMethodDescriptionList.argtypes = [c_void_p, c_bool, c_bool, POINTER(c_uint)] # objc_property_t * protocol_copyPropertyList(Protocol *protocol, unsigned int *outCount) objc.protocol_copyPropertyList.restype = c_void_p objc.protocol_copyPropertyList.argtypes = [c_void_p, POINTER(c_uint)] # Protocol **protocol_copyProtocolList(Protocol *proto, unsigned int *outCount) objc.protocol_copyProtocolList = POINTER(c_void_p) objc.protocol_copyProtocolList.argtypes = [c_void_p, POINTER(c_uint)] # struct objc_method_description protocol_getMethodDescription(Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod) objc.protocol_getMethodDescription.restype = OBJC_METHOD_DESCRIPTION objc.protocol_getMethodDescription.argtypes = [c_void_p, c_void_p, c_bool, c_bool] # const char *protocol_getName(Protocol *p) objc.protocol_getName.restype = c_char_p objc.protocol_getName.argtypes = [c_void_p] ###################################################################### # const char* sel_getName(SEL aSelector) objc.sel_getName.restype = c_char_p objc.sel_getName.argtypes = [c_void_p] # SEL sel_getUid(const char *str) # Use sel_registerName instead. # BOOL sel_isEqual(SEL lhs, SEL rhs) objc.sel_isEqual.restype = c_bool objc.sel_isEqual.argtypes = [c_void_p, c_void_p] # SEL sel_registerName(const char *str) objc.sel_registerName.restype = c_void_p objc.sel_registerName.argtypes = [c_char_p] ###################################################################### def ensure_bytes(x): if isinstance(x, bytes): return x return x.encode('ascii') ###################################################################### def get_selector(name): return c_void_p(objc.sel_registerName(ensure_bytes(name))) def get_class(name): return c_void_p(objc.objc_getClass(ensure_bytes(name))) def get_object_class(obj): return c_void_p(objc.object_getClass(obj)) def get_metaclass(name): return c_void_p(objc.objc_getMetaClass(ensure_bytes(name))) def get_superclass_of_object(obj): cls = c_void_p(objc.object_getClass(obj)) return c_void_p(objc.class_getSuperclass(cls)) # http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html # http://www.x86-64.org/documentation/abi-0.99.pdf (pp.17-23) # executive summary: on x86-64, who knows? def x86_should_use_stret(restype): """Try to figure out when a return type will be passed on stack.""" if type(restype) != type(Structure): return False if not __LP64__ and sizeof(restype) <= 8: return False if __LP64__ and sizeof(restype) <= 16: # maybe? I don't know? return False return True # http://www.sealiesoftware.com/blog/archive/2008/11/16/objc_explain_objc_msgSend_fpret.html def should_use_fpret(restype): """Determine if objc_msgSend_fpret is required to return a floating point type.""" if not __i386__: # Unneeded on non-intel processors return False if __LP64__ and restype == c_longdouble: # Use only for long double on x86_64 return True if not __LP64__ and restype in (c_float, c_double, c_longdouble): return True return False # By default, assumes that restype is c_void_p # and that all arguments are wrapped inside c_void_p. # Use the restype and argtypes keyword arguments to # change these values. restype should be a ctypes type # and argtypes should be a list of ctypes types for # the arguments of the message only. def send_message(receiver, selName, *args, **kwargs): if isinstance(receiver, str): receiver = get_class(receiver) selector = get_selector(selName) restype = kwargs.get('restype', c_void_p) # print('send_message', receiver, selName, args, kwargs) argtypes = kwargs.get('argtypes', []) # Choose the correct version of objc_msgSend based on return type. if should_use_fpret(restype): objc.objc_msgSend_fpret.restype = restype objc.objc_msgSend_fpret.argtypes = [c_void_p, c_void_p] + argtypes result = objc.objc_msgSend_fpret(receiver, selector, *args) elif x86_should_use_stret(restype): objc.objc_msgSend_stret.argtypes = [POINTER(restype), c_void_p, c_void_p] + argtypes result = restype() objc.objc_msgSend_stret(byref(result), receiver, selector, *args) else: objc.objc_msgSend.restype = restype objc.objc_msgSend.argtypes = [c_void_p, c_void_p] + argtypes result = objc.objc_msgSend(receiver, selector, *args) if restype == c_void_p: result = c_void_p(result) return result class OBJC_SUPER(Structure): _fields_ = [('receiver', c_void_p), ('class', c_void_p)] OBJC_SUPER_PTR = POINTER(OBJC_SUPER) # http://stackoverflow.com/questions/3095360/what-exactly-is-super-in-objective-c # # `superclass_name` is optional and can be used to force finding the superclass # by name. It is used to circumvent a bug in which the superclass was resolved # incorrectly which lead to an infinite recursion: # https://github.com/pyglet/pyglet/issues/5 def send_super(receiver, selName, *args, superclass_name=None, **kwargs): if hasattr(receiver, '_as_parameter_'): receiver = receiver._as_parameter_ if superclass_name is None: superclass = get_superclass_of_object(receiver) else: superclass = get_class(superclass_name) super_struct = OBJC_SUPER(receiver, superclass) selector = get_selector(selName) restype = kwargs.get('restype', c_void_p) argtypes = kwargs.get('argtypes', None) objc.objc_msgSendSuper.restype = restype if argtypes: objc.objc_msgSendSuper.argtypes = [OBJC_SUPER_PTR, c_void_p] + argtypes else: objc.objc_msgSendSuper.argtypes = None result = objc.objc_msgSendSuper(byref(super_struct), selector, *args) if restype == c_void_p: result = c_void_p(result) return result ###################################################################### cfunctype_table = {} def parse_type_encoding(encoding): """Takes a type encoding string and outputs a list of the separated type codes. Currently does not handle unions or bitfields and strips out any field width specifiers or type specifiers from the encoding. For Python 3.2+, encoding is assumed to be a bytes object and not unicode. Examples: parse_type_encoding('^v16@0:8') --> ['^v', '@', ':'] parse_type_encoding('{CGSize=dd}40@0:8{CGSize=dd}16Q32') --> ['{CGSize=dd}', '@', ':', '{CGSize=dd}', 'Q'] """ type_encodings = [] brace_count = 0 # number of unclosed curly braces bracket_count = 0 # number of unclosed square brackets typecode = b'' for c in encoding: # In Python 3, c comes out as an integer in the range 0-255. In Python 2, c is a single character string. # To fix the disparity, we convert c to a bytes object if necessary. if isinstance(c, int): c = bytes([c]) if c == b'{': # Check if this marked the end of previous type code. if typecode and typecode[-1:] != b'^' and brace_count == 0 and bracket_count == 0: type_encodings.append(typecode) typecode = b'' typecode += c brace_count += 1 elif c == b'}': typecode += c brace_count -= 1 assert (brace_count >= 0) elif c == b'[': # Check if this marked the end of previous type code. if typecode and typecode[-1:] != b'^' and brace_count == 0 and bracket_count == 0: type_encodings.append(typecode) typecode = b'' typecode += c bracket_count += 1 elif c == b']': typecode += c bracket_count -= 1 assert (bracket_count >= 0) elif brace_count or bracket_count: # Anything encountered while inside braces or brackets gets stuck on. typecode += c elif c in b'0123456789': # Ignore field width specifiers for now. pass elif c in b'rnNoORV': # Also ignore type specifiers. pass elif c in b'^cislqCISLQfdBv*@#:b?': if typecode and typecode[-1:] == b'^': # Previous char was pointer specifier, so keep going. typecode += c else: # Add previous type code to the list. if typecode: type_encodings.append(typecode) # Start a new type code. typecode = c # Add the last type code to the list if typecode: type_encodings.append(typecode) return type_encodings # Limited to basic types and pointers to basic types. # Does not try to handle arrays, arbitrary structs, unions, or bitfields. # Assume that encoding is a bytes object and not unicode. def cfunctype_for_encoding(encoding): # Check if we've already created a CFUNCTYPE for this encoding. # If so, then return the cached CFUNCTYPE. if encoding in cfunctype_table: return cfunctype_table[encoding] # Otherwise, create a new CFUNCTYPE for the encoding. typecodes = {b'c': c_char, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'*': c_char_p, b'@': c_void_p, b'#': c_void_p, b':': c_void_p, NSPointEncoding: NSPoint, NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange, PyObjectEncoding: py_object} argtypes = [] for code in parse_type_encoding(encoding): if code in typecodes: argtypes.append(typecodes[code]) elif code[0:1] == b'^' and code[1:] in typecodes: argtypes.append(POINTER(typecodes[code[1:]])) else: raise Exception('unknown type encoding: ' + code) cfunctype = CFUNCTYPE(*argtypes) # Cache the new CFUNCTYPE in the cfunctype_table. # We do this mainly because it prevents the CFUNCTYPE # from being garbage-collected while we need it. cfunctype_table[encoding] = cfunctype return cfunctype ###################################################################### # After calling create_subclass, you must first register # it with register_subclass before you may use it. # You can add new methods after the class is registered, # but you cannot add any new ivars. def create_subclass(superclass, name): if isinstance(superclass, str): superclass = get_class(superclass) return c_void_p(objc.objc_allocateClassPair(superclass, ensure_bytes(name), 0)) def register_subclass(subclass): objc.objc_registerClassPair(subclass) # types is a string encoding the argument types of the method. # The first type code of types is the return type (e.g. 'v' if void) # The second type code must be '@' for id self. # The third type code must be ':' for SEL cmd. # Additional type codes are for types of other arguments if any. def add_method(cls, selName, method, types): type_encodings = parse_type_encoding(types) assert (type_encodings[1] == b'@') # ensure id self typecode assert (type_encodings[2] == b':') # ensure SEL cmd typecode selector = get_selector(selName) cfunctype = cfunctype_for_encoding(types) imp = cfunctype(method) objc.class_addMethod.argtypes = [c_void_p, c_void_p, cfunctype, c_char_p] objc.class_addMethod(cls, selector, imp, types) return imp def add_ivar(cls, name, vartype): return objc.class_addIvar(cls, ensure_bytes(name), sizeof(vartype), alignment(vartype), encoding_for_ctype(vartype)) def set_instance_variable(obj, varname, value, vartype): objc.object_setInstanceVariable.argtypes = [c_void_p, c_char_p, vartype] objc.object_setInstanceVariable(obj, ensure_bytes(varname), value) def get_instance_variable(obj, varname, vartype): variable = vartype() objc.object_getInstanceVariable(obj, ensure_bytes(varname), byref(variable)) return variable.value ###################################################################### class ObjCMethod: """This represents an unbound Objective-C method (really an IMP).""" # Note, need to map 'c' to c_byte rather than c_char, because otherwise # ctypes converts the value into a one-character string which is generally # not what we want at all, especially when the 'c' represents a bool var. typecodes = {b'c': c_byte, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'Vv': None, b'*': c_char_p, b'@': c_void_p, b'#': c_void_p, b':': c_void_p, b'^v': c_void_p, b'?': c_void_p, NSPointEncoding: NSPoint, NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange, PyObjectEncoding: py_object} cfunctype_table = {} def __init__(self, method): """Initialize with an Objective-C Method pointer. We then determine the return type and argument type information of the method.""" self.selector = c_void_p(objc.method_getName(method)) self.name = objc.sel_getName(self.selector) self.pyname = self.name.replace(b':', b'_') self.encoding = objc.method_getTypeEncoding(method) self.return_type = objc.method_copyReturnType(method) self.nargs = objc.method_getNumberOfArguments(method) self.imp = c_void_p(objc.method_getImplementation(method)) self.argument_types = [] for i in range(self.nargs): buffer = c_buffer(512) objc.method_getArgumentType(method, i, buffer, len(buffer)) self.argument_types.append(buffer.value) # Get types for all the arguments. try: self.argtypes = [self.ctype_for_encoding(t) for t in self.argument_types] except: # print(f'no argtypes encoding for {self.name} ({self.argument_types})') self.argtypes = None # Get types for the return type. try: if self.return_type == b'@': self.restype = ObjCInstance elif self.return_type == b'#': self.restype = ObjCClass else: self.restype = self.ctype_for_encoding(self.return_type) except: # print(f'no restype encoding for {self.name} ({self.return_type})') self.restype = None self.func = None def ctype_for_encoding(self, encoding): """Return ctypes type for an encoded Objective-C type.""" if encoding in self.typecodes: return self.typecodes[encoding] elif encoding[0:1] == b'^' and encoding[1:] in self.typecodes: return POINTER(self.typecodes[encoding[1:]]) elif encoding[0:1] == b'^' and encoding[1:] in [CGImageEncoding, NSZoneEncoding]: # special cases return c_void_p elif encoding[0:1] == b'r' and encoding[1:] in self.typecodes: # const decorator, don't care return self.typecodes[encoding[1:]] elif encoding[0:2] == b'r^' and encoding[2:] in self.typecodes: # const pointer, also don't care return POINTER(self.typecodes[encoding[2:]]) else: raise Exception('unknown encoding for %s: %s' % (self.name, encoding)) def get_prototype(self): """Returns a ctypes CFUNCTYPE for the method.""" if self.restype == ObjCInstance or self.restype == ObjCClass: # Some hacky stuff to get around ctypes issues on 64-bit. Can't let # ctypes convert the return value itself, because it truncates the pointer # along the way. So instead, we must do set the return type to c_void_p to # ensure we get 64-bit addresses and then convert the return value manually. self.prototype = CFUNCTYPE(c_void_p, *self.argtypes) else: self.prototype = CFUNCTYPE(self.restype, *self.argtypes) return self.prototype def __repr__(self): return "" % (self.name, self.encoding) def get_callable(self): """Returns a python-callable version of the method's IMP.""" if not self.func: prototype = self.get_prototype() self.func = cast(self.imp, prototype) if self.restype == ObjCInstance or self.restype == ObjCClass: self.func.restype = c_void_p else: self.func.restype = self.restype self.func.argtypes = self.argtypes return self.func def __call__(self, objc_id, *args): """Call the method with the given id and arguments. You do not need to pass in the selector as an argument since it will be automatically provided.""" f = self.get_callable() try: result = f(objc_id, self.selector, *args) # Convert result to python type if it is a instance or class pointer. if self.restype == ObjCInstance: result = ObjCInstance(result) elif self.restype == ObjCClass: result = ObjCClass(result) return result except ArgumentError as error: # Add more useful info to argument error exceptions, then reraise. error.args += ('selector = ' + str(self.name), 'argtypes =' + str(self.argtypes), 'encoding = ' + str(self.encoding)) raise ###################################################################### class ObjCBoundMethod: """This represents an Objective-C method (an IMP) which has been bound to some id which will be passed as the first parameter to the method.""" def __init__(self, method, objc_id): """Initialize with a method and ObjCInstance or ObjCClass object.""" self.method = method self.objc_id = objc_id def __repr__(self): return '' % (self.method.name, self.objc_id) def __call__(self, *args): """Call the method with the given arguments.""" return self.method(self.objc_id, *args) ###################################################################### class ObjCClass: """Python wrapper for an Objective-C class.""" # We only create one Python object for each Objective-C class. # Any future calls with the same class will return the previously # created Python object. Note that these aren't weak references. # After you create an ObjCClass, it will exist until the end of the # program. _registered_classes = {} def __new__(cls, class_name_or_ptr): """Create a new ObjCClass instance or return a previously created instance for the given Objective-C class. The argument may be either the name of the class to retrieve, or a pointer to the class.""" # Determine name and ptr values from passed in argument. if isinstance(class_name_or_ptr, str): name = class_name_or_ptr ptr = get_class(name) else: ptr = class_name_or_ptr # Make sure that ptr value is wrapped in c_void_p object # for safety when passing as ctypes argument. if not isinstance(ptr, c_void_p): ptr = c_void_p(ptr) name = objc.class_getName(ptr) # Check if we've already created a Python object for this class # and if so, return it rather than making a new one. if name in cls._registered_classes: return cls._registered_classes[name] # Otherwise create a new Python object and then initialize it. objc_class = super(ObjCClass, cls).__new__(cls) objc_class.ptr = ptr objc_class.name = name objc_class.instance_methods = {} # mapping of name -> instance method objc_class.class_methods = {} # mapping of name -> class method objc_class._as_parameter_ = ptr # for ctypes argument passing # Store the new class in dictionary of registered classes. cls._registered_classes[name] = objc_class # Not sure this is necessary... objc_class.cache_instance_methods() objc_class.cache_class_methods() return objc_class def __repr__(self): return "" % (self.name, str(self.ptr.value)) def cache_instance_methods(self): """Create and store python representations of all instance methods implemented by this class (but does not find methods of superclass).""" count = c_uint() method_array = objc.class_copyMethodList(self.ptr, byref(count)) for i in range(count.value): method = c_void_p(method_array[i]) objc_method = ObjCMethod(method) self.instance_methods[objc_method.pyname] = objc_method def cache_class_methods(self): """Create and store python representations of all class methods implemented by this class (but does not find methods of superclass).""" count = c_uint() method_array = objc.class_copyMethodList(objc.object_getClass(self.ptr), byref(count)) for i in range(count.value): method = c_void_p(method_array[i]) objc_method = ObjCMethod(method) self.class_methods[objc_method.pyname] = objc_method def get_instance_method(self, name): """Returns a python representation of the named instance method, either by looking it up in the cached list of methods or by searching for and creating a new method object.""" if name in self.instance_methods: return self.instance_methods[name] else: # If method name isn't in the cached list, it might be a method of # the superclass, so call class_getInstanceMethod to check. selector = get_selector(name.replace(b'_', b':')) method = c_void_p(objc.class_getInstanceMethod(self.ptr, selector)) if method.value: objc_method = ObjCMethod(method) self.instance_methods[name] = objc_method return objc_method return None def get_class_method(self, name): """Returns a python representation of the named class method, either by looking it up in the cached list of methods or by searching for and creating a new method object.""" if name in self.class_methods: return self.class_methods[name] else: # If method name isn't in the cached list, it might be a method of # the superclass, so call class_getInstanceMethod to check. selector = get_selector(name.replace(b'_', b':')) method = c_void_p(objc.class_getClassMethod(self.ptr, selector)) if method.value: objc_method = ObjCMethod(method) self.class_methods[name] = objc_method return objc_method return None def __getattr__(self, name): """Returns a callable method object with the given name.""" # If name refers to a class method, then return a callable object # for the class method with self.ptr as hidden first parameter. name = ensure_bytes(name) method = self.get_class_method(name) if method: return ObjCBoundMethod(method, self.ptr) # If name refers to an instance method, then simply return the method. # The caller will need to supply an instance as the first parameter. method = self.get_instance_method(name) if method: return method # Otherwise, raise an exception. raise AttributeError('ObjCClass %s has no attribute %s' % (self.name, name)) ###################################################################### class ObjCInstance: """Python wrapper for an Objective-C instance.""" _cached_objects = {} def __new__(cls, object_ptr): """Create a new ObjCInstance or return a previously created one for the given object_ptr which should be an Objective-C id.""" # Make sure that object_ptr is wrapped in a c_void_p. if not isinstance(object_ptr, c_void_p): object_ptr = c_void_p(object_ptr) # If given a nil pointer, return None. if not object_ptr.value: return None # Check if we've already created an python ObjCInstance for this # object_ptr id and if so, then return it. A single ObjCInstance will # be created for any object pointer when it is first encountered. # This same ObjCInstance will then persist until the object is # deallocated. if object_ptr.value in cls._cached_objects: return cls._cached_objects[object_ptr.value] # Otherwise, create a new ObjCInstance. objc_instance = super(ObjCInstance, cls).__new__(cls) objc_instance.ptr = object_ptr objc_instance._as_parameter_ = object_ptr # Determine class of this object. class_ptr = c_void_p(objc.object_getClass(object_ptr)) objc_instance.objc_class = ObjCClass(class_ptr) # Store new object in the dictionary of cached objects, keyed # by the (integer) memory address pointed to by the object_ptr. cls._cached_objects[object_ptr.value] = objc_instance # Create a DeallocationObserver and associate it with this object. # When the Objective-C object is deallocated, the observer will remove # the ObjCInstance corresponding to the object from the cached objects # dictionary, effectively destroying the ObjCInstance. observer = send_message(send_message('DeallocationObserver', 'alloc'), 'initWithObject:', objc_instance) objc.objc_setAssociatedObject(objc_instance, observer, observer, 0x301) # The observer is retained by the object we associate it to. We release # the observer now so that it will be deallocated when the associated # object is deallocated. send_message(observer, 'release') return objc_instance def __repr__(self): if self.objc_class.name == b'NSCFString': # Display contents of NSString objects from .cocoalibs import cfstring_to_string string = cfstring_to_string(self) return "" % (id(self), self.objc_class.name, string, str(self.ptr.value)) return "" % (id(self), self.objc_class.name, str(self.ptr.value)) def __getattr__(self, name): """Returns a callable method object with the given name.""" # Search for named instance method in the class object and if it # exists, return callable object with self as hidden argument. # Note: you should give self and not self.ptr as a parameter to # ObjCBoundMethod, so that it will be able to keep the ObjCInstance # alive for chained calls like MyClass.alloc().init() where the # object created by alloc() is not assigned to a variable. name = ensure_bytes(name) method = self.objc_class.get_instance_method(name) if method: return ObjCBoundMethod(method, self) # Else, search for class method with given name in the class object. # If it exists, return callable object with a pointer to the class # as a hidden argument. method = self.objc_class.get_class_method(name) if method: return ObjCBoundMethod(method, self.objc_class.ptr) # Otherwise raise an exception. raise AttributeError('ObjCInstance %s has no attribute %s' % (self.objc_class.name, name)) ###################################################################### def convert_method_arguments(encoding, args): """Used by ObjCSubclass to convert Objective-C method arguments to Python values before passing them on to the Python-defined method.""" new_args = [] arg_encodings = parse_type_encoding(encoding)[3:] for e, a in zip(arg_encodings, args): if e == b'@': new_args.append(ObjCInstance(a)) elif e == b'#': new_args.append(ObjCClass(a)) else: new_args.append(a) return new_args # ObjCSubclass is used to define an Objective-C subclass of an existing # class registered with the runtime. When you create an instance of # ObjCSubclass, it registers the new subclass with the Objective-C # runtime and creates a set of function decorators that you can use to # add instance methods or class methods to the subclass. # # Typical usage would be to first create and register the subclass: # # MySubclass = ObjCSubclass('NSObject', 'MySubclassName') # # then add methods with: # # @MySubclass.method('v') # def methodThatReturnsVoid(self): # pass # # @MySubclass.method('Bi') # def boolReturningMethodWithInt_(self, x): # return True # # @MySubclass.classmethod('@') # def classMethodThatReturnsId(self): # return self # # It is probably a good idea to organize the code related to a single # subclass by either putting it in its own module (note that you don't # actually need to expose any of the method names or the ObjCSubclass) # or by bundling it all up inside a python class definition, perhaps # called MySubclassImplementation. # # It is also possible to add Objective-C ivars to the subclass, however # if you do so, you must call the __init__ method with register=False, # and then call the register method after the ivars have been added. # But rather than creating the ivars in Objective-C land, it is easier # to just define python-based instance variables in your subclass's init # method. # # This class is used only to *define* the interface and implementation # of an Objective-C subclass from python. It should not be used in # any other way. If you want a python representation of the resulting # class, create it with ObjCClass. # # Instances are created as a pointer to the objc object by using: # # myinstance = send_message('MySubclassName', 'alloc') # myinstance = send_message(myinstance, 'init') # # or wrapped inside an ObjCInstance object by using: # # myclass = ObjCClass('MySubclassName') # myinstance = myclass.alloc().init() # class ObjCSubclass: """Use this to create a subclass of an existing Objective-C class. It consists primarily of function decorators which you use to add methods to the subclass.""" def __init__(self, superclass, name, register=True): self._imp_table = {} self.name = name self.objc_cls = create_subclass(superclass, name) self._as_parameter_ = self.objc_cls if register: self.register() def register(self): """Register the new class with the Objective-C runtime.""" objc.objc_registerClassPair(self.objc_cls) # We can get the metaclass only after the class is registered. self.objc_metaclass = get_metaclass(self.name) def add_ivar(self, varname, vartype): """Add instance variable named varname to the subclass. varname should be a string. vartype is a ctypes type. The class must be registered AFTER adding instance variables.""" return add_ivar(self.objc_cls, varname, vartype) def add_method(self, method, name, encoding): imp = add_method(self.objc_cls, name, method, encoding) self._imp_table[name] = imp # http://iphonedevelopment.blogspot.com/2008/08/dynamically-adding-class-objects.html def add_class_method(self, method, name, encoding): imp = add_method(self.objc_metaclass, name, method, encoding) self._imp_table[name] = imp def rawmethod(self, encoding): """Decorator for instance methods without any fancy shenanigans. The function must have the signature f(self, cmd, *args) where both self and cmd are just pointers to objc objects.""" # Add encodings for hidden self and cmd arguments. encoding = ensure_bytes(encoding) typecodes = parse_type_encoding(encoding) typecodes.insert(1, b'@:') encoding = b''.join(typecodes) def decorator(f): name = f.__name__.replace('_', ':') self.add_method(f, name, encoding) return f return decorator def method(self, encoding): """Function decorator for instance methods.""" # Add encodings for hidden self and cmd arguments. encoding = ensure_bytes(encoding) typecodes = parse_type_encoding(encoding) typecodes.insert(1, b'@:') encoding = b''.join(typecodes) def decorator(f): def objc_method(objc_self, objc_cmd, *args): py_self = ObjCInstance(objc_self) py_self.objc_cmd = objc_cmd args = convert_method_arguments(encoding, args) result = f(py_self, *args) if isinstance(result, ObjCClass): result = result.ptr.value elif isinstance(result, ObjCInstance): result = result.ptr.value return result name = f.__name__.replace('_', ':') self.add_method(objc_method, name, encoding) return objc_method return decorator def classmethod(self, encoding): """Function decorator for class methods.""" # Add encodings for hidden self and cmd arguments. encoding = ensure_bytes(encoding) typecodes = parse_type_encoding(encoding) typecodes.insert(1, b'@:') encoding = b''.join(typecodes) def decorator(f): def objc_class_method(objc_cls, objc_cmd, *args): py_cls = ObjCClass(objc_cls) py_cls.objc_cmd = objc_cmd args = convert_method_arguments(encoding, args) result = f(py_cls, *args) if isinstance(result, ObjCClass): result = result.ptr.value elif isinstance(result, ObjCInstance): result = result.ptr.value return result name = f.__name__.replace('_', ':') self.add_class_method(objc_class_method, name, encoding) return objc_class_method return decorator ###################################################################### # Instances of DeallocationObserver are associated with every # Objective-C object that gets wrapped inside an ObjCInstance. # Their sole purpose is to watch for when the Objective-C object # is deallocated, and then remove the object from the dictionary # of cached ObjCInstance objects kept by the ObjCInstance class. # # The methods of the class defined below are decorated with # rawmethod() instead of method() because DeallocationObservers # are created inside of ObjCInstance's __new__ method and we have # to be careful to not create another ObjCInstance here (which # happens when the usual method decorator turns the self argument # into an ObjCInstance), or else get trapped in an infinite recursion. class DeallocationObserver_Implementation: DeallocationObserver = ObjCSubclass('NSObject', 'DeallocationObserver', register=False) DeallocationObserver.add_ivar('observed_object', c_void_p) DeallocationObserver.register() @DeallocationObserver.rawmethod('@@') def initWithObject_(self, cmd, anObject): self = send_super(self, 'init') self = self.value set_instance_variable(self, 'observed_object', anObject, c_void_p) return self @DeallocationObserver.rawmethod('v') def dealloc(self, cmd): anObject = get_instance_variable(self, 'observed_object', c_void_p) ObjCInstance._cached_objects.pop(anObject, None) send_super(self, 'dealloc') @DeallocationObserver.rawmethod('v') def finalize(self, cmd): # Called instead of dealloc if using garbage collection. # (which would have to be explicitly started with # objc_startCollectorThread(), so probably not too much reason # to have this here, but I guess it can't hurt.) anObject = get_instance_variable(self, 'observed_object', c_void_p) ObjCInstance._cached_objects.pop(anObject, None) send_super(self, 'finalize')