-
-
Save fjolnir/2211379 to your computer and use it in GitHub Desktop.
-- 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 = true, | |
frameworkSearchPaths = { | |
"/System/Library/Frameworks/%s.framework/%s", | |
"/Library/Frameworks/%s.framework/%s", | |
"~/Library/Frameworks/%s.framework/%s" | |
} | |
} | |
-- Automatically load classes when requested (On subsequent accesses they will not be reloaded) | |
setmetatable(objc, { | |
__index = function(t, key) | |
return objc.loadClass(key) | |
end | |
}) | |
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, ...); | |
id objc_getMetaClass(const char *name); | |
BOOL class_isMetaClass(Class cls); | |
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); | |
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) | |
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 implementations | |
local _objc_classMethodRegistry = {} | |
local _objc_instanceMethodRegistry = {} | |
-- Memoization for method calls (A weakly keyed table so that garbage collection is not affected) | |
local _classMethodCache = {} | |
setmetatable(_classMethodCache, { __mode = "k" }) | |
local _instanceMethodCache = {} | |
setmetatable(_instanceMethodCache, { __mode = "k" }) | |
-- 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 | |
local function _readMethods(obj, cache) | |
local count = ffi.new("unsigned int[1]") | |
local list = C.class_copyMethodList(obj, count) | |
for i=0, count[0]-1 do | |
local method = list[i] | |
local selector = C.method_getName(method) | |
local selStr = _selToStr(selector) | |
cache[selStr] = _readMethod(method) | |
end | |
C.free(list) | |
end | |
ffi.metatype("struct objc_class", { | |
__index = function(self,selArg) | |
local cached = _classMethodCache[self][selArg] | |
if cached ~= nil then | |
return cached | |
end | |
-- Else | |
selStr = selArg:gsub("_", ":") | |
return function(...) | |
-- Append missing colons to the selector | |
selStr = selStr .. (":"):rep(#{...}-1 - #selStr:gsub("[^:]", "")) | |
local className = ffi.string(C.class_getName(self)) | |
if objc.debug then _log("Calling +["..className.." "..selStr.."]") end | |
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 = C.class_getClassMethod(self, SEL(selStr)) | |
if methodDesc ~= nil then | |
method = _readMethod(methodDesc) | |
methods[selStr] = method | |
else | |
method = C.objc_msgSend | |
end | |
end | |
-- Cache the calling block and executeit | |
_classMethodCache[self][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 | |
_instanceMethodCache[ret] = _instanceMethodCache[ret] or {} | |
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[self][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) | |
selStr = selStr:gsub("_", ":") | |
local className = C.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,selArg) | |
local cached = _instanceMethodCache[self][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("[^:]", "")) | |
local className = ffi.string(C.object_getClassName(ffi.cast("id", self))) | |
if objc.debug then _log("Calling -["..className.." "..selStr.."]") end | |
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 = C.class_getInstanceMethod(C.object_getClass(self), SEL(selStr)) | |
if methodDesc ~= nil then | |
method = _readMethod(methodDesc) | |
methods[selStr] = method | |
else | |
method = C.objc_msgSend | |
end | |
end | |
-- Cache the calling block and executeit | |
_instanceMethodCache[self][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 | |
_instanceMethodCache[ret] = _instanceMethodCache[ret] or {} | |
-- 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 | |
_instanceMethodCache[ret] = _instanceMethodCache[ret] or {} | |
ret = ffi.gc(ret, C.CFRelease) | |
end | |
return ret | |
end | |
return _instanceMethodCache[self][selArg](...) | |
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 = C.objc_getClass(aClassName) | |
if class == nil then | |
return nil | |
elseif _objc_classMethodRegistry[aClassName] then | |
return class | |
end | |
local metaClass = C.objc_getMetaClass(aClassName) | |
_classMethodCache[class] = {} | |
_objc_classMethodRegistry[aClassName] = _objc_classMethodRegistry[aClassName] or { } | |
_objc_instanceMethodRegistry[aClassName] = _objc_instanceMethodRegistry[aClassName] or { } | |
_readMethods(ffi.cast("Class", metaClass), _objc_classMethodRegistry[aClassName]) | |
_readMethods(class, _objc_instanceMethodRegistry[aClassName]) | |
objc[aClassName] = class | |
return class | |
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 |
Thanks for the feedback, you should find the latest version more pleasing :)
Added fix for snow leopard.
Updated example.
Cleaned up, added more functions to aid in objc introspection
Cool stuff (even though I don't own a Mac right now, so I can't test it).
You may want to add memoization to the __index metamethods.
You can use weak tables to implement it efficiently. See http://www.lua.org/pil/17.html for the details.
Here's the link to that thread with Mike Pall on it:
http://lua-users.org/lists/lua-l/2012-03/msg00861.html
To address another of Mike Pall's criticism:
- ... And all those log() calls always evaluate their arguments ...
local _log = false
if objc.debug == true then
_log = function(...)
local args = {...}
for i = 1, #args do args[i] = tostring(args[i]); end
io.stderr:write("[objc] " .. table.concat(args, ", ") .. "\n")
end
end
if _log then _log(some, stuff) end
Regarding the "unspeakable horrific things" in __indexes, I think that most of the logic before the pcall could be placed before the function you return (and chached in weak tables, as pointed above).
Pretty major overhaul,
Removed the initial "class loading" in favor of memoizing method implementations on usage. (thanks pygy) Should be a bit more performant now.
This required me to switch from the . syntax to using : for method calls, (so that the GC wouldn't hold onto object references forever).
There should be no log calls evaluated except on the first call to a method anymore, so that's not a major speed concern.
Much better now. It would be best to move this into a repository so it can be distributed using LuaRocks/LuaDist. I would also welcome automated conversion between NSString <-> String and NSArray/NSDictionary <-> Table for convenience.
You're welcome :-)
Some more suggestions:
I removed all assignments from the fast path, and added a cache for the class names as well (with weak keys).
I also rewrote the column appending code, with the assumption that there is at most one trailing column. More readable and efficient this way, but it doesn't notice when people pass too many parameters (beside the first one). Since this check is only performed the first time a method is called, I supposed that I could drop it.
On the same topic:
I'm not very familiar with ObjC, so I may be wrong, but isn't it possible to have both msg
and msg:
as distinct messages on the same class? If yes, then the current caching behavior is flawed, and you'd have to make trailing underscores mandatory (if you want to keep the doc simple g), witch is ugly, but you may not have the choice...
This is 100% untested (typed in the browser).
local _empty = {}
local _cacheMT = {__mode = 'k'}
local _classNameCache = setmetatable({}, _cacheMT)
-- you may want to do the same for the other caches.
ffi.metatype("struct objc_class", {
__index = function(self,selArg)
local cached = (
_classMethodCache[
_classNameCache[self]
]
or _empty --no need to create a new table each time.
)[selArg]
if cached ~= nil then
return cached
end
-- Else
return function(...)
local selStr = selArg:gsub("_", ":")
-- Append missing colons to the selector
if select('#',...) ~= 0 and selStr:sub(-1, -1) ~= ":" then
selStr = selStr .. ":"
end
if _log 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
_classNameCache[self] = _classNameCache[self] or ffi.string(C.class_getName(self))
local className = _classNameCache[self]
_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,
Edit: I also updated my log function suggestion above to make it more robust. table.concat
doesn't generate the intermediate strings, which otherwise have to be interned and collected.
Edit2 : These are still globals (and should be defined with ffi.typeof()
according to Mike Pall. I'm not familiar with the FFI, but I suppose he's right g)
CGPoint = ffi.metatype("CGPoint", {})
CGSize = ffi.metatype("CGSize", {})
CGRect = ffi.metatype("CGRect", {})
CGAffineTransform = ffi.metatype("CGAffineTransform", {})
NSRange = ffi.metatype("NSRange", {})
Fixed cache.
Added option to toggle automatically appending underscores on selectors(enabled by default)
Moved CG types from global to objc namespace
Added __tostring to objects and selectors so you can now print() them directly
fixes
Added convenience functions to convert lua values to objects. NSArr(), NSDic(), NSNum(), NSStr() and the general Obj() which type checks and calls the correct function
This is awesome, thanks for sharing it. Some comments:
Switch the top to this (keep ffi in a local and don't depend on a global):
local ffi = require("ffi") -- this will error() if FFI is not available
Make a local table to store your api and return that to the requirer at the end:
local tlc = {}
function tlc.objc_loadClass(aClassName) ... end
return tlc
Also, read the "To Cache or not to cache" at the bottom of this:
http://luajit.org/ext_ffi_tutorial.html#cache
You should consider caching ffi.C not the individual functions