public
Last active

LuaJIT ObjC bridge

  • Download Gist
tlc.lua
Lua
1
The bridge is now located at https://github.com/fjolnir/TLC

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

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

-------- Original Message --------
Subject: Re: TCL - Tiny Lua Cocoa bridge by Fjölnir Ásgeirsson
Date: Wed, 28 Mar 2012 23:37:24 +0200
From: Mike Pall mikelu-1203@mike.de
Reply-To: Lua mailing list lua-l@lists.lua.org
To: Lua mailing list lua-l@lists.lua.org
Newsgroups: gmane.comp.lang.lua.general
References:

76DE641F-80AC-4ACE-801B-D5103C66F4BE@gmail.com

Peter Drahoš wrote:

I did play a little with it and made a quick non-global
polluting variant[1] that can be used from plain LuaJIT2 on OSX.
[...] I will clean up the code a bit and refine this as a module
for LuaDist.

While you're cleaning it up:

  • local xyz = ffi.C.xyz is against recommendations:
    http://luajit.org/ext_ffi_tutorial.html#cache

  • CGPoint = ffi.metatype("CGPoint", {}) ... etc.
    Global-polluting and pointless. Probably wants to use ffi.typeof().

  • typeEncodingToCType() could use a hash-table and/or string.gsub.

  • readMethod() is very inefficient. And all those log() calls
    always evaluate their arguments ...

  • Those __index methods do unspeakable horrific things ...

Oh well ...

--Mike

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.

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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.