Created
December 4, 2016 22:47
-
-
Save tlinnet/746a18788dd51f0827fb4840b9a8631c to your computer and use it in GitHub Desktop.
objc_util.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
I love the idea behind this code, but every time I am importing it and trying to use it with another program it just says "ValueError: no Objective-C class named 'b'NSString'' found".
Maybe because you´re not under OS X/iOS ?
@tlinnet any chance that this code can get a license? Without it, it cannot legally be used in other projects.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi,
I love the idea behind this code, but every time I am importing it and trying to use it with another program it just says "ValueError: no Objective-C class named 'b'NSString'' found".
This error occurs as soon as I import anything from the file.
I am currently using Python 3.8 in a venv.