teams-bots/write-message/venv/lib/python3.10/site-packages/rubicon/objc/api.py

2416 lines
96 KiB
Python

import collections.abc
import decimal
import enum
import inspect
import threading
import typing
import weakref
from ctypes import (
CFUNCTYPE,
POINTER,
Array,
Structure,
Union,
addressof,
byref,
c_bool,
c_char_p,
c_int,
c_uint,
c_uint8,
c_ulong,
c_void_p,
cast,
py_object,
sizeof,
string_at,
)
from .runtime import (
SEL,
Class,
add_ivar,
add_method,
ensure_bytes,
get_class,
get_ivar,
libc,
libobjc,
objc_block,
objc_id,
objc_property_attribute_t,
object_isClass,
send_message,
send_super,
set_ivar,
)
from .types import (
compound_value_for_sequence,
ctype_for_type,
ctypes_for_method_encoding,
encoding_for_ctype,
register_ctype_for_type,
)
__all__ = [
"Block",
"NSArray",
"NSData",
"NSDecimalNumber",
"NSDictionary",
"NSMutableArray",
"NSMutableDictionary",
"NSNumber",
"NSObject",
"NSObjectProtocol",
"NSString",
"ObjCBlock",
"ObjCClass",
"ObjCInstance",
"ObjCMetaClass",
"ObjCProtocol",
"Protocol",
"at",
"for_objcclass",
"get_type_for_objcclass_map",
"ns_from_py",
"objc_classmethod",
"objc_const",
"objc_ivar",
"objc_method",
"objc_property",
"objc_rawmethod",
"py_from_ns",
"register_type_for_objcclass",
"type_for_objcclass",
"unregister_type_for_objcclass",
]
# Dictionary to keep references to Python objects which are stored in declared
# properties or dynamically created attributes of Objective-C objects. This ensures that
# the Python objects are not destroyed if they are otherwise no Python references left.
_keep_alive_objects = {}
# Methods that return an object which is implicitly retained by the caller.
# See https://clang.llvm.org/docs/AutomaticReferenceCounting.html#semantics-of-method-families.
_RETURNS_RETAINED_FAMILIES = {"init", "alloc", "new", "copy", "mutableCopy"}
def get_method_family(method_name: str) -> str:
"""Returns the method family from the method name. See
https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families for
documentation on method families and corresponding selector names."""
first_component = method_name.lstrip("_").split(":")[0]
for family in _RETURNS_RETAINED_FAMILIES:
if first_component.startswith(family):
remainder = first_component.removeprefix(family)
if remainder == "" or not remainder[0].islower():
return family
return ""
def method_name_to_tuple(name: str) -> (str, tuple[str, ...]):
"""
Performs the following transformation:
"methodWithArg0:withArg1:withArg2:" -> "methodWithArg0", ("", "withArg1", "withArg2")
"methodWithArg0:" -> "methodWithArg0", ("", )
"method" -> "method", ()
The first element of the returned tuple is the "base name" of the method. The second
element is a tuple with its argument names.
"""
# Selectors end with a colon if the method takes arguments.
if name.endswith(":"):
first, *rest, _ = name.split(":")
# Insert an empty string in order to indicate that the method
# takes a first argument as a positional argument.
rest.insert(0, "")
rest = tuple(rest)
else:
first = name
rest = ()
return first, rest
def encoding_from_annotation(f, offset=1):
argspec = inspect.getfullargspec(inspect.unwrap(f))
hints = typing.get_type_hints(f)
encoding = [hints.get("return", ObjCInstance), ObjCInstance, SEL]
for varname in argspec.args[offset:]:
encoding.append(hints.get(varname, ObjCInstance))
return encoding
class ObjCMethod:
"""An unbound Objective-C method. This is Rubicon's high-level equivalent
of :class:`~rubicon.objc.runtime.Method`.
:class:`ObjCMethod` objects normally don't need to be used directly. To call
a method on an Objective-C object, you should use the method call syntax
supported by :class:`ObjCInstance`, or the
:func:`~rubicon.objc.runtime.send_message` function.
.. note::
This is *not* the same class as the one used for *bound* Objective-C
methods, as returned from :meth:`ObjCInstance.__getattr__`. Currently,
Rubicon doesn't provide any documented way to get an unbound
:class:`ObjCMethod` object for an instance method of an
:class:`ObjCClass`.
"""
def __init__(self, method):
"""The constructor takes a :class:`~rubicon.objc.runtime.Method`
object, whose information is used to create an :class:`ObjCMethod`.
This can be used to call or introspect a
:class:`~rubicon.objc.runtime.Method` pointer received from the
Objective-C runtime.
"""
self.selector = libobjc.method_getName(method)
self.name = self.selector.name
self.encoding = libobjc.method_getTypeEncoding(method)
self.restype, *self.imp_argtypes = ctypes_for_method_encoding(self.encoding)
assert self.imp_argtypes[:2] == [objc_id, SEL]
self.method_argtypes = self.imp_argtypes[2:]
def __repr__(self):
return f"<{type(self).__qualname__}: {self.name.decode()} {self.encoding.decode()}>"
def __call__(self, receiver, *args, convert_args=True, convert_result=True):
"""Call the method on an object with the given arguments.
The passed arguments are automatically converted to the expected
argument types as needed:
* :class:`enum.Enum` objects are replaced by their
:attr:`~enum.Enum.value` before further conversion
* For parameters that expect a block, Python callables are converted to
:class:`Block`\\s
* For parameters that expect an Objective-C object, Python objects are
converted using :func:`ns_from_py`
* For parameters that expect a C structure, Python sequences are
converted using
:func:`~rubicon.objc.types.compound_value_for_sequence`.
* Finally, :mod:`ctypes` applies its normal function argument
conversions.
The above argument conversions (except those performed by :mod:`ctypes`)
can be disabled by setting the ``convert_args`` keyword argument to
``False``.
If the method returns an Objective-C object, it is automatically
converted to an :class:`ObjCInstance`. This conversion can be disabled
by setting the ``convert_result`` keyword argument to ``False``, in
which case the object is returned as a raw
:class:`~rubicon.objc.runtime.objc_id` value.
The ``_cmd`` selector argument does *not* need to be passed in manually
--- the method's :attr:`selector` is automatically added between the
receiver and the method arguments.
"""
if len(args) != len(self.method_argtypes):
raise TypeError(
f"Method {self.name} takes {len(args)} arguments, but got {len(self.method_argtypes)} arguments"
)
if convert_args:
converted_args = []
for argtype, arg in zip(self.method_argtypes, args):
if isinstance(arg, enum.Enum):
# Convert Python enum objects to their values
arg = arg.value
if issubclass(argtype, objc_block):
if arg is None:
# allow for 'nil' block args, which some objc methods accept
arg = ns_from_py(arg)
elif callable(arg) and not isinstance(
arg, Block
): # <-- guard against someone someday making Block callable
# Note: We need to keep the temp. Block instance
# around at least until the objc method is called.
# _as_parameter_ is used in the actual ctypes marshalling below.
arg = Block(arg)
# ^ For blocks at this point either arg is a Block instance
# (making use of _as_parameter_), is None, or if it isn't either of
# those two, an ArgumentError will be raised below.
elif issubclass(argtype, objc_id):
# Convert Python objects to Foundation objects
arg = ns_from_py(arg)
elif isinstance(arg, collections.abc.Sequence) and issubclass(
argtype, (Structure, Array)
):
arg = compound_value_for_sequence(arg, argtype)
converted_args.append(arg)
else:
converted_args = args
# Init methods consume their `self` argument (the receiver), see
# https://clang.llvm.org/docs/AutomaticReferenceCounting.html#semantics-of-init.
# To ensure the receiver pointer remains valid if `init` does not return `self`
# but a different object or None, we issue an additional retain. This needs to
# be done before calling the method.
# Note that if `init` does return the same object, it will already be in our
# cache and balanced with a `release` on cache retrieval.
method_family = get_method_family(self.name.decode())
if method_family == "init":
send_message(receiver, "retain", restype=objc_id, argtypes=[])
result = send_message(
receiver,
self.selector,
*converted_args,
restype=self.restype,
argtypes=self.method_argtypes,
)
if not convert_result:
return result
# Convert result to python type if it is an instance or class pointer.
# Explicitly retain the instance on first handover to Python unless we
# received it from a method that gives us ownership already.
if self.restype is not None and issubclass(self.restype, objc_id):
implicitly_owned = method_family in _RETURNS_RETAINED_FAMILIES
result = ObjCInstance(result, _implicitly_owned=implicitly_owned)
return result
class ObjCPartialMethod:
_sentinel = object()
def __init__(self, name_start):
super().__init__()
self.name_start = name_start
# A dictionary mapping from a tuple of argument names to the full method name.
# Initialized in ObjCClass._load_methods
self.methods: dict[tuple[str, ...], str] = {}
def __repr__(self):
return f"{type(self).__qualname__}({self.name_start!r})"
def __call__(self, receiver, first_arg=_sentinel, **kwargs):
# Ignore parts of argument names after "__".
order = tuple(argname.split("__")[0] for argname in kwargs)
args = [arg for arg in kwargs.values()]
if first_arg is ObjCPartialMethod._sentinel:
if kwargs:
raise TypeError("Missing first (positional) argument")
rest = order
else:
args.insert(0, first_arg)
rest = ("",) + order
# Try to use cached ObjCBoundMethod
try:
name = self.methods[rest]
meth = receiver.objc_class._cache_method(name)
return meth(receiver, *args)
except KeyError:
pass
# Reconstruct the full method name from arguments and look up actual method.
if first_arg is self._sentinel:
name = self.name_start
else:
name = f"{self.name_start}:{':'.join(kwargs.keys())}:"
meth = receiver.objc_class._cache_method(name)
if meth:
# Update methods cache and call method.
self.methods[rest] = name
return meth(receiver, *args)
raise ValueError(
f"Invalid selector {name}. Available selectors are: "
f"{', '.join(sel for sel in self.methods.values())}"
) from None
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, receiver):
"""Initialize with a method and ObjCInstance or ObjCClass object."""
self.method = method
if type(receiver) is Class:
self.receiver = cast(receiver, objc_id)
else:
self.receiver = receiver
def __repr__(self):
return f"{type(self).__qualname__}({self.method}, {self.receiver})"
def __call__(self, *args, **kwargs):
"""Call the method with the given arguments."""
return self.method(self.receiver, *args, **kwargs)
def convert_method_arguments(encoding, args):
"""Used to convert Objective-C method arguments to Python values before
passing them on to the Python-defined method.
"""
new_args = []
for e, a in zip(encoding[3:], args):
if issubclass(e, (objc_id, ObjCInstance)):
new_args.append(ObjCInstance(a))
else:
new_args.append(a)
return new_args
class objc_method:
"""Exposes the decorated method as an Objective-C instance method in a
custom class or protocol.
In a custom Objective-C class, decorating a method with :func:`@objc_method
<objc_method>` makes it available to Objective-C: a corresponding
Objective-C method is created in the new Objective-C class, whose
implementation calls the decorated Python method. The Python method receives
all arguments (including ``self``) from the Objective-C method call, and its
return value is passed back to Objective-C.
In a custom Objective-C protocol, the behavior is similar, but the method
body is ignored, since Objective-C protocol methods have no implementations.
By convention, the method body in this case should be empty (``pass``).
(Since the method is never called, you could put any other code there as
well, but doing so is misleading and discouraged.)
"""
def __init__(self, py_method):
super().__init__()
self.py_method = py_method
self.encoding = encoding_from_annotation(py_method)
def __call__(self, objc_self, objc_cmd, *args):
py_self = ObjCInstance(objc_self)
args = convert_method_arguments(self.encoding, args)
result = self.py_method(py_self, *args)
if self.encoding[0] is not None and issubclass(
self.encoding[0], (objc_id, ObjCInstance)
):
result = ns_from_py(result)
if result is not None:
result = result.ptr
if isinstance(result, c_void_p):
return result.value
else:
return result
def class_register(self, class_ptr, attr_name):
name = attr_name.replace("_", ":")
add_method(class_ptr, name, self, self.encoding)
def protocol_register(self, proto_ptr, attr_name):
name = attr_name.replace("_", ":")
types = b"".join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding)
libobjc.protocol_addMethodDescription(proto_ptr, SEL(name), types, True, True)
class objc_classmethod:
"""Exposes the decorated method as an Objective-C class method in a custom
class or protocol.
This decorator behaves exactly like :func:`@objc_method <objc_method>`,
except that the decorated method becomes a class method, so it is exposed
on the Objective-C class rather than its instances.
"""
def __init__(self, py_method):
super().__init__()
self.py_method = py_method
self.encoding = encoding_from_annotation(py_method)
def __call__(self, objc_cls, objc_cmd, *args):
py_cls = ObjCClass(objc_cls)
args = convert_method_arguments(self.encoding, args)
result = self.py_method(py_cls, *args)
if self.encoding[0] is not None and issubclass(
self.encoding[0], (objc_id, ObjCInstance)
):
result = ns_from_py(result)
if result is not None:
result = result.ptr
if isinstance(result, c_void_p):
return result.value
else:
return result
def class_register(self, class_ptr, attr_name):
name = attr_name.replace("_", ":")
add_method(libobjc.object_getClass(class_ptr), name, self, self.encoding)
def protocol_register(self, proto_ptr, attr_name):
name = attr_name.replace("_", ":")
types = b"".join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding)
libobjc.protocol_addMethodDescription(proto_ptr, SEL(name), types, True, False)
class objc_ivar:
"""Defines an ``ivar`` in a custom Objective-C class.
If you want to store additional data on a custom Objective-C class, it is
recommended to use properties (:func:`objc_property`) instead of ``ivars``.
Properties are a more modern and high-level Objective-C feature, which
automatically deal with reference counting for objects, and creation of
getters and setters.
The ``ivar`` type may be any :mod:`ctypes` type.
Unlike properties, the contents of an ``ivar`` cannot be accessed or
modified using Python attribute syntax. Instead, the :func:`get_ivar`
and :func:`set_ivar` functions need to be used.
"""
def __init__(self, vartype):
self.vartype = vartype
def class_register(self, class_ptr, attr_name):
return add_ivar(class_ptr, attr_name, self.vartype)
def protocol_register(self, proto_ptr, attr_name):
raise TypeError("Objective-C protocols cannot have ivars")
class objc_property:
"""Defines a property in a custom Objective-C class or protocol.
This class should be called in the body of an Objective-C subclass or
protocol, for example:
.. code-block:: python
class MySubclass(NSObject):
counter = objc_property(NSInteger)
The property type may be any :mod:`ctypes` type, as well as any of the
Python types accepted by :func:`~rubicon.objc.types.ctype_for_type`.
Defining a property automatically defines a corresponding getter and setter.
Following standard Objective-C naming conventions, for a property ``name``
the getter is called ``name`` and the setter is called ``setName:``.
In a custom Objective-C class, implementations for the getter and setter are
also generated, which store the property's value in an ``ivar`` called
``_name``. If the property has an object type, the generated setter keeps
the stored object retained, and releases it when it is replaced.
In a custom Objective-C protocol, only the metadata for the property is
generated.
If ``weak`` is ``True``, the property will be created as a weak property.
When assigning an object to it, the reference count of the object will not
be increased. When the object is deallocated, the property value is set to
None. Weak properties are only supported for Objective-C or Python object
types.
"""
def __init__(self, vartype=objc_id, weak=False):
super().__init__()
self.vartype = ctype_for_type(vartype)
self.weak = weak
self._is_py_object = issubclass(self.vartype, py_object)
self._is_objc_object = issubclass(self.vartype, objc_id)
# Weakly referenced Python objects are still stored in strong ivars.
# Check here if we need a weak or strong ivar.
self._ivar_weak = self.weak and not self._is_py_object
if self.weak and not (self._is_py_object or self._is_objc_object):
raise TypeError(
"Incompatible type for ivar {!r}: Weak properties are only supported "
"for Objective-C or Python object types".format(vartype)
)
def _get_property_attributes(self):
attrs = [
# Type: vartype
objc_property_attribute_t(b"T", encoding_for_ctype(self.vartype)),
]
if self._is_objc_object:
reference = b"W" if self.weak else b"&"
attrs.append(objc_property_attribute_t(reference, b""))
return (objc_property_attribute_t * len(attrs))(*attrs)
def class_register(self, class_ptr, attr_name):
ivar_name = "_" + attr_name
add_ivar(class_ptr, ivar_name, self.vartype)
# Implementation note:
# 1. Objective-C objects are stored as strong or weak references in the
# ivar if the property was declared as strong or weak, respectively.
# In case of strong properties, we retain the object when storing it
# in the ivar and release it when the ivar is changed.
# 2. Python objects are wrapped as `ctypes.py_object` which are then
# always stored as a strong reference in the ivar. Since this does
# not increase the reference count of the Python object itself, we
# keep a reference to it in `_keep_alive_objects`. For weak
# properties, we store a Python `wearef` to the object instead. This
# weakref is similarly kept alive.
def _objc_getter(objc_self, _cmd):
value = get_ivar(objc_self, ivar_name, weak=self._ivar_weak)
# ctypes complains when a callback returns a "boxed" primitive type,
# so we have to manually unbox it. If the data object has a value
# attribute and is not a structure or union, assume that it is a
# primitive and unbox it.
if not isinstance(value, (Structure, Union)):
try:
value = value.value
except AttributeError:
pass
if self.weak and self._is_py_object:
# Unpack the Python weakref.
value = value()
return value
def _objc_setter(objc_self, _cmd, new_value):
if self._is_py_object and self.weak:
# Don't store the object itself but only a Python weakref.
new_value = weakref.ref(new_value)
if not isinstance(new_value, self.vartype):
# If vartype is a primitive, then new_value may be unboxed. If
# that is the case, box it manually.
new_value = self.vartype(new_value)
if self._is_objc_object and not self.weak:
# If vartype is objc_id, retrieve the old object stored in the
# ivar to release it later.
old_value = get_ivar(objc_self, ivar_name, weak=self.weak)
if new_value.value == old_value.value:
# Old and new value are the same, nothing to do.
return
set_ivar(objc_self, ivar_name, new_value, weak=self._ivar_weak)
# Perform reference management.
if self._is_objc_object and not self.weak:
if old_value:
# If the old value is a non-null Objective-C object, release it.
send_message(old_value, "release", restype=None, argtypes=[])
if new_value:
# Retain the object on the Objective-C side.
send_message(new_value, "retain", restype=objc_id, argtypes=[])
elif self._is_py_object:
# Retain the Python object in dictionary, this replaces any
# previous entry for this property.
_keep_alive_objects[(objc_self.value, self)] = new_value.value
setter_name = "set" + attr_name[0].upper() + attr_name[1:] + ":"
add_method(
class_ptr,
attr_name,
_objc_getter,
[self.vartype, ObjCInstance, SEL],
)
add_method(
class_ptr,
setter_name,
_objc_setter,
[None, ObjCInstance, SEL, self.vartype],
)
attrs = self._get_property_attributes()
libobjc.class_addProperty(class_ptr, ensure_bytes(attr_name), attrs, len(attrs))
def dealloc_callback(self, objc_self, attr_name):
ivar_name = "_" + attr_name
if self._ivar_weak:
# Clean up weak reference.
set_ivar(objc_self, ivar_name, self.vartype(None), weak=True)
elif self._is_objc_object:
# If the old value is a non-null object, release it. There is no
# need to set the actual ivar to nil.
old_value = get_ivar(objc_self, ivar_name, weak=self.weak)
send_message(old_value, "release", restype=None, argtypes=[])
# Remove any Python objects that are kept alive.
_keep_alive_objects.pop((objc_self.value, self), None)
def protocol_register(self, proto_ptr, attr_name):
attrs = self._get_property_attributes()
libobjc.protocol_addProperty(
proto_ptr, ensure_bytes(attr_name), attrs, len(attrs), True, True
)
class objc_rawmethod:
"""Exposes the decorated method as an Objective-C instance method in a
custom class, with fewer convenience features than :func:`objc_method`.
This decorator behaves similarly to :func:`@objc_method <objc_method>`.
However, unlike with :func:`objc_method`, no automatic conversions are
performed (aside from those by :mod:`ctypes`). This means that all parameter
and return types must be provided as :mod:`ctypes` types (no
:func:`~rubicon.objc.types.ctype_for_type` conversion is performed), all
arguments are passed in their raw form as received from :mod:`ctypes`, and
the return value must be understood by :mod:`ctypes`.
In addition, the implicit ``_cmd`` parameter is exposed to the Python
method, which is not the case when using :func:`objc_method`. This means
that the decorated Python method must always have an additional ``_cmd``
parameter after ``self``; if it is missing, there will be errors at runtime
due to mismatched argument counts. Like ``self``, ``_cmd`` never needs to be
annotated, and any annotations on it are ignored.
"""
def __init__(self, py_method):
super().__init__()
self.py_method = py_method
self.encoding = encoding_from_annotation(py_method, offset=2)
def __call__(self, *args, **kwargs):
return self.py_method(*args, **kwargs)
def class_register(self, class_ptr, attr_name):
name = attr_name.replace("_", ":")
add_method(class_ptr, name, self, self.encoding)
def protocol_register(self, proto_ptr, attr_name):
raise TypeError(
"Protocols cannot have method implementations, "
"use objc_method instead of objc_rawmethod"
)
_type_for_objcclass_map = {}
def type_for_objcclass(objcclass):
"""Look up the :class:`ObjCInstance` subclass used to represent instances
of the given Objective-C class in Python.
If the exact Objective-C class is not registered, each superclass is also
checked, defaulting to :class:`ObjCInstance` if none of the classes in the
superclass chain is registered. Afterwards, all searched superclasses are
registered for the :class:`ObjCInstance` subclass that was found. (This
speeds up future lookups, and ensures that previously computed mappings are
not changed by unrelated registrations.)
This method is mainly intended for internal use by Rubicon, but is exposed
in the public API for completeness.
"""
if isinstance(objcclass, ObjCClass):
objcclass = objcclass.ptr
superclass = objcclass
traversed_classes = []
pytype = ObjCInstance
while superclass.value is not None:
try:
pytype = _type_for_objcclass_map[superclass.value]
except KeyError:
traversed_classes.append(superclass)
superclass = libobjc.class_getSuperclass(superclass)
else:
break
for cls in traversed_classes:
register_type_for_objcclass(pytype, cls)
return pytype
def register_type_for_objcclass(pytype, objcclass):
"""Register a conversion from an Objective-C class to an
:class:`ObjCInstance` subclass.
After a call of this function, when Rubicon wraps an Objective-C object that
is an instance of ``objcclass`` (or a subclass), the Python object will have
the class ``pytype`` rather than :class:`ObjCInstance`. See
:func:`type_for_objcclass` for a full description of the lookup process.
.. warning::
This function should only be called if no instances of ``objcclass`` (or
a subclass) have been wrapped by Rubicon yet. If the function is called
later, it will not fully take effect: the types of existing instances do
not change, and mappings for subclasses of ``objcclass`` are not
updated.
"""
if isinstance(objcclass, ObjCClass):
objcclass = objcclass.ptr
_type_for_objcclass_map[objcclass.value] = pytype
def unregister_type_for_objcclass(objcclass):
"""Unregister a conversion from an Objective-C class to an
:class:`ObjCInstance` subclass.
.. warning::
This function should only be called if no instances of ``objcclass`` (or
a subclass) have been wrapped by Rubicon yet. If the function is called
later, it will not fully take effect: the types of existing instances do
not change, and mappings for subclasses of ``objcclass`` are not
removed.
"""
if isinstance(objcclass, ObjCClass):
objcclass = objcclass.ptr
del _type_for_objcclass_map[objcclass.value]
def get_type_for_objcclass_map():
"""Get a copy of all currently registered :class:`ObjCInstance` subclasses
as a mapping.
Keys are Objective-C class addresses as :class:`int`\\s.
"""
return dict(_type_for_objcclass_map)
def for_objcclass(objcclass):
"""Decorator for registering a conversion from an Objective-C class to an
:class:`ObjCInstance` subclass.
This is equivalent to calling :func:`register_type_for_objcclass` on
the decorated class.
"""
def _for_objcclass(pytype):
register_type_for_objcclass(pytype, objcclass)
return pytype
return _for_objcclass
class ObjCInstance:
"""Python wrapper for an Objective-C instance."""
# Cache dictionary containing every currently existing ObjCInstance object,
# with the key being the memory address (as an integer) of the Objective-C
# object that it wraps. Because this is a weak value dictionary, entries are
# automatically removed if the ObjCInstance is no longer referenced from
# Python. (The object may still have references in Objective-C, and a new
# ObjCInstance might be created for it if it is wrapped again later.)
_cached_objects = weakref.WeakValueDictionary()
# A re-entrant thread lock moderating access to
# ObjCInstance._cached_objects. When creating new instances, there is a time
# gap between determining there has been a cache miss, and the addition of a
# new instance into the cache. This leaves a gap where a separate thread
# could wrap the same pointer, and creating a second wrapper; whichever
# wrapper is written to the cache first will be overwritten by the second.
# This probably won't cause any observable problems - both instances will be
# valid wrappers around the same memory address, but it's memory that we
# don't need to allocate. The lock is re-entrant because allocating an
# instance can cause the creation of additional instances, especially at
# time of bootstrapping.
#
# Refs #251.
_instance_lock = threading.RLock()
@property
def objc_class(self):
"""The Objective-C object's class, as an :class:`ObjCClass`."""
# This property is used inside __getattr__, so any attribute accesses must be done through
# super(...).__getattribute__ to prevent infinite recursion.
try:
return super(ObjCInstance, type(self)).__getattribute__(self, "_objc_class")
except AttributeError:
# This assumes that objects never change their class after they are
# seen by Rubicon. This can occur because the Objective-C runtime provides a
# function object_setClass that can change an object's class after creation,
# and some code manipulates objects' isa pointers directly (although the
# latter is no longer officially supported by Apple). This is not commonly
# done in practice, and even then it is usually only done during object
# creation/initialization, so it's basically safe to assume that an object's
# class will never change after it's been wrapped in an ObjCInstance.
super(ObjCInstance, type(self)).__setattr__(
self, "_objc_class", ObjCClass(libobjc.object_getClass(self))
)
return super(ObjCInstance, type(self)).__getattribute__(self, "_objc_class")
@staticmethod
def _associated_attr_key_for_name(name):
return SEL(f"rubicon.objc.py_attr.{name}")
def __new__(
cls, object_ptr, _name=None, _bases=None, _ns=None, _implicitly_owned=False
):
"""The constructor accepts an :class:`~rubicon.objc.runtime.objc_id` or
anything that can be cast to one, such as a :class:`~ctypes.c_void_p`,
or an existing :class:`ObjCInstance`.
:class:`ObjCInstance` objects are cached --- this means that for every
Objective-C object there can be at most one :class:`ObjCInstance` object
at any time. Rubicon will automatically create new
:class:`ObjCInstance`\\s or return existing ones as needed.
The returned object's Python class is not always exactly
:class:`ObjCInstance`. For example, if the passed pointer refers to a
class or a metaclass, an instance of :class:`ObjCClass` or
:class:`ObjCMetaClass` is returned as appropriate. Additional custom
:class:`ObjCInstance` subclasses may be defined and registered using
:func:`register_type_for_objcclass`. Creating an :class:`ObjCInstance`
from a ``nil`` pointer returns ``None``.
Rubicon retains an Objective-C object when it is wrapped in an
:class:`ObjCInstance` and autoreleases it when the :class:`ObjCInstance` is
garbage collected.
The only exception to this are objects returned by methods which create an
object (starting with "alloc", "new", "copy", or "mutableCopy"). We do not
explicitly retain them because we already own objects created by us, but we do
autorelease them on garbage collection of the Python wrapper.
This ensures that the :class:`ObjCInstance` can always be used from Python
without segfaults while preventing Rubicon from leaking memory.
"""
# Make sure that object_ptr is wrapped in an objc_id.
if not isinstance(object_ptr, objc_id):
object_ptr = cast(object_ptr, objc_id)
# If given a nil pointer, return None.
if not object_ptr.value:
return None
with ObjCInstance._instance_lock:
try:
# If an ObjCInstance already exists for the Objective-C object,
# reuse it instead of creating a second ObjCInstance for the
# same object.
cached_obj = cls._cached_objects[object_ptr.value]
# We can get a cache hit for methods that return an implicitly retained
# object. This is typically the case when:
#
# 1. A `copy` returns the original object if it is immutable. This is
# typically done for optimization. See
# https://developer.apple.com/documentation/foundation/nscopying.
# 2. An `init` call returns an object which we already own from a
# previous `alloc` call. See `init` handling in ObjCMethod. __call__.
#
# If the object is already in our cache, we end up owning more than one
# refcount. We release this additional refcount to prevent memory leaks.
if _implicitly_owned:
send_message(object_ptr, "release", restype=objc_id, argtypes=[])
return cached_obj
except KeyError:
pass
# Explicitly retain the instance on first handover to Python unless we
# received it from a method that gives us ownership already.
if not _implicitly_owned:
send_message(object_ptr, "retain", restype=objc_id, argtypes=[])
# If the given pointer points to a class, return an ObjCClass instead (if we're not already creating one).
if not issubclass(cls, ObjCClass) and object_isClass(object_ptr):
return ObjCClass(object_ptr)
# Otherwise, create a new ObjCInstance.
if issubclass(cls, type):
# Special case for ObjCClass to pass on the class name, bases and namespace to the type constructor.
self = super().__new__(cls, _name, _bases, _ns)
else:
if isinstance(object_ptr, objc_block):
cls = ObjCBlockInstance
else:
cls = type_for_objcclass(libobjc.object_getClass(object_ptr))
self = super().__new__(cls)
super(ObjCInstance, type(self)).__setattr__(self, "ptr", object_ptr)
super(ObjCInstance, type(self)).__setattr__(
self, "_as_parameter_", object_ptr
)
if isinstance(object_ptr, objc_block):
super(ObjCInstance, type(self)).__setattr__(
self, "block", ObjCBlock(object_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] = self
return self
def __del__(self):
# Autorelease our reference on garbage collection of the Python wrapper. We use
# autorelease instead of release to allow ObjC to take ownership of an object when
# it is returned from a factory method.
try:
send_message(self, "autorelease", restype=objc_id, argtypes=[])
except (NameError, TypeError):
# Handle interpreter shutdown gracefully where send_message might be deleted
# (NameError) or set to None (TypeError).
pass
def __str__(self):
"""Get a human-readable representation of ``self``.
By default, ``self.description`` converted to a Python string is
returned. If ``self.description`` is ``nil``,
``self.debugDescription`` converted to a Python is returned
instead. If that is also ``nil``, ``repr(self)`` is returned as
a fallback.
"""
desc = self.description
if desc is not None:
return str(desc)
desc = self.debugDescription
if desc is not None:
return str(desc)
return repr(self)
def __repr__(self):
"""Get a debugging representation of ``self``, which includes the
Objective-C object's class and ``debugDescription``."""
return (
f"<{type(self).__qualname__}: {self.objc_class.name} at "
f"{id(self):#x}: {self.debugDescription}>"
)
def __getattr__(self, name):
"""Allows accessing Objective-C properties and methods using Python
attribute syntax.
If ``self`` has a Python attribute with the given name, its value is
returned.
If there is an Objective-C property with the given name, its value is
returned using its getter method. An attribute is considered a property
if any of the following are true:
* A property with the name is present on the class (i.e. declared using
``@property`` in the source code)
* There is both a getter and setter method for the name
* The name has been declared as a property using
:meth:`ObjCClass.declare_property`
Otherwise, a method matching the given name is looked up.
:class:`ObjCInstance` understands two syntaxes for calling Objective-C
methods:
* "Flat" syntax: the Objective-C method name is spelled out in the
attribute name, with all colons replaced with underscores, and all
arguments are passed as positional arguments. For example, the
Objective-C method call ``[self initWithWidth:w height:h]`` translates
to ``self.initWithWidth_height_(w, h)``.
* "Interleaved" syntax: the Objective-C method name is split up between
the attribute name and the keyword arguments passed to the returned
method. For example, the Objective-C method call ``[self initWithRed:r
green:g blue:b]`` translates to ``self.initWithRed(r, green=g,
blue=b)``.
The "interleaved" syntax is usually preferred, since it looks more
similar to normal Objective-C syntax. However, the "flat" syntax is also
fully supported. If two arguments have the same name (e.g.
``performSelector:withObject:withObject:``), you can use ``__`` in the
keywords to disambiguate (e.g., ``performSelector(..., withObject__1=...,
withObject__2=...)``. Any content after and including the ``__`` in an argument
will be ignored.
"""
# 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.
# If there's a property with this name; return the value directly.
# If the name ends with _, we can shortcut this step, because it's
# clear that we're dealing with a method call.
if not name.endswith("_"):
method = self.objc_class._cache_property_accessor(name)
if method:
return ObjCBoundMethod(method, self)()
# See if there's a partial method starting with the given name,
# either on self's class or any of the superclasses.
cls = self.objc_class
while cls is not None:
# Load the class's methods if we haven't done so yet.
with cls.cache_lock:
if cls.methods_ptr is None:
cls._load_methods()
try:
method = cls.partial_methods[name]
break
except KeyError:
cls = cls.superclass
else:
method = None
if method is None or set(method.methods) == {()}:
# Find a method whose full name matches the given name if no partial
# method was found, or the partial method can only resolve to a
# single method that takes no arguments. The latter case avoids
# returning partial methods in cases where a regular method works
# just as well.
method = self.objc_class._cache_method(name.replace("_", ":"))
if method:
return ObjCBoundMethod(method, self)
# Check if the attribute name corresponds to an instance attribute defined at
# runtime from Python. Return it if yes, raise an AttributeError otherwise.
key = self._associated_attr_key_for_name(name)
pyo_wrapper = libobjc.objc_getAssociatedObject(self, key)
if pyo_wrapper.value is None:
raise AttributeError(
f"{type(self).__module__}.{type(self).__qualname__} {self.objc_class.name} has no attribute {name}"
)
address = get_ivar(pyo_wrapper, "wrapped_pointer")
pyo = cast(address.value, py_object)
return pyo.value
def __setattr__(self, name, value):
"""Allows modifying Objective-C properties using Python syntax.
If ``self`` has a Python attribute with the given name, it is set.
Otherwise, the name should refer to an Objective-C property, whose
setter method is called with ``value``.
"""
if name in self.__dict__:
# For attributes already in __dict__, use the default __setattr__.
super(ObjCInstance, type(self)).__setattr__(self, name, value)
else:
method = self.objc_class._cache_property_mutator(name)
if method:
# Convert enums to their underlying values.
if isinstance(value, enum.Enum):
value = value.value
ObjCBoundMethod(method, self)(value)
else:
# Wrap the Python object in a WrappedPyObject instance.
# A reference will be retained as long as the WrappedPyObject is alive.
wrapper = send_message(
send_message(
get_class("WrappedPyObject"),
"alloc",
restype=objc_id,
argtypes=[],
),
"initWithObjectId:",
id(value),
restype=objc_id,
argtypes=[objc_id],
)
# Set the Python value as an associated object. This will release
# any previous wrapper object with the same key.
key = self._associated_attr_key_for_name(name)
libobjc.objc_setAssociatedObject(self, key, wrapper, 0x301)
# Release the wrapper object, it will be retained by the association.
send_message(wrapper, "release", restype=objc_id, argtypes=[])
def __delattr__(self, name):
if name in self.__dict__:
# For attributes already in __dict__, use the default __delattr__.
super(ObjCInstance, type(self)).__delattr__(self, name)
else:
key = self._associated_attr_key_for_name(name)
# Check for instance attributes defined at runtime.
pyo_wrapper = libobjc.objc_getAssociatedObject(self, key)
if pyo_wrapper.value is None:
raise AttributeError(
f"{type(self).__module__}.{type(self).__qualname__} {self.objc_class.name} has no attribute {name}"
)
# If set, clear the instance attribute / associated object.
libobjc.objc_setAssociatedObject(self, key, None, 0x301)
# The inheritance order is important here.
# type must come after ObjCInstance, so super() refers to ObjCInstance.
# This allows the ObjCInstance constructor to receive the class pointer
# as well as the name, bases, attrs arguments.
# The other way around this would not be possible, because then
# the type constructor would be called before ObjCInstance's, and there
# would be no opportunity to pass extra arguments.
class ObjCClass(ObjCInstance, type):
"""Python wrapper for an Objective-C class.
:class:`ObjCClass` is a subclass of :class:`ObjCInstance` and supports the
same syntaxes for calling methods and accessing properties.
"""
@property
def superclass(self):
"""The superclass of this class, or ``None`` if this is a root class
(such as :class:`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))
auto_rename = False
"""A :any:`bool` value describing whether a defined class should be renamed automatically
if a class with the same name already exists in the Objective C runtime."""
@classmethod
def _new_from_name(cls, name):
name = ensure_bytes(name)
ptr = get_class(name)
if ptr.value is None:
raise NameError(f"ObjC Class {name} couldn't be found.")
return ptr, name
@classmethod
def _new_from_ptr(cls, ptr):
ptr = cast(ptr, Class)
if ptr.value is None:
raise ValueError("Cannot create ObjCClass from nil pointer")
elif not object_isClass(ptr):
raise ValueError(
f"Pointer {ptr} ({ptr.value:#x}) does not refer to a class"
)
name = libobjc.class_getName(ptr)
return ptr, name
@classmethod
def _new_from_class_statement(cls, name, bases, attrs, *, protocols, auto_rename):
basename = name
name = ensure_bytes(name)
if get_class(name).value is not None:
if auto_rename or auto_rename is None and cls.auto_rename:
suffix = 1
while get_class(name).value is not None:
suffix += 1
name = f"{basename}_{suffix}".encode()
else:
raise RuntimeError(
f"An Objective-C class named {name!r} already exists"
)
try:
(superclass,) = bases
except ValueError:
raise ValueError(
f"An Objective-C class must have exactly one base class, not {len(bases)}"
)
# Check that the superclass is an ObjCClass.
if not isinstance(superclass, ObjCClass):
raise TypeError(
f"The superclass of an Objective-C class must be an ObjCClass, "
f"not a {type(superclass).__module__}.{type(superclass).__qualname__}"
)
# Check that all protocols are ObjCProtocols, and that there are no duplicates.
for proto in protocols:
if not isinstance(proto, ObjCProtocol):
raise TypeError(
f"The protocols list of an Objective-C class must contain ObjCProtocol objects, "
f"not {type(proto).__module__}.{type(proto).__qualname__}"
)
elif protocols.count(proto) > 1:
raise ValueError(f"Protocol {proto.name} is adopted more than once")
# Create the ObjC class description
ptr = libobjc.objc_allocateClassPair(superclass, name, 0)
if ptr is None:
raise RuntimeError("Class pair allocation failed")
# Adopt all the protocols.
for proto in protocols:
if not libobjc.class_addProtocol(ptr, proto):
raise RuntimeError(f"Failed to adopt protocol {proto.name}")
# Register all methods, properties, ivars, etc.
for attr_name, obj in attrs.items():
if attr_name != "dealloc":
try:
class_register = obj.class_register
except AttributeError:
pass
else:
class_register(ptr, attr_name)
# Register any user-defined dealloc method. We treat dealloc differently to
# inject our own cleanup code for properties, ivars, etc.
user_dealloc = attrs.get("dealloc", None)
def _new_delloc(objc_self, _cmd):
# Invoke user-defined dealloc.
if user_dealloc:
user_dealloc(objc_self, _cmd)
# Invoke dealloc callback of each attribute. Currently
# defined for properties only.
for attr_name, obj in attrs.items():
try:
dealloc_callback = obj.dealloc_callback
except AttributeError:
pass
else:
dealloc_callback(objc_self, attr_name)
# Invoke super dealloc.
send_super(
ptr,
objc_self,
"dealloc",
restype=None,
argtypes=[],
_allow_dealloc=True,
)
add_method(ptr, "dealloc", _new_delloc, [None, ObjCInstance, SEL])
# Register the ObjC class
libobjc.objc_registerClassPair(ptr)
return ptr, name, attrs
def __new__(
cls,
name_or_ptr,
bases=None,
attrs=None,
*,
protocols=(),
auto_rename=None,
):
"""The constructor accepts either the name of an Objective-C class to
look up (as :class:`str` or :class:`bytes`), or a pointer to an existing
class object (in any form accepted by :class:`ObjCInstance`).
If given a pointer, it must refer to an Objective-C class; pointers to
other objects are not accepted. (Use :class:`ObjCInstance` to wrap a
pointer that might also refer to other kinds of objects.) If the pointer
refers to a metaclass, an instance of :class:`ObjCMetaClass` is returned
instead. Creating an :class:`ObjCClass` from a ``Nil`` pointer returns
``None``.
:class:`ObjCClass` can also be called like :class:`type`, with three
arguments (name, bases list, namespace mapping). This form is called
implicitly by Python's ``class`` syntax, and is used to create a new
Objective-C class from Python (see :ref:`custom-classes-and-protocols`).
The bases list must contain exactly one :class:`ObjCClass` to be
extended by the new class. An optional ``protocols`` keyword argument is
also accepted, which must be a sequence of :class:`ObjCProtocol`\\s for
the new class to adopt.
If the name of the class has already registered with the Objective C
runtime, the ``auto_rename`` option can be used to ensure that the
Objective C name for the new class will be unique. A numeric suffix will
be appended to the Objective C name to ensure uniqueness (for example,
``MyClass`` will be renamed to ``MyClass_2``, ``MyClass_3`` etc until a
unique name is found). By default, classes will *not* be renamed, unless
:attr:`ObjCClass.auto_rename` is set at the class level.
"""
if (bases is None) ^ (attrs is None):
raise TypeError("ObjCClass arguments 2 and 3 must be given together")
if bases is None and attrs is None:
# A single argument provided. If it's a string, treat it as
# a class name. Anything else treat as a class pointer.
if protocols:
raise ValueError(
"protocols kwarg is not allowed for the single-argument form of ObjCClass"
)
attrs = {}
if isinstance(name_or_ptr, (bytes, str)):
ptr, name = cls._new_from_name(name_or_ptr)
else:
ptr, name = cls._new_from_ptr(name_or_ptr)
if not issubclass(cls, ObjCMetaClass) and libobjc.class_isMetaClass(
ptr
):
return ObjCMetaClass(ptr)
else:
ptr, name, attrs = cls._new_from_class_statement(
name_or_ptr,
bases,
attrs,
protocols=protocols,
auto_rename=auto_rename,
)
objc_class_name = name.decode("utf-8")
new_attrs = {
"name": objc_class_name,
"methods_ptr": None,
# Mapping of name -> method pointer
"instance_method_ptrs": {},
# Mapping of name -> instance method
"instance_methods": {},
# Mapping of name -> (accessor method, mutator method)
"instance_properties": {},
# Explicitly declared properties
"forced_properties": set(),
# Mapping of first keyword -> ObjCPartialMethod instances
"partial_methods": {},
# A re-entrant thread lock moderating access to the ObjCClass
# method/property cache. This ensures that only one thread populates
# the cache of methods/properties on each class. The lock is
# re-entrant because there are some dependencies between caches
# (e.g., cache_property_accessor calls cache_method).
"cache_lock": threading.RLock(),
}
# On Python 3.6 and later, the class namespace may contain a
# __classcell__ attribute that must be passed on to type.__new__. See
# https://docs.python.org/3/reference/datamodel.html#creating-the-class-object
if "__classcell__" in attrs:
new_attrs["__classcell__"] = attrs["__classcell__"]
# Create the class object. If there is already a cached instance for ptr,
# it is returned and the additional arguments are ignored.
# Logically this can only happen when creating an ObjCClass from an existing
# name or pointer, not when creating a new class.
# If there is no cached instance for ptr, a new one is created and cached.
self = super().__new__(cls, ptr, objc_class_name, (ObjCInstance,), new_attrs)
return self
def __init__(self, *args, **kwargs):
# Prevent kwargs from being passed on to type.__init__, which does not
# accept any kwargs in Python < 3.6.
super().__init__(*args)
def _cache_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."""
with self.cache_lock:
try:
# Try to return an existing cached method for the name
return self.instance_methods[name]
except KeyError:
supercls = self
objc_method = None
while supercls is not None:
# Load the class's methods if we haven't done so yet.
if supercls.methods_ptr is None:
supercls._load_methods()
try:
objc_method = supercls.instance_methods[name]
break
except KeyError:
pass
try:
objc_method = ObjCMethod(supercls.instance_method_ptrs[name])
break
except KeyError:
pass
supercls = supercls.superclass
if objc_method is None:
return None
else:
self.instance_methods[name] = objc_method
return objc_method
def _cache_property_methods(self, name):
"""Return the accessor and mutator for the named property."""
if name.endswith("_"):
# If the requested name ends with _, that's a marker that we're
# dealing with a method call, not a property, so we can shortcut
# the process.
methods = None
else:
# Check 1: Does the class respond to the property?
responds = libobjc.class_getProperty(self, name.encode("utf-8"))
# Check 2: Does the class have an instance method to retrieve the given name
accessor = self._cache_method(name)
# Check 3: Is there a setName: method to set the property with the given name
mutator = self._cache_method("set" + name[0].title() + name[1:] + ":")
# Check 4: Is this a forced property on this class or a superclass?
forced = False
superclass = self
while superclass is not None:
if name in superclass.forced_properties:
forced = True
break
superclass = superclass.superclass
# If the class responds as a property, or it has both an accessor *and*
# and mutator, then treat it as a property in Python.
if responds or (accessor and mutator) or forced:
methods = (accessor, mutator)
else:
methods = None
return methods
def _cache_property_accessor(self, name):
"""Returns a python representation of an accessor for the named
property.
Existence of a property is done by looking for the write
selector (set<Name>:).
"""
with self.cache_lock:
try:
methods = self.instance_properties[name]
except KeyError:
methods = self._cache_property_methods(name)
self.instance_properties[name] = methods
if methods:
return methods[0]
return None
def _cache_property_mutator(self, name):
"""Returns a python representation of an accessor for the named
property.
Existence of a property is done by looking for the write
selector (set<Name>:).
"""
with self.cache_lock:
try:
methods = self.instance_properties[name]
except KeyError:
methods = self._cache_property_methods(name)
self.instance_properties[name] = methods
if methods:
return methods[1]
return None
def declare_property(self, name):
"""Declare the instance method ``name`` to be a property getter.
This causes the attribute named ``name`` on instances of this class to be
treated as a property rather than a method --- accessing it returns the
property's value, without requiring an explicit method call. See
:class:`ObjCInstance.__getattr__` for a full description of how attribute access
behaves for properties.
Most properties do not need to be declared explicitly using this method, as they
are detected automatically by :class:`ObjCInstance.__getattr__`. This method
only needs to be used for properties that are read-only and don't have a
``@property`` declaration in the source code, because Rubicon cannot tell such
properties apart from normal zero-argument methods.
.. note::
In the standard Apple SDKs, some properties are introduced as regular
methods in one system version, and then declared as properties in a later
system version. For example, the ``description`` method/property of
:class:`NSObject` was declared as a regular method `up to OS X 10.9
<https://github.com/phracker/MacOSX-SDKs/blob/9fc3ed0ad0345950ac25c28695b0427846eea966/MacOSX10.9.sdk/usr/include/objc/NSObject.h#L40>`__,
but changed to a property `as of OS X 10.10
<https://github.com/phracker/MacOSX-SDKs/blob/9fc3ed0ad0345950ac25c28695b0427846eea966/MacOSX10.10.sdk/usr/include/objc/NSObject.h#L43>`__.
Such properties cause compatibility issues when accessed from Rubicon:
``obj.description()`` works on 10.9 but is a :class:`TypeError` on 10.10,
whereas ``obj.description`` works on 10.10 but returns a method object on
10.9. To solve this issue, the property can be declared explicitly using
``NSObject.declare_property('description')``, so that it can always be
accessed using ``obj.description``.
"""
self.forced_properties.add(name)
def declare_class_property(self, name):
"""Declare the class method ``name`` to be a property getter.
This is equivalent to
``self.objc_class.declare_property(name)``.
"""
self.objc_class.forced_properties.add(name)
def __repr__(self):
return f"<{type(self).__qualname__}: {self.name}>"
def __str__(self):
return f"{type(self).__name__}({self.name!r})"
def __del__(self):
libc.free(self.methods_ptr)
def __instancecheck__(self, instance):
"""Check whether the given object is an instance of this class.
If the given object is not an Objective-C object, ``False`` is returned.
This method allows using :class:`ObjCClass`\\es as the second argument
of :func:`isinstance`: ``isinstance(obj, NSString)`` is equivalent to
``obj.isKindOfClass(NSString)``.
"""
if isinstance(instance, ObjCInstance):
return bool(instance.isKindOfClass(self))
else:
return False
def __subclasscheck__(self, subclass):
"""Check whether the given class is a subclass of this class.
If the given object is not an Objective-C class, :class:`TypeError` is
raised.
This method allows using :class:`ObjCClass`\\es as the second argument
of :func:`issubclass`: ``issubclass(cls, NSValue)`` is equivalent to
``obj.isSubclassOfClass(NSValue)``.
"""
if isinstance(subclass, ObjCClass):
return bool(subclass.isSubclassOfClass(self))
else:
raise TypeError(
f"issubclass(X, {self!r}) arg 1 must be an ObjCClass, "
f"not {type(subclass).__module__}.{type(subclass).__qualname__}"
)
def _load_methods(self):
if self.methods_ptr is not None:
raise RuntimeError(f"{self}._load_methods cannot be called more than once")
# Traverse superclasses and load methods.
superclass = self.superclass
while superclass is not None:
if superclass.methods_ptr is None:
with superclass.cache_lock:
superclass._load_methods()
# Prime this class' partials list with a list from the superclass.
for first, superpartial in superclass.partial_methods.items():
partial = ObjCPartialMethod(first)
self.partial_methods[first] = partial
partial.methods.update(superpartial.methods)
superclass = superclass.superclass
# Load methods for this class.
methods_ptr_count = c_uint(0)
methods_ptr = libobjc.class_copyMethodList(self, byref(methods_ptr_count))
for i in range(methods_ptr_count.value):
method = methods_ptr[i]
name = libobjc.method_getName(method).name.decode("utf-8")
self.instance_method_ptrs[name] = method
base_name, argument_names = method_name_to_tuple(name)
try:
partial = self.partial_methods[base_name]
except KeyError:
partial = ObjCPartialMethod(base_name)
self.partial_methods[base_name] = partial
partial.methods[argument_names] = name
# Set the list of methods for the class to the computed list.
self.methods_ptr = methods_ptr
class ObjCMetaClass(ObjCClass):
"""Python wrapper for an Objective-C metaclass.
:class:`ObjCMetaClass` is a subclass of :class:`ObjCClass` and supports
almost exactly the same operations and methods. However, there is usually no
need to look up a metaclass manually. The main reason why
:class:`ObjCMetaClass` is a separate class is to differentiate it from
:class:`ObjCClass` in the :func:`repr`. (Otherwise there would be no way to
tell classes and metaclasses apart, since metaclasses are also classes, and
have exactly the same name as their corresponding class.)
"""
def __new__(cls, name_or_ptr):
"""The constructor accepts either the name of an Objective-C metaclass
to look up (as :class:`str` or :class:`bytes`), or a pointer to an
existing metaclass object (in any form accepted by
:class:`ObjCInstance`).
If given a pointer, it must refer to an Objective-C metaclass; pointers
to other objects are not accepted. (Use :class:`ObjCInstance` to wrap a
pointer that might also refer to other kinds of objects.) Creating an
:class:`ObjCMetaClass` from a ``Nil`` pointer returns ``None``.
"""
if isinstance(name_or_ptr, (bytes, str)):
name = ensure_bytes(name_or_ptr)
ptr = libobjc.objc_getMetaClass(name)
if ptr.value is None:
raise NameError(f"Objective-C metaclass {name} not found")
else:
ptr = cast(name_or_ptr, Class)
if ptr.value is None:
raise ValueError("Cannot create ObjCMetaClass for nil pointer")
elif not object_isClass(ptr) or not libobjc.class_isMetaClass(ptr):
raise ValueError(
f"Pointer {ptr} ({ptr.value:#x}) does not refer to a metaclass"
)
return super().__new__(cls, ptr)
register_ctype_for_type(ObjCInstance, objc_id)
register_ctype_for_type(ObjCClass, Class)
NSObject = ObjCClass("NSObject")
NSObject.declare_property("debugDescription")
NSObject.declare_property("description")
NSNumber = ObjCClass("NSNumber")
NSDecimalNumber = ObjCClass("NSDecimalNumber")
NSString = ObjCClass("NSString")
NSString.declare_property("UTF8String")
NSData = ObjCClass("NSData")
NSArray = ObjCClass("NSArray")
NSMutableArray = ObjCClass("NSMutableArray")
NSDictionary = ObjCClass("NSDictionary")
NSMutableDictionary = ObjCClass("NSMutableDictionary")
Protocol = ObjCClass("Protocol")
def py_from_ns(nsobj):
"""Convert a Foundation object into an equivalent Python object if
possible.
Currently supported types:
* :class:`~rubicon.objc.runtime.objc_id`: Wrapped in an
:class:`ObjCInstance` and converted as below
* :class:`NSString`: Converted to :class:`str`
* :class:`NSData`: Converted to :class:`bytes`
* :class:`NSDecimalNumber`: Converted to :class:`decimal.Decimal`
* :class:`NSDictionary`: Converted to :class:`dict`, with all keys and
values converted recursively
* :class:`NSArray`: Converted to :class:`list`, with all elements converted
recursively
* :class:`NSNumber`: Converted to a :class:`bool`, :class:`int` or
:class:`float` based on the type of its contents
Other objects are returned unmodified as an :class:`ObjCInstance`.
"""
if isinstance(nsobj, (objc_id, Class)):
nsobj = ObjCInstance(nsobj)
if not isinstance(nsobj, ObjCInstance):
return nsobj
if nsobj.isKindOfClass(NSDecimalNumber):
return decimal.Decimal(str(nsobj.descriptionWithLocale(None)))
elif nsobj.isKindOfClass(NSNumber):
# Choose the property to access based on the type encoding. The actual
# conversion is done by ctypes. Signed and unsigned integers are in
# separate cases to prevent overflow with unsigned long longs.
objc_type = nsobj.objCType
if objc_type == b"B":
return nsobj.boolValue
elif objc_type in b"csilq":
return nsobj.longLongValue
elif objc_type in b"CSILQ":
return nsobj.unsignedLongLongValue
elif objc_type in b"fd":
return nsobj.doubleValue
else:
raise TypeError(
f"NSNumber containing unsupported type {objc_type!r} "
"cannot be converted to a Python object"
)
elif nsobj.isKindOfClass(NSString):
return str(nsobj)
elif nsobj.isKindOfClass(NSData):
# Despite the name, string_at converts the data at the address to a
# bytes object, not str.
return string_at(
send_message(nsobj, "bytes", restype=POINTER(c_uint8), argtypes=[]),
nsobj.length,
)
elif nsobj.isKindOfClass(NSDictionary):
return {py_from_ns(k): py_from_ns(v) for k, v in nsobj.items()}
elif nsobj.isKindOfClass(NSArray):
return [py_from_ns(o) for o in nsobj]
else:
return nsobj
def ns_from_py(pyobj):
"""Convert a Python object into an equivalent Foundation object. The
returned object is autoreleased.
This function is also available under the name :func:`at`, because its
functionality is very similar to that of the Objective-C ``@`` operator and
literals.
Currently supported types:
* ``None``, :class:`ObjCInstance`: Returned as-is
* :class:`enum.Enum`: Replaced by their :attr:`~enum.Enum.value` and
converted as below
* :class:`str`: Converted to :class:`NSString`
* :class:`bytes`: Converted to :class:`NSData`
* :class:`decimal.Decimal`: Converted to :class:`NSDecimalNumber`
* :class:`dict`: Converted to :class:`NSDictionary`, with all keys and
values converted recursively
* :class:`list`: Converted to :class:`NSArray`, with all elements converted
recursively
* :class:`bool`, :class:`int`, :class:`float`: Converted to
:class:`NSNumber`
Other types cause a :class:`TypeError`.
"""
if isinstance(pyobj, enum.Enum):
pyobj = pyobj.value
# Many Objective-C method calls here use the convert_result=False kwarg to
# disable automatic conversion of return values, because otherwise most of
# the Objective-C objects would be converted back to Python objects.
if pyobj is None or isinstance(pyobj, ObjCInstance):
return pyobj
elif isinstance(pyobj, str):
return ObjCInstance(
NSString.stringWithUTF8String_(pyobj.encode("utf-8"), convert_result=False)
)
elif isinstance(pyobj, bytes):
return ObjCInstance(NSData.dataWithBytes(pyobj, length=len(pyobj)))
elif isinstance(pyobj, decimal.Decimal):
return ObjCInstance(
NSDecimalNumber.decimalNumberWithString_(
pyobj.to_eng_string(), convert_result=False
)
)
elif isinstance(pyobj, dict):
dikt = NSMutableDictionary.dictionaryWithCapacity(len(pyobj))
for k, v in pyobj.items():
dikt.setObject(v, forKey=k)
return dikt
elif isinstance(pyobj, list):
array = NSMutableArray.arrayWithCapacity(len(pyobj))
for v in pyobj:
array.addObject(v)
return array
elif isinstance(pyobj, bool):
return ObjCInstance(NSNumber.numberWithBool_(pyobj, convert_result=False))
elif isinstance(pyobj, int):
return ObjCInstance(NSNumber.numberWithLong_(pyobj, convert_result=False))
elif isinstance(pyobj, float):
return ObjCInstance(NSNumber.numberWithDouble_(pyobj, convert_result=False))
else:
raise TypeError(
f"Don't know how to convert a {type(pyobj).__module__}.{type(pyobj).__qualname__} to a Foundation object"
)
at = ns_from_py
@for_objcclass(Protocol)
class ObjCProtocol(ObjCInstance):
"""Python wrapper for an Objective-C protocol."""
@property
def name(self):
"""The name of this protocol, as a :class:`str`."""
return libobjc.protocol_getName(self).decode("utf-8")
@property
def protocols(self):
"""The protocols that this protocol extends."""
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))
auto_rename = False
"""A :class:`bool` value whether a defined protocol should be renamed
automatically if a protocol with the same name is already exists."""
def __new__(cls, name_or_ptr, bases=None, ns=None, auto_rename=None):
"""The constructor accepts either the name of an Objective-C protocol
to look up (as :class:`str` or :class:`bytes`), or a pointer to an
existing protocol object (in any form accepted by
:class:`ObjCInstance`).
If given a pointer, it must refer to an Objective-C protocol; pointers
to other objects are not accepted. (Use :class:`ObjCInstance` to wrap a
pointer that might also refer to other kinds of objects.) Creating an
:class:`ObjCProtocol` from a ``nil`` pointer returns ``None``.
:class:`ObjCProtocol` can also be called like :class:`type`, with three
arguments (name, bases list, namespace mapping). This form is called
implicitly by Python's ``class`` syntax, and is used to create a new
Objective-C protocol from Python (see
:ref:`custom-classes-and-protocols`). The bases list can contain any
number of :class:`ObjCProtocol` objects to be extended by the new
protocol.
If the name of the protocol has already registered with the Objective C
runtime, the ``auto_rename`` option can be used to ensure that the
Objective C name for the new protocol will be unique. A numeric suffix
will be appended to the Objective C name to ensure uniqueness (for
example, ``MyProtocol`` will be renamed to ``MyProtocol_2``,
``MyProtocol_3`` etc until a unique name is found). By default,
protocols will *not* be renamed, unless
:attr:`ObjCProtocol.auto_rename` is set at the class level.
"""
if (bases is None) ^ (ns is None):
raise TypeError("ObjCProtocol arguments 2 and 3 must be given together")
if bases is None and ns is None:
if isinstance(name_or_ptr, (bytes, str)):
name = ensure_bytes(name_or_ptr)
ptr = libobjc.objc_getProtocol(name)
if ptr.value is None:
raise NameError(f"Objective-C protocol {name} not found")
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(
f"Pointer {ptr} ({ptr.value:#x}) does not refer to a protocol"
)
else:
basename = name_or_ptr
name = ensure_bytes(name_or_ptr)
# Rename the protocol that will be defined if the auto_rename option is True.
if libobjc.objc_getProtocol(name).value is not None:
if auto_rename or auto_rename is None and cls.auto_rename:
suffix = 1
while libobjc.objc_getProtocol(name).value is not None:
suffix += 1
name = f"{basename}_{suffix}".encode()
else:
raise RuntimeError(
f"An Objective-C protocol named {name!r} already exists"
)
# Check that all bases are protocols.
for base in bases:
if not isinstance(base, ObjCProtocol):
raise TypeError(
f"An Objective-C protocol can only extend ObjCProtocol objects, "
f"not {type(base).__module__}.{type(base).__qualname__}"
)
# Allocate the protocol object.
ptr = libobjc.objc_allocateProtocol(name)
if ptr is None:
raise RuntimeError("Protocol allocation failed")
# Adopt all the protocols.
for proto in bases:
libobjc.protocol_addProtocol(ptr, proto)
# Register all methods and properties.
for attr_name, obj in ns.items():
if hasattr(obj, "protocol_register"):
obj.protocol_register(ptr, attr_name)
# Register the protocol object
libobjc.objc_registerProtocol(ptr)
return super().__new__(cls, ptr)
def __repr__(self):
return f"<{type(self).__qualname__}: {self.name}>"
def __instancecheck__(self, instance):
"""Check whether the given object conforms to this protocol.
If the given object is not an Objective-C object, ``False`` is returned.
This method allows using :class:`ObjCProtocol`\\s as the second argument
of :func:`isinstance`: ``isinstance(obj, NSCopying)`` is equivalent to
``obj.conformsToProtocol(NSCopying)``.
"""
if isinstance(instance, ObjCInstance):
return bool(instance.conformsToProtocol(self))
else:
return False
def __subclasscheck__(self, subclass):
"""Check whether the given class or protocol conforms to this protocol.
If the given object is not an Objective-C class or protocol,
:class:`TypeError` is raised.
This method allows using :class:`ObjCProtocol`\\s as the second argument
of :func:`issubclass`: ``issubclass(cls, NSCopying)`` is equivalent to
``cls.conformsToProtocol(NSCopying)``, and ``issubclass(proto,
NSCopying)`` is equivalent to ``protocol_conformsToProtocol(proto,
NSCopying))``.
"""
if isinstance(subclass, ObjCClass):
return bool(subclass.conformsToProtocol(self))
elif isinstance(subclass, ObjCProtocol):
return bool(libobjc.protocol_conformsToProtocol(subclass, self))
else:
raise TypeError(
f"issubclass(X, {self!r}) arg 1 must be an ObjCClass or ObjCProtocol, "
f"not {type(subclass).__module__}.{type(subclass).__qualname__}"
)
# 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")
# When a Python object is assigned to a new ObjCInstance attribute, the Python
# object should be kept alive for the lifetime of the ObjCInstance. This is done
# by wrapping the Python object as a WrappedPyObject that increments the
# reference count during assignment and decrements it when the WrappedPyObject
# and the owning ObjCInstance are deallocated.
#
# The methods of the class defined below are decorated with rawmethod() instead
# of method() because WrappedPyObject 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.
#
# Try to reuse an existing WrappedPyObject class. This allows reloading the
# module without having to restart the interpreter, although any changes to
# WrappedPyObject itself are only applied after a restart of course.
try:
WrappedPyObject = ObjCClass("WrappedPyObject")
except NameError:
class WrappedPyObject(NSObject):
wrapped_pointer = objc_ivar(c_void_p)
@objc_rawmethod
def initWithObjectId_(self, cmd, address):
self = send_message(self, "init", restype=objc_id, argtypes=[])
if self is not None:
pyo = cast(address, py_object)
_keep_alive_objects[(self.value, address.value)] = pyo.value
set_ivar(self, "wrapped_pointer", address)
return self.value
@objc_rawmethod
def dealloc(self, cmd) -> None:
address = get_ivar(self, "wrapped_pointer")
if address.value:
del _keep_alive_objects[(self.value, address.value)]
@objc_rawmethod
def finalize(self, cmd) -> None:
# 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.)
address = get_ivar(self, "wrapped_pointer")
if address.value:
del _keep_alive_objects[(self.value, address.value)]
send_super(__class__, self, "finalize", restype=None, argtypes=[])
def objc_const(dll, name):
"""Create an :class:`ObjCInstance` from a global pointer variable in a
:class:`~ctypes.CDLL`.
This function is most commonly used to access constant object pointers
defined by a library/framework, such as `NSCocoaErrorDomain
<https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc>`__.
"""
return ObjCInstance(objc_id.in_dll(dll, name))
_cfunc_type_block_invoke = CFUNCTYPE(c_void_p, c_void_p)
_cfunc_type_block_dispose = CFUNCTYPE(c_void_p, c_void_p)
_cfunc_type_block_copy = CFUNCTYPE(c_void_p, c_void_p, c_void_p)
class ObjCBlockStruct(Structure):
_fields_ = [
("isa", c_void_p),
("flags", c_int),
("reserved", c_int),
("invoke", _cfunc_type_block_invoke),
("descriptor", c_void_p),
]
class BlockDescriptor(Structure):
_fields_ = [
("reserved", c_ulong),
("size", c_ulong),
("copy_helper", _cfunc_type_block_copy),
("dispose_helper", _cfunc_type_block_dispose),
("signature", c_char_p),
]
class BlockLiteral(Structure):
_fields_ = [
("isa", c_void_p),
("flags", c_int),
("reserved", c_int),
("invoke", c_void_p), # NB: this must be c_void_p due to variadic nature
("descriptor", c_void_p),
]
def create_block_descriptor_struct(has_helpers, has_signature):
descriptor_fields = [
("reserved", c_ulong),
("size", c_ulong),
]
if has_helpers:
descriptor_fields.extend(
[
("copy_helper", _cfunc_type_block_copy),
("dispose_helper", _cfunc_type_block_dispose),
]
)
if has_signature:
descriptor_fields.extend(
[
("signature", c_char_p),
]
)
return type("ObjCBlockDescriptor", (Structure,), {"_fields_": descriptor_fields})
def cast_block_descriptor(block):
descriptor_struct = create_block_descriptor_struct(
block.has_helpers, block.has_signature
)
return cast(block.struct.contents.descriptor, POINTER(descriptor_struct))
AUTO = object()
class BlockConsts:
HAS_COPY_DISPOSE = 1 << 25
HAS_CTOR = 1 << 26
IS_GLOBAL = 1 << 28
HAS_STRET = 1 << 29
HAS_SIGNATURE = 1 << 30
class ObjCBlock:
"""Python wrapper for an Objective-C block object.
This class is used to manually wrap an Objective-C block so that it
can be called from Python. Usually Rubicon will do this
automatically, if the block object was returned from an Objective-C
method whose return type is declared to be a block type. If this
automatic detection fails, for example if the method's return type
is generic ``id``, Rubicon has no way to tell that the object in
question is a block rather than a regular Objective-C object. In
that case, the object needs to be manually wrapped using
:class:`ObjCBlock`.
"""
def __init__(self, pointer, restype=AUTO, *argtypes):
"""The constructor takes a block object, which can be either an
:class:`ObjCInstance`, or a raw :class:`~rubicon.objc.runtime.objc_id`
pointer.
.. note::
:class:`~rubicon.objc.runtime.objc_block` is also accepted,
because it is a subclass of :class:`~rubicon.objc.runtime.objc_id`).
Normally you do not need to make use of this, because in most cases
Rubicon will automatically convert
:class:`~rubicon.objc.runtime.objc_block`\\s to a callable object.
In most cases, Rubicon can automatically determine the block's return
type and parameter types. If a block object doesn't have return/parameter
type information at runtime, Rubicon will raise an error when attempting
to convert it. In that case, you need to explicitly pass the correct
return type and parameter types to :class:`ObjCBlock` using the
``restype`` and ``argtypes`` parameters.
"""
if isinstance(pointer, ObjCInstance):
pointer = pointer.ptr
self.pointer = pointer
self.struct = cast(self.pointer, POINTER(ObjCBlockStruct))
self.has_helpers = self.struct.contents.flags & BlockConsts.HAS_COPY_DISPOSE
self.has_signature = self.struct.contents.flags & BlockConsts.HAS_SIGNATURE
self.descriptor = cast_block_descriptor(self)
self.signature = (
self.descriptor.contents.signature if self.has_signature else None
)
if restype is AUTO:
if argtypes:
raise ValueError("Cannot use argtypes with restype AUTO")
if not self.has_signature:
raise ValueError("Cannot use AUTO types for blocks without signatures")
restype, *argtypes = ctypes_for_method_encoding(self.signature)
# If the argtypes have been derived from the signature, they will include
# the block as the first argument.
block_arg = []
else:
# If the argtypes are explicitly provided, they *won't* include the
# first required argument - the block itself.
block_arg = [objc_id]
# If you set restype and argtypes on the invoke function that is in the
# ObjCBlockStruct, subsequent gets won't reflect those changes, because it's not
# a distinct Python object that ctypes can use to attach a type hint. Store the
# ctypes annotations, and apply them just before invocation. We're going to have
# to do some light type conversion in some cases anyway, so this works out well.
self.invoke_restype = ctype_for_type(restype)
self.invoke_argtypes = block_arg + [
ctype_for_type(arg_type) for arg_type in argtypes
]
def __repr__(self):
representation = f"<ObjCBlock@{hex(addressof(self.pointer))}"
if self.has_helpers:
representation += ",has_helpers"
if self.has_signature:
representation += ",has_signature:" + self.signature.decode("utf-8")
representation += ">"
return representation
def __call__(self, *args):
"""Invoke the block object with the given arguments.
The arguments and return value are converted from/to Python
objects according to the default ``ctypes`` rules, based on the
block's return and parameter types.
"""
# If any of the arguments are structures, they may be anonymous - that is, we
# have a descriptor like "{=ii}", which tells us there are two integer fields,
# but doesn't provide a name for the structure. ctypes looks for an exact match
# of type names, so even if the field types of a structure provided as an
# argument match, ctypes will raise a TypeError.
#
# To avoid this, if an argument is a structure, and the argtype for that
# argument is a structure, look for the `__anonymous__` property on the argtype
# structure definition - this property is added automatically to structures when
# a structure type is constructed from a type descriptor that doesn't provide a
# name. If it exists, the structure has been anonymously declared; so we check
# that the provided argument matches the "shape" of the anonymous structure. If
# it matches, modify the invoke signature to match the type of the argument that
# was actually provided. The first argument to invoke is the block being
# invoked, so we can ignore that type hint.
for i, argtype in enumerate(self.invoke_argtypes[1:]):
if (
isinstance(args[i], Structure)
and issubclass(argtype, Structure)
and getattr(argtype, "__anonymous__", False)
):
anon_fields = [f[1] for f in argtype._fields_]
arg_fields = [f[1] for f in args[i]._fields_]
if anon_fields != arg_fields:
raise TypeError(
f"Expected structure with field types {anon_fields} "
f"for argument {i+1}; got {type(args[i]).__name__} "
f"with field types {arg_fields}"
)
self.invoke_argtypes[i + 1] = type(args[i])
# Apply the ctypes hints to the invoke function for the block.
invoke = self.struct.contents.invoke
invoke.restype = self.invoke_restype
invoke.argtypes = self.invoke_argtypes
return invoke(self.pointer, *args)
class ObjCBlockInstance(ObjCInstance):
def __call__(self, *args):
return self.block(*args)
_NSConcreteStackBlock = (c_void_p * 32).in_dll(libc, "_NSConcreteStackBlock")
NOTHING = object()
class Block:
"""A wrapper that exposes a Python callable object to Objective-C as a
block.
.. note::
:class:`Block` instances are currently *not* callable from Python,
unlike :class:`ObjCBlock`.
"""
_keep_alive_blocks_ = {}
def __init__(self, func, restype=NOTHING, *argtypes):
"""The constructor accepts any Python callable object.
If the callable has parameter and return type annotations, they are used
as the block's parameter and return types. This allows using
:class:`Block` as a decorator:
.. code-block:: python
@Block
def the_block(arg: NSInteger) -> NSUInteger:
return abs(arg)
For callables without type annotations, the parameter and return types
need to be passed to the :class:`Block` constructor in the ``restype``
and ``argtypes`` arguments:
.. code-block:: python
the_block = Block(abs, NSUInteger, NSInteger)
"""
if not callable(func):
raise TypeError("Blocks must be callable")
self.func = func
if restype is NOTHING:
if argtypes:
# This can't happen unless the caller does something hacky, but
# guard against it just in case.
raise ValueError("Cannot pass argtypes without a restype")
# No explicit restype/argtypes were passed into the constructor,
# so try to extract them from the function's type annotations.
try:
hints = typing.get_type_hints(func)
signature = inspect.signature(func)
except (TypeError, ValueError):
raise ValueError(
"Could not retrieve function signature information - "
"please pass return and argument types directly into Block"
)
try:
restype = hints["return"]
except KeyError:
raise ValueError(
"Function has no return type annotation - "
"please add one, or pass return and argument types directly into Block"
)
argtypes = []
for name in signature.parameters:
try:
argtypes.append(hints[name])
except KeyError:
raise ValueError(
f"Function has no argument type annotation for parameter {name!r} - "
f"please add one, or pass return and argument types directly into Block"
)
signature = tuple(ctype_for_type(tp) for tp in argtypes)
restype = ctype_for_type(restype)
cfunc_type = CFUNCTYPE(restype, c_void_p, *signature)
self.literal = BlockLiteral()
self.literal.isa = addressof(_NSConcreteStackBlock)
self.literal.flags = (
BlockConsts.HAS_STRET
| BlockConsts.HAS_SIGNATURE
| BlockConsts.HAS_COPY_DISPOSE
)
self.literal.reserved = 0
cfunc_wrapper = cfunc_type(self.wrapper)
self.literal.invoke = cast(cfunc_wrapper, c_void_p)
self.descriptor = BlockDescriptor()
self.descriptor.reserved = 0
self.descriptor.size = sizeof(BlockLiteral)
self.cfunc_copy_helper = _cfunc_type_block_copy(self.copy_helper)
self.cfunc_dispose_helper = _cfunc_type_block_dispose(self.dispose_helper)
self.descriptor.copy_helper = self.cfunc_copy_helper
self.descriptor.dispose_helper = self.cfunc_dispose_helper
self.descriptor.signature = (
encoding_for_ctype(restype)
+ b"@?"
+ b"".join(encoding_for_ctype(arg) for arg in signature)
)
self.literal.descriptor = cast(byref(self.descriptor), c_void_p)
self.block = cast(byref(self.literal), objc_block)
self._as_parameter_ = self.block
def wrapper(self, block, *args):
# ObjC blocks take the block as the first argument when they're invoked;
# but since this is a wrapper around a Python object, we know the function
# that has to be invoked.
return self.func(*args)
def dispose_helper(self, dst):
Block._keep_alive_blocks_.pop(dst, None)
def copy_helper(self, dst, src):
# Update our keepalive table because objc just informed us that it
# took ownership of a block/copied a block we are concerned with.
# Note that sometime later we can expect calls to dispose_helper
# for each of the 'dst' blocks objc told us about, but until then we
# need to make sure the python code they reference stays in memory,
# so basically put self in a class variable dictionary so it is
# guaranteed to stay around until dispose_helper tells us they are all
# gone.
Block._keep_alive_blocks_[dst] = self