Skip to content

Instantly share code, notes, and snippets.

@lgastako
Forked from fjolnir/tlc.lua
Created March 29, 2012 20:15
Show Gist options
  • Save lgastako/2243243 to your computer and use it in GitHub Desktop.
Save lgastako/2243243 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.6 & 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:
-- Accessing a class: MyClass = objc.MyClass
-- Loading a framework: objc.loadFramework("AppKit")
-- Foundation is loaded by default.
-- The table objc.frameworkSearchPaths, contains a list of paths to search (formatted like /System/Library/Frameworks/%s.framework/%s)
-- 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
-- Demo:
-- objc = require("objc")
-- objc.loadFramework("AppKit")
-- pool = objc.NSAutoreleasePool:new()
-- objc.NSSpeechSynthesizer:new():startSpeakingString(objc.strToObj("Hello From Lua!"))
local ffi = require("ffi")
local objc = {
debug = false,
frameworkSearchPaths = {
"/System/Library/Frameworks/%s.framework/%s",
"/Library/Frameworks/%s.framework/%s",
"~/Library/Frameworks/%s.framework/%s"
}
}
local function _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("[objc] "..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;
struct objc_method_description { SEL name; char *types; };
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_getInstanceMethod(Class aClass, SEL aSelector);
Method class_getClassMethod(Class aClass, SEL aSelector);
Class object_getClass(id object);
const char *object_getClassName(id obj);
SEL method_getName(Method method);
unsigned method_getNumberOfArguments(Method method);
void method_getReturnType(Method method, char *dst, size_t dst_len);
void method_getArgumentType(Method method, unsigned int index, char *dst, size_t dst_len);
IMP method_getImplementation(Method method);
SEL sel_registerName(const char *str);
const char* sel_getName(SEL aSelector);
void free(void *ptr);
void CFRelease(id obj);
// Used to check if a file exists
int access(const char *path, int amode);
// 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;
struct Object;
]])
local C = ffi.C
ffi.load("/usr/lib/libobjc.A.dylib", true)
setmetatable(objc, {
__index = function(t, key)
return C.objc_getClass(key)
end
})
function objc.loadFramework(name)
local canRead = bit.lshift(1,2)
for i,path in pairs(objc.frameworkSearchPaths) do
path = path:format(name,name)
if C.access(path, canRead) == 0 then
return ffi.load(path, true)
end
end
error("Error! Framework '"..name.."' not found.")
end
if ffi.arch ~= "arm" then
objc.loadFramework("Foundation")
objc.loadFramework("CoreFoundation")
end
CGPoint = ffi.metatype("CGPoint", {})
CGSize = ffi.metatype("CGSize", {})
CGRect = ffi.metatype("CGRect", {})
CGAffineTransform = ffi.metatype("CGAffineTransform", {})
NSRange = ffi.metatype("NSRange", {})
local function _selToStr(sel)
return ffi.string(ffi.C.sel_getName(sel))
end
local function _strToSel(str)
return ffi.C.sel_registerName(str)
end
local SEL=function(str)
return ffi.C.sel_registerName(str)
end
objc.SEL = SEL
-- Stores references to method wrappers
local _classMethodCache = {}
local _instanceMethodCache = {}
-- Takes a single ObjC type encoded, and converts it to a C type specifier
local _typeEncodings = {
["@"] = "id", ["#"] = "Class", ["c"] = "char", ["C"] = "unsigned char",
["s"] = "short", ["S"] = "unsigned short", ["i"] = "int", ["I"] = "unsigned int",
["l"] = "long", ["L"] = "unsigned long", ["q"] = "long long", ["Q"] = "unsigned long long",
["f"] = "float", ["d"] = "double", ["B"] = "BOOL", ["v"] = "void", ["^"] = "void *",
["*"] = "char *", [":"] = "SEL", ["?"] = "void", ["{"] = "struct", ["("] = "union"
}
objc.typeEncodingToCType = function(aEncoding)
local i = 1
local ret = ""
local isPtr = false
if aEncoding:sub(i,i) == "^" then
isPtr = true
i = i+1
end
if aEncoding:sub(i,i) == "r" then
ret = "const "
i = i+1
end
-- Unused qualifiers
aEncoding = aEncoding:gsub("^[noNRV]", "")
-- Then type encodings
local type = _typeEncodings[aEncoding:sub(i,i)]
if type == nil then
_log("Error! type encoding '"..aEncoding.."' is not supported")
return nil
elseif type == "union" then
local name = aEncoding:sub(aEncoding:find("[^=^(]+"))
if name == "?" then
_log("Error! Anonymous unions not supported: "..aEncoding)
return nil
end
ret = string.format("%s %s %s", ret, type, name)
elseif type == "struct" then
local name = aEncoding:sub(aEncoding:find('[^=^{]+'))
if name == "?" then
_log("Error! Anonymous structs not supported "..aEncoding)
return nil
end
ret = string.format("%s %s %s", ret, type, name)
else
ret = string.format("%s %s", ret, type)
end
if isPtr == true then
ret = ret.."*"
end
return ret
end
-- Creates a C function signature string for the given types
function objc.impSignatureForTypeEncoding(retType, argTypes)
retType = retType or "v"
argTypes = argTypes or {}
retType = objc.typeEncodingToCType(retType)
if retType == nil then
return nil
end
local signature = retType.." (*)("
for i,type in pairs(argTypes) do
type = objc.typeEncodingToCType(type)
if type == nil then
return nil
end
if i < #argTypes then
type = type..","
end
signature = signature..type
end
return signature..")"
end
-- Creates a C function signature string for the IMP of a method
function objc.impSignatureForMethod(method)
local typePtr = ffi.new("char[512]")
C.method_getReturnType(method, typePtr, 512)
local retType = ffi.string(typePtr)
local argCount = C.method_getNumberOfArguments(method)
local argTypes = {}
for j=0, argCount-1 do
C.method_getArgumentType(method, j, typePtr, 512);
table.insert(argTypes, ffi.string(typePtr))
end
return objc.impSignatureForTypeEncoding(retType, argTypes)
end
-- Returns the IMP of the method correctly typecast
local function _readMethod(method)
local impTypeStr = objc.impSignatureForMethod(method)
if impTypeStr == nil then
return nil
end
_log("Loading method:",_selToStr(C.method_getName(method)), impTypeStr)
local imp = C.method_getImplementation(method);
return ffi.cast(impTypeStr, imp)
end
ffi.metatype("struct objc_class", {
__index = function(self,selArg)
local className = ffi.string(C.class_getName(self))
local cache = _classMethodCache[className] or {}
local cached = cache[selArg]
if cached ~= nil then
return cached
end
-- Else
local selStr = selArg:gsub("_", ":")
return function(...)
-- Append missing colons to the selector
selStr = selStr .. (":"):rep(#{...}-1 - #selStr:gsub("[^:]", ""))
if objc.debug then _log("Calling +["..className.." "..selStr.."]") end
local method
local methodDesc = C.class_getClassMethod(self, SEL(selStr))
if methodDesc ~= nil then
method = _readMethod(methodDesc)
else
method = C.objc_msgSend
end
-- Cache the calling block and execute it
_classMethodCache[className] = _classMethodCache[className] or {}
_classMethodCache[className][selArg] = function(self, ...)
if self == nil then
return nil -- Passing nil to self means crashing
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) and ret ~= nil then
if (selStr:sub(1,5) ~= "alloc" and selStr ~= "new") then
ret:retain()
end
if selStr:sub(1,5) ~= "alloc" then
ret = ffi.gc(ret, C.CFRelease)
end
end
return ret
end
return _classMethodCache[className][selArg](...)
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)
local className = C.class_getName(ffi.cast("Class", self))
local methods = _instanceMethodCache[className]
if not (methods == nil) then
methods[selStr] = lambda
end
end
})
ffi.metatype("struct objc_object", {
__index = function(self,selArg)
local className = ffi.string(C.object_getClassName(ffi.cast("id", self)))
local cache = _instanceMethodCache[className] or {}
local cached = cache[selArg]
if cached ~= nil then
return cached
end
-- Else
local selStr = selArg:gsub("_", ":")
return function(...)
-- Append missing colons to the selector
selStr = selStr .. (":"):rep(#{...}-1 - #selStr:gsub("[^:]", ""))
_log("Calling -["..className.." "..selStr.."]")
local method
local methodDesc = C.class_getInstanceMethod(C.object_getClass(self), SEL(selStr))
if methodDesc ~= nil then
method = _readMethod(methodDesc)
else
method = C.objc_msgSend
end
-- Cache the calling block and executeit
_instanceMethodCache[className] = _instanceMethodCache[className] or {}
_instanceMethodCache[className][selArg] = function(self, ...)
if self == nil then
return nil -- Passing nil to self means crashing
end
local success, ret = pcall(method, self, SEL(selStr), ...)
if success == false then
error(ret.."\n"..debug.traceback())
end
if ffi.istype("struct objc_object*", ret) and ret ~= nil and not (selStr == "retain" or selStr == "release") 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") then
ret:retain()
end
ret = ffi.gc(ret, C.CFRelease)
end
return ret
end
return _instanceMethodCache[className][selArg](...)
end
end
})
-- Convenience functions
function objc.strToObj(aStr)
return objc.NSString:stringWithUTF8String_(aStr)
end
function objc.objToStr(aObj)
local str = aObj:description():UTF8String()
return ffi.string(str)
end
-- Blocks
local _sharedBlockDescriptor = ffi.new("struct __block_descriptor_1")
_sharedBlockDescriptor.reserved = 0;
_sharedBlockDescriptor.size = ffi.sizeof("struct __block_literal_1")
-- Wraps a function to be used with a block
local function _createBlockWrapper(lambda, retType, argTypes)
-- Build a function definition string to cast to
retType = retType or "v"
argTypes = argTypes or {}
table.insert(argTypes, 1, "^v")
local funTypeStr = objc.impSignatureForTypeEncoding(retType, argTypes)
_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 = C._NSConcreteGlobalBlock
block.flags = bit.lshift(1, 29)
block.reserved = 0
block.invoke = ffi.cast("void*", _createBlockWrapper(lambda, retType, argTypes))
block.descriptor = _sharedBlockDescriptor
return ffi.cast("id", block)
end
return objc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment