
--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 <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 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('&quot;', 1, true) then --gsub alone is way slower
					val = val:gsub('&quot;', '"') --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 <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.

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
