Created
May 11, 2017 21:36
-
-
Save rweichler/006a6ed752a434e0e0b381de3ccedfed to your computer and use it in GitHub Desktop.
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
--Objecive-C runtime and bridgesupport binding. | |
--Written by Cosmin Apreutesei. Public domain. | |
--Ideas and code from TLC by Fjölnir Ásgeirsson (c) 2012, MIT license. | |
--Tested with with LuaJIT 2.0.3, 32bit and 64bit on OSX 10.9 and 10.7. | |
local ffi = require'ffi' | |
local cast = ffi.cast | |
local OSX = ffi.os == 'OSX' | |
local x64 = ffi.abi'64bit' | |
if OSX 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. | |
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 <primitive>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 as many as 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 <signatures> 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. | |
local path = _('%s/Resources/BridgeSupport/%s.bridgesupport', basepath, name) | |
if canread(path) then | |
load_bridgesupport(path) | |
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 <Prtocol1, Protocol2,...>' 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. | |
cbframe = false --use cbframe for struct-by-val callbacks | |
local cbframe_stack = {} | |
local function use_cbframe() | |
table.insert(cbframe_stack, cbframe) | |
cbframe = true | |
end | |
local function stop_using_cbframe() | |
cbframe = table.remove(cbframe_stack) | |
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 = method or class_method(cls, sel) | |
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 and not DONT_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 | |
--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 and false 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" | |
return caller(cls) | |
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') | |
if not(type(val) == 'function') then | |
error('not allowed') | |
end | |
--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') | |
if type(field) == 'number' then | |
error("what the fuck") | |
end | |
--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 and false then | |
local caller = method_caller(cls, property_getter(prop)) | |
if caller then --the getter is an instance method so this is an "instance property" | |
return caller(obj) | |
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) | |
if not(type(val)) == 'function' then | |
error('not allowed') | |
end | |
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_inner | |
if ffi.sizeof(intptr_ct) > 4 then | |
function object_tostring_inner(obj) | |
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_inner(obj) | |
return _('<%s>0x%08x', class_name(obj), tonumber(cast('uintptr_t', obj))) | |
end | |
end | |
local function get_obj_count(obj) | |
-- newer versions of Objective C have | |
-- count as a property | |
local count = obj.count | |
if type(count) == 'function' then | |
return count(obj) | |
else | |
return count | |
end | |
end | |
local function get_double_value(obj) | |
local d = obj.doubleValue | |
if type(d) == 'function' then | |
return d(obj) | |
else | |
return d | |
end | |
end | |
local object_tostring | |
function object_tostring(obj) | |
if obj == nil then | |
return 'nil' | |
elseif isa(obj, objc.NSNumber) then | |
return '@'..get_double_value(obj) | |
elseif isa(obj, objc.NSString) then | |
return '@"'..obj:UTF8String()..'"' | |
elseif isa(obj, objc.NSDictionary) then | |
local count = tonumber(get_obj_count(obj)) | |
local vals = ffi.new('id[?]', count) | |
local keys = ffi.new('id[?]', count) | |
obj:getObjects_andKeys(vals, keys) | |
local str = {'@{'} | |
for i=0,count-1 do | |
local v = keys[i] | |
table.insert(str, object_tostring(k)) | |
table.insert(str, ' : ') | |
table.insert(str, object_tostring(v)) | |
if i ~= #keys then | |
table.insert(str, ', ') | |
end | |
end | |
table.insert(str, '}') | |
return table.concat(str) | |
elseif isa(obj, objc.NSArray) then | |
local str = {'@['} | |
local count = tonumber(get_obj_count(obj)) | |
for i=0,count-1 do | |
table.insert(str, object_tostring(obj:objectAtIndex(i))) | |
if i ~= count-1 then | |
table.insert(str, ', ') | |
end | |
end | |
table.insert(str, ']') | |
return table.concat(str) | |
else | |
return object_tostring_inner(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 --------------------------------------------------------------------------------------------------- | |
local function toobj(v) --convert a lua value to an objc object representing that value | |
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 | |
local function tolua(obj) --convert an objc object that converts naturally to a lua value | |
if isa(obj, objc.NSNumber) then | |
return get_double_value(obj) | |
elseif isa(obj, objc.NSString) then | |
return obj:UTF8String() | |
elseif isa(obj, objc.NSDictionary) then | |
local t = {} | |
local count = tonumber(get_obj_count(obj)) | |
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(get_obj_count(obj))-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) | |
if #ftype == 0 then | |
return function() | |
return convert_ret(ftype, func()) | |
end | |
elseif #ftype == 1 then | |
return function(arg) | |
return convert_ret(ftype, func(convert_arg(ftype, 1, arg))) | |
end | |
elseif #ftype == 2 and ftype[1] == '@' and ftype[2] == ':' then --method, 0 args | |
return function(arg1, arg2) | |
return convert_ret(ftype, func(toobj(arg1), selector(arg2))) | |
end | |
elseif #ftype == 3 and ftype[1] == '@' and ftype[2] == ':' then --method, 1 arg | |
return function(arg1, arg2, arg3) | |
return convert_ret(ftype, func(toobj(arg1), selector(arg2), | |
convert_arg(ftype, 3, arg3))) | |
end | |
else | |
return function(...) | |
return convert_ret(ftype, func(convert_args(ftype, 1, ...))) | |
end | |
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 function array_next(arr, i) | |
if i >= get_obj_count(arr) 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.stop_using_cbframe = stop_using_cbframe | |
--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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment