|
|
@@ -0,0 +1,1203 @@ |
|
|
# coding: utf-8 |
|
|
|
|
|
__all__ = ['c', 'LP64', 'CGFloat', 'NSInteger', 'NSUInteger', 'NSNotFound', 'NSUTF8StringEncoding', 'NS_UTF8', 'CGPoint', 'CGSize', 'CGVector', 'CGRect', 'CGAffineTransform', 'UIEdgeInsets', 'NSRange', 'sel', 'ObjCClass', 'ObjCInstance', 'ObjCClassMethod', 'ObjCInstanceMethod', 'NSObject', 'NSArray', 'NSMutableArray', 'NSDictionary', 'NSMutableDictionary', 'NSSet', 'NSMutableSet', 'NSString', 'NSMutableString', 'NSData', 'NSMutableData', 'NSNumber', 'NSURL', 'NSEnumerator', 'NSThread', 'NSBundle', 'UIColor', 'UIImage', 'UIBezierPath', 'UIApplication', 'UIView', 'ObjCBlock', 'ns', 'nsurl', 'retain_global', 'release_global', 'on_main_thread', 'create_objc_class', |
|
|
'Structure', 'sizeof', 'byref', 'c_void_p', 'c_char', 'c_byte', 'c_char_p', 'c_double', 'c_float', 'c_int', 'c_longlong', 'c_short', 'c_bool', 'c_long', 'c_int32', 'c_ubyte', 'c_uint', 'c_ushort', 'c_ulong', 'c_ulonglong', 'POINTER', 'pointer', 'load_framework', 'nsdata_to_bytes', 'uiimage_to_png'] |
|
|
|
|
|
try: |
|
|
import ctypes |
|
|
except ImportError: |
|
|
raise NotImplementedError("objc_util requires ctypes, which doesn't seem to be available.") |
|
|
|
|
|
from ctypes import Structure, sizeof, byref, cdll, pydll, c_void_p, c_char, c_byte, c_char_p, c_double, c_float, c_int, c_longlong, c_short, c_bool, c_long, c_int32, c_ubyte, c_uint, c_ushort, c_ulong, c_ulonglong, POINTER, pointer |
|
|
import re |
|
|
import sys |
|
|
import os |
|
|
import itertools |
|
|
import ui |
|
|
import weakref |
|
|
import string |
|
|
import pyparsing as pp |
|
|
import inspect |
|
|
import functools |
|
|
|
|
|
PY3 = sys.version_info[0] >= 3 |
|
|
|
|
|
if PY3: |
|
|
basestring = str |
|
|
string_lowercase = string.ascii_lowercase |
|
|
xrange = range |
|
|
long = int |
|
|
else: |
|
|
bytes = str |
|
|
string_lowercase = string.lowercase |
|
|
|
|
|
def filter_list(*args, **kwargs): |
|
|
if PY3: |
|
|
return list(filter(*args, **kwargs)) |
|
|
return filter(*args, **kwargs) |
|
|
|
|
|
LP64 = (sizeof(c_void_p) == 8) |
|
|
|
|
|
_retained_globals = [] |
|
|
_cached_classes = {} |
|
|
_cached_instances = weakref.WeakValueDictionary() |
|
|
_tracefunc = None |
|
|
|
|
|
c = cdll.LoadLibrary(None) |
|
|
|
|
|
class_getName = c.class_getName |
|
|
class_getName.restype = c_char_p |
|
|
class_getName.argtypes = [c_void_p] |
|
|
|
|
|
class_getSuperclass = c.class_getSuperclass |
|
|
class_getSuperclass.restype = c_void_p |
|
|
class_getSuperclass.argtypes = [c_void_p] |
|
|
|
|
|
class_addMethod = c.class_addMethod |
|
|
class_addMethod.restype = c_bool |
|
|
class_addMethod.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p] |
|
|
|
|
|
class_getInstanceMethod = c.class_getInstanceMethod |
|
|
class_getInstanceMethod.restype = c_void_p |
|
|
class_getInstanceMethod.argtypes = [c_void_p, c_void_p] |
|
|
|
|
|
class_getClassMethod = c.class_getClassMethod |
|
|
class_getClassMethod.restype = c_void_p |
|
|
class_getClassMethod.argtypes = [c_void_p, c_void_p] |
|
|
|
|
|
objc_allocateClassPair = c.objc_allocateClassPair |
|
|
objc_allocateClassPair.restype = c_void_p |
|
|
objc_allocateClassPair.argtypes = [c_void_p, c_char_p, c_int] |
|
|
|
|
|
objc_registerClassPair = c.objc_registerClassPair |
|
|
objc_registerClassPair.restype = None |
|
|
objc_registerClassPair.argtypes = [c_void_p] |
|
|
|
|
|
objc_getClass = c.objc_getClass |
|
|
objc_getClass.argtypes = [c_char_p] |
|
|
objc_getClass.restype = c_void_p |
|
|
|
|
|
objc_getClassList = c.objc_getClassList |
|
|
objc_getClassList.restype = c_int |
|
|
objc_getClassList.argtypes = [c_void_p, c_int] |
|
|
|
|
|
method_getTypeEncoding = c.method_getTypeEncoding |
|
|
method_getTypeEncoding.argtypes = [c_void_p] |
|
|
method_getTypeEncoding.restype = c_char_p |
|
|
|
|
|
sel_getName = c.sel_getName |
|
|
sel_getName.restype = c_char_p |
|
|
sel_getName.argtypes = [c_void_p] |
|
|
|
|
|
sel_registerName = c.sel_registerName |
|
|
sel_registerName.restype = c_void_p |
|
|
sel_registerName.argtypes = [c_char_p] |
|
|
|
|
|
object_getClass = c.object_getClass |
|
|
object_getClass.argtypes = [c_void_p] |
|
|
object_getClass.restype = c_void_p |
|
|
|
|
|
class_copyMethodList = c.class_copyMethodList |
|
|
class_copyMethodList.restype = ctypes.POINTER(c_void_p) |
|
|
class_copyMethodList.argtypes = [c_void_p, ctypes.POINTER(ctypes.c_uint)] |
|
|
|
|
|
class_getProperty = c.class_getProperty |
|
|
class_getProperty.restype = c_void_p |
|
|
class_getProperty.argtypes = [c_void_p, c_char_p] |
|
|
|
|
|
property_getAttributes = c.property_getAttributes |
|
|
property_getAttributes.argtypes = [c_void_p] |
|
|
property_getAttributes.restype = c_char_p |
|
|
|
|
|
method_getName = c.method_getName |
|
|
method_getName.restype = c_void_p |
|
|
method_getName.argtypes = [c_void_p] |
|
|
|
|
|
class objc_method_description (Structure): |
|
|
_fields_ = [('sel', c_void_p), ('types', c_char_p)] |
|
|
|
|
|
objc_getProtocol = c.objc_getProtocol |
|
|
objc_getProtocol.restype = c_void_p |
|
|
objc_getProtocol.argtypes = [c_char_p] |
|
|
|
|
|
protocol_getMethodDescription = c.protocol_getMethodDescription |
|
|
protocol_getMethodDescription.restype = objc_method_description |
|
|
protocol_getMethodDescription.argtypes = [c_void_p, c_void_p, c_bool, c_bool] |
|
|
|
|
|
objc_msgSend = c.objc_msgSend |
|
|
if not LP64: |
|
|
objc_msgSend_stret = c.objc_msgSend_stret |
|
|
|
|
|
free = c.free |
|
|
free.argtypes = [c_void_p] |
|
|
free.restype = None |
|
|
|
|
|
CGFloat = c_double if LP64 else c_float |
|
|
NSInteger = c_long if LP64 else c_int |
|
|
NSUInteger = c_ulong if LP64 else c_uint |
|
|
|
|
|
if PY3: |
|
|
NSNotFound = sys.maxsize |
|
|
else: |
|
|
NSNotFound = sys.maxint |
|
|
|
|
|
NSUTF8StringEncoding = 4 |
|
|
NS_UTF8 = NSUTF8StringEncoding |
|
|
|
|
|
class CGPoint (Structure): |
|
|
_fields_ = [('x', CGFloat), ('y', CGFloat)] |
|
|
|
|
|
class CGSize (Structure): |
|
|
_fields_ = [('width', CGFloat), ('height', CGFloat)] |
|
|
|
|
|
class CGVector (Structure): |
|
|
_fields_ = [('dx', CGFloat), ('dy', CGFloat)] |
|
|
|
|
|
class CGRect (Structure): |
|
|
_fields_ = [('origin', CGPoint), ('size', CGSize)] |
|
|
|
|
|
class CGAffineTransform (Structure): |
|
|
_fields_ = [('a', CGFloat), ('b', CGFloat), ('c', CGFloat), ('d', CGFloat), ('tx', CGFloat), ('ty', CGFloat)] |
|
|
|
|
|
class UIEdgeInsets (Structure): |
|
|
_fields_ = [('top', CGFloat), ('left', CGFloat), ('bottom', CGFloat), ('right', CGFloat)] |
|
|
|
|
|
class NSRange (Structure): |
|
|
_fields_ = [('location', NSUInteger), ('length', NSUInteger)] |
|
|
|
|
|
# c.f. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html |
|
|
type_encodings = {'c': c_byte, 'i': c_int, 's': c_short, 'l': c_int32, 'q': c_longlong, |
|
|
'C': c_ubyte, 'I': c_uint, 'S': c_ushort, 'L': c_ulong, 'Q': c_ulonglong, |
|
|
'f': c_float, 'd': c_double, 'B': c_bool, 'v': None, '*': c_char_p, |
|
|
'@': c_void_p, '#': c_void_p, ':': c_void_p, |
|
|
'{CGPoint}': CGPoint, '{CGSize}': CGSize, '{CGRect}': CGRect, '{CGVector}': CGVector, |
|
|
'{CGAffineTransform}': CGAffineTransform, '{UIEdgeInsets}': UIEdgeInsets, '{_NSRange}': NSRange, |
|
|
'?': c_void_p, '@?': c_void_p |
|
|
} |
|
|
|
|
|
def split_encoding(encoding): |
|
|
# This function is mostly copied from pybee's rubicon.objc |
|
|
# (https://github.com/pybee/rubicon-objc) |
|
|
# License: https://github.com/pybee/rubicon-objc/blob/master/LICENSE |
|
|
type_encodings = [] |
|
|
braces, brackets = 0, 0 |
|
|
typecode = '' |
|
|
if PY3 and isinstance(encoding, bytes): |
|
|
encoding = encoding.decode('ascii') |
|
|
for c in encoding: |
|
|
if c == '{': |
|
|
if typecode and typecode[-1:] != '^' and braces == 0 and brackets == 0: |
|
|
type_encodings.append(typecode) |
|
|
typecode = '' |
|
|
typecode += c |
|
|
braces += 1 |
|
|
elif c == '}': |
|
|
typecode += c |
|
|
braces -= 1 |
|
|
elif c == '[': |
|
|
if typecode and typecode[-1:] != '^' and braces == 0 and brackets == 0: |
|
|
type_encodings.append(typecode) |
|
|
typecode = '' |
|
|
typecode += c |
|
|
brackets += 1 |
|
|
elif c == ']': |
|
|
typecode += c |
|
|
brackets -= 1 |
|
|
elif braces or brackets: |
|
|
typecode += c |
|
|
elif c in string.digits: |
|
|
pass |
|
|
elif c in 'rnNoORV': |
|
|
pass |
|
|
elif c in '^cislqCISLQfdBv*@#:b?': |
|
|
if c == '?' and typecode[-1:] == '@': |
|
|
typecode += c |
|
|
elif typecode and typecode[-1:] == '^': |
|
|
typecode += c |
|
|
else: |
|
|
if typecode: |
|
|
type_encodings.append(typecode) |
|
|
typecode = c |
|
|
if typecode: |
|
|
type_encodings.append(typecode) |
|
|
return type_encodings |
|
|
|
|
|
|
|
|
def struct_from_tuple(cls, t): |
|
|
args = [] |
|
|
for i, field_value in enumerate(t): |
|
|
if isinstance(field_value, tuple): |
|
|
args.append(struct_from_tuple(cls._fields_[i][1], field_value)) |
|
|
else: |
|
|
args.append(field_value) |
|
|
return cls(*args) |
|
|
|
|
|
def _struct_class_from_fields(fields): |
|
|
class AnonymousStructure (Structure): |
|
|
from_tuple = classmethod(struct_from_tuple) |
|
|
struct_fields = [] |
|
|
for i, field in enumerate(fields): |
|
|
if isinstance(field, tuple): |
|
|
struct_fields.append(field) |
|
|
else: |
|
|
struct_fields.append((string_lowercase[i], _struct_class_from_fields(field))) |
|
|
i += 1 |
|
|
AnonymousStructure._fields_ = struct_fields |
|
|
return AnonymousStructure |
|
|
|
|
|
def _list_to_fields(result): |
|
|
fields = [] |
|
|
i = 0 |
|
|
if isinstance(result, basestring): |
|
|
for char in split_encoding(result): |
|
|
c_type = parse_types(char)[0] |
|
|
fields.append((string_lowercase[i], c_type)) |
|
|
i += 1 |
|
|
else: |
|
|
if len(result) == 1: |
|
|
return _list_to_fields(result[0]) |
|
|
for r in result: |
|
|
fields.append(_list_to_fields(r)) |
|
|
return fields |
|
|
|
|
|
def _result_to_list(result): |
|
|
struct_result = [] |
|
|
for member in result: |
|
|
if isinstance(member, basestring): |
|
|
struct_result.append(member) |
|
|
else: |
|
|
struct_result.append(_result_to_list([member[2]])) |
|
|
return struct_result |
|
|
|
|
|
_enclosed = pp.Forward() |
|
|
_nested_curlies = pp.nestedExpr('{', '}', content=_enclosed) |
|
|
_enclosed << (pp.Word(pp.alphas + '?_' + pp.nums) | '=' | _nested_curlies) |
|
|
|
|
|
def parse_struct(encoding): |
|
|
comps = _enclosed.parseString(encoding).asList()[0] |
|
|
struct_name = comps[0] |
|
|
struct_members = comps[2:] |
|
|
fields = _list_to_fields(_result_to_list(struct_members)) |
|
|
struct_class = _struct_class_from_fields(fields) |
|
|
struct_class.__name__ = (struct_name + '_Structure').replace('?', '_') |
|
|
return struct_class |
|
|
|
|
|
_cached_parse_types_results = {} |
|
|
|
|
|
def parse_types(type_encoding): |
|
|
'''Take an Objective-C type encoding string and convert it to a tuple of (restype, argtypes) appropriate for objc_msgSend()''' |
|
|
cached_result = _cached_parse_types_results.get(type_encoding) |
|
|
if cached_result: |
|
|
return cached_result |
|
|
def get_type_for_code(enc_str): |
|
|
if enc_str.startswith('{'): |
|
|
struct_name = enc_str[0:enc_str.find('=')] + '}' |
|
|
if struct_name in type_encodings: |
|
|
return type_encodings[struct_name] |
|
|
else: |
|
|
return parse_struct(enc_str) |
|
|
if enc_str[0] in 'rnNoORV': #const, in, inout... don't care about these |
|
|
enc_str = enc_str[1:] |
|
|
if enc_str[0] == '^': |
|
|
if re.match(r'\^\{\w+=#?\}', enc_str): |
|
|
return c_void_p # pointer to opaque type, e.g. CGPathRef, CGImageRef... |
|
|
else: |
|
|
return POINTER(get_type_for_code(enc_str[1:])) |
|
|
if enc_str[0] == '[': #array |
|
|
return c_void_p |
|
|
try: |
|
|
t = type_encodings[enc_str] |
|
|
return t |
|
|
except KeyError: |
|
|
raise NotImplementedError('Unsupported type encoding (%s)' % (enc_str,)) |
|
|
encoded_types = filter_list(lambda x: bool(x), split_encoding(type_encoding)) |
|
|
encoded_argtypes = encoded_types[3:] |
|
|
argtypes = [get_type_for_code(x) for x in encoded_argtypes] |
|
|
restype = get_type_for_code(encoded_types[0]) |
|
|
cached_result = (restype, [c_void_p, c_void_p] + argtypes, encoded_argtypes) |
|
|
_cached_parse_types_results[type_encoding] = cached_result |
|
|
return cached_result |
|
|
|
|
|
def sel(sel_name): |
|
|
'''Convenience function to convert a string to a selector''' |
|
|
if PY3 and isinstance(sel_name, str): |
|
|
sel_name = sel_name.encode('ascii') |
|
|
return sel_registerName(sel_name) |
|
|
|
|
|
def _first_letter_cap(s): |
|
|
if not s: |
|
|
return '' |
|
|
return s[0].upper() + s[1:] |
|
|
|
|
|
def get_possible_method_names(name, args, kwargs): |
|
|
# Return a list of possible ObjC method names for a given combination of arguments. Basically all permutations of keyword args... |
|
|
if '_' in name: |
|
|
# Should never actually happen... |
|
|
return [(name, [])] |
|
|
if name.endswith('With'): |
|
|
name = name[:-4] |
|
|
possible_names = [] |
|
|
if len(args) == 0 and len(kwargs) == 0: |
|
|
possible_names.append((name, [])) |
|
|
elif len(kwargs) == 0 and len(args) == 1: |
|
|
possible_names.append((name + '_', [])) |
|
|
else: |
|
|
permutations = itertools.permutations(kwargs.keys()) |
|
|
found_methods = [] |
|
|
for perm in permutations: |
|
|
name1 = name + '_' + '_'.join(perm) + '_' |
|
|
possible_names.append((name1, list(perm))) |
|
|
if len(args) == 0: |
|
|
name2 = name + 'With%s_' % (_first_letter_cap(perm[0]),) + '_'.join(perm[1:]) |
|
|
if len(perm) > 1: |
|
|
name2 += '_' |
|
|
possible_names.append((name2, list(perm))) |
|
|
name3 = name + _first_letter_cap(perm[0]) + '_' + '_'.join(perm[1:]) |
|
|
if len(perm) > 1: |
|
|
name3 += '_' |
|
|
possible_names.append((name3, list(perm))) |
|
|
|
|
|
return possible_names |
|
|
|
|
|
def resolve_cls_method(cls, name, args, kwargs): |
|
|
# Return (method_name, ordered_kwargs), or raise an AttributeError if either no method could be found, or if the mapping was ambiguous. |
|
|
kwargs_copy = dict(kwargs) |
|
|
if 'argtypes' in kwargs_copy: |
|
|
del kwargs_copy['argtypes'] |
|
|
if 'restype' in kwargs_copy: |
|
|
del kwargs_copy['restype'] |
|
|
possible_names = get_possible_method_names(name, args, kwargs_copy) |
|
|
valid_names = [] |
|
|
for method_name, kwarg_order in possible_names: |
|
|
try: |
|
|
meth = ObjCClassMethod(cls, method_name) |
|
|
valid_names.append((method_name, kwarg_order)) |
|
|
except AttributeError: |
|
|
pass |
|
|
if len(valid_names) == 1: |
|
|
return valid_names[0] |
|
|
elif len(valid_names) == 0: |
|
|
raise AttributeError('No method found for %s' % (name,)) |
|
|
else: |
|
|
raise AttributeError('Method name is ambiguous') |
|
|
|
|
|
def resolve_instance_method(obj, name, args, kwargs): |
|
|
# Return (method_name, ordered_kwargs), or raise an AttributeError if either no method could be found, or if the mapping was ambiguous. |
|
|
kwargs_copy = dict(kwargs) |
|
|
if 'argtypes' in kwargs_copy: |
|
|
del kwargs_copy['argtypes'] |
|
|
if 'restype' in kwargs_copy: |
|
|
del kwargs_copy['restype'] |
|
|
possible_names = get_possible_method_names(name, args, kwargs_copy) |
|
|
valid_names = [] |
|
|
for method_name, kwarg_order in possible_names: |
|
|
try: |
|
|
possibly_property = len(args) == 0 and len(kwargs) == 0 |
|
|
meth = ObjCInstanceMethod(obj, method_name, allow_property=possibly_property) |
|
|
valid_names.append((method_name, kwarg_order)) |
|
|
except AttributeError: |
|
|
pass |
|
|
if len(valid_names) == 1: |
|
|
return valid_names[0] |
|
|
elif len(valid_names) == 0: |
|
|
raise AttributeError('No method found for %s' % (name,)) |
|
|
else: |
|
|
raise AttributeError('Method name is ambiguous') |
|
|
|
|
|
class ObjCClass (object): |
|
|
'''Wrapper for a pointer to an Objective-C class; acts as a proxy for calling Objective-C class methods. Method calls are converted to Objective-C messages on-the-fly -- this is done by replacing underscores in the method name with colons in the selector name, and using the selector and arguments for a call to the low-level objc_msgSend function in the Objective-C runtime. For example, calling `NSDictionary.dictionaryWithObject_forKey_(obj, key)` (Python) is translated to `[NSDictionary dictionaryWithObject:obj forKey:key]` (Objective-C). If a method call returns an Objective-C object, it is wrapped in an ObjCInstance, so calls can be chained (ObjCInstance uses an equivalent proxy mechanism).''' |
|
|
def __new__(cls, name): |
|
|
if PY3 and isinstance(name, str): |
|
|
name = name.encode('ascii') |
|
|
|
|
|
# Memoize classes by name (get identical object every time): |
|
|
cached_class = _cached_classes.get(name) |
|
|
if cached_class: |
|
|
return cached_class |
|
|
cached_class = super(ObjCClass, cls).__new__(cls) |
|
|
_cached_classes[name] = cached_class |
|
|
return cached_class |
|
|
|
|
|
def __init__(self, name): |
|
|
if PY3 and isinstance(name, str): |
|
|
name = name.encode('ascii') |
|
|
|
|
|
self.ptr = objc_getClass(name) |
|
|
if self.ptr is None: |
|
|
raise ValueError('no Objective-C class named \'%s\' found' % (name,)) |
|
|
self._as_parameter_ = self.ptr |
|
|
self.class_name = name |
|
|
self._cached_methods = {} |
|
|
|
|
|
def __str__(self): |
|
|
return '<ObjCClass: %s>' % (self.class_name,) |
|
|
|
|
|
def __eq__(self, other): |
|
|
return isinstance(other, ObjCClass) and self.class_name == other.class_name |
|
|
|
|
|
def __getattr__(self, attr): |
|
|
cached_method = self._cached_methods.get(attr, None) |
|
|
if not cached_method: |
|
|
if '_' in attr: |
|
|
# Old method call syntax |
|
|
cached_method = ObjCClassMethod(self, attr) |
|
|
else: |
|
|
# New syntax, actual method resolution is done at call time |
|
|
cached_method = ObjCClassMethodProxy(self, attr) |
|
|
self._cached_methods[attr] = cached_method |
|
|
return cached_method |
|
|
|
|
|
def __dir__(self): |
|
|
objc_class_ptr = object_getClass(self.ptr) |
|
|
py_methods = [] |
|
|
while objc_class_ptr is not None: |
|
|
num_methods = c_uint(0) |
|
|
method_list_ptr = class_copyMethodList(objc_class_ptr, byref(num_methods)) |
|
|
for i in xrange(num_methods.value): |
|
|
selector = method_getName(method_list_ptr[i]) |
|
|
sel_name = sel_getName(selector) |
|
|
if PY3: |
|
|
sel_name = sel_name.decode('ascii') |
|
|
py_method_name = sel_name.replace(':', '_') |
|
|
if '.' not in py_method_name: |
|
|
py_methods.append(py_method_name) |
|
|
free(method_list_ptr) |
|
|
# Walk up the class hierarchy to add methods from superclasses: |
|
|
objc_class_ptr = class_getSuperclass(objc_class_ptr) |
|
|
if objc_class_ptr == object_getClass(NSObject): |
|
|
# Don't list all methods from NSObject (too much cruft from categories) |
|
|
py_methods += NSObject_class_methods |
|
|
break |
|
|
py_methods = sorted(set(py_methods)) |
|
|
return py_methods |
|
|
|
|
|
@classmethod |
|
|
def get_names(cls, prefix=None): |
|
|
num_classes = objc_getClassList(None, 0) |
|
|
buffer = (c_void_p * num_classes)() |
|
|
objc_getClassList(buffer, num_classes) |
|
|
class_names = [] |
|
|
for i in xrange(num_classes): |
|
|
n = class_getName(buffer[i]) |
|
|
if PY3: |
|
|
n = n.decode('ascii') |
|
|
class_names.append(n) |
|
|
filtered_list = class_names if prefix is None else filter_list(lambda x: x.startswith(prefix), class_names) |
|
|
return sorted(filtered_list) |
|
|
|
|
|
@classmethod |
|
|
def create(cls, *args, **kwargs): |
|
|
return create_objc_class(*args, **kwargs) |
|
|
|
|
|
|
|
|
class ObjCIterator (object): |
|
|
'''Wrapper for an NSEnumerator object -- this is used for supporting `for ... in` iteration for Objective-C collection types (NSArray, NSDictionary, NSSet).''' |
|
|
def __init__(self, obj): |
|
|
self.enumerator = obj.objectEnumerator() |
|
|
|
|
|
def __iter__(self): |
|
|
return self |
|
|
|
|
|
def next(self): |
|
|
next_obj = self.enumerator.nextObject() |
|
|
if next_obj is None: |
|
|
raise StopIteration() |
|
|
return next_obj |
|
|
|
|
|
def __next__(self): |
|
|
return self.next() |
|
|
|
|
|
class ObjCInstance (object): |
|
|
'''Wrapper for a pointer to an Objective-C instance; acts as a proxy for sending messages to the object. Method calls are converted to Objective-C messages on-the-fly -- this is done by replacing underscores in the method name with colons in the selector name, and using the selector and arguments for a call to the low-level objc_msgSend function in the Objective-C runtime. For example, calling `obj.setFoo_withBar_(foo, bar)` (Python) is translated to `[obj setFoo:foo withBar:bar]` (Objective-C). If a method call returns an Objective-C object, it is also wrapped in an ObjCInstance, so calls can be chained.''' |
|
|
|
|
|
def __new__(cls, ptr): |
|
|
# If there is already an instance that wraps this pointer, return the same object... |
|
|
# This makes it a little easier to put auxiliary data into the instance (e.g. to use in an ObjC callback) |
|
|
# Note however that a new instance may be created for the same underlying ObjC object if the last instance gets garbage-collected. |
|
|
if isinstance(ptr, ObjCInstance): |
|
|
return ptr |
|
|
if hasattr(ptr, '_objc_ptr'): |
|
|
ptr = ptr._objc_ptr |
|
|
if isinstance(ptr, c_void_p): |
|
|
ptr = ptr.value |
|
|
cached_instance = _cached_instances.get(ptr) |
|
|
if cached_instance is not None: |
|
|
return cached_instance |
|
|
objc_instance = super(ObjCInstance, cls).__new__(cls) |
|
|
_cached_instances[ptr] = objc_instance |
|
|
objc_instance.ptr = ptr |
|
|
objc_instance._as_parameter_ = ptr |
|
|
objc_instance._cached_methods = {} |
|
|
objc_instance.weakrefs = weakref.WeakValueDictionary() |
|
|
if ptr: |
|
|
# Retain the ObjC object, so it doesn't get freed while a pointer to it exists: |
|
|
objc_instance.retain(restype=c_void_p, argtypes=[]) |
|
|
return objc_instance |
|
|
|
|
|
def __str__(self): |
|
|
objc_msgSend = c['objc_msgSend'] |
|
|
objc_msgSend.argtypes = [c_void_p, c_void_p] |
|
|
objc_msgSend.restype = c_void_p |
|
|
desc = objc_msgSend(self.ptr, sel('description')) |
|
|
objc_msgSend.argtypes = [c_void_p, c_void_p] |
|
|
objc_msgSend.restype = c_char_p |
|
|
desc_str = objc_msgSend(desc, sel('UTF8String')) |
|
|
if PY3: |
|
|
return desc_str.decode('utf-8') |
|
|
return desc_str |
|
|
|
|
|
def _get_objc_classname(self): |
|
|
return class_getName(object_getClass(self.ptr)) |
|
|
|
|
|
def __repr__(self): |
|
|
return '<%s: %s>' % (self._get_objc_classname(), str(self)) |
|
|
|
|
|
def __eq__(self, other): |
|
|
return isinstance(other, ObjCInstance) and self.ptr == other.ptr |
|
|
|
|
|
def __hash__(self): |
|
|
return hash(self.ptr) |
|
|
|
|
|
def __iter__(self): |
|
|
if any(self.isKindOfClass_(c) for c in (NSArray, NSDictionary, NSSet)): |
|
|
return ObjCIterator(self) |
|
|
raise TypeError('%s is not iterable' % (self._get_objc_classname(),)) |
|
|
|
|
|
def __nonzero__(self): |
|
|
try: |
|
|
return len(self) != 0 |
|
|
except TypeError: |
|
|
return self.ptr != None |
|
|
|
|
|
def __bool__(self): |
|
|
return self.__nonzero__() |
|
|
|
|
|
def __len__(self): |
|
|
if any(self.isKindOfClass_(c) for c in (NSArray, NSDictionary, NSSet)): |
|
|
return self.count() |
|
|
raise TypeError('object of type \'%s\' has no len()' % (self._get_objc_classname(),)) |
|
|
|
|
|
def __getitem__(self, key): |
|
|
if self.isKindOfClass_(NSArray): |
|
|
if not isinstance(key, (int, long)): |
|
|
raise TypeError('array indices must be integers not %s' % (type(key),)) |
|
|
array_length = self.count() |
|
|
if key < 0: |
|
|
# a[-1] is equivalent to a[len(a) - 1] |
|
|
key = array_length + key |
|
|
if key < 0 or key >= array_length: |
|
|
raise IndexError('array index out of range') |
|
|
return self.objectAtIndex_(key) |
|
|
elif self.isKindOfClass_(NSDictionary): |
|
|
# allow to use Python strings as keys, convert to NSString implicitly: |
|
|
ns_key = ns(key) |
|
|
# NOTE: Unlike Python dicts, NSDictionary returns nil (None) for keys that don't exist. |
|
|
return self.objectForKey_(ns_key) |
|
|
raise TypeError('%s does not support __getitem__' % (self._get_objc_classname(),)) |
|
|
|
|
|
def __delitem__(self, key): |
|
|
if self.isKindOfClass_(NSMutableArray): |
|
|
if not isinstance(key, (int, long)): |
|
|
raise TypeError('array indices must be integers not %s' % (type(key),)) |
|
|
array_length = self.count() |
|
|
if key < 0: |
|
|
# a[-1] is equivalent to a[len(a) - 1] |
|
|
key = array_length + key |
|
|
if key < 0 or key >= array_length: |
|
|
raise IndexError('array index out of range') |
|
|
self.removeObjectAtIndex_(key) |
|
|
elif self.isKindOfClass_(NSMutableDictionary): |
|
|
ns_key = ns(key) |
|
|
return self.removeObjectForKey_(ns_key) |
|
|
else: |
|
|
raise TypeError('%s does not support __delitem__' % (self._get_objc_classname(),)) |
|
|
|
|
|
def __setitem__(self, key, value): |
|
|
if self.isKindOfClass_(NSMutableArray): |
|
|
if not isinstance(key, (int, long)): |
|
|
raise TypeError('array indices must be integers not %s' % (type(key),)) |
|
|
array_length = self.count() |
|
|
if key < 0: |
|
|
# a[-1] is equivalent to a[len(a) - 1] |
|
|
key = array_length + key |
|
|
if key < 0 or key >= array_length: |
|
|
raise IndexError('array index out of range') |
|
|
self.replaceObjectAtIndex_withObject_(key, ns(value)) |
|
|
elif self.isKindOfClass_(NSMutableDictionary): |
|
|
self.setObject_forKey_(ns(value), ns(key)) |
|
|
else: |
|
|
raise TypeError('%s does not support __setitem__' % (self._get_objc_classname(),)) |
|
|
|
|
|
def __getattr__(self, attr): |
|
|
cached_method = self._cached_methods.get(attr, None) |
|
|
if not cached_method: |
|
|
if '_' in attr: |
|
|
# Old method call syntax |
|
|
cached_method = ObjCInstanceMethod(self, attr) |
|
|
else: |
|
|
# New syntax, actual method resolution is done at call time |
|
|
cached_method = ObjCInstanceMethodProxy(self, attr) |
|
|
self._cached_methods[attr] = cached_method |
|
|
return cached_method |
|
|
|
|
|
def __setattr__(self, name, value): |
|
|
if name in ('ptr', 'weakrefs', '_cached_methods', '_as_parameter_'): |
|
|
self.__dict__[name] = value |
|
|
return |
|
|
try: |
|
|
setter_method = getattr(self, 'set%s%s_' % (name[0].upper(), name[1:])) |
|
|
setter_method(value) |
|
|
except AttributeError: |
|
|
self.__dict__[name] = value |
|
|
|
|
|
def __dir__(self): |
|
|
objc_class_ptr = object_getClass(self.ptr) |
|
|
py_methods = [] |
|
|
while objc_class_ptr is not None: |
|
|
num_methods = c_uint(0) |
|
|
method_list_ptr = class_copyMethodList(objc_class_ptr, byref(num_methods)) |
|
|
for i in xrange(num_methods.value): |
|
|
selector = method_getName(method_list_ptr[i]) |
|
|
sel_name = sel_getName(selector) |
|
|
if PY3: |
|
|
sel_name = sel_name.decode('ascii') |
|
|
py_method_name = sel_name.replace(':', '_') |
|
|
if '.' not in py_method_name: |
|
|
py_methods.append(py_method_name) |
|
|
free(method_list_ptr) |
|
|
# Walk up the class hierarchy to add methods from superclasses: |
|
|
objc_class_ptr = class_getSuperclass(objc_class_ptr) |
|
|
if objc_class_ptr == NSObject.ptr: |
|
|
# Don't list all NSObject methods (too much cruft from categories...) |
|
|
py_methods += NSObject_instance_methods |
|
|
break |
|
|
return sorted(set(py_methods)) |
|
|
|
|
|
def __del__(self): |
|
|
# Release the ObjC object's memory: |
|
|
objc_msgSend = c['objc_msgSend'] |
|
|
objc_msgSend.argtypes = [c_void_p, c_void_p] |
|
|
objc_msgSend.restype = None |
|
|
objc_msgSend(self.ptr, sel('release')) |
|
|
#self.release(restype=None, argtypes=[]) |
|
|
|
|
|
def _get_possible_selector_names(method_name): |
|
|
# Generate all possible selector names from a Python method name. For most methods, this isn't necessary, |
|
|
# and the selector is generated simply by replacing underscores with colons, but in case the selector also |
|
|
# contains underscores, the mapping is ambiguous, so all permutations of colons and underscores need to be checked. |
|
|
izip_longest = itertools.zip_longest if PY3 else itertools.izip_longest |
|
|
return [''.join([x+y for x, y in izip_longest(method_name.split('_'), s, fillvalue='')]) for s in [''.join(x) for x in itertools.product(':_', repeat=len(method_name.split('_'))-1)]] |
|
|
|
|
|
def _auto_wrap(arg, typecode, argtype): |
|
|
'''Helper function for `ObjCInstance/ClassMethod.__call__`''' |
|
|
if typecode == '@' or typecode.startswith('^{'): |
|
|
return ns(arg) |
|
|
elif typecode == ':' and isinstance(arg, basestring): |
|
|
# if a selector is expected, also allow a string: |
|
|
return sel(arg) |
|
|
elif issubclass(argtype, Structure) and isinstance(arg, tuple): |
|
|
# Automatically convert tuples to structs |
|
|
return struct_from_tuple(argtype, arg) |
|
|
return arg |
|
|
|
|
|
|
|
|
class ObjCClassMethodProxy (object): |
|
|
'''A proxy for an ObjCClassMethod that is resolved to an actual method when calling it. A proxy may represent different methods, depending on the keyword arguments that are passed (and used to construct an ObjC selector from the call).''' |
|
|
def __init__(self, cls, name): |
|
|
self.cls = weakref.ref(cls) |
|
|
self.name = name |
|
|
# Determining the method for a given combination of args/kwargs can be expensive, so it's cached by 'num_args/sorted_kwarg_keys'. The cache contains a pair of (ObjCClassMethod, ordered_kwarg_keys). |
|
|
self.method_cache = {} |
|
|
|
|
|
def __call__(self, *args, **kwargs): |
|
|
cls = self.cls() |
|
|
if cls is None: |
|
|
# If the class doesn't exist anymore, don't do anything |
|
|
# (this is unlikely to happen in practice) |
|
|
return |
|
|
cache_key = '%i/' % (len(args),) + ','.join(sorted(kwargs.keys())) |
|
|
cached = self.method_cache.get(cache_key) |
|
|
if cached: |
|
|
method, kwarg_order = cached |
|
|
else: |
|
|
method_name, kwarg_order = resolve_cls_method(cls, self.name, args, kwargs) |
|
|
method = ObjCClassMethod(cls, method_name) |
|
|
self.method_cache[cache_key] = (method, kwarg_order) |
|
|
ordered_args = list(args) + [kwargs[key] for key in kwarg_order] |
|
|
# Pass through restype and argtypes keyword args: |
|
|
kw = {k: kwargs[k] for k in ('restype', 'kwargs') if k in kwargs} |
|
|
return method(*ordered_args, **kw) |
|
|
|
|
|
|
|
|
class ObjCClassMethod (object): |
|
|
'''Wrapper for an Objective-C class method. ObjCClass generates these objects automatically when accessing an attribute, you typically don't need use this class directly.''' |
|
|
def __init__(self, cls, method_name): |
|
|
self.cls = cls |
|
|
self.sel_name = method_name.replace('_', ':') |
|
|
method = class_getClassMethod(cls.ptr, sel(self.sel_name)) |
|
|
if not method: |
|
|
# Couldn't find a method, try all combinations of underscores and colons... |
|
|
# For selectors that contain underscores, the mapping from Python method name to selector name is ambiguous. |
|
|
possible_selector_names = _get_possible_selector_names(method_name) |
|
|
for possible_sel_name in possible_selector_names: |
|
|
method = class_getClassMethod(cls.ptr, sel(possible_sel_name)) |
|
|
if method: |
|
|
self.sel_name = possible_sel_name |
|
|
break |
|
|
if method: |
|
|
self.method = method |
|
|
self.encoding = method_getTypeEncoding(method) |
|
|
else: |
|
|
raise AttributeError('No class method found for selector "%s"' % (self.sel_name)) |
|
|
|
|
|
def __call__(self, *args, **kwargs): |
|
|
cls = self.cls |
|
|
if cls is None: |
|
|
return |
|
|
type_encoding = self.encoding |
|
|
try: |
|
|
argtypes = kwargs['argtypes'] |
|
|
restype = kwargs['restype'] |
|
|
argtypes = [c_void_p, c_void_p] + argtypes |
|
|
except KeyError: |
|
|
restype, argtypes, argtype_encodings = parse_types(type_encoding) |
|
|
if len(args) != len(argtypes) - 2: |
|
|
raise TypeError('expected %i arguments, got %i' % (len(argtypes) - 2, len(args))) |
|
|
# Automatically convert Python strings to NSString etc. for object arguments |
|
|
# (this is a no-op for arguments that are already `ObjCInstance` objects): |
|
|
args = tuple(_auto_wrap(a, argtype_encodings[i], argtypes[i+2]) for i, a in enumerate(args)) |
|
|
objc_msgSend = c['objc_msgSend'] |
|
|
objc_msgSend.argtypes = argtypes |
|
|
objc_msgSend.restype = restype |
|
|
res = objc_msgSend(cls, sel(self.sel_name), *args) |
|
|
return_type_enc = chr(type_encoding[0]) if PY3 else type_encoding[0] |
|
|
if res and return_type_enc == '@': |
|
|
return ObjCInstance(res) |
|
|
return res |
|
|
|
|
|
class ObjCInstanceMethodProxy (object): |
|
|
'''A proxy for an ObjCInstanceMethod that is resolved to an actual method when calling it. A proxy may represent different methods, depending on the keyword arguments that are passed (and used to construct an ObjC selector from the call).''' |
|
|
def __init__(self, obj, name): |
|
|
self.obj = weakref.ref(obj) |
|
|
self.name = name |
|
|
# Determining the method for a given combination of args/kwargs can be expensive, so it's cached by 'num_args/sorted_kwarg_keys'. The cache contains a pair of (ObjCClassMethod, ordered_kwarg_keys). |
|
|
self.method_cache = {} |
|
|
|
|
|
def __call__(self, *args, **kwargs): |
|
|
obj = self.obj() |
|
|
if obj is None: |
|
|
# If the instance doesn't exist anymore, don't do anything |
|
|
# (this is unlikely to happen in practice) |
|
|
return |
|
|
cache_key = '%i/' % (len(args),) + ','.join(sorted(kwargs.keys())) |
|
|
cached = self.method_cache.get(cache_key) |
|
|
if cached: |
|
|
method, kwarg_order = cached |
|
|
else: |
|
|
method_name, kwarg_order = resolve_instance_method(obj, self.name, args, kwargs) |
|
|
method = ObjCInstanceMethod(obj, method_name) |
|
|
self.method_cache[cache_key] = (method, kwarg_order) |
|
|
ordered_args = list(args) + [kwargs[key] for key in kwarg_order] |
|
|
# Pass through restype and argtypes keyword args: |
|
|
kw = {k: kwargs[k] for k in ('restype', 'kwargs') if k in kwargs} |
|
|
return method(*ordered_args, **kw) |
|
|
|
|
|
|
|
|
class ObjCInstanceMethod (object): |
|
|
'''Wrapper for an Objective-C instance method. ObjCInstance generates these objects automatically when accessing an attribute, you typically don't need to use this class directly.''' |
|
|
def __init__(self, obj, method_name, allow_property=True): |
|
|
self.obj = obj |
|
|
objc_class = object_getClass(obj.ptr) |
|
|
self.encoding = None |
|
|
method = None |
|
|
self.sel_name = method_name.replace('_', ':') |
|
|
method = class_getInstanceMethod(objc_class, sel(self.sel_name)) |
|
|
self._objc_msgSend = None |
|
|
if not method and '_' in method_name: |
|
|
# Couldn't find a method, try all combinations of underscores and colons... |
|
|
# For selectors that contain underscores, the mapping from Python method name to selector name is ambiguous. |
|
|
possible_selector_names = _get_possible_selector_names(method_name) |
|
|
for possible_sel_name in possible_selector_names: |
|
|
method = class_getInstanceMethod(objc_class, sel(possible_sel_name)) |
|
|
if method: |
|
|
self.sel_name = possible_sel_name |
|
|
break |
|
|
if allow_property and not method and ((method_name.startswith('set') and self.sel_name.endswith(':')) or self.sel_name.find(':') == -1): |
|
|
#Looks like it could be a property |
|
|
prop_name = method_name |
|
|
if method_name.startswith('set'): |
|
|
prop_name = method_name[3].lower() + method_name[4:-1] |
|
|
else: |
|
|
prop_name = method_name |
|
|
if PY3 and isinstance(prop_name, str): |
|
|
prop_name = prop_name.encode('ascii') |
|
|
prop = class_getProperty(objc_class, prop_name) |
|
|
if prop: |
|
|
#TODO: Check if the property is read-only when a setter is used... |
|
|
prop_attrs = property_getAttributes(prop) |
|
|
if PY3: |
|
|
prop_attrs = prop_attrs.decode('ascii') |
|
|
prop_type_encoding = re.search('T(.+?),', prop_attrs).group(1) |
|
|
if method_name.startswith('set'): |
|
|
#NOTE: The offsets/sizes are obviously incorrect... (should still work though) |
|
|
self.encoding = 'v0@0:0' + prop_type_encoding + '0' |
|
|
if PY3: |
|
|
self.encoding = self.encoding.encode('ascii') |
|
|
sel_name_match = re.search(r'S(.*?)(:?,.*?|$)', prop_attrs) |
|
|
if sel_name_match: |
|
|
self.sel_name = sel_name_match.group(1) |
|
|
else: |
|
|
self.encoding = prop_type_encoding + '0@0:0' |
|
|
if PY3: |
|
|
self.encoding = self.encoding.encode('ascii') |
|
|
sel_name_match = re.search(r'G(.*?)(:?,.*?|$)', prop_attrs) |
|
|
if sel_name_match: |
|
|
self.sel_name = sel_name_match.group(1) |
|
|
if not self.encoding and method: |
|
|
self.method = method |
|
|
self.encoding = method_getTypeEncoding(method) |
|
|
elif not self.encoding: |
|
|
raise AttributeError('No method found for selector "%s"' % (self.sel_name)) |
|
|
|
|
|
def __call__(self, *args, **kwargs): |
|
|
obj = self.obj |
|
|
if obj is None: |
|
|
return |
|
|
type_encoding = self.encoding |
|
|
try: |
|
|
argtypes = kwargs['argtypes'] |
|
|
restype = kwargs['restype'] |
|
|
argtypes = [c_void_p, c_void_p] + argtypes |
|
|
except KeyError: |
|
|
restype, argtypes, argtype_encodings = parse_types(type_encoding) |
|
|
if len(args) != len(argtypes) - 2: |
|
|
raise TypeError('expected %i arguments, got %i' % (len(argtypes) - 2, len(args))) |
|
|
# Automatically convert Python strings to NSString etc. for object arguments |
|
|
# (this is a no-op for arguments that are already `ObjCInstance` objects): |
|
|
args = tuple(_auto_wrap(a, argtype_encodings[i], argtypes[i+2]) for i, a in enumerate(args)) |
|
|
|
|
|
if not LP64 and restype and issubclass(restype, Structure): |
|
|
retval = restype() |
|
|
objc_msgSend_stret = c['objc_msgSend_stret'] |
|
|
objc_msgSend_stret.argtypes = [c_void_p] + argtypes |
|
|
objc_msgSend_stret.restype = None |
|
|
objc_msgSend_stret(byref(retval), obj.ptr, sel(self.sel_name), *args) |
|
|
return retval |
|
|
else: |
|
|
# NOTE: In order to be a little more thread-safe, we need a "private" handle for objc_msgSend(...). |
|
|
# Otherwise, a different thread could modify argtypes/restype before the call is made... |
|
|
objc_msgSend = self._objc_msgSend |
|
|
if not objc_msgSend: |
|
|
objc_msgSend = c['objc_msgSend'] |
|
|
objc_msgSend.argtypes = argtypes |
|
|
objc_msgSend.restype = restype |
|
|
# Cache the prepared function: |
|
|
self._objc_msgSend = objc_msgSend |
|
|
|
|
|
res = objc_msgSend(obj.ptr, sel(self.sel_name), *args) |
|
|
return_type_enc = chr(type_encoding[0]) if PY3 else type_encoding[0] |
|
|
if res and return_type_enc == '@': |
|
|
# If an object is returned, wrap the pointer in an ObjCInstance: |
|
|
if res == obj.ptr: |
|
|
return obj |
|
|
return ObjCInstance(res) |
|
|
if restype == c_void_p and isinstance(res, int): |
|
|
res = c_void_p(res) |
|
|
return res |
|
|
|
|
|
#Some commonly-used Foundation/UIKit classes: |
|
|
NSObject = ObjCClass('NSObject') |
|
|
NSDictionary = ObjCClass('NSDictionary') |
|
|
NSMutableDictionary = ObjCClass('NSMutableDictionary') |
|
|
NSArray = ObjCClass('NSArray') |
|
|
NSMutableArray = ObjCClass('NSMutableArray') |
|
|
NSSet = ObjCClass('NSSet') |
|
|
NSMutableSet = ObjCClass('NSMutableSet') |
|
|
NSString = ObjCClass('NSString') |
|
|
NSMutableString = ObjCClass('NSMutableString') |
|
|
NSData = ObjCClass('NSData') |
|
|
NSMutableData = ObjCClass('NSMutableData') |
|
|
NSNumber = ObjCClass('NSNumber') |
|
|
NSURL = ObjCClass('NSURL') |
|
|
NSEnumerator = ObjCClass('NSEnumerator') |
|
|
NSThread = ObjCClass('NSThread') |
|
|
NSBundle = ObjCClass('NSBundle') |
|
|
|
|
|
UIColor = ObjCClass('UIColor') |
|
|
UIImage = ObjCClass('UIImage') |
|
|
UIBezierPath = ObjCClass('UIBezierPath') |
|
|
UIApplication = ObjCClass('UIApplication') |
|
|
UIView = ObjCClass('UIView') |
|
|
|
|
|
def load_framework(name): |
|
|
return NSBundle.bundleWithPath_('/System/Library/Frameworks/%s.framework' % (name,)).load() |
|
|
|
|
|
# These are hard-coded for __dir__ (listing all NSObject methods dynamically would add tons of cruft from categories) |
|
|
# NOTE: This only includes *commonly-used* methods (a lot of this is pretty low-level runtime stuff that isn't needed |
|
|
# very often. It can still be listed when calling dir() on an actual instance of NSObject) |
|
|
NSObject_class_methods = ['alloc', 'new', 'superclass', 'isSubclassOfClass_', 'instancesRespondToSelector_', 'description', 'cancelPreviousPerformRequestsWithTarget_', 'cancelPreviousPerformRequestsWithTarget_selector_object_'] |
|
|
NSObject_instance_methods = ['init', 'copy', 'mutableCopy', 'dealloc', 'performSelector_withObject_afterDelay_', 'performSelectorOnMainThread_withObject_waitUntilDone_', 'performSelectorInBackground_withObject_'] |
|
|
|
|
|
class _block_descriptor (Structure): |
|
|
_fields_ = [('reserved', c_ulong), ('size', c_ulong), ('copy_helper', c_void_p), ('dispose_helper', c_void_p), ('signature', c_char_p)] |
|
|
|
|
|
class ObjCBlock (object): |
|
|
def __init__(self, func, restype=None, argtypes=None): |
|
|
if not callable(func): |
|
|
raise TypeError('%s is not callable' % func) |
|
|
if argtypes is None: |
|
|
argtypes = [] |
|
|
InvokeFuncType = ctypes.CFUNCTYPE(restype, *argtypes) |
|
|
class block_literal(Structure): |
|
|
_fields_ = [('isa', c_void_p), ('flags', c_int), ('reserved', c_int), ('invoke', InvokeFuncType), ('descriptor', _block_descriptor)] |
|
|
block = block_literal() |
|
|
block.flags = (1<<28) |
|
|
block.invoke = InvokeFuncType(func) |
|
|
block.isa = ObjCClass('__NSGlobalBlock__').ptr |
|
|
self.func = func |
|
|
self._as_parameter_ = block if LP64 else byref(block) |
|
|
|
|
|
@classmethod |
|
|
def from_param(cls, param): |
|
|
if isinstance(param, ObjCBlock) or param is None: |
|
|
return param |
|
|
elif callable(param): |
|
|
block = ObjCBlock(param) |
|
|
# Put a reference to the block into the function, so it doesn't get deallocated prematurely... |
|
|
param._block = block |
|
|
return block |
|
|
raise TypeError('cannot convert parameter to block') |
|
|
|
|
|
def __call__(self, *args): |
|
|
return self.func(*args) |
|
|
|
|
|
type_encodings['@?'] = ObjCBlock |
|
|
|
|
|
|
|
|
def ns(py_obj): |
|
|
'''Convert common Python objects to their ObjC equivalents, i.e. str => NSString, int/float => NSNumber, list => NSMutableArray, dict => NSMutableDictionary, bytearray => NSData, set => NSMutableSet. Nested structures (list/dict/set) are supported. If an object is already an instance of ObjCInstance, it is left untouched. |
|
|
''' |
|
|
if isinstance(py_obj, ObjCInstance): |
|
|
return py_obj |
|
|
if isinstance(py_obj, c_void_p): |
|
|
return ObjCInstance(py_obj) |
|
|
if hasattr(py_obj, '_objc_ptr'): |
|
|
return ObjCInstance(py_obj) |
|
|
|
|
|
if PY3: |
|
|
if isinstance(py_obj, str): |
|
|
return NSString.stringWithUTF8String_(py_obj.encode('utf-8')) |
|
|
if isinstance(py_obj, bytes): |
|
|
return NSData.dataWithBytes_length_(py_obj, len(py_obj)) |
|
|
else: |
|
|
if isinstance(py_obj, str): |
|
|
return NSString.stringWithUTF8String_(py_obj) |
|
|
if isinstance(py_obj, unicode): |
|
|
return NSString.stringWithUTF8String_(py_obj.encode('utf-8')) |
|
|
if isinstance(py_obj, bytearray): |
|
|
return NSData.dataWithBytes_length_(str(py_obj), len(py_obj)) |
|
|
if isinstance(py_obj, int): |
|
|
return NSNumber.numberWithInt_(py_obj) |
|
|
if isinstance(py_obj, float): |
|
|
return NSNumber.numberWithDouble_(py_obj) |
|
|
if isinstance(py_obj, bool): |
|
|
return NSNumber.numberWithBool_(py_obj) |
|
|
if isinstance(py_obj, list): |
|
|
arr = NSMutableArray.array() |
|
|
for obj in py_obj: |
|
|
arr.addObject_(ns(obj)) |
|
|
return arr |
|
|
if isinstance(py_obj, set): |
|
|
s = NSMutableSet.set() |
|
|
for obj in py_obj: |
|
|
s.addObject_(ns(obj)) |
|
|
return s |
|
|
if isinstance(py_obj, dict): |
|
|
dct = NSMutableDictionary.dictionary() |
|
|
for key, value in (py_obj.items() if PY3 else py_obj.iteritems()): |
|
|
dct.setObject_forKey_(ns(value), ns(key)) |
|
|
return dct |
|
|
|
|
|
def nsurl(url_or_path): |
|
|
if not isinstance(url_or_path, basestring): |
|
|
raise TypeError('expected a string') |
|
|
if ':' in url_or_path: |
|
|
return NSURL.URLWithString_(ns(url_or_path)) |
|
|
return NSURL.fileURLWithPath_(ns(url_or_path)) |
|
|
|
|
|
def nsdata_to_bytes(data): |
|
|
if not isinstance(data, ObjCInstance) or not data.isKindOfClass_(NSData): |
|
|
raise TypeError('expected an NSData object') |
|
|
_len = data.length() |
|
|
if _len == 0: |
|
|
return b'' |
|
|
ArrayType = ctypes.c_char * _len |
|
|
buffer = ArrayType() |
|
|
data.getBytes_length_(byref(buffer), _len) |
|
|
return buffer[:_len] |
|
|
|
|
|
def uiimage_to_png(img): |
|
|
if not isinstance(img, ObjCInstance) or not img.isKindOfClass_(UIImage): |
|
|
raise TypeError('expected a UIImage object') |
|
|
UIImagePNGRepresentation = c.UIImagePNGRepresentation |
|
|
UIImagePNGRepresentation.restype = c_void_p |
|
|
UIImagePNGRepresentation.argtypes = [c_void_p] |
|
|
data = ObjCInstance(UIImagePNGRepresentation(img)) |
|
|
return nsdata_to_bytes(data) |
|
|
|
|
|
def retain_global(obj): |
|
|
'''Keep an object alive''' |
|
|
_retained_globals.append(obj) |
|
|
|
|
|
def release_global(obj): |
|
|
try: |
|
|
_retained_globals.remove(obj) |
|
|
except ValueError: |
|
|
pass |
|
|
|
|
|
def OMMainThreadDispatcher_invoke_imp(self, cmd): |
|
|
if _tracefunc: |
|
|
sys.settrace(_tracefunc) |
|
|
self_instance = ObjCInstance(self) |
|
|
func = self_instance.func |
|
|
args = self_instance.args |
|
|
kwargs = self_instance.kwargs |
|
|
retval = func(*args, **kwargs) |
|
|
self_instance.retval = retval |
|
|
|
|
|
|
|
|
OMMainThreadDispatcher_name = b'OMMainThreadDispatcher_3' if PY3 else 'OMMainThreadDispatcher' |
|
|
try: |
|
|
OMMainThreadDispatcher = ObjCClass(OMMainThreadDispatcher_name) |
|
|
except ValueError: |
|
|
IMPTYPE = ctypes.CFUNCTYPE(None, c_void_p, c_void_p) |
|
|
imp = IMPTYPE(OMMainThreadDispatcher_invoke_imp) |
|
|
retain_global(imp) |
|
|
NSObject = ObjCClass('NSObject') |
|
|
class_ptr = objc_allocateClassPair(NSObject.ptr, OMMainThreadDispatcher_name, 0) |
|
|
class_addMethod(class_ptr, sel('invoke'), imp, b'v16@0:0') |
|
|
objc_registerClassPair(class_ptr) |
|
|
OMMainThreadDispatcher = ObjCClass(OMMainThreadDispatcher_name) |
|
|
|
|
|
def on_main_thread(func): |
|
|
if not callable(func): |
|
|
raise TypeError('expected a callable') |
|
|
@functools.wraps(func) |
|
|
def new_func(*args, **kwargs): |
|
|
if NSThread.isMainThread(restype=c_bool, argtypes=[]): |
|
|
return func(*args, **kwargs) |
|
|
dispatcher = OMMainThreadDispatcher.new() |
|
|
dispatcher.func = func |
|
|
dispatcher.args = args |
|
|
dispatcher.kwargs = kwargs |
|
|
dispatcher.retval = None |
|
|
dispatcher.performSelectorOnMainThread_withObject_waitUntilDone_(sel('invoke'), None, True) |
|
|
retval = dispatcher.retval |
|
|
dispatcher.release() |
|
|
return retval |
|
|
return new_func |
|
|
|
|
|
def _add_method(method, class_ptr, superclass, basename, protocols, is_classmethod=False): |
|
|
'''Helper function for create_objc_class, don't use directly (will usually crash)!''' |
|
|
if hasattr(method, 'selector'): |
|
|
sel_name = method.selector |
|
|
else: |
|
|
method_name = method.__name__ |
|
|
if method_name.startswith(basename + '_'): |
|
|
method_name = method_name[len(basename)+1:] |
|
|
sel_name = method_name.replace('_', ':') |
|
|
type_encoding = None |
|
|
if hasattr(method, 'encoding'): |
|
|
# Explicit encoding was provided, trust that unconditionally... |
|
|
type_encoding = method.encoding |
|
|
else: |
|
|
# No explicit encoding given, we have to guess... |
|
|
# First, try to derive it from overridden methods in the superclass(es)... |
|
|
if is_classmethod: |
|
|
superclass_method = class_getClassMethod(superclass, sel(sel_name)) |
|
|
else: |
|
|
superclass_method = class_getInstanceMethod(superclass, sel(sel_name)) |
|
|
if superclass_method: |
|
|
type_encoding = method_getTypeEncoding(superclass_method) |
|
|
else: |
|
|
# Try to find a matching method in one of the protocols |
|
|
for protocol_name in protocols: |
|
|
if PY3 and isinstance(protocol_name, str): |
|
|
protocol_name = protocol_name.encode('ascii') |
|
|
protocol = objc_getProtocol(protocol_name) |
|
|
if protocol: |
|
|
# Try optional method first... |
|
|
method_desc = protocol_getMethodDescription(protocol, sel(sel_name), False, True) |
|
|
if not method_desc or not method_desc.types: |
|
|
#... then required method |
|
|
method_desc = protocol_getMethodDescription(protocol, sel(sel_name), True, True) |
|
|
if method_desc and method_desc.types: |
|
|
type_encoding = method_desc.types |
|
|
break |
|
|
if not type_encoding: |
|
|
# Fall back to "action" type encoding as the default, i.e. void return type, all arguments are objects... |
|
|
num_args = len(re.findall(':', sel_name)) |
|
|
type_encoding = 'v%i@0:8%s' % (sizeof(c_void_p) * (num_args + 2), ''.join('@%i' % ((i+2) * sizeof(c_void_p),) for i in xrange(num_args))) |
|
|
|
|
|
if hasattr(method, 'restype') and hasattr(method, 'argtypes'): |
|
|
restype = method.restype |
|
|
argtypes = [c_void_p, c_void_p] + method.argtypes |
|
|
else: |
|
|
parsed_types = parse_types(type_encoding) |
|
|
restype = parsed_types[0] |
|
|
argtypes = parsed_types[1] |
|
|
# Check if the number of arguments derived from the selector matches the actual function: |
|
|
argspec = inspect.getargspec(method) |
|
|
if len(argspec.args) != len(argtypes): |
|
|
raise ValueError('%s has %i arguments (expected %i)' % (method, len(argspec.args), len(argtypes))) |
|
|
IMPTYPE = ctypes.CFUNCTYPE(restype, *argtypes) |
|
|
imp = IMPTYPE(method) |
|
|
retain_global(imp) |
|
|
if PY3 and isinstance(type_encoding, str): |
|
|
type_encoding = type_encoding.encode('ascii') |
|
|
class_addMethod(class_ptr, sel(sel_name), imp, type_encoding) |
|
|
|
|
|
def create_objc_class(name, superclass=NSObject, methods=[], classmethods=[], protocols=[], debug=True): |
|
|
'''Create and return a new Objective-C class''' |
|
|
basename = name |
|
|
if debug: |
|
|
# When debug is True (the default) and a class with the given name already exists in the runtime, |
|
|
# append an incrementing numeric suffix, until a class name is found that doesn't exist yet. |
|
|
# While this does leak some memory, it makes development much easier. Note however that the returned |
|
|
# class may not have the name you passed in (use the return value directly instead of relying on the name) |
|
|
suffix = 1 |
|
|
while True: |
|
|
try: |
|
|
existing_class = ObjCClass(name) |
|
|
suffix += 1 |
|
|
name = '%s_%i' % (basename, suffix) |
|
|
except ValueError: |
|
|
break |
|
|
else: |
|
|
# When debug is False, assume that any class is created only once, and return an existing class |
|
|
# with the given name. Note that this ignores all other parameters. This is intended to avoid |
|
|
# unnecessary memory leaks when the class definition doesn't change anymore. |
|
|
try: |
|
|
existing_class = ObjCClass(basename) |
|
|
return existing_class |
|
|
except ValueError: |
|
|
pass |
|
|
if PY3 and isinstance(name, str): |
|
|
name = name.encode('ascii') |
|
|
class_ptr = objc_allocateClassPair(superclass, name, 0) |
|
|
|
|
|
for method in methods: |
|
|
_add_method(method, class_ptr, superclass, basename, protocols) |
|
|
|
|
|
objc_registerClassPair(class_ptr) |
|
|
|
|
|
for method in classmethods: |
|
|
metaclass = object_getClass(class_ptr) |
|
|
super_metaclass = object_getClass(class_getSuperclass(class_ptr)) |
|
|
_add_method(method, metaclass, super_metaclass, basename, [], True) |
|
|
return ObjCClass(name) |
|
|
|
|
|
def settrace(func): |
|
|
# used for on_main_thread() |
|
|
global _tracefunc |
|
|
_tracefunc = func |