--Objecive-C runtime and bridgesupport binding. --Written by Cosmin Apreutesei. Public domain. --Tested with with LuaJIT 2.1.0, 32bit and 64bit on OSX 10.9 and 10.12. local ffi = require'ffi' local cast = ffi.cast local OSX = ffi.os == 'OSX' local x64 = ffi.abi'64bit' if OSX and ffi.arch ~= 'arm' then ffi.load('libobjc.A.dylib', true) end if x64 then ffi.cdef[[ typedef double CGFloat; typedef long NSInteger; typedef unsigned long NSUInteger; ]] else ffi.cdef[[ typedef float CGFloat; typedef int NSInteger; typedef unsigned int NSUInteger; ]] end ffi.cdef[[ typedef signed char BOOL; typedef struct objc_class *Class; typedef struct objc_object *id; typedef struct objc_selector *SEL; typedef struct objc_method *Method; typedef id (*IMP) (id, SEL, ...); typedef struct Protocol Protocol; typedef struct objc_property *objc_property_t; typedef struct objc_ivar *Ivar; struct objc_class { Class isa; }; struct objc_object { Class isa; }; struct objc_method_description { SEL name; char *types; }; //stdlib int access(const char *path, int amode); // used to check if a file exists void free (void*); // used for freeing returned dyn. allocated objects //selectors SEL sel_registerName(const char *str); const char* sel_getName(SEL aSelector); //classes Class objc_getClass(const char *name); const char *class_getName(Class cls); Class class_getSuperclass(Class cls); Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes); void objc_registerClassPair(Class cls); void objc_disposeClassPair(Class cls); BOOL class_isMetaClass(Class cls); //instances Class object_getClass(void* object); // use this instead of obj.isa because of tagged pointers //methods Method class_getInstanceMethod(Class aClass, SEL aSelector); SEL method_getName(Method method); const char *method_getTypeEncoding(Method method); IMP method_getImplementation(Method method); BOOL class_respondsToSelector(Class cls, SEL sel); IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); void method_exchangeImplementations(Method m1, Method m2); //protocols Protocol *objc_getProtocol(const char *name); const char *protocol_getName(Protocol *p); struct objc_method_description protocol_getMethodDescription(Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod); BOOL class_conformsToProtocol(Class cls, Protocol *protocol); BOOL class_addProtocol(Class cls, Protocol *protocol); //properties objc_property_t class_getProperty(Class cls, const char *name); objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty); const char *property_getName(objc_property_t property); const char *property_getAttributes(objc_property_t property); //ivars Ivar class_getInstanceVariable(Class cls, const char* name); const char *ivar_getName(Ivar ivar); const char *ivar_getTypeEncoding(Ivar ivar); ptrdiff_t ivar_getOffset(Ivar ivar); //inspection Class *objc_copyClassList(unsigned int *outCount); Protocol **objc_copyProtocolList(unsigned int *outCount); Method *class_copyMethodList(Class cls, unsigned int *outCount); struct objc_method_description *protocol_copyMethodDescriptionList(Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount); objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount); objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount); Protocol **class_copyProtocolList(Class cls, unsigned int *outCount); Protocol **protocol_copyProtocolList(Protocol *proto, unsigned int *outCount); Ivar * class_copyIvarList(Class cls, unsigned int *outCount); ]] local C = ffi.C --C namespace local P = setmetatable({}, {__index = _G}) --private namespace local objc = {} --public namespace setfenv(1, P) --globals go in P, which is published as objc.debug --helpers ---------------------------------------------------------------------------------------------------------------- local _ = string.format local id_ct = ffi.typeof'id' local function ptr(p) --convert NULL pointer to nil for easier handling (say 'not ptr' instead of 'ptr == nil') if p == nil then return nil end return p end local intptr_ct = ffi.typeof'intptr_t' local function nptr(p) --convert pointer to Lua number for using as table key if p == nil then return nil end local np = cast(intptr_ct, p) local n = tonumber(np) if x64 and cast(intptr_ct, n) ~= np then --hi address: fall back to slower tostring() n = tostring(np) end return n end local function own(p) --own a malloc()'ed pointer return p ~= nil and ffi.gc(p, C.free) or nil end local function csymbol_(name) return C[name] end local function csymbol(name) local ok, sym = pcall(csymbol_, name) if not ok then return end return sym end local function memoize(func, cache) --special memoize that works with pointer arguments too cache = cache or {} return function(input) local key = input if type(key) == 'cdata' then key = nptr(key) end if key == nil then return end local ret = rawget(cache, key) if ret == nil then ret = func(input) if ret == nil then return end rawset(cache, key, ret) end return ret end end local function memoize2(func, cache1) --memoize a two-arg. function (: local memoized = memoize(function(arg1) return memoize(function(arg2) --each unique arg1 gets 2 closures + 1 table of overhead return func(arg1, arg2) end) end, cache1) return function(arg1, arg2) return memoized(arg1)(arg2) end end local function canread(path) --check that a file is readable without having to open it return C.access(path, 2^2) == 0 end local function citer(a) --return an iterator for a null-terminated C array local i = -1 return function() if a == nil then return end i = i + 1 if a[i] == nil then return end return a[i] end end --debugging -------------------------------------------------------------------------------------------------------------- errors = true --log non-fatal errors to stderr errcount = {} --error counts per topic logtopics = {} --topics to log (none by default) local function writelog(topic, fmt, ...) io.stderr:write(_('[objc] %-16s %s\n', topic, _(fmt, ...))) end local function log(topic, ...) if logtopics[topic] then writelog(topic, ...) end end local function err(topic, ...) errcount[topic] = (errcount[topic] or 0) + 1 if errors then writelog(topic, ...) end end local function check(ok, fmt, ...) --assert with formatted strings if ok then return ok end error(_(fmt or 'assertion failed!', ...), 3) end --ffi declarations ------------------------------------------------------------------------------------------------------- checkredef = false --check incompatible redefinition attempts (makes parsing slower) printcdecl = false --print C declarations to stdout (then you can grab them and make static ffi headers) cnames = {global = {0}, struct = {0}} --C namespaces; ns[1] holds the count local function defined(name, namespace) --check if a name is already defined in a C namespace return not checkredef and cnames[namespace][name] end local function redefined(name, namespace, new_cdecl) --check cdecl redefinitions and report on incompatible ones local old_cdecl = cnames[namespace][name] if not old_cdecl then return end if not checkredef then return end if old_cdecl == new_cdecl then return true end --already defined but same def. err('redefinition', '%s\nold:\n\t%s\nnew:\n\t%s', name, old_cdecl, new_cdecl) return true end local function declare(name, namespace, cdecl) --define a C type, const or function via ffi.cdef if redefined(name, namespace, cdecl) then return end local ok, cdeferr = pcall(ffi.cdef, cdecl) if ok then cnames[namespace][1] = cnames[namespace][1] + 1 if printcdecl then print(cdecl .. ';') end else if cdeferr == 'table overflow' then --fatal error from luajit: no more space for ctypes error'too many ctypes' end err('cdef', '%s\n\t%s', cdeferr, cdecl) end cnames[namespace][name] = checkredef and cdecl or true --only store the cdecl if needed return ok end --type encodings: parsing and conversion to C types ---------------------------------------------------------------------- -- stype: a value type encoding, eg. 'B', '^[8i]', '{CGPoint="x"d"y"d}'; converts to a ctype. -- mtype: a method type encoding, eg. 'v12@0:4c8' or just 'v@:c'; converts to a ftype. -- ftype: a function/method type encoding in table form, eg. {retval='v', '@', ':', 'c'}. converts to a ctype. -- ctype: a C type encoding for a stype, eg. 'B' -> 'BOOL', or for a ftype, eg. 'v:#c' -> 'void (*) (SEL, Class, char)'. -- ct: a ffi C type object for a ctype string, eg. ffi.typeof('void (*) (id, SEL)') -> ct. --ftype spec: -- variadic = true|nil --vararg function -- isblock = true|nil --block or function (only for function pointers) -- [argindex] = stype --arg stype (argindex is 1,2,... or 'retval') -- fp = {[argindex] = ftype} --ftypes for function-pointer type args local function optname(name) --format an optional name: if not nil, return it with a space in front return name and ' '..name or '' end local stype_ctype --fw. decl. local function array_ctype(s, name, ...) --('[Ntype]', 'name') -> ctype('type', 'name[N]') local n,s = s:match'^%[(%d+)(.-)%]$' --protect pointers to arrays by enclosing the name, because `[]` has precedence over `*` in C declarations. --so for instance '^[8]' results in 'int (*)[8]` instead of `int *[8]`. if name and name:sub(1,1) == '*' then name = _('(%s)', name) end name = _('%s[%d]', name or '', n) return stype_ctype(s, name, ...) end --note: `tag` means the struct tag in the C struct namespace; `name` means the typedef name in the C global namespace. --for named structs only 'struct `tag`' is returned; for anonymous structs the full 'struct {fields...}' is returned. --before returning, named structs are recursively cdef'ed (unless deftype ~= 'cdef' which skips this step). local function struct_ctype(s, name, deftype, indent) --('{CGPoint="x"d"y"d}', 'NSPoint') -> 'struct CGPoint NSPoint' --break the struct/union def. in its constituent parts: keyword, tag, fields local kw, tag, fields = s:match'^(.)([^=]*)=?(.*).$' -- '{tag=fields}' kw = kw == '{' and 'struct' or 'union' if tag == '?' or tag == '' then tag = nil end -- ? or empty means anonymous struct if fields == '' then fields = nil end -- empty definition means opaque struct if not fields and not tag then --rare case: '{?}' coming from '^{?}' return 'void'..optname(name) end if not fields or deftype ~= 'cdef' then --opaque named struct, or asked by caller not to be cdef'ed if not tag then err('parse', 'anonymous struct not valid here: %s', s) return 'void'..optname(name) end return _('%s %s%s', kw, tag, optname(name)) end if not tag or not defined(tag, 'struct') then --anonymous or not alreay defined: parse it --parse the fields which come as '"name1"type1"name2"type2...' local t = {} local function addfield(name, s) if name == '' then name = nil end --empty field name means unnamed struct (different from anonymous) table.insert(t, stype_ctype(s, name, 'cdef', true)) --eg. 'struct _NSPoint origin' return '' --remove the match end local s = fields local n while s ~= '' do s,n = s:gsub('^"([^"]*)"([%^]*%b{})', addfield) --try "field"{...} if n == 0 then s,n = s:gsub('^"([^"]*)"([%^]*%b())', addfield) end --try "field"(...) if n == 0 then s,n = s:gsub('^"([^"]+)"([%^]*%b[])', addfield) end --try "field"[...] if n == 0 then s,n = s:gsub('^"([^"]+)"(@)%?', addfield) end --try "field"@? (block type) if n == 0 then s,n = s:gsub('^"([^"]+)"(@"[A-Z][^"]+")', addfield) end --try "field"@"Class" if n == 0 then s,n = s:gsub('^"([^"]*)"([^"]+)', addfield) end --try "field"... assert(n > 0, s) end local ctype = _('%s%s {\n\t%s;\n}', kw, optname(tag), table.concat(t, ';\n\t')) --anonymous struct: return the full definition if not tag then if indent then --this is the only multiline output that can be indented ctype = ctype:gsub('\n', '\n\t') end return _('%s%s', ctype, optname(name)) end --named struct: cdef it. --note: duplicate struct cdefs are rejected by luajit 2.0 with an error. we guard against that. declare(tag, 'struct', ctype) end return _('%s %s%s', kw, tag, optname(name)) end local function bitfield_ctype(s, name, deftype) --('bN', 'name') -> 'unsigned name: N'; N must be <= 32 local n = s:match'^b(%d+)$' return _('unsigned %s: %d', name or '_', n) end local function pointer_ctype(s, name, ...) --('^type', 'name') -> ctype('type', '*name') return stype_ctype(s:sub(2), '*'..(name or ''), ...) end local function char_ptr_ctype(s, ...) --('*', 'name') -> 'char *name' return pointer_ctype('^c', ...) end local function primitive_ctype(ctype) return function(s, name) return ctype .. optname(name) end end local function const_ctype(s, ...) return 'const ' .. stype_ctype(s:sub(2), ...) end local ctype_decoders = { ['c'] = primitive_ctype'char', --also for `BOOL` (boolean-ness is specified through method type annotations) ['i'] = primitive_ctype'int', ['s'] = primitive_ctype'short', ['l'] = primitive_ctype'long', --treated as a 32-bit quantity on 64-bit programs ['q'] = primitive_ctype'long long', ['C'] = primitive_ctype'unsigned char', ['I'] = primitive_ctype'unsigned int', ['S'] = primitive_ctype'unsigned short', ['L'] = primitive_ctype'unsigned long', ['Q'] = primitive_ctype'unsigned long long', ['f'] = primitive_ctype'float', ['d'] = primitive_ctype'double', ['D'] = primitive_ctype'long double', ['B'] = primitive_ctype'BOOL', --does not appear in the runtime, but in bridgesupport ['v'] = primitive_ctype'void', ['?'] = primitive_ctype'void', --unknown type; used for function pointers among other things ['@'] = primitive_ctype'id', --@ or @? or @"ClassName" ['#'] = primitive_ctype'Class', [':'] = primitive_ctype'SEL', ['['] = array_ctype, -- [Ntype] ; N = number of elements ['{'] = struct_ctype, -- {name=fields} ; struct ['('] = struct_ctype, -- (name=fields) ; union ['b'] = bitfield_ctype, -- bN ; N = number of bits ['^'] = pointer_ctype, -- ^type ; pointer ['*'] = char_ptr_ctype, -- * ; char* pointer ['r'] = const_ctype, } --convert a value type encoding (stype) to its C type, or, if name given, its C declaration. --3rd arg = 'cdef' means that named structs contain field names and thus can and should be cdef'ed before returning. function stype_ctype(s, name, ...) local decoder = assert(ctype_decoders[s:sub(1,1)], s) return decoder(s, name, ...) end --decode a method type encoding (mtype), and return its table representation (ftype). --note: other type annotations like `variadic` and `isblock` come from bridgesupport attributes. --note: offsets are meaningless on current architectures. local function mtype_ftype(mtype) --eg. 'v12@0:4c8' (retval offset arg1 offset arg2 offset ...) local ftype = {} local retval local function addarg(annotations, s) if annotations:find'r' then s = 'r' .. s end if not retval then retval = s else table.insert(ftype, s) end return '' --remove the match end local s,n = mtype while s ~= '' do s,n = s:gsub('^([rnNoORV]*)([%^]*%b{})%d*', addarg) --try {...}offset if n == 0 then s,n = s:gsub('^([rnNoORV]*)([%^]*%b())%d*', addarg) end --try (...)offset if n == 0 then s,n = s:gsub('^([rnNoORV]*)([%^]*%b[])%d*', addarg) end --try [...]offset if n == 0 then s,n = s:gsub('^([rnNoORV]*)(@%?)%d*', addarg) end --try @? (block type) if n == 0 then s,n = s:gsub('^([rnNoORV]*)(@"[A-Z][^"]+")%d*', addarg) end --try @"Class"offset if n == 0 then s,n = s:gsub('^([rnNoORV]*)([%^]*[cislqCISLQfdDBv%?@#%:%*])%d*', addarg) end --try offset assert(n > 0, mtype) end if retval ~= 'v' then ftype.retval = retval end return ftype end --check if a ftype cannot be fully used with ffi callbacks, so we need to employ workarounds. local function ftype_needs_wrapping(ftype) --ffi callbacks don't work with vararg methods. if ftype.variadic then return true end --ffi callbacks don't work with pass-by-value structs. for i = 1, #ftype do if ftype[i]:find'^[%{%(]' then return true end end --they also can't return structs directly. if ftype.retval and ftype.retval:find'^[%{%(]' then return true end end --format a table representation of a method or function (ftype) to its C type or, if name given, its C declaration. --3rd arg = true means the type will be used for a ffi callback, which incurs some limitations. local function ftype_ctype(ftype, name, for_callback) local retval = ftype.retval local lastarg = #ftype if for_callback then --ffi callbacks don't work with pass-by-value structs, so we're going to stop at the first one. for i = 1, #ftype do if ftype[i]:find'^[%{%(]' then lastarg = i - 1 end end --they also can't return structs directly. if retval and retval:find'^[%{%(]' then retval = nil end end local t = {} for i = 1, lastarg do t[i] = stype_ctype(ftype[i]) end local args = table.concat(t, ', ') local retval = retval and stype_ctype(retval) or 'void' local vararg = not for_callback and ftype.variadic and (#t > 0 and ', ...' or '...') or '' if name then return _('%s %s (%s%s)', retval, name, args, vararg) else return _('%s (*) (%s%s)', retval, args, vararg) end end local function ftype_mtype(ftype) --convert ftype to method type encoding return (ftype.retval or 'v') .. table.concat(ftype) end local static_mtype_ftype = memoize(function(mtype) --ftype cache for non-anotated method types return mtype_ftype(mtype) end) --cache anonymous function objects by their signature because we can only make 64K anonymous ct objects --in luajit2 and there are a lot of duplicate method and function-pointer signatures (named functions are separate). local ctype_ct = memoize(function(ctype) local ok,ct = pcall(ffi.typeof, ctype) check(ok, 'ctype error for "%s": %s', ctype, ct) return ct end) local function ftype_ct(ftype, name, for_callback) local cachekey = 'cb_ct' or 'ct' local ct = ftype[cachekey] or ctype_ct(ftype_ctype(ftype, name, for_callback)) ftype[cachekey] = ct --cache it, useful for static ftypes return ct end --bridgesupport file parsing --------------------------------------------------------------------------------------------- lazyfuncs = true --cdef functions on the first call rather than at the time of parsing the xml (see below) loaddeps = false --load dependencies specified in the bridgesupport file (usually too many to be useful) --rename tables to prevent name clashes rename = {string = {}, enum = {}, typedef = {}, const = {}, ['function'] = {}} --rename table to solve name clashing rename.typedef.mach_timebase_info = 'mach_timebase_info_t' rename.const.TkFont = 'const_TkFont' local function global(name, kind) --return the "fixed" name for a given global name return rename[kind][name] or name end --xml tag handlers local tag = {} --{tag = start_tag_handler} function tag.depends_on(attrs) if not loaddeps then return end local ok, loaderr = pcall(load_framework, attrs.path) if not ok then err('load', '%s', loaderr) end end local typekey = x64 and 'type64' or 'type' local valkey = x64 and 'value64' or 'value' function tag.string_constant(attrs) --note: some of these are NSStrings but we load them all as Lua strings. rawset(objc, global(attrs.name, 'string'), attrs.value) end function tag.enum(attrs) if attrs.ignore == 'true' then return end local s = attrs[valkey] or attrs.value if not s then return end --value not available on this platform rawset(objc, global(attrs.name, 'enum'), tonumber(s)) end local function cdef_node(attrs, typedecl, deftype) local name = global(attrs.name, typedecl) --note: duplicate typedef and const defs are ignored by luajit 2.0 and don't overflow its ctype table, --but this is an implementation detail that we shouldn't rely on, so we guard against redefinitions. if defined(name, 'global') then return end local s = attrs[typekey] or attrs.type if not s then return end --type not available on this platform local ctype = stype_ctype(s, name, deftype) declare(name, 'global', _('%s %s', typedecl, ctype)) end function tag.constant(attrs) cdef_node(attrs, 'const') end function tag.struct(attrs) cdef_node(attrs, 'typedef', attrs.opaque ~= 'true' and 'cdef' or nil) end function tag.cftype(attrs) cdef_node(attrs, 'typedef', 'cdef') end function tag.opaque(attrs) cdef_node(attrs, 'typedef') end --arg or retval tag with function_pointer attribute local function fp_arg(argtag, attrs, getwhile) if attrs.function_pointer ~= 'true' then return end local argtype = attrs[typekey] or attrs.type local fp = {isblock = argtype == '@?' or nil} if fp.isblock then fp[1] = '^v' end --adjust type: arg#1 is a pointer to the block object for tag, attrs in getwhile(argtag) do if tag == 'arg' or tag == 'retval' then if fp then local argtype = attrs[typekey] or attrs.type if not argtype then --type not available on this platform: skip the entire argtag fp = nil else local argindex = tag == 'retval' and 'retval' or #fp + 1 if not (argindex == 'retval' and argtype == 'v') then fp[argindex] = argtype end end end local fp1 = fp_arg(tag, attrs, getwhile) --fpargs can have fpargs too if fp and fp1 then local argindex = tag == 'retval' and 'retval' or #fp + 1 fp.fp = fp.fp or {} fp.fp[argindex] = fp1 end for _ in getwhile(tag) do end --eat it because it might be the same as argtag end end return fp end --function tag local function_caller --fw. decl. local function add_function(name, ftype, lazy) --cdef and call-wrap a global C function if lazy == nil then lazy = lazyfuncs end local function addfunc() declare(name, 'global', ftype_ctype(ftype, name)) local cfunc = csymbol(name) if not cfunc then err('symbol', 'missing C function: %s', name) return end local caller = function_caller(ftype, cfunc) rawset(objc, name, caller) --overshadow the C function with the caller return caller end if lazy then --delay cdef'ing the function until the first call, to avoid polluting the C namespace with unused declarations. --this is because in luajit2 can only hold 64k ctypes total. rawset(objc, name, function(...) local func = addfunc() if not func then return end return func(...) end) else addfunc() end end tag['function'] = function(attrs, getwhile) local name = global(attrs.name, 'function') --get the "fixed" name --note: duplicate function defs are ignored by luajit 2.0 but they do overflow its ctype table, --so it's necessary that we guard against redefinitions. if defined(name, 'global') then return end local ftype = {variadic = attrs.variadic == 'true' or nil} for tag, attrs in getwhile'function' do if ftype and (tag == 'arg' or tag == 'retval') then local argtype = attrs[typekey] or attrs.type if not argtype then --type not available on this platform: skip the entire function ftype = nil else local argindex = tag == 'retval' and 'retval' or #ftype + 1 if not (argindex == 'retval' and argtype == 'v') then ftype[argindex] = argtype end local fp = fp_arg(tag, attrs, getwhile) if fp then ftype.fp = ftype.fp or {} ftype.fp[argindex] = fp end end end end if ftype then add_function(name, ftype) end end --informal_protocol tag local add_informal_protocol --fw. decl. local add_informal_protocol_method --fw. decl. function tag.informal_protocol(attrs, getwhile) local proto = add_informal_protocol(attrs.name) for tag, attrs in getwhile'informal_protocol' do if proto and tag == 'method' then local mtype = attrs[typekey] or attrs.type if mtype then add_informal_protocol_method(proto, attrs.selector, attrs.class_method ~= 'true', mtype) end end end end --class tag --method type annotations: {[is_instance] = {classname = {methodname = partial-ftype}}. --only boolean retvals and function pointer args are recorded. local mta = {[true] = {}, [false] = {}} function tag.class(attrs, getwhile) local inst_methods = {} local class_methods = {} local classname = attrs.name for tag, attrs in getwhile'class' do if tag == 'method' then local meth = {} local inst = attrs.class_method ~= 'true' meth.variadic = attrs.variadic == 'true' or nil local methodname = attrs.selector for tag, attrs in getwhile'method' do if meth and (tag == 'arg' or tag == 'retval') then local argtype = attrs[typekey] or attrs.type --attrs.index is the arg. index starting from 0 after the first two arguments (obj, sel). local argindex = tag == 'retval' and 'retval' or attrs.index + 1 + 2 if tag == 'retval' and argtype == 'B' then meth.retval = 'B' end local fp = fp_arg(tag, attrs, getwhile) if fp then meth.fp = meth.fp or {} meth.fp[argindex] = fp end end end if meth and next(meth) then if inst then inst_methods[methodname] = meth else class_methods[methodname] = meth end end end end if next(inst_methods) then mta[true][classname] = inst_methods end if next(class_methods) then mta[false][classname] = class_methods end end local function get_raw_mta(classname, selname, inst) local cls = mta[inst][classname] return cls and cls[selname] end --function_alias tag function tag.function_alias(attrs) --these tags always come after the 'function' tags local name = attrs.name local original = attrs.original --delay getting a cdef to the original function until the first call to the alias rawset(objc, name, function(...) local origfunc = objc[original] rawset(objc, name, origfunc) --replace this wrapper with the original function return origfunc(...) end) end --xml tag processor that dispatches the processing of tags inside tag to a table of tag handlers. --the tag handler gets the tag attributes and a conditional iterator to get any subtags. local function process_tags(gettag) local function nextwhile(endtag) local start, tag, attrs = gettag() if not start then if tag == endtag then return end return nextwhile(endtag) end return tag, attrs end local function getwhile(endtag) --iterate tags until `endtag` ends, returning (tag, attrs) for each tag return nextwhile, endtag end for tagname, attrs in getwhile'signatures' do if tag[tagname] then tag[tagname](attrs, getwhile) end end end --fast, push-style xml parser that works with the simple cocoa generated xml files. local function readfile(name) local f = assert(io.open(name, 'rb')) local s = f:read'*a' f:close() return s end local function parse_xml(path, write) local s = readfile(path) for endtag, tag, attrs, tagends in s:gmatch'<(/?)([%a_][%w_]*)([^/>]*)(/?)>' do if endtag == '/' then write(false, tag) else local t = {} for name, val in attrs:gmatch'([%a_][%w_]*)=["\']([^"\']*)["\']' do if val:find('"', 1, true) then --gsub alone is way slower val = val:gsub('"', '"') --the only escaping found in all xml files tested end t[name] = val end write(true, tag, t) if tagends == '/' then write(false, tag) end end end end --xml processor driver. runs a user-supplied tag processor function in a coroutine. --the processor receives a gettags() function to pull tags with, as its first argument. usexpat = false --choice of xml parser: expat or the lua-based parser above. local function process_xml(path, processor, ...) local send = coroutine.wrap(processor) send(coroutine.yield, ...) --start the parser by passing it the gettag() function and other user args. if usexpat then local expat = require'expat' expat.parse({path = path}, { start_tag = function(name, attrs) send(true, name, attrs) end, end_tag = function(name) send(false, name) end, }) else parse_xml(path, send) end end function load_bridgesupport(path) process_xml(path, process_tags) end --loading frameworks ----------------------------------------------------------------------------------------------------- loadtypes = true --load bridgesupport files local searchpaths = { '/System/Library/Frameworks', '/Library/Frameworks', '~/Library/Frameworks', } function find_framework(name) --given a framework name or its full path, return its full path and its name if name:find'^/' then -- try 'path/foo.framework' local path = name local name = path:match'([^/]+)%.framework$' if not name then -- try 'path/foo.framework/foo' name = path:match'([^/]+)$' path = name and path:sub(1, -#name-2) end if name and canread(path) then return path, name end else local subname = name:gsub('%.framework', '%$') --escape the '.framework' suffix subname = subname:gsub('%.', '.framework/Versions/Current/Frameworks/') --expand 'Framework.Subframework' syntax subname = subname:gsub('%$', '.framework') --unescape it name = name:match'([^%./]+)$' --strip relative path from name for i,path in pairs(searchpaths) do path = _('%s/%s.framework', path, subname) if canread(path) then return path, name end end end end loaded = {} --{framework_name = true} loaded_bs = {} --{framework_name = true} function load_framework(namepath, option) --load a framework given its name or full path if not OSX then error('platform not OSX', 2) end local basepath, name = find_framework(namepath) check(basepath, 'framework not found %s', namepath) if not loaded[basepath] then --load the framework binary which contains classes, functions and protocols local path = _('%s/%s', basepath, name) if canread(path) then ffi.load(path, true) end --load the bridgesupport dylib which contains callable versions of inline functions (NSMakePoint, etc.) local path = _('%s/Resources/BridgeSupport/%s.dylib', basepath, name) if canread(path) then ffi.load(path, true) end log('load', '%s', basepath) loaded[basepath] = true end if loadtypes and option ~= 'notypes' and not loaded_bs[basepath] then loaded_bs[basepath] = true --set it before loading the file to prevent recursion from depends_on tag --load the bridgesupport xml file which contains typedefs and constants which we can't get from the runtime. --try a local copy first because see https://github.com/luapower/objc/issues/5. local path = _('bridgesupport/%s.bridgesupport', name) if canread(path) then load_bridgesupport(path) else local path = _('%s/Resources/BridgeSupport/%s.bridgesupport', basepath, name) if canread(path) then load_bridgesupport(path) end end end end --objective-c runtime ---------------------------------------------------------------------------------------------------- --selectors local selector_object = memoize(function(name) --cache to prevent string creation on each method call (worth it?) --replace '_' with ':' except at the beginning name = name:match('^_*') .. name:gsub('^_*', ''):gsub('_', ':') return ptr(C.sel_registerName(name)) end) local function selector(name) if type(name) ~= 'string' then return name end return selector_object(name) end local function selector_name(sel) return ffi.string(C.sel_getName(sel)) end ffi.metatype('struct objc_selector', { __tostring = selector_name, __index = { name = selector_name, }, }) --formal protocols local function formal_protocols() return citer(own(C.objc_copyProtocolList(nil))) end local function formal_protocol(name) return ptr(C.objc_getProtocol(name)) end local function formal_protocol_name(proto) return ffi.string(C.protocol_getName(proto)) end local function formal_protocol_protocols(proto) --protocols of superprotocols not included return citer(own(C.protocol_copyProtocolList(proto, nil))) end local function formal_protocol_properties(proto) --inherited properties not included return citer(own(C.protocol_copyPropertyList(proto, nil))) end local function formal_protocol_property(proto, name, required, readonly) --looks in superprotocols too return ptr(C.protocol_getProperty(proto, name, required, readonly)) end local function formal_protocol_methods(proto, inst, required) --inherited methods not included local desc = own(C.protocol_copyMethodDescriptionList(proto, required, inst, nil)) local i = -1 return function() i = i + 1 if desc == nil then return end if desc[i].name == nil then return end --note: we return the name of the selector instead of the selector itself to match the informal protocol API return selector_name(desc[i].name), ffi.string(desc[i].types) end end local function formal_protocol_mtype(proto, sel, inst, required) --looks in superprotocols too local desc = C.protocol_getMethodDescription(proto, sel, required, inst) if desc.name == nil then return end return ffi.string(desc.types) end local function formal_protocol_ftype(...) return static_mtype_ftype(formal_protocol_mtype(...)) end local function formal_protocol_ctype(proto, sel, inst, required, for_callback) return ftype_ctype(formal_protocol_ftype(proto, sel, inst, required), nil, for_callback) end local function formal_protocol_ct(proto, sel, inst, required, for_callback) return ftype_ct(formal_protocol_ftype(proto, sel, inst, required), nil, for_callback) end ffi.metatype('struct Protocol', { __tostring = formal_protocol_name, __index = { formal = true, name = formal_protocol_name, protocols = formal_protocol_protocols, properties = formal_protocol_properties, property = formal_protocol_property, methods = formal_protocol_methods, --iterator() -> selname, mtype mtype = formal_protocol_mtype, ftype = formal_protocol_ftype, ctype = formal_protocol_ctype, ct = formal_protocol_ct, }, }) --informal protocols (must have the exact same API as formal protocols) local informal_protocols = {} --{name = proto} local infprot = {formal = false} local infprot_meta = {__index = infprot} local function informal_protocol(name) return informal_protocols[name] end function add_informal_protocol(name) if OSX and formal_protocol(name) then return end --prevent needless duplication of formal protocols local proto = setmetatable({_name = name, _methods = {}}, infprot_meta) informal_protocols[name] = proto return proto end function add_informal_protocol_method(proto, selname, inst, mtype) proto._methods[selname] = {_inst = inst, _mtype = mtype} end function infprot:name() return self._name end infprot_meta.__tostring = infprot.name local function noop() return end function infprot:protocols() return noop --not in bridgesupport end function infprot:properties() return noop --not in bridgesupport end infprot.property = noop function infprot:methods(inst, required) if required then return noop end --by definition, informal protocols do not contain required methods return coroutine.wrap(function() for sel, m in pairs(self._methods) do if m._inst == inst then coroutine.yield(sel, m._mtype) end end end) end function infprot:mtype(sel, inst, required) if required then return end --by definition, informal protocols do not contain required methods local m = self._methods[selector_name(sel)] return m and m._inst == inst and m._mtype or nil end function infprot:ftype(...) return static_mtype_ftype(self:mtype(...)) end function infprot:ctype(sel, inst, required, for_callback) return ftype_ctype(self:ftype(sel, inst, required), nil, for_callback) end function infprot:ct(sel, inst, required, for_callback) return ftype_ct(self:ftype(sel, inst, required), nil, for_callback) end --all protocols local function protocols() --list all loaded protocols return coroutine.wrap(function() for proto in formal_protocols() do coroutine.yield(proto) end for name, proto in pairs(informal_protocols) do coroutine.yield(proto) end end) end local function protocol(name) --protocol by name if type(name) ~= 'string' then return name end return check(formal_protocol(name) or informal_protocol(name), 'unknown protocol %s', name) end --properties local function property_name(prop) return ffi.string(C.property_getName(prop)) end local prop_attr_decoders = { --TODO: copy, retain, nonatomic, dynamic, weak, gc. T = function(s, t) t.stype = s end, V = function(s, t) t.ivar = s end, G = function(s, t) t.getter = s end, S = function(s, t) t.setter = s end, R = function(s, t) t.readonly = true end, } local property_attrs = memoize(function(prop) --cache to prevent parsing on each property access local s = ffi.string(C.property_getAttributes(prop)) local attrs = {} for k,v in (s..','):gmatch'(.)([^,]*),' do local decode = prop_attr_decoders[k] if decode then decode(v, attrs) end end return attrs end) local function property_getter(prop) local attrs = property_attrs(prop) if not attrs.getter then attrs.getter = property_name(prop) --default getter; cache it end return attrs.getter end local function property_setter(prop) local attrs = property_attrs(prop) if attrs.readonly then return end if not attrs.setter then local name = property_name(prop) attrs.setter = _('set%s%s:', name:sub(1,1):upper(), name:sub(2)) --'name' -> 'setName:' end return attrs.setter end local function property_stype(prop) return property_attrs(prop).stype end local function property_ctype(prop) local attrs = property_attrs(prop) if not attrs.ctype then attrs.ctype = stype_ctype(attrs.stype) --cache it end return attrs.ctype end local function property_readonly(prop) return property_attrs(prop).readonly == true end local function property_ivar(prop) return property_attrs(prop).ivar end ffi.metatype('struct objc_property', { __tostring = property_name, __index = { name = property_name, getter = property_getter, setter = property_setter, stype = property_stype, ctype = property_ctype, readonly = property_readonly, ivar = property_ivar, }, }) --methods local function method_selector(method) return ptr(C.method_getName(method)) end local function method_name(method) return selector_name(method_selector(method)) end local function method_mtype(method) --NOTE: this runtime mtype might look different if corected by mta return ffi.string(C.method_getTypeEncoding(method)) end local function method_raw_ftype(method) --NOTE: this is the raw runtime ftype, not corrected by mta return mtype_ftype(method_mtype(method)) end local function method_raw_ctype(method) --NOTE: this is the raw runtime ctype, not corrected by mta return ftype_ctype(method_raw_ftype(method)) end local function method_raw_ctype_cb(method) return ftype_ctype(method_raw_ftype(method), nil, true) end local function method_imp(method) --NOTE: this is of type IMP (i.e. vararg, untyped). return ptr(C.method_getImplementation(method)) end local method_exchange_imp = OSX and C.method_exchangeImplementations ffi.metatype('struct objc_method', { __tostring = method_name, __index = { selector = method_selector, name = method_name, mtype = method_mtype, raw_ftype = method_raw_ftype, raw_ctype = method_raw_ctype, raw_ctype_cb = method_raw_ctype_cb, imp = method_imp, exchange_imp = method_exchange_imp, }, }) --classes local function classes() --list all loaded classes return citer(own(C.objc_copyClassList(nil))) end local add_class_protocol --fw. decl. local function isobj(x) return ffi.istype(id_ct, x) end local class_ct = ffi.typeof'Class' local function isclass(x) return ffi.istype(class_ct, x) end local function ismetaclass(cls) return C.class_isMetaClass(cls) == 1 end local classof = OSX and C.object_getClass local function class(name, super, proto, ...) --find or create a class if super == nil then --want to find a class, not to create one if isclass(name) then --class object: pass through return name end if isobj(name) then --instance: return its class return classof(name) end check(type(name) == 'string', 'object, class, or class name expected, got %s', type(name)) return ptr(C.objc_getClass(name)) else check(type(name) == 'string', 'class name expected, got %s', type(name)) end --given a second arg., check for 'SuperClass ' syntax if type(super) == 'string' then local supername, protos = super:match'^%s*([^%<%s]+)%s*%<%s*([^%>]+)%>%s*$' if supername then local t = {} for proto in (protos..','):gmatch'([^,%s]+)%s*,%s*' do t[#t+1] = proto end t[#t+1] = proto for i = 1, select('#', ...) do t[#t+1] = select(i, ...) end return class(name, supername, unpack(t)) end end local superclass if super then superclass = class(super) check(superclass, 'superclass not found %s', super) end check(not class(name), 'class already defined %s', name) local cls = check(ptr(C.objc_allocateClassPair(superclass, name, 0))) C.objc_registerClassPair(cls) --TODO: we can't dispose the class if it has subclasses, so figure out --a way to dispose it only after the last subclass has been disposed. --ffi.gc(cls, C.objc_disposeClassPair) if proto then add_class_protocol(cls, proto, ...) end return cls end local function class_name(cls) if isobj(cls) then cls = classof(cls) end return ffi.string(C.class_getName(class(cls))) end local function superclass(cls) --note: superclass(metaclass(cls)) == metaclass(superclass(cls)) if isobj(cls) then cls = classof(cls) end return ptr(C.class_getSuperclass(class(cls))) end local function metaclass(cls) --note: metaclass(metaclass(cls)) == nil cls = class(cls) if isobj(cls) then cls = classof(cls) end if ismetaclass(cls) then return nil end --OSX sets metaclass.isa to garbage return ptr(classof(cls)) end local function isa(cls, what) what = class(what) if isobj(cls) then return classof(cls) == what or isa(classof(cls), what) end local super = superclass(cls) if super == what then return true elseif not super then return false end return isa(super, what) end --class protocols local class_informal_protocols = {} --{[nptr(cls)] = {name = informal_protocol,...}} local function class_protocols(cls) --does not include protocols of superclasses return coroutine.wrap(function() for proto in citer(own(C.class_copyProtocolList(cls, nil))) do coroutine.yield(proto) end local t = class_informal_protocols[nptr(cls)] if not t then return end for name, proto in pairs(t) do coroutine.yield(proto) end end) end local function class_conforms(cls, proto) cls = class(cls) proto = protocol(proto) if proto.formal then return C.class_conformsToProtocol(cls, proto) == 1 else local t = class_informal_protocols[nptr(cls)] return t and t[proto:name()] and true or false end end function add_class_protocol(cls, proto, ...) cls = class(cls) proto = protocol(proto) if proto.formal then C.class_addProtocol(class(cls), proto) else local t = class_informal_protocols[nptr(cls)] if not t then t = {} class_informal_protocols[nptr(cls)] = t end t[proto:name()] = proto end if ... then add_class_protocol(cls, ...) end end --find a selector in conforming protocols and if found, return its type local function conforming_mtype(cls, sel) local inst = not ismetaclass(cls) for proto in class_protocols(cls) do local mtype = proto:mtype(sel, inst, false) or proto:mtype(sel, inst, true) if mtype then return mtype end end if superclass(cls) then return conforming_mtype(superclass(cls), sel) end end --class properties local function class_properties(cls) --inherited properties not included return citer(own(C.class_copyPropertyList(cls, nil))) end local function class_property(cls, name) --looks in superclasses too return ptr(C.class_getProperty(cls, name)) end --class methods local function class_methods(cls) --inherited methods not included return citer(own(C.class_copyMethodList(class(cls), nil))) end local function class_method(cls, sel) --looks for inherited methods too return ptr(C.class_getInstanceMethod(class(cls), selector(sel))) end local function class_responds(cls, sel) --looks for inherited methods too return C.class_respondsToSelector(superclass(cls), selector(sel)) == 1 end local callback_caller -- fw. decl. local cbframe = false --use cbframe for struct-by-val callbacks local function use_cbframe(use) local old_cbframe = cbframe if use ~= nil then cbframe = use end return old_cbframe end local function add_class_method(cls, sel, func, ftype) cls = class(cls) sel = selector(sel) ftype = ftype or 'v@:' local mtype = ftype if type(ftype) == 'string' then --it's a mtype, parse it ftype = mtype_ftype(mtype) else mtype = ftype_mtype(ftype) end local imp if cbframe and ftype_needs_wrapping(ftype) then local cbframe = require'cbframe' --runtime dependency, only needed with `cbframe` debug option. local callback = cbframe.new(func) --note: pins func; also, it will never be released. imp = cast('IMP', callback.p) else local func = function(obj, sel, ...) --wrap to skip sel arg return func(obj, ...) end local func = callback_caller(ftype, func) --wrapper that converts args and return values. local ct = ftype_ct(ftype, nil, true) --get the callback ctype stripped of pass-by-val structs local callback = cast(ct, func) --note: pins func; also, it will never be released. imp = cast('IMP', callback) end C.class_replaceMethod(cls, sel, imp, mtype) --add or replace if logtopics.addmethod then log('addmethod', ' %-40s %-40s %-8s %s', class_name(cls), selector_name(sel), ismetaclass(cls) and 'class' or 'inst', ftype_ctype(ftype, nil, true)) end end --ivars local function class_ivars(cls) return citer(own(C.class_copyIvarList(cls, nil))) end local function class_ivar(cls, name) return ptr(C.class_getInstanceVariable(cls, name)) end local function ivar_name(ivar) return ffi.string(C.ivar_getName(ivar)) end local function ivar_offset(ivar) --this could be just an alias but we want to load this module in windows too return C.ivar_getOffset(ivar) end local function ivar_stype(ivar) return ffi.string(C.ivar_getTypeEncoding(ivar)) end local function ivar_stype_ctype(stype) local stype = stype:match'^[rnNoORV]*(.*)' return stype_ctype('^'..stype, nil, stype:find'^[%{%(]%?' and 'cdef') end local function ivar_ctype(ivar) --NOTE: bitfield ivars not supported (need ivar layouts for that) return ivar_stype_ctype(ivar_stype(ivar)) end local ivar_stype_ct = memoize(function(stype) --cache to avoid re-parsing and ctype creation return ffi.typeof(ivar_stype_ctype(stype)) end) local function ivar_ct(ivar) return ivar_stype_ct(ivar_stype(ivar)) end local byteptr_ct = ffi.typeof'uint8_t*' local function ivar_get_value(obj, name, ivar) return cast(ivar_ct(ivar), cast(byteptr_ct, obj) + ivar_offset(ivar))[0] end local function ivar_set_value(obj, name, ivar, val) cast(ivar_ct(ivar), cast(byteptr_ct, obj) + ivar_offset(ivar))[0] = val end ffi.metatype('struct objc_ivar', { __tostring = ivar_name, __index = { name = ivar_name, stype = ivar_stype, ctype = ivar_ctype, ct = ivar_ct, offset = ivar_offset, }, }) --class/instance luavars local luavars = {} --{[nptr(cls|obj)] = {var1 = val1, ...}} local function get_luavar(obj, var) local vars = luavars[nptr(obj)] return vars and vars[var] end local function set_luavar(obj, var, val) local vars = luavars[nptr(obj)] if not vars then vars = {} luavars[nptr(obj)] = vars end vars[var] = val end --class/instance/protocol method finding based on loose selector names. --loose selector names are those that may or may not contain a trailing '_'. local function find_method(cls, selname) local sel = selector(selname) local meth = class_method(cls, sel) if meth then return sel, meth end --method not found, try again with a trailing '_' or ':' if not (selname:find('_', #selname, true) or selname:find(':', #selname, true)) then return find_method(cls, selname..'_') end end local function find_conforming_mtype(cls, selname) local sel = selector(selname) local mtype = conforming_mtype(cls, sel) if mtype then return sel, mtype end if not selname:find'[_%:]$' then --method not found, try again with a trailing '_' return find_conforming_mtype(cls, selname..'_') end end --method ftype annotation local function get_mta(cls, sel) --looks in superclasses too local mta = get_raw_mta(class_name(cls), selector_name(sel), not ismetaclass(cls)) if mta then return mta end cls = superclass(cls) if not cls then return end return get_mta(cls, sel) end local function annotate_ftype(ftype, mta) if mta then --the mta is a partial ftype: add it over for k,v in pairs(mta) do ftype[k] = v end end return ftype end local function method_ftype(cls, sel, method) method = assert(method or class_method(cls, sel), 'method not found') local mta = get_mta(cls, sel) if mta then return annotate_ftype(method_raw_ftype(method), mta) else return static_mtype_ftype(method_mtype(method)) end end local function method_arg_ftype(cls, selname, argindex) --for constructing blocks to pass to methods check(argindex, 'argindex expected') local sel, method = find_method(cls, selname) if not sel then return end local ftype = method_ftype(cls, sel, method) argindex = argindex or 1 argindex = argindex == 'retval' and argindex or argindex + 2 return ftype, argindex end --class/instance method caller based on loose selector names. --NOTE: ffi.gc() applies to cdata objects, not to the identities that they hold. Thus you can easily get --the same object from two different method invocations into two distinct cdata objects. Setting ffi.gc() --on both will result in your finalizer being called twice, each time when each cdata gets collected. --This means that references to objects need to be refcounted if per-object resources need to be released on gc. local refcounts = {} --number of collectable cdata references to an object local function inc_refcount(obj, n) local refcount = (refcounts[nptr(obj)] or 0) + n assert(refcount >= 0, 'over-releasing') refcounts[nptr(obj)] = refcount ~= 0 and refcount or nil return refcount end local function release_object(obj) if inc_refcount(obj, -1) == 0 then luavars[nptr(obj)] = nil end end local function collect_object(obj) --note: assume this will be called multiple times on the same obj! obj:release() end --methods for which we should refrain from retaining the result object noretain = {release=1, autorelease=1, retain=1, alloc=1, new=1, copy=1, mutableCopy=1} --cache it to avoid re-parsing, annotating, formatting, casting, function-wrapping, method-wrapping. local method_caller = memoize2(function(cls, selname) local sel, method = find_method(cls, selname) if not sel then return end local ftype = method_ftype(cls, sel, method) local ct = ftype_ct(ftype) local func = method_imp(method) local func = cast(ct, func) local func = function_caller(ftype, func) local can_retain = not noretain[selname] local is_release = selname == 'release' or selname == 'autorelease' local log_refcount = (is_release or selname == 'retain') and logtopics.refcount return function(obj, ...) local before_rc, after_rc, objstr, before_luarc, after_luarc if log_refcount then --get stuff from obj now because after the call obj can be a dead parrot objstr = tostring(obj) before_rc = tonumber(obj:retainCount()) before_luarc = inc_refcount(obj, 0) end local ok, ret = xpcall(func, debug.traceback, obj, sel, ...) if not ok then check(false, '[%s %s] %s', tostring(cls), tostring(sel), ret) end if is_release then ffi.gc(obj, nil) --disown this reference to obj release_object(obj) if before_rc == 1 then after_rc = 0 end elseif isobj(ret) then if can_retain then ret = ret:retain() --retain() will make ret a strong reference so we don't have to else ffi.gc(ret, collect_object) inc_refcount(ret, 1) end end if log_refcount then after_rc = after_rc or tonumber(obj:retainCount()) after_luarc = inc_refcount(obj, 0) log('refcount', '%s: %d -> %d (%d -> %d)', objstr, before_luarc, after_luarc, before_rc, after_rc) end return ret end end) --add, replace or override an existing/conforming instance/class method based on a loose selector name local function override(cls, selname, func, ftype) --returns true if a method was found and created --look to override an existing method local sel, method = find_method(cls, selname) if sel then ftype = ftype or method_ftype(cls, sel, method) add_class_method(cls, sel, func, ftype) return true end --look to override/create a conforming method local sel, mtype = find_conforming_mtype(cls, selname) if sel then ftype = ftype or static_mtype_ftype(mtype) add_class_method(cls, sel, func, ftype) return true end --try again on the metaclass cls = metaclass(cls) if cls then return override(cls, selname, func, ftype) end end --call a method in the superclass of obj local function callsuper(obj, selname, ...) local super = superclass(obj) if not super then return end return method_caller(super, selname)(obj, ...) end --swap two instance/class methods of a class. --the second selector can be a new selector, in which case: -- 1) it can't be a loose selector. -- 2) its implementation (func) must be given. local function swizzle(cls, selname1, selname2, func) cls = class(cls) local sel1, method1 = find_method(cls, selname1) local sel2, method2 = find_method(cls, selname2) if not sel1 then --try again on the metaclass cls = metaclass(cls) if cls then return swizzle(cls, selname1, selname2, func) else check(false, 'method not found: %s', selname1) end end if not sel2 then check(func, 'implementation required for swizzling with new selector') local ftype = method_ftype(cls, sel1, method1) sel2 = selector(selname2) add_class_method(cls, sel2, func, ftype) method2 = class_method(cls, sel2) assert(method2) else check(not func, 'second selector already implemented') end method1:exchange_imp(method2) end --OSX 10.10+ "modernized" Cocoa by making properties out of what before were --getter methods with the same name. This does not break compatibility in --Obj-C because getter invocation is still available for compiling old code --or for writing backwards-compatible code, but it is a problem in Lua which --can't distinguish between method invocation and table access at runtime. --So if we care about writing code that works on OSX older than 10.10 we have --to disable property access via dot notation and access properties through --their getters. local _use_properties = false local function use_properties(use) --should be inlinable local old_use = _use_properties if use ~= nil then _use_properties = use end return old_use end local function with_properties_wrapper(with) --should be inlinable return function(func) local props local function pass(...) use_properties(props) return ... end return function(...) --keep upvalues to a minimum in this func props = use_properties(with) return pass(func(...)) end end end local with_properties = with_properties_wrapper(true) local without_properties = with_properties_wrapper(false) --class fields --try to get, in order: -- a class luavar -- a readable class property -- a class method -- a class luavar from a superclass local function get_class_field(cls, field) assert(cls ~= nil, 'attempt to index a NULL class') --look for an existing class luavar local val = get_luavar(cls, field) if val ~= nil then return val end --look for a class property local prop = class_property(cls, field) if prop then local caller = method_caller(metaclass(cls), property_getter(prop)) if caller then --the getter is a class method so this is a "class property" if _use_properties then return caller(cls) else return caller end end end --look for a class method local meth = method_caller(metaclass(cls), field) if meth then return meth end --look for an existing class luavar in a superclass cls = superclass(cls) while cls do local val = get_luavar(cls, field) if val ~= nil then return val end cls = superclass(cls) end end -- try to set, in order: -- an existing class luavar -- a writable class property -- an instance method -- a conforming instance method -- a class method -- a conforming class method -- an existing class luavar in a superclass local function set_existing_class_field(cls, field, val) --look to set an existing class luavar if get_luavar(cls, field) ~= nil then set_luavar(cls, field, val) return true end --look to set a writable class property local prop = class_property(cls, field) if prop then local setter = property_setter(prop) if setter then --not read-only local caller = method_caller(metaclass(cls), setter) if caller then --the setter is a class method so this is a "class property" caller(cls, val) return true end end end --look to override an instance/instance-conforming/class/class-conforming method, in this order if override(cls, field, val) then return true end --look to set an existing class luavar in a superclass cls = superclass(cls) while cls do if get_luavar(cls, field) ~= nil then set_luavar(cls, field, val) return true end cls = superclass(cls) end end --try to set, in order: -- an existing class field (see above) -- a new class luavar local function set_class_field(cls, field, val) assert(cls ~= nil, 'attempt to index a NULL class') --look to set an existing class field if set_existing_class_field(cls, field, val) then return end --finally, set a new class luavar set_luavar(cls, field, val) end ffi.metatype('struct objc_class', { __tostring = class_name, __index = get_class_field, __newindex = set_class_field, }) --instance fields --try to get, in order; -- an instance luavar -- a readable instance property -- an ivar -- an instance method -- a class field (see above) local function get_instance_field(obj, field) assert(obj ~= nil, 'attempt to index a NULL object') --shortcut: look for an existing instance luavar local val = get_luavar(obj, field) if val ~= nil then return val end local cls = classof(obj) --look for an instance property local prop = class_property(cls, field) if prop then local caller = method_caller(cls, property_getter(prop)) if caller then --the getter is an instance method so this is an "instance property" if _use_properties then return caller(obj) else return caller end end end --look for an ivar local ivar = class_ivar(cls, field) if ivar then return ivar_get_value(obj, field, ivar) end --look for an instance method local caller = method_caller(cls, field) if caller then return caller end --finally, look for a class field return get_class_field(cls, field) end --try to set, in order: -- an existing instance luavar -- a writable instance property -- an ivar -- an existing class field (see above) -- a new instance luavar local function set_instance_field(obj, field, val) assert(obj ~= nil, 'attempt to index a NULL object') --shortcut: look to set an existing instance luavar if get_luavar(obj, field) ~= nil then set_luavar(obj, field, val) return end local cls = classof(obj) --look to set a writable instance property local prop = class_property(cls, field) if prop then local setter = property_setter(prop) if setter then --not read-only local caller = method_caller(cls, setter) if caller then --the setter is an instance method so this is an "instance property" caller(obj, val) return end else check(false, 'attempt to write to read/only property "%s"', field) end end --look to set an ivar local ivar = class_ivar(cls, field) if ivar then ivar_set_value(obj, field, ivar, val) return end --look to set an existing class field if set_existing_class_field(cls, field, val) then return end --finally, add a new luavar set_luavar(obj, field, val) end local object_tostring if ffi.sizeof(intptr_ct) > 4 then function object_tostring(obj) if obj == nil then return 'nil' end local i = cast('uintptr_t', obj) local lo = tonumber(i % 2^32) local hi = math.floor(tonumber(i / 2^32)) return _('<%s: 0x%s>', class_name(obj), hi ~= 0 and _('%x%08x', hi, lo) or _('%x', lo)) end else function object_tostring(obj) if obj == nil then return 'nil' end return _('<%s>0x%08x', class_name(obj), tonumber(cast('uintptr_t', obj))) end end ffi.metatype('struct objc_object', { __tostring = object_tostring, __index = get_instance_field, __newindex = set_instance_field, }) --blocks ----------------------------------------------------------------------------------------------------------------- --http://clang.llvm.org/docs/Block-ABI-Apple.html ffi.cdef[[ typedef void (*dispose_helper_t) (void *src); typedef void (*copy_helper_t) (void *dst, void *src); struct block_descriptor { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct block_literal) copy_helper_t copy_helper; // IFF (1<<25) dispose_helper_t dispose_helper; // IFF (1<<25) }; struct block_literal { struct block_literal *isa; int flags; int reserved; void *invoke; struct block_descriptor *descriptor; struct block_descriptor d; // because they come in pairs }; struct block_literal *_NSConcreteGlobalBlock; struct block_literal *_NSConcreteStackBlock; ]] local voidptr_ct = ffi.typeof'void*' local block_ct = ffi.typeof'struct block_literal' local copy_helper_ct = ffi.typeof'copy_helper_t' local dispose_helper_ct = ffi.typeof'dispose_helper_t' --create a block and return it typecast to 'id'. --note: the automatic memory management part adds an overhead of 2 closures + 2 ffi callback objects. local function block(func, ftype) if isobj(func) then return func --must be a block, pass it through end ftype = ftype or {'v'} if type(ftype) == 'string' then ftype = mtype_ftype(ftype) end if not ftype.isblock then --not given a block ftype, adjust it ftype.isblock = true table.insert(ftype, 1, '^v') --first arg. is the block object end local callback, callback_ptr if cbframe and ftype_needs_wrapping(ftype) then local cbframe = require'cbframe' --runtime dependency, only needed with `cbframe` debug option. callback = cbframe.new(func) callback_ptr = callback.p else local func = callback_caller(ftype, func) --wrapper to convert args and retvals local function caller(block, ...) --wrapper to remove the first arg return func(...) end local ct = ftype_ct(ftype, nil, true) callback = cast(ct, caller) callback_ptr = callback end local refcount = 1 local function copy(dst, src) refcount = refcount + 1 log('block', 'copy\trefcount: %-8d', refcount) assert(refcount >= 2) end local block local copy_callback local dispose_callback local function dispose(src) refcount = refcount - 1 if refcount == 0 then block = nil --unpin it. this reference also serves to pin it until refcount is 0. callback:free() copy_callback:free() dispose_callback:free() end log('block', 'dispose\trefcount: %-8d', refcount) assert(refcount >= 0) end copy_callback = cast(copy_helper_ct, copy) dispose_callback = cast(dispose_helper_ct, dispose) block = block_ct() block.isa = C._NSConcreteStackBlock --stack block because global blocks are not copied/disposed block.flags = 2^25 --has copy & dispose helpers block.reserved = 0 block.invoke = cast(voidptr_ct, callback_ptr) --callback is pinned by dispose() block.descriptor = block.d block.d.reserved = 0 block.d.size = ffi.sizeof(block_ct) block.d.copy_helper = copy_callback block.d.dispose_helper = dispose_callback local block_object = cast(id_ct, block) --block remains pinned by dispose() ffi.gc(block_object, dispose) log('block', 'create\trefcount: %-8d', refcount) return block_object end --Lua type conversions --------------------------------------------------------------------------------------------------- --convert a Lua value to an objc object representing that value local toobj local t = {} toobj = without_properties(function(v) if type(v) == 'number' then return objc.NSNumber:numberWithDouble(v) elseif type(v) == 'string' then return objc.NSString:stringWithUTF8String(v) elseif type(v) == 'table' then if #v == 0 then local dic = objc.NSMutableDictionary:dictionary() for k,v in pairs(v) do dic:setObject_forKey(toobj(v), toobj(k)) end return dic else local arr = objc.NSMutableArray:array() for i,v in ipairs(v) do arr:addObject(toobj(v)) end return arr end elseif isclass(v) then return cast(id_ct, v) --needed to convert arg#1 for class methods else return v --pass through end end) --convert an objc object that converts naturally to a Lua value local tolua tolua = without_properties(function(obj) if isa(obj, objc.NSNumber) then return obj:doubleValue() elseif isa(obj, objc.NSString) then return obj:UTF8String() elseif isa(obj, objc.NSDictionary) then local t = {} local count = tonumber(obj:count()) local vals = ffi.new('id[?]', count) local keys = ffi.new('id[?]', count) obj:getObjects_andKeys(vals, keys) for i = 0, count-1 do t[tolua(keys[i])] = tolua(vals[i]) end return t elseif isa(obj, objc.NSArray) then local t = {} for i = 0, tonumber(obj:count())-1 do t[#t+1] = tolua(obj:objectAtIndex(i)) end return t else return obj --pass through end end) --convert arguments and retvals for functions and methods local function convert_fp_arg(ftype, arg) if type(arg) ~= 'function' then return arg --pass through end if ftype.isblock then return block(arg, ftype) else local ct = ftype_ct(ftype, nil, true) return cast(ct, arg) --note: to get a chance to free this callback, you must get it with toarg() end end local function convert_arg(ftype, i, arg) local argtype = ftype[i] if argtype == ':' then return selector(arg) --selector, string elseif argtype == '#' then return class(arg) --class, obj, classname elseif argtype == '@' then return toobj(arg) --string, number, array-table, dict-table elseif ftype.fp and ftype.fp[i] then return convert_fp_arg(ftype.fp[i], arg) --function else return arg --pass through end end --not a tailcall and not JITed but at least it doesn't make any garbage. --NOTE: this stumbles on "call unroll limit reached" and doing it with --an accumulator table triggers "NYI: return to lower frame". local function convert_args(ftype, i, ...) if select('#', ...) == 0 then return end return convert_arg(ftype, i, ...), convert_args(ftype, i + 1, select(2, ...)) end local function toarg(cls, selname, argindex, arg) local ftype, argindex = method_arg_ftype(cls, selname, argindex) if not ftype then return end return convert_arg(ftype, argindex, arg) end --wrap a function for automatic type conversion of its args and return value. local function convert_ret(ftype, ret) if ret == nil then return nil --NULL -> nil elseif ftype.retval == 'B' then return ret == 1 --BOOL -> boolean elseif ftype.retval == '*' or ftype.retval == 'r*' then return ffi.string(ret) else return ret --pass through end end function function_caller(ftype, func) return function(...) return convert_ret(ftype, func(convert_args(ftype, 1, ...))) end end --convert arguments and retvals for callbacks, i.e. overriden methods and blocks local function convert_cb_fp_arg(ftype, arg) if ftype.isblock then return arg --let the user use it as an object, and call :invoke(), :retain() etc. else return cast(ftype_ct(ftype), arg) end end local function convert_cb_arg(ftype, i, arg) if ftype.fp and ftype.fp[i] then return convert_cb_fp_arg(ftype.fp[i], arg) else return arg --pass through end end local function convert_cb_args(ftype, i, ...) --not a tailcall but at least it doesn't make any garbage if select('#', ...) == 0 then return end return convert_cb_arg(ftype, i, ...), convert_cb_args(ftype, i + 1, select(2, ...)) end --wrap a callback for automatic type conversion of its args and return value. function callback_caller(ftype, func) if not ftype.fp then if ftype.retval == '@' then --only the return value to convert return function(...) return toobj(func(...)) end else --nothing to convert return func end end return function(...) local ret = func(convert_cb_args(ftype, 1, ...)) if ftype.retval == '@' then return toobj(ret) else return ret end end end --iterators -------------------------------------------------------------------------------------------------------------- local array_next = without_properties(function(arr, i) if i >= arr:count() then return end return i + 1, arr:objectAtIndex(i) end) local function array_ipairs(arr) return array_next, arr, 0 end --publish everything ----------------------------------------------------------------------------------------------------- local function objc_protocols(cls) --compressed API if not cls then return protocols() else return class_protocols(cls) end end --debug objc.C = C objc.debug = P objc.use_cbframe = use_cbframe objc.use_properties = use_properties objc.with_properties = with_properties objc.without_properties = without_properties --manual declarations objc.addfunction = add_function objc.addprotocol = add_informal_protocol objc.addprotocolmethod = add_informal_protocol_method --loading frameworks objc.load = load_framework objc.searchpaths = searchpaths objc.memoize = memoize objc.findframework = find_framework --low-level type conversions (mostly for testing) objc.stype_ctype = stype_ctype objc.mtype_ftype = mtype_ftype objc.ftype_ctype = ftype_ctype objc.ctype_ct = ctype_ct objc.ftype_ct = ftype_ct objc.method_ftype = method_ftype --runtime/get objc.SEL = selector objc.protocols = objc_protocols objc.protocol = protocol objc.classes = classes objc.isclass = isclass objc.isobj = isobj objc.ismetaclass = ismetaclass objc.class = class objc.classname = class_name objc.superclass = superclass objc.metaclass = metaclass objc.isa = isa objc.conforms = class_conforms objc.properties = class_properties objc.property = class_property objc.methods = class_methods objc.method = class_method objc.responds = class_responds objc.ivars = class_ivars objc.ivar = class_ivar objc.conform = add_class_protocol objc.toarg = toarg --runtime/add objc.override = override objc.addmethod = add_class_method objc.swizzle = swizzle --runtime/call objc.caller = function(cls, selname) return method_caller(class(cls), tostring(selname)) or method_caller(metaclass(cls), tostring(selname)) end objc.callsuper = callsuper --hi-level type conversions objc.block = block objc.toobj = toobj objc.tolua = tolua objc.nptr = nptr objc.ipairs = array_ipairs --autoload local submodules = { inspect = 'objc_inspect', --inspection tools dispatch = 'objc_dispatch', --GCD binding } local function autoload(k) return submodules[k] and require(submodules[k]) and objc[k] end --dynamic namespace setmetatable(objc, { __index = function(t, k) return class(k) or csymbol(k) or autoload(k) end, __autoload = submodules, --for inspection }) --print namespace if not ... then for k,v in pairs(objc) do print(_('%-10s %s', type(v), 'objc.'..k)) if k == 'debug' then for k,v in pairs(P) do print(_('%-10s %s', type(v), 'objc.debug.'..k)) end end end end return objc