Skip to content

Instantly share code, notes, and snippets.

@seanjensengrey
Forked from fjolnir/tlc.lua
Created March 28, 2012 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seanjensengrey/2227773 to your computer and use it in GitHub Desktop.
Save seanjensengrey/2227773 to your computer and use it in GitHub Desktop.
LuaJIT ObjC bridge
-- TLC - The Tiny Lua Cocoa bridge
-- Note: Only tested with LuaJit 2 Beta 9 on x86_64 with OS X >=10.7.3 & iPhone 4 with iOS 5
-- Copyright (c) 2012, Fjölnir Ásgeirsson
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-- Usage:
-- Loading a class: objc_loadClass("MyClass")
-- Creating objects: MyClass.new() or MyClass.alloc().init()
-- Retaining&Releasing objects is handled by the lua garbage collector so you should never need to call retain/release
-- Calling methods: myInstance:doThis_withThis_andThat(this, this, that)
-- Colons in selectors are converted to underscores (last one being optional)
-- Creating blocks: objc_createBlock(myFunction, returnType, argTypes)
-- returnType: An encoded type specifying what the block should return (Consult https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html for reference)
-- argTypes: An array of encoded types specifying the argument types the block expects
-- Both return and argument types default to void if none are passed
if ffi == nil then
ffi = require("ffi")
end
local objc_debug = false
local function objc_log(...)
if objc_debug == true then
local output
for i,arg in pairs({...}) do
if i == 1 then
output = tostring(arg)
else
output = output .. ", " .. tostring(arg)
end
end
io.stderr:write(output .. "\n")
end
end
if ffi.abi("64bit") 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 struct objc_class *Class;
struct objc_class { Class isa; };
typedef struct objc_object { Class isa; } *id;
typedef struct objc_selector *SEL;
typedef id (*IMP)(id, SEL, ...);
typedef signed char BOOL;
typedef struct objc_method *Method;
id objc_msgSend(id theReceiver, SEL theSelector, ...);
Class objc_getClass(const char *name);
const char *class_getName(Class cls);
Method class_getClassMethod(Class aClass, SEL aSelector);
IMP class_getMethodImplementation(Class cls, SEL name);
Method *class_copyMethodList(Class cls, unsigned int *outCount);
SEL method_getName(Method method);
unsigned method_getNumberOfArguments(Method method);
void method_getReturnType(Method method, char *dst, size_t dst_len);
const char * method_getTypeEncoding(Method method);
void method_getArgumentType(Method method, unsigned int index, char *dst, size_t dst_len);
IMP method_getImplementation(Method method);
Class object_getClass(id object);
Method class_getInstanceMethod(Class aClass, SEL aSelector);
Method class_getClassMethod(Class aClass, SEL aSelector);
SEL sel_registerName(const char *str);
const char* sel_getName(SEL aSelector);
const char *object_getClassName(id obj);
id objc_getMetaClass(const char *name);
BOOL class_isMetaClass(Class cls);
id class_getSuperclass(Class cls);
void free(void *ptr);
void CFRelease(id obj);
// http://clang.llvm.org/docs/Block-ABI-Apple.txt
struct __block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct __block_literal_1)
}
struct __block_literal_1 {
struct __block_literal_1 *isa;
int flags;
int reserved;
void *invoke;
struct __block_descriptor_1 *descriptor;
}
struct __block_literal_1 *_NSConcreteGlobalBlock;
// NSObject dependencies
typedef struct CGPoint { CGFloat x; CGFloat y; } CGPoint;
typedef struct CGSize { CGFloat width; CGFloat height; } CGSize;
typedef struct CGRect { CGPoint origin; CGSize size; } CGRect;
typedef struct CGAffineTransform { CGFloat a; CGFloat b; CGFloat c; CGFloat d; CGFloat tx; CGFloat ty; } CGAffineTransform;
typedef struct _NSRange { NSUInteger location; NSUInteger length; } NSRange;
typedef struct _NSZone NSZone;
// Opaque dependencies
struct _NSStringBuffer;
struct __CFCharacterSet;
struct __GSFont;
struct __CFString;
struct __CFDictionary;
struct __CFArray;
struct __CFAllocator;
struct _NSModalSession;
]])
CGPoint = ffi.metatype("CGPoint", {})
CGSize = ffi.metatype("CGSize", {})
CGRect = ffi.metatype("CGRect", {})
CGAffineTransform = ffi.metatype("CGAffineTransform", {})
NSRange = ffi.metatype("NSRange", {})
local objc_msgSend = ffi.C.objc_msgSend
local objc_getClass = ffi.C.objc_getClass
local objc_getMetaClass = ffi.C.objc_getMetaClass
local class_getInstanceMethod = ffi.C.class_getInstanceMethod
local class_getClassMethod = ffi.C.class_getClassMethod
local class_isMetaClass = ffi.C.class_isMetaClass
local class_getSuperclass = ffi.C.class_getSuperclass
local class_copyMethodList = ffi.C.class_copyMethodList
class_getName = function(class)
return ffi.string(ffi.C.class_getName(class))
end
local object_getClass = ffi.C.object_getClass
local object_getClassName = function(name)
return ffi.string(ffi.C.object_getClassName(name))
end
local method_getNumberOfArguments = ffi.C.method_getNumberOfArguments
local method_getImplementation = ffi.C.method_getImplementation
local method_getReturnType = ffi.C.method_getReturnType
local method_getArgumentType = ffi.C.method_getArgumentType
local function objc_selToStr(sel)
return ffi.string(ffi.C.sel_getName(sel))
end
local function objc_strToSel(str)
return ffi.C.sel_registerName(str)
end
local SEL=objc_strToSel
local free = ffi.C.free
local CFRelease = ffi.C.CFRelease
-- Stores references to method implementations
objc_classMethodRegistry = {}
objc_instanceMethodRegistry = {}
-- Takes a single ObjC type encoded, and converts it to a C type specifier
local function objc_typeEncodingToCType(aEncoding)
local i = 1
local ret = ""
local isPtr = false
if aEncoding:sub(i,i) == "^" then
isPtr = true
i = i+1
end
-- First check type qualifiers
if aEncoding:sub(i,i) == "r" then
ret = ret .. "const "
i = i+1
end
-- Unused qualifiers
if aEncoding:sub(i,i) == "n" then i = i+1
elseif aEncoding:sub(i,i) == "o" then i = i+1
elseif aEncoding:sub(i,i) == "N" then i = i+1
end
if aEncoding:sub(i,i) == "R" then i = i+1; end
if aEncoding:sub(i,i) == "V" then i = i+1; end
-- Then type encodings
local c = aEncoding:sub(i,i)
if c == "@" then
ret = ret .. "id"
elseif c == "#" then
ret = ret .. "Class"
elseif c == "c" then
ret = ret .. "char"
elseif c == "C" then
ret = ret .. "unsigned char"
elseif c == "s" then
ret = ret .. "short"
elseif c == "S" then
ret = ret .. "unsigned short"
elseif c == "i" then
ret = ret .. "int"
elseif c == "I" then
ret = ret .. "unsigned int"
elseif c == "l" then
ret = ret .. "long"
elseif c == "L" then
ret = ret .. "unsigned long"
elseif c == "q" then
ret = ret .. "long long"
elseif c == "Q" then
ret = ret .. "unsigned long long"
elseif c == "f" then
ret = ret .. "float"
elseif c == "d" then
ret = ret .. "double"
elseif c == "B" then
ret = ret .. "BOOL"
elseif c == "v" then
ret = ret .. "void"
elseif c == "^" then
ret = ret .. "void *"
elseif c == "*" then
ret = ret .. "char *"
elseif c == ":" then
ret = ret .. "SEL"
elseif c == "?" then
ret = ret .. "void"
elseif c == "(" then
local name = aEncoding:sub(aEncoding:find("[^=^(]+"))
if name == "?" then
objc_log("Anonymous unions not supported: "..aEncoding)
return nil
end
ret = ret .. "union "..name
elseif c == "{" then
local name = aEncoding:sub(aEncoding:find("[^=^{]+"))
if name == "?" then
objc_log("Anonymous structs not supported "..aEncoding)
return nil
end
ret = ret .. "struct "..name
else
objc_log("Error! type encoding '"..aEncoding.."' is not supported")
return nil
end
if isPtr == true then
ret = ret.."*"
end
return ret
end
local function _objc_readMethod(method)
local imp = method_getImplementation(method);
-- Typecast the IMP
local typePtr = ffi.new("char[512]")
method_getReturnType(method, typePtr, 512)
local retTypeStr = objc_typeEncodingToCType(ffi.string(typePtr))
if retTypeStr == nil then
return nil
end
local impTypeStr = ""..retTypeStr.." (*)("
local argCount = method_getNumberOfArguments(method)
for j=0, argCount-1 do
method_getArgumentType(method, j, typePtr, 512);
local typeStr = ffi.string(typePtr)
typeStr = objc_typeEncodingToCType(typeStr)
-- If we encounter an unsupported type, we skip loading this method
if typeStr == nil then
return nil
end
if j < argCount-1 then
typeStr = typeStr..","
end
impTypeStr = impTypeStr..typeStr
end
impTypeStr = impTypeStr..")"
objc_log("Loading method:",objc_selToStr(ffi.C.method_getName(method)), impTypeStr)
return ffi.cast(impTypeStr, imp)
end
local function _objc_readMethods(obj, cache)
local count = ffi.new("unsigned int[1]")
local list = class_copyMethodList(obj, count)
for i=0, count[0]-1 do
local method = list[i]
local selector = ffi.C.method_getName(method)
local selStr = objc_selToStr(selector)
cache[selStr] = _objc_readMethod(method)
end
free(list)
end
ffi.metatype("struct objc_class", {
__index = function(self,selStr)
selStr = selStr:gsub("_", ":")
return function(...)
local argCount = #{...}
if argCount > 0 and selStr:sub(-1,-1) ~= ":" then
for i=1, argCount do selStr = selStr..":" end
end
local className = class_getName(ffi.cast("Class", self))
objc_log("Calling +["..className.." "..selStr.."]")
local methods = objc_classMethodRegistry[className]
local method = methods[selStr]
if method == nil then
-- Try loading it (in case it was implemented in a superclass)
local methodDesc = class_getClassMethod(self, SEL(selStr))
if ffi.cast("void*", methodDesc) > nil then
method = _objc_readMethod(methodDesc)
methods[selStr] = method
else
method = objc_msgSend
end
end
local success, ret = pcall(method, ffi.cast("id", self), SEL(selStr), ...)
if success == false then
error(ret.."\n"..debug.traceback())
end
if ffi.istype("struct objc_object*", ret) then
if not (selStr:sub(1,5) == "alloc" or selStr == "new") then
ret.retain()
ret = ffi.gc(ret, CFRelease)
end
end
return ret
end
end,
-- Grafts a lua function onto the class as an instance method, it will only be callable from lua though
__newindex = function(self,selStr,lambda)
selStr = selStr:gsub("_", ":")
local className = class_getName(ffi.cast("Class", self))
local methods = objc_instanceMethodRegistry[className]
if not (methods == nil) then
methods[selStr] = lambda
end
end
})
ffi.metatype("struct objc_object", {
__index = function(self,selStr)
selStr = selStr:gsub("_", ":")
return function(...)
local argCount = #{...}
if argCount > 0 and selStr:sub(-1,-1) ~= ":" then
for i=1, argCount do selStr = selStr..":" end
end
local className = object_getClassName(ffi.cast("id", self))
objc_log("Calling -["..className.." "..selStr.."]")
local methods = objc_instanceMethodRegistry[className]
-- If the class hasn't been loaded already, load it
if methods == nil then
objc_loadClass(className)
methods = objc_instanceMethodRegistry[className]
if methods == nil then
error("Could not find class "..className.."\n"..debug.traceback())
end
end
local method = methods[selStr]
if method == nil then
-- Try loading it (in case it was implemented in a superclass)
local methodDesc = class_getInstanceMethod(object_getClass(self), SEL(selStr))
if ffi.cast("void*", methodDesc) > nil then
method = _objc_readMethod(methodDesc)
methods[selStr] = method
else
method = objc_msgSend
end
end
local success, ret = pcall(method, ffi.cast("id", self), SEL(selStr), ...)
if success == false then
error(ret.."\n"..debug.traceback())
end
if ffi.istype("id", ret) then
-- Retain objects that need to be retained
if not (selStr:sub(1,4) == "init" or selStr:sub(1,4) == "copy" or selStr:sub(1,11) == "mutableCopy" or selStr == "retain" or selStr == "release") then
ret.retain()
ret = ffi.gc(ret, CFRelease)
end
end
return ret
end
end
})
-- Loads the class for a given name (Only caches the methods defined in the class itself, other methods are cached on first usage)
function objc_loadClass(aClassName)
local class = objc_getClass(aClassName)
if(objc_classMethodRegistry[aClassName]) then
return class
end
local metaClass = objc_getMetaClass(aClassName)
objc_classMethodRegistry[aClassName] = objc_classMethodRegistry[aClassName] or { }
objc_instanceMethodRegistry[aClassName] = objc_instanceMethodRegistry[aClassName] or { }
_objc_readMethods(ffi.cast("Class", metaClass), objc_classMethodRegistry[aClassName])
_objc_readMethods(class, objc_instanceMethodRegistry[aClassName])
_G[aClassName] = class
return class
end
-- Convenience functions
objc_loadClass("NSString")
function objc_strToObj(aStr)
return NSString.stringWithUTF8String_(aStr)
end
function objc_objToStr(aObj)
local str = aObj.description().UTF8String()
return ffi.string(str)
end
-- Blocks
local objc_sharedBlockDescriptor = ffi.new("struct __block_descriptor_1")
objc_sharedBlockDescriptor.reserved = 0;
objc_sharedBlockDescriptor.size = ffi.sizeof("struct __block_literal_1")
local objc_NSConcreteGlobalBlock = ffi.C._NSConcreteGlobalBlock
-- Wraps a function to be used with a block
local function _objc_createBlockWrapper(lambda, retType, argTypes)
-- Build a function definition string to cast to
retType = retType or "v"
argTypes = argTypes or {"v"}
retType = objc_typeEncodingToCType(retType)
if retType == nil then
return nil
end
local funTypeStr = ""..retType.." (*)(void *,"
for i,typeStr in pairs(argTypes) do
typeStr = objc_typeEncodingToCType(typeStr)
-- If we encounter an unsupported type, we skip loading this method
if typeStr == nil then
return nil
end
if i < #argTypes then
typeStr = typeStr..","
end
funTypeStr = funTypeStr..typeStr
end
funTypeStr = funTypeStr..")"
objc_log("Created block with signature:", funTypeStr)
ret = function(theBlock, ...)
return lambda(...)
end
return ffi.cast(funTypeStr, ret)
end
-- Creates a block and returns it typecast to 'id'
function objc_createBlock(lambda, retType, argTypes)
if not lambda then
return nil
end
local block = ffi.new("struct __block_literal_1")
block.isa = objc_NSConcreteGlobalBlock
block.flags = bit.lshift(1, 29)
block.reserved = 0
block.invoke = ffi.cast("void*", _objc_createBlockWrapper(lambda, retType, argTypes))
block.descriptor = objc_sharedBlockDescriptor
return ffi.cast("id", block)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment