import os import warnings from contextlib import contextmanager from ctypes import ( CDLL, CFUNCTYPE, POINTER, ArgumentError, Structure, Union, addressof, alignment, byref, c_bool, c_char_p, c_double, c_float, c_int, c_longdouble, c_size_t, c_uint, c_uint8, c_void_p, cast, memmove, sizeof, util, ) from . import ctypes_patch from .types import ( __arm__, __i386__, __x86_64__, c_ptrdiff_t, ctype_for_encoding, ctype_for_type, encoding_for_ctype, with_encoding, with_preferred_encoding, ) __all__ = [ "Class", "Foundation", "IMP", "Ivar", "Method", "SEL", "add_ivar", "add_method", "autoreleasepool", "get_class", "get_ivar", "libc", "libobjc", "load_library", "objc_block", "objc_id", "objc_method_description", "objc_property_t", "objc_super", "object_isClass", "send_message", "send_super", "set_ivar", "should_use_fpret", "should_use_stret", ] ###################################################################### _lib_path = ["/usr/lib"] _framework_path = ["/System/Library/Frameworks"] def load_library(name): """Load and return the C library with the given name. If the library could not be found, a :class:`ValueError` is raised. Internally, this function uses :func:`ctypes.util.find_library` to search for the library in the system-standard locations. If the library cannot be found this way, it is attempted to load the library from certain hard-coded locations, as a fallback for systems where ``find_library`` does not work (such as iOS). """ path = util.find_library(name) if path is not None: return CDLL(path) # On iOS (and probably also watchOS and tvOS), ctypes.util.find_library # doesn't work and always returns None. This is because the sandbox hides # all system libraries from the filesystem and pretends they don't exist. # However, they can still be loaded if the path is known, so we try to load # the library from a few known locations. for loc in _lib_path: try: return CDLL(os.path.join(loc, "lib" + name + ".dylib")) except OSError: pass for loc in _framework_path: try: return CDLL(os.path.join(loc, name + ".framework", name)) except OSError: pass raise ValueError(f"Library {name!r} not found") libc = load_library("c") libobjc = load_library("objc") Foundation = load_library("Foundation") @with_encoding(b"@") class objc_id(c_void_p): """The `id `__ type from ````. """ @with_encoding(b"@?") class objc_block(objc_id): """The low-level type of block pointers. This type tells Rubicon's internals that the object in question is a block and not just a regular Objective-C object, which affects method argument and return value conversions. For more details, see :ref:`objc_blocks`. .. note:: This type does not correspond to an actual C type or Objective-C class. Although the internal structure of block objects is documented, as well as the fact that they are Objective-C objects, they do not have a documented type or class name and are not fully defined in any header file. Aside from the special conversion behavior, this type is equivalent to :class:`objc_id`. """ @with_preferred_encoding(b":") class SEL(c_void_p): """The `SEL `__ type from ````. """ @property def name(self): """The selector's name as :class:`bytes`.""" if self.value is None: raise ValueError("Cannot get name of null selector") return libobjc.sel_getName(self) def __new__(cls, init=None): """The constructor can be called with a :class:`bytes` or :class:`str` object to obtain a selector with that value. (The normal arguments supported by :class:`~ctypes.c_void_p` are still accepted.) """ if isinstance(init, (bytes, str)): self = libobjc.sel_registerName(ensure_bytes(init)) self._inited = True return self else: self = super().__new__(cls, init) self._inited = False return self def __init__(self, init=None): if not self._inited: super().__init__(init) def __repr__(self): return "{cls.__module__}.{cls.__qualname__}({name!r})".format( cls=type(self), name=None if self.value is None else self.name ) @with_preferred_encoding(b"#") class Class(objc_id): """The `Class `__ type from ````. """ class IMP(c_void_p): """The `IMP `__ type from ````. An :class:`IMP` cannot be called directly --- it must be cast to the correct :func:`~ctypes.CFUNCTYPE` first, to provide the necessary information about its signature. """ class Method(c_void_p): """The `Method `__ type from ````. """ class Ivar(c_void_p): """The `Ivar `__ type from ````. """ class objc_property_t(c_void_p): """The `objc_property_t `__ type from ````. """ class objc_property_attribute_t(Structure): """The `objc_property_attribute_t `__ structure from ````. """ # noqa: E501 _fields_ = [ ("name", c_char_p), ("value", c_char_p), ] ###################################################################### # void free(void *) libc.free.restype = None libc.free.argtypes = [c_void_p] # BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types) libobjc.class_addIvar.restype = c_bool libobjc.class_addIvar.argtypes = [Class, c_char_p, c_size_t, c_uint8, c_char_p] # BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) libobjc.class_addMethod.restype = c_bool libobjc.class_addMethod.argtypes = [Class, SEL, IMP, c_char_p] # BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, # unsigned int attributeCount) libobjc.class_addProperty.restype = c_bool libobjc.class_addProperty.argtypes = [ Class, c_char_p, POINTER(objc_property_attribute_t), c_uint, ] # BOOL class_addProtocol(Class cls, Protocol *protocol) libobjc.class_addProtocol.restype = c_bool libobjc.class_addProtocol.argtypes = [Class, objc_id] # BOOL class_conformsToProtocol(Class cls, Protocol *protocol) libobjc.class_conformsToProtocol.restype = c_bool libobjc.class_conformsToProtocol.argtypes = [Class, objc_id] # 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. libobjc.class_copyIvarList.restype = POINTER(Ivar) libobjc.class_copyIvarList.argtypes = [Class, 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. libobjc.class_copyMethodList.restype = POINTER(Method) libobjc.class_copyMethodList.argtypes = [Class, 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. libobjc.class_copyPropertyList.restype = POINTER(objc_property_t) libobjc.class_copyPropertyList.argtypes = [Class, 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. libobjc.class_copyProtocolList.restype = POINTER(objc_id) libobjc.class_copyProtocolList.argtypes = [Class, POINTER(c_uint)] # Method class_getClassMethod(Class aClass, SEL aSelector) # Will also search superclass for implementations. libobjc.class_getClassMethod.restype = Method libobjc.class_getClassMethod.argtypes = [Class, SEL] # Ivar class_getClassVariable(Class cls, const char* name) libobjc.class_getClassVariable.restype = Ivar libobjc.class_getClassVariable.argtypes = [Class, c_char_p] # Method class_getInstanceMethod(Class aClass, SEL aSelector) # Will also search superclass for implementations. libobjc.class_getInstanceMethod.restype = Method libobjc.class_getInstanceMethod.argtypes = [Class, SEL] # size_t class_getInstanceSize(Class cls) libobjc.class_getInstanceSize.restype = c_size_t libobjc.class_getInstanceSize.argtypes = [Class] # Ivar class_getInstanceVariable(Class cls, const char* name) libobjc.class_getInstanceVariable.restype = Ivar libobjc.class_getInstanceVariable.argtypes = [Class, c_char_p] # const char *class_getIvarLayout(Class cls) libobjc.class_getIvarLayout.restype = c_char_p libobjc.class_getIvarLayout.argtypes = [Class] # IMP class_getMethodImplementation(Class cls, SEL name) libobjc.class_getMethodImplementation.restype = IMP libobjc.class_getMethodImplementation.argtypes = [Class, SEL] # const char * class_getName(Class cls) libobjc.class_getName.restype = c_char_p libobjc.class_getName.argtypes = [Class] # objc_property_t class_getProperty(Class cls, const char *name) libobjc.class_getProperty.restype = objc_property_t libobjc.class_getProperty.argtypes = [Class, c_char_p] # Class class_getSuperclass(Class cls) libobjc.class_getSuperclass.restype = Class libobjc.class_getSuperclass.argtypes = [Class] # int class_getVersion(Class theClass) libobjc.class_getVersion.restype = c_int libobjc.class_getVersion.argtypes = [Class] # const char *class_getWeakIvarLayout(Class cls) libobjc.class_getWeakIvarLayout.restype = c_char_p libobjc.class_getWeakIvarLayout.argtypes = [Class] # BOOL class_isMetaClass(Class cls) libobjc.class_isMetaClass.restype = c_bool libobjc.class_isMetaClass.argtypes = [Class] # IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) libobjc.class_replaceMethod.restype = IMP libobjc.class_replaceMethod.argtypes = [Class, SEL, IMP, c_char_p] # BOOL class_respondsToSelector(Class cls, SEL sel) libobjc.class_respondsToSelector.restype = c_bool libobjc.class_respondsToSelector.argtypes = [Class, SEL] # void class_setIvarLayout(Class cls, const char *layout) libobjc.class_setIvarLayout.restype = None libobjc.class_setIvarLayout.argtypes = [Class, c_char_p] # void class_setVersion(Class theClass, int version) libobjc.class_setVersion.restype = None libobjc.class_setVersion.argtypes = [Class, c_int] # void class_setWeakIvarLayout(Class cls, const char *layout) libobjc.class_setWeakIvarLayout.restype = None libobjc.class_setWeakIvarLayout.argtypes = [Class, c_char_p] ###################################################################### # const char * ivar_getName(Ivar ivar) libobjc.ivar_getName.restype = c_char_p libobjc.ivar_getName.argtypes = [Ivar] # ptrdiff_t ivar_getOffset(Ivar ivar) libobjc.ivar_getOffset.restype = c_ptrdiff_t libobjc.ivar_getOffset.argtypes = [Ivar] # const char * ivar_getTypeEncoding(Ivar ivar) libobjc.ivar_getTypeEncoding.restype = c_char_p libobjc.ivar_getTypeEncoding.argtypes = [Ivar] ###################################################################### # void method_exchangeImplementations(Method m1, Method m2) libobjc.method_exchangeImplementations.restype = None libobjc.method_exchangeImplementations.argtypes = [Method, Method] # IMP method_getImplementation(Method method) libobjc.method_getImplementation.restype = IMP libobjc.method_getImplementation.argtypes = [Method] # SEL method_getName(Method method) libobjc.method_getName.restype = SEL libobjc.method_getName.argtypes = [Method] # const char * method_getTypeEncoding(Method method) libobjc.method_getTypeEncoding.restype = c_char_p libobjc.method_getTypeEncoding.argtypes = [Method] # IMP method_setImplementation(Method method, IMP imp) libobjc.method_setImplementation.restype = IMP libobjc.method_setImplementation.argtypes = [Method, IMP] ###################################################################### # Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) libobjc.objc_allocateClassPair.restype = Class libobjc.objc_allocateClassPair.argtypes = [Class, c_char_p, c_size_t] # void *objc_autoreleasePoolPush(void) libobjc.objc_autoreleasePoolPush.restype = c_void_p libobjc.objc_autoreleasePoolPush.argtypes = [] # void objc_autoreleasePoolPop(void *pool) libobjc.objc_autoreleasePoolPop.restype = None libobjc.objc_autoreleasePoolPop.argtypes = [c_void_p] # id objc_autoreleaseReturnValue(id value) libobjc.objc_autoreleaseReturnValue.restype = objc_id libobjc.objc_autoreleaseReturnValue.argtypes = [objc_id] # Protocol **objc_copyProtocolList(unsigned int *outCount) # Returns an array of *outcount pointers followed by NULL terminator. # You must free() the array. libobjc.objc_copyProtocolList.restype = POINTER(objc_id) libobjc.objc_copyProtocolList.argtypes = [POINTER(c_int)] # id objc_getAssociatedObject(id object, void *key) libobjc.objc_getAssociatedObject.restype = objc_id libobjc.objc_getAssociatedObject.argtypes = [objc_id, c_void_p] # Class objc_getClass(const char *name) libobjc.objc_getClass.restype = Class libobjc.objc_getClass.argtypes = [c_char_p] # Class objc_getMetaClass(const char *name) libobjc.objc_getMetaClass.restype = Class libobjc.objc_getMetaClass.argtypes = [c_char_p] # Protocol *objc_getProtocol(const char *name) libobjc.objc_getProtocol.restype = objc_id libobjc.objc_getProtocol.argtypes = [c_char_p] # id objc_loadWeakRetained(id *object) libobjc.objc_loadWeakRetained.restype = objc_id libobjc.objc_loadWeakRetained.argtypes = [c_void_p] # id objc_storeWeak(id *object, id value) libobjc.objc_storeWeak.restype = objc_id libobjc.objc_storeWeak.argtypes = [c_void_p, objc_id] # 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 _stret variants only exist on x86-based architectures and ARM32. if __i386__ or __x86_64__ or __arm__: # void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...) libobjc.objc_msgSendSuper_stret.restype = None # void objc_msgSend_stret(void * stretAddr, id theReceiver, SEL theSelector, ...) libobjc.objc_msgSend_stret.restype = None # The _fpret variant only exists on x86-based architectures. if __i386__ or __x86_64__: # double objc_msgSend_fpret(id self, SEL op, ...) libobjc.objc_msgSend_fpret.restype = c_double # void objc_registerClassPair(Class cls) libobjc.objc_registerClassPair.restype = None libobjc.objc_registerClassPair.argtypes = [Class] # void objc_removeAssociatedObjects(id object) libobjc.objc_removeAssociatedObjects.restype = None libobjc.objc_removeAssociatedObjects.argtypes = [objc_id] # void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) libobjc.objc_setAssociatedObject.restype = None libobjc.objc_setAssociatedObject.argtypes = [objc_id, c_void_p, objc_id, c_int] ###################################################################### # Class object_getClass(id object) libobjc.object_getClass.restype = Class libobjc.object_getClass.argtypes = [objc_id] # object_isClass exists as a native function only since OS X 10.10 and iOS 8. # If unavailable, we emulate it: an object is a class iff its class is a metaclass. try: object_isClass = libobjc.object_isClass except AttributeError: def object_isClass(obj): """Return whether the given Objective-C object is a class (or a metaclass). This is the emulated version of the object_isClass runtime function, for systems older than OS X 10.10 or iOS 8, where the real function doesn't exist yet. """ return libobjc.class_isMetaClass(libobjc.object_getClass(obj)) else: # BOOL object_isClass(id obj) object_isClass.restype = c_bool object_isClass.argtypes = [objc_id] # const char *object_getClassName(id obj) libobjc.object_getClassName.restype = c_char_p libobjc.object_getClassName.argtypes = [objc_id] # Note: The following functions only work for exactly pointer-sized ivars. # To use non-pointer-sized ivars reliably, the memory location must be calculated manually (using ivar_getOffset) # and then used as a pointer. This "manual" way can be used for all ivars except weak object ivars - these must be # accessed through the runtime functions in order to work correctly. # id object_getIvar(id object, Ivar ivar) libobjc.object_getIvar.restype = objc_id libobjc.object_getIvar.argtypes = [objc_id, Ivar] # void object_setIvar(id object, Ivar ivar, id value) libobjc.object_setIvar.restype = None libobjc.object_setIvar.argtypes = [objc_id, Ivar, objc_id] ###################################################################### # const char *property_getAttributes(objc_property_t property) libobjc.property_getAttributes.restype = c_char_p libobjc.property_getAttributes.argtypes = [objc_property_t] # const char *property_getName(objc_property_t property) 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)] ###################################################################### class objc_method_description(Structure): """The `objc_method_description `__ structure from ````. """ # noqa: E501 _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) # You must free() the returned array. libobjc.protocol_copyMethodDescriptionList.restype = POINTER(objc_method_description) libobjc.protocol_copyMethodDescriptionList.argtypes = [ objc_id, c_bool, c_bool, POINTER(c_uint), ] # objc_property_t * protocol_copyPropertyList(Protocol *protocol, unsigned int *outCount) libobjc.protocol_copyPropertyList.restype = POINTER(objc_property_t) libobjc.protocol_copyPropertyList.argtypes = [objc_id, POINTER(c_uint)] # Protocol **protocol_copyProtocolList(Protocol *proto, unsigned int *outCount) libobjc.protocol_copyProtocolList.restype = POINTER(objc_id) libobjc.protocol_copyProtocolList.argtypes = [objc_id, POINTER(c_uint)] # struct objc_method_description protocol_getMethodDescription( # Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod) libobjc.protocol_getMethodDescription.restype = objc_method_description libobjc.protocol_getMethodDescription.argtypes = [objc_id, SEL, c_bool, c_bool] # const char *protocol_getName(Protocol *p) 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) libobjc.sel_getName.restype = c_char_p libobjc.sel_getName.argtypes = [SEL] # BOOL sel_isEqual(SEL lhs, SEL rhs) libobjc.sel_isEqual.restype = c_bool libobjc.sel_isEqual.argtypes = [SEL, SEL] # SEL sel_registerName(const char *str) libobjc.sel_registerName.restype = SEL libobjc.sel_registerName.argtypes = [c_char_p] ###################################################################### def ensure_bytes(x): """Convert the given string to :class:`bytes` if necessary. If the argument is already :class:`bytes`, it is returned unchanged; if it is :class:`str`, it is encoded as UTF-8. """ if isinstance(x, bytes): return x # "All char * in the runtime API should be considered to have UTF-8 encoding." # https://developer.apple.com/documentation/objectivec/objective_c_runtime?preferredLanguage=occ return x.encode("utf-8") ###################################################################### def get_class(name): """Get the Objective-C class with the given name as a :class:`Class` object. If no class with the given name is loaded, ``None`` is returned, and the Objective-C runtime will log a warning message. """ return libobjc.objc_getClass(ensure_bytes(name)) # https://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html # https://web.archive.org/web/20160810184034/http://www.x86-64.org/documentation/abi-0.99.pdf (pp.17-23) # executive summary: on x86-64, who knows? def should_use_stret(restype): """Return whether a method returning the given type must be called using ``objc_msgSend_stret`` on the current system.""" if type(restype) is not type(Structure): # Not needed when restype is not a structure. return False elif __i386__: # On i386: Use for structures not sized exactly like an integer (1, 2, 4, or 8 bytes). return sizeof(restype) not in (1, 2, 4, 8) elif __x86_64__: # On x86_64: Use for structures larger than 16 bytes. # (The ABI docs say that there are some special cases # for vector types, but those can't really be used # with ctypes anyway.) return sizeof(restype) > 16 elif __arm__: # On ARM32: Use for all structures, regardless of size. return True else: # Other platforms: Doesn't exist. return False # https://www.sealiesoftware.com/blog/archive/2008/11/16/objc_explain_objc_msgSend_fpret.html def should_use_fpret(restype): """Return whether a method returning the given type must be called using ``objc_msgSend_fpret`` on the current system.""" if __x86_64__: # On x86_64: Use only for long double. return restype == c_longdouble elif __i386__: # On i386: Use for all floating-point types. return restype in (c_float, c_double, c_longdouble) else: # Other platforms: Doesn't exist. return False _msg_send_cache = {} def _msg_send_for_types(restype, argtypes): """Get the appropriate variant of ``objc_msgSend`` for calling a method with the given return and argument types. :param restype: The return type of the method to be called. :param argtypes: The argument types of the method to be called, excluding the self and _cmd arguments. :return: A C function for ``objc_msgSend`` or one of its variants, with its return and argument types configured correctly based on the ``restype`` and ``argtypes`` arguments. The ``restype`` and ``argtypes`` attributes of the returned function *must not* be modified. """ try: # Looking up a C function is relatively slow, so use an existing cached function if possible. return _msg_send_cache[(restype, *argtypes)] except KeyError: # Choose the correct version of objc_msgSend based on return type. if should_use_fpret(restype): send_name = "objc_msgSend_fpret" elif should_use_stret(restype): send_name = "objc_msgSend_stret" else: send_name = "objc_msgSend" # Looking up a C function via attribute access (e.g. # libobjc.objc_msgSend) always returns the same function object. Because # we need to set the function object's restype and argtypes, this would # not be thread safe, and it also makes it impossible to cache multiple # differently configured copies of the same function like we do here. # Instead, we look up the C function using subscript syntax (e.g. # libobjc['objc_msgSend']), which returns a new function object every # time. send = libobjc[send_name] send.restype = restype send.argtypes = [objc_id, SEL] + argtypes # Cache the fully set up objc_msgSend function object to speed up future calls with the same types. _msg_send_cache[(restype, *argtypes)] = send return send def send_message(receiver, selector, *args, restype, argtypes=None, varargs=None): """Call a method on the receiver with the given selector and arguments. This is the equivalent of an Objective-C method call like ``[receiver sel:args]``. .. note:: Some Objective-C methods take variadic arguments (``varargs``), for example `+[NSString stringWithFormat:] `_. When using :func:`send_message`, variadic arguments are treated differently from regular arguments: they are not passed as normal function arguments in ``*args``, but as a list in a separate ``varargs`` keyword argument. This explicit separation of regular and variadic arguments protects against accidentally passing too many arguments into a method. By default these extra arguments would be considered ``varargs`` and passed on to the method, even if the method in question doesn't take ``varargs``. Because of how the Objective-C runtime and most C calling conventions work, this error would otherwise be silently ignored. The types of ``varargs`` are not included in the ``argtypes`` list. Instead, the values are automatically converted to C types using the default :mod:`ctypes` argument conversion rules. To ensure that all ``varargs`` are converted to the expected C types, it is recommended to manually convert all ``varargs`` to :mod:`ctypes` types instead of relying on automatic conversions. For example: .. code-block:: python send_message( NSString, "stringWithFormat:", at("%i %s %@"), restype=objc_id, argtypes=[objc_id], varargs=[c_int(123), cast(b"C string", c_char_p), at("ObjC string")], ) :param receiver: The object on which to call the method, as an :class:`~rubicon.objc.api.ObjCInstance` or :class:`.objc_id`. :param selector: The name of the method as a :class:`str`, :class:`bytes`, or :class:`SEL`. :param args: The method arguments. :param restype: The return type of the method. :param argtypes: The argument types of the method, as a :class:`list`. Defaults to ``[]``. :param varargs: Variadic arguments for the method, as a :class:`list`. Defaults to ``[]``. These arguments are converted according to the default :mod:`ctypes` conversion rules. """ # noqa: E501 try: receiver = receiver._as_parameter_ except AttributeError: pass if not isinstance(receiver, objc_id): raise TypeError( f"Receiver must be an ObjCInstance or objc_id, " f"not {type(receiver).__module__}.{type(receiver).__qualname__}" ) if not isinstance(selector, SEL): selector = SEL(selector) if argtypes is None: argtypes = [] if varargs is None: varargs = [] if len(args) != len(argtypes): raise TypeError( f"Inconsistent number of arguments ({len(args)}) and argument types ({len(argtypes)})" ) send = _msg_send_for_types(restype, argtypes) try: result = send(receiver, selector, *args, *varargs) except ArgumentError as error: # Add more useful info to the default error message, then reraise. err = error.args[0] sel = selector.name.decode(errors="backslashreplace") valid_args = ", ".join(t.__name__ for t in argtypes) error.args = [f"{sel} {err}; argtypes: {valid_args}"] raise error if restype == c_void_p: result = c_void_p(result) return result class objc_super(Structure): """The `objc_super `__ structure from ````. """ _fields_ = [ ("receiver", objc_id), ("super_class", Class), ] # https://stackoverflow.com/questions/3095360/what-exactly-is-super-in-objective-c def send_super( cls, receiver, selector, *args, restype=c_void_p, argtypes=None, varargs=None, _allow_dealloc=False, ): """In the context of the given class, call a superclass method on the receiver with the given selector and arguments. This is the equivalent of an Objective-C method call like ``[super sel:args]`` in the class ``cls``. In practice, the first parameter should always be the special variable ``__class__``, and the second parameter should be ``self``. A typical :func:`send_super` call would be ``send_super(__class__, self, 'init')`` for example. The special variable ``__class__`` is defined by Python and stands for the class object that is being created by the current ``class`` block. The exact reasons why ``__class__`` must be passed manually are somewhat technical, and are not directly relevant to users of :func:`send_super`. For a full explanation, see issue `beeware/rubicon-objc#107 `__ and PR `beeware/rubicon-objc#108 `__. Although it is possible to pass other values than ``__class__`` and ``self`` for the first two parameters, this is strongly discouraged. Doing so is not supported by the Objective-C language, and relies on implementation details of the superclasses. :param cls: The class in whose context the ``super`` call is happening, as an :class:`~rubicon.objc.api.ObjCClass` or :class:`Class`. :param receiver: The object on which to call the method, as an :class:`~rubicon.objc.api.ObjCInstance`, :class:`.objc_id`, or :class:`~ctypes.c_void_p`. :param selector: The name of the method as a :class:`str`, :class:`bytes`, or :class:`SEL`. :param args: The method arguments. :param restype: The return type of the method. :param argtypes: The argument types of the method, as a :class:`list`. Defaults to ``[]``. :param varargs: Variadic arguments for the method, as a :class:`list`. Defaults to ``[]``. These arguments are converted according to the default :mod:`ctypes` conversion rules. """ # Unwrap ObjCClass to Class if necessary try: cls = cls._as_parameter_ except AttributeError: pass # Convert str / bytes to selector if not isinstance(selector, SEL): selector = SEL(selector) if argtypes is None: argtypes = [] if varargs is None: varargs = [] if len(args) != len(argtypes): raise TypeError( f"Inconsistent number of arguments ({len(args)}) and argument types ({len(argtypes)})" ) if not isinstance(cls, Class): # Kindly remind the caller that the API has changed raise TypeError( f"Missing or invalid cls argument: expected an ObjCClass or Class, " f"not {type(cls).__module__}.{type(cls).__qualname__}\n" f"send_super requires the current class to be passed explicitly as the first argument. " f"To fix this error, pass the special name __class__ as the first argument to send_super." ) if not _allow_dealloc and selector.name == b"dealloc": warnings.warn( "You should not call the superclass dealloc manually when overriding dealloc. Rubicon-objc " "will call it for you after releasing objects stored in properties and ivars.", stacklevel=2, ) return try: receiver = receiver._as_parameter_ except AttributeError: pass if isinstance(receiver, objc_id): pass elif type(receiver) is c_void_p: receiver = cast(receiver, objc_id) else: raise TypeError( f"Invalid type for receiver: {type(receiver).__module__}.{type(receiver).__qualname__}" ) super_ptr = libobjc.class_getSuperclass(cls) if super_ptr.value is None: class_name = libobjc.class_getName(cls).decode("utf-8") raise ValueError( f"The specified class {class_name!r} is a root class, it cannot be used with send_super" ) super_struct = objc_super(receiver, super_ptr) if should_use_stret(restype): send = libobjc["objc_msgSendSuper_stret"] else: send = libobjc["objc_msgSendSuper"] send.restype = restype send.argtypes = [POINTER(objc_super), SEL] + argtypes result = send(byref(super_struct), selector, *args, *varargs) if restype == c_void_p: result = c_void_p(result) return result # Collection of the ctypes C function pointer objects of the implementations of # all Python-defined Objective-C methods. When an Objective-C method implemented # in Python is created, the Python callable that implements the method is # wrapped as a C function pointer using ctypes. This C function pointer object # must be kept alive manually from Python for as long as the C function pointer # is in use. Objective-C method implementations almost always remain referenced # until the process terminates, so the function pointer objects are never # removed again from this collection after they are added. (There are rare cases # where method implementations might become unreferenced early, for example if a # method is swizzled and the old implementation is never called again, but it's # impossible to detect when this happens and is rare enough that it's not worth # handling.) _keep_alive_imps = [] def add_method(cls, selector, method, encoding, replace=False): """Add a new instance method to the given class. To add a class method, add an instance method to the metaclass. :param cls: The Objective-C class to which to add the method, as an :class:`~rubicon.objc.api.ObjCClass` or :class:`Class`. :param selector: The name for the new method, as a :class:`str`, :class:`bytes`, or :class:`SEL`. :param method: The method implementation, as a Python callable or a C function address. :param encoding: The method's signature (return type and argument types) as a :class:`list`. The types of the implicit ``self`` and ``_cmd`` parameters must be included in the signature. :param replace: If the class already implements a method with the given name, replaces the current implementation if ``True``. Raises a :class:`ValueError` error otherwise. :return: The ctypes C function pointer object that was created for the method's implementation. This return value can be ignored. (In version 0.4.0 and older, callers were required to manually keep a reference to this function pointer object to ensure that it isn't garbage-collected. Rubicon now does this automatically.) """ signature = [ctype_for_type(tp) for tp in encoding] assert signature[1] == objc_id # ensure id self typecode assert signature[2] == SEL # ensure SEL cmd typecode if signature[0] is not None and issubclass(signature[0], (Structure, Union)): # Patch struct/union return types to make them work in callbacks. # See the source code of the ctypes_patch module for details. ctypes_patch.make_callback_returnable(signature[0]) selector = SEL(selector) types = b"".join(encoding_for_ctype(ctype) for ctype in signature) cfunctype = CFUNCTYPE(*signature) imp = cfunctype(method) if replace: libobjc.class_replaceMethod(cls, selector, cast(imp, IMP), types) else: res = libobjc.class_addMethod(cls, selector, cast(imp, IMP), types) if not res: raise ValueError(f"A method with the name {selector.name!r} already exists") _keep_alive_imps.append(imp) return imp def add_ivar(cls, name, vartype): """Add a new instance variable of type ``vartype`` to ``cls``.""" return libobjc.class_addIvar( cls, ensure_bytes(name), sizeof(vartype), alignment(vartype), encoding_for_ctype(ctype_for_type(vartype)), ) def get_ivar(obj, varname, weak=False): """Get the value of obj's ``ivar`` named ``varname``. The returned object is a :mod:`ctypes` data object. For non-object types (everything except :class:`.objc_id` and subclasses), the returned data object is backed by the ``ivar``'s actual memory. This means that the data object is only usable as long as the "owner" object is alive, and writes to it will directly change the ``ivar``'s value. For object types, the returned data object is independent of the ``ivar``'s memory. This is because object ``ivars`` may be weak, and thus cannot always be accessed directly by their address. """ try: obj = obj._as_parameter_ except AttributeError: pass ivar = libobjc.class_getInstanceVariable( libobjc.object_getClass(obj), ensure_bytes(varname) ) vartype = ctype_for_encoding(libobjc.ivar_getTypeEncoding(ivar)) if weak: value = libobjc.objc_loadWeakRetained(obj.value + libobjc.ivar_getOffset(ivar)) return libobjc.objc_autoreleaseReturnValue(value) elif issubclass(vartype, objc_id): return cast(libobjc.object_getIvar(obj, ivar), vartype) else: return vartype.from_address(obj.value + libobjc.ivar_getOffset(ivar)) def set_ivar(obj, varname, value, weak=False): """Set obj's ``ivar`` ``varname`` to value. If ``weak`` is ``True``, only a weak reference to the value is stored. value must be a :mod:`ctypes` data object whose type matches that of the ``ivar``. """ try: obj = obj._as_parameter_ except AttributeError: pass ivar = libobjc.class_getInstanceVariable( libobjc.object_getClass(obj), ensure_bytes(varname) ) vartype = ctype_for_encoding(libobjc.ivar_getTypeEncoding(ivar)) if not isinstance(value, vartype): raise TypeError( f"Incompatible type for ivar {varname!r}: {type(value)!r} " f"is not a subclass of the ivar's type {vartype!r}" ) elif sizeof(type(value)) != sizeof(vartype): raise TypeError( f"Incompatible type for ivar {varname!r}: {type(value)!r} " f"has size {sizeof(type(value))}, but the ivar's type {vartype!r} " f"has size {sizeof(vartype)}" ) if weak: libobjc.objc_storeWeak(obj.value + libobjc.ivar_getOffset(ivar), value) elif issubclass(vartype, objc_id): libobjc.object_setIvar(obj, ivar, value) else: memmove( obj.value + libobjc.ivar_getOffset(ivar), addressof(value), sizeof(vartype) ) @contextmanager def autoreleasepool(): """A context manager that has the same effect as a @autoreleasepool block in Objective-C. Any objects that are autoreleased within the context will receive a release message when exiting the context. When running an event loop, AppKit will create an autorelease pool at the beginning of each cycle of the event loop and drain it at the end. You therefore do not need to use @autoreleasepool blocks when running an event loop. However, they may be still be useful when your code temporarily allocates large amounts of memory which you want to explicitly free before the end of a cycle. """ pool = libobjc.objc_autoreleasePoolPush() try: yield finally: libobjc.objc_autoreleasePoolPop(pool)