commit a9fcca4246b10593160b863e4c9d25bbe0186e0e Author: guilevi Date: Sun Aug 27 14:32:38 2023 +0200 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4598eca Binary files /dev/null and b/.DS_Store differ diff --git a/.alsoftrc b/.alsoftrc new file mode 100644 index 0000000..499fa4d --- /dev/null +++ b/.alsoftrc @@ -0,0 +1 @@ +hrtf = false diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e4a712 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "love2talk"] + path = love2talk + url = https://github.com/pitermach/love2talk diff --git a/Love2talk.lua b/Love2talk.lua new file mode 100644 index 0000000..e55b7d2 --- /dev/null +++ b/Love2talk.lua @@ -0,0 +1,27 @@ + +local os=love.system.getOS() +if os =="Windows" then + backend=require "Tolk" + backend.trySAPI(true) +elseif os=="OS X" then + backend=require "macspeech" +end + +local function say(text, interrupt) + interrupt=interrupt or false + if os=="Windows" then + backend.output(text, interrupt) + else + backend.output(text) + end + +end + +local function isSpeaking() + return backend.isSpeaking() +end + +return {say=say, isSpeaking=isSpeaking} + + + diff --git a/alsoft.conf b/alsoft.conf new file mode 100644 index 0000000..499fa4d --- /dev/null +++ b/alsoft.conf @@ -0,0 +1 @@ +hrtf = false diff --git a/audio/.DS_Store b/audio/.DS_Store new file mode 100644 index 0000000..8043bae Binary files /dev/null and b/audio/.DS_Store differ diff --git a/audio/beep.flac b/audio/beep.flac new file mode 100644 index 0000000..da90ef7 Binary files /dev/null and b/audio/beep.flac differ diff --git a/audio/enemies/.DS_Store b/audio/enemies/.DS_Store new file mode 100644 index 0000000..5d77cc4 Binary files /dev/null and b/audio/enemies/.DS_Store differ diff --git a/audio/enemies/1/loop.flac b/audio/enemies/1/loop.flac new file mode 100644 index 0000000..240bf0b Binary files /dev/null and b/audio/enemies/1/loop.flac differ diff --git a/audio/enemies/1/oloop.flac b/audio/enemies/1/oloop.flac new file mode 100644 index 0000000..9d53ff0 Binary files /dev/null and b/audio/enemies/1/oloop.flac differ diff --git a/audio/meh.wav b/audio/meh.wav new file mode 100644 index 0000000..e538e3f Binary files /dev/null and b/audio/meh.wav differ diff --git a/audio/tracks/.DS_Store b/audio/tracks/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/audio/tracks/.DS_Store differ diff --git a/audio/tracks/clicky.flac b/audio/tracks/clicky.flac new file mode 100644 index 0000000..b7562ec Binary files /dev/null and b/audio/tracks/clicky.flac differ diff --git a/audio/tracks/gourmet.mp3 b/audio/tracks/gourmet.mp3 new file mode 100644 index 0000000..e388103 Binary files /dev/null and b/audio/tracks/gourmet.mp3 differ diff --git a/audio/weapons/laser.flac b/audio/weapons/laser.flac new file mode 100644 index 0000000..a591ee4 Binary files /dev/null and b/audio/weapons/laser.flac differ diff --git a/classic.lua b/classic.lua new file mode 100644 index 0000000..56fbb09 --- /dev/null +++ b/classic.lua @@ -0,0 +1,68 @@ +-- +-- classic +-- +-- Copyright (c) 2014, rxi +-- +-- This module is free software; you can redistribute it and/or modify it under +-- the terms of the MIT license. See LICENSE for details. +-- + + +local Object = {} +Object.__index = Object + + +function Object:new() +end + + +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + + +function Object:implement(...) + for _, cls in pairs({...}) do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == "function" then + self[k] = v + end + end + end +end + + +function Object:is(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + + +function Object:__tostring() + return "Object" +end + + +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + + +return Object diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..17e1c8e --- /dev/null +++ b/conf.lua @@ -0,0 +1,12 @@ +function love.conf(t) + t.identity="rsi" + t.window.title="rsi" +t.window.vsync=0 + + + t.modules.font=true + --t.modules.graphics=false + t.modules.image=false + t.modules.mouse=false + t.modules.touch=false +end diff --git a/encoding.lua b/encoding.lua new file mode 100644 index 0000000..65ec003 --- /dev/null +++ b/encoding.lua @@ -0,0 +1,40 @@ +local ffi = require "ffi" +local kernel32 = ffi.load("kernel32") +ffi.cdef[[ +int Beep(int x, int y); +typedef unsigned int UINT; +typedef unsigned int DWORD; +typedef const char * LPCSTR; +typedef char * LPSTR; +typedef wchar_t * LPWSTR; +typedef const wchar_t *LPCWSTR; +typedef int *LPBOOL; +int WideCharToMultiByte(UINT CodePage, +DWORD dwFlags, +LPCWSTR lpWideCharStr, int cchWideChar, +LPSTR lpMultiByteStr, int cbMultiByte, +LPCSTR lpDefaultChar, +LPBOOL lpUsedDefaultChar +); +int MultiByteToWideChar(UINT CodePage, +DWORD dwFlags, +LPCSTR lpMultiByteStr, int cbMultiByte, +LPWSTR lpWideCharStr, int cchWideChar); +]] + +local CP_UTF8 = 65001 +local function to_utf16(s) +local needed = kernel32.MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0) +local buf = ffi.new("wchar_t[?]", needed) +local written = kernel32.MultiByteToWideChar(CP_UTF8, 0, s, -1, buf, needed) +return ffi.string(buf, written*2) +end + +local function to_utf8(s) +local needed = kernel32.WideCharToMultiByte(CP_UTF8, 0, s, -1, nil, 0, nil, nil) +local buf = ffi.new("char[?]", needed) +local written = kernel32.WideCharToMultiByte(CP_UTF8, 0, s, -1, buf, needed, nil, nil) +return ffi.string(buf, written - 1) +end + +return {to_utf8=to_utf8, to_utf16=to_utf16} diff --git a/enemies/.DS_Store b/enemies/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/enemies/.DS_Store differ diff --git a/enemies/basic.lua b/enemies/basic.lua new file mode 100644 index 0000000..38dc6f7 --- /dev/null +++ b/enemies/basic.lua @@ -0,0 +1,9 @@ +enemyBasic=enemy:extend() + +function enemyBasic:new(x,y) +sounds=group("audio/enemies/1") +sounds.loop:setLooping(true) +enemyBasic.super.new(self,x,y,sounds) +sounds.loop:play() + +end \ No newline at end of file diff --git a/enemy.lua b/enemy.lua new file mode 100644 index 0000000..fa9b827 --- /dev/null +++ b/enemy.lua @@ -0,0 +1,25 @@ +enemy=object:extend() + +function enemy:new(x,y,sounds) +self.x=x +self.y=y +self.dx=0 +self.dy=-1 +self.sounds=sounds; +sounds.loop:setPosition(x,y,0) +self.lastMoveTick=0 +self.rate=game.currentTrack.info.beatDivisions +end -- new + +function enemy:update(dt) +if game.ticker.ticks-self.lastMoveTick>=self.rate then +self:move() +end -- rate +end -- update + +function enemy:move() +self.lastMoveTick=game.ticker.ticks +self.x=self.x+self.dx +self.y=self.y+self.dy +self.sounds.loop:setPosition(self.x,self.y,0) +end \ No newline at end of file diff --git a/field.lua b/field.lua new file mode 100644 index 0000000..4bcbfc3 --- /dev/null +++ b/field.lua @@ -0,0 +1,13 @@ +field=object:extend() + +function field:new(width,height) +self.width=width +self.height=height +self.contents={} +end -- new + +function field:update(dt) +for k,v in pairs(self.contents) do +v:update(dt) +end -- for field +end \ No newline at end of file diff --git a/game.lua b/game.lua new file mode 100644 index 0000000..23874c8 --- /dev/null +++ b/game.lua @@ -0,0 +1,69 @@ +require "field" + +game={} +game.field=field(40,20) +game.player=player +game.ticker=ticker(0.125) +game.currentTrack={} +game.trackRunning=false +game.useTrack=true +game.events={} -- events are tables that contain recurring (bool), i (period or counter), and func + + + +function game.update(dt) +if game.useTrack then +if not game.trackRunning and c.music:tell()>=c.info.startTime then +game.ticker:calibrate(love.timer.getTime()-(game.currentTrack.music:tell()-game.currentTrack.info.startTime)) +game.trackRunning=true +end -- if calibration +end -- if usetrack +local ticked=game.ticker:update(dt) +player.update(dt) +game.field:update(dt) +end -- update + +function game.loadTrack(t) +game.currentTrack=t +game.ticker.tickTime=t.timeStep +t.music:setVolume(t.info.volumeBase) +game.trackRunning=false +end -- loadtrack + +function game.init() +table.insert(game.field.contents, enemyBasic(5,20)) +function game.ticker:tick() +for k,event in pairs(game.events) do +local adjustedTicks=game.ticker.ticks-1+(event.shift or 0) +if event.recurring then +if adjustedTicks%event.i==0 then +event.func() +end -- if fire recurring event +else +event.i=event.i-1 +if event.i==0 then +event.func() +event=nil +end -- if fire one-shot event +end -- if recurring +end -- for events +end +local click={ +i=2, +recurring=true, +func=function() +aud.meh:stop() +aud.meh:play() +end +} +local beep={ +i=4, +recurring=true, +func=function() +aud.beep:stop() +aud.beep:play() +end +} +-- game.events.click=click +-- game.events.beep=beep +end \ No newline at end of file diff --git a/love2talk b/love2talk new file mode 160000 index 0000000..a67c4f2 --- /dev/null +++ b/love2talk @@ -0,0 +1 @@ +Subproject commit a67c4f273a5325e654d7b6dc86547bf8a28fee97 diff --git a/macspeech.lua b/macspeech.lua new file mode 100644 index 0000000..f78f303 --- /dev/null +++ b/macspeech.lua @@ -0,0 +1,19 @@ + +objc=require "objc/objc" +local synth=objc.NSSpeechSynthesizer:alloc():init() +local function output(text) + if type(text) ~="string" then + text=tostring(text) + end + synth:startSpeakingString(text) +end +local function isSpeaking() + if synth:isSpeaking()==1 then + return true + else + return false + end +end + +return {output=output, isSpeaking=isSpeaking} + diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..c151088 --- /dev/null +++ b/main.lua @@ -0,0 +1,51 @@ +div=0.2 +curtime=0. +lastdiv=0 +calib=false + + +function love.load() +tts=require "love2talk" +object=require "classic" +local tick=require "tick" +tick.rate=.001 +require "soundgroups" +require "ticker" +require "track" +require "utils" + +require "player" +require "game" +require "enemy" +require "enemies/basic" +aud=group("audio") +c=require "tracks/gourmet" +love.timer.sleep(1) +game.loadTrack(c) +game.init() +game.currentTrack.music:play() +end -- load + + +function love.update(dt) +game.update() +end -- update + +function love.keypressed(key) +if key=="right" then +player.direction=1 +elseif key=="left" then +player.direction=-1 +elseif key=="c" then +tts.say(player.x) +elseif key=="d" then +print(love.audio.getOrientation()) +love.audio.setPosition(100,0,30) +end +end -- keyPressed + +function love.keyreleased(key) +if key=="left" or key=="right" then +player.direction=0 +end +end -- keyReleased diff --git a/objc/.mgit/bridgesupport.sh b/objc/.mgit/bridgesupport.sh new file mode 100644 index 0000000..426e75b --- /dev/null +++ b/objc/.mgit/bridgesupport.sh @@ -0,0 +1,9 @@ +# Copy bridgesupport files from an OSX older than 10.13 to luapower/bridgesupport directory. +# NOTE: Not sure if *.bridgesupport files are copyrightable/redistributable. Use at your own risk. + +usage() { echo "Usage: mgit bridgesupport copy VOLUME_DIR"; exit 1; } +[ "$1" == copy ] || usage +[ -d "$2" ] || usage + +echo "Copying $2/System/Library/Frameworks/*.bridgesupport to bridgesupport ..." +find "$2/System/Library/Frameworks" -name '*.bridgesupport' -exec cp '{}' bridgesupport \; diff --git a/objc/objc.lua b/objc/objc.lua new file mode 100644 index 0000000..d0e9442 --- /dev/null +++ b/objc/objc.lua @@ -0,0 +1,2403 @@ + +--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 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 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('"', 1, true) then --gsub alone is way slower + val = val:gsub('"', '"') --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 ' 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 diff --git a/objc/objc.md b/objc/objc.md new file mode 100644 index 0000000..8d1d81b --- /dev/null +++ b/objc/objc.md @@ -0,0 +1,562 @@ +--- +tagline: Obj-C & Cocoa bridge +platforms: osx64 +--- + +## `local objc = require'objc'` + +## Features + + * Coverage + * full access to Cocoa classes, protocols, C functions, structs, enums, constants + * access to methods, properties and ivars + * creating classes and overriding methods + * exploring and searching the Objective-C runtime + * Platforms + * tested with __OSX 10.7 to 10.12__ (__32bit__ and __64bit__) + * Dependencies + * none for Cocoa (XML parser included), [expat] for non-standard bridgesupport files + * Type Bridging + * methods and functions return Lua booleans + * Lua numbers, strings and tables can be passed for NSNumber, NSStrings, NSArray and NSDictionary args + * string names can be passed for class and selector args + * Lua functions can be passed for block and function-pointer args without specifying a type signature + * overriding methods does not require specifying the method type signature + * method signatures are inferred from existing supermethods and conforming protocols + * formal and informal protocols supported + * function-pointer args on overriden methods and blocks can be called without specifying a type signature + * GC Bridging + * attaching Lua variables to classes and objects + * Lua variables follow the lifetime of Obj-C objects + * Lua variables attached to classes are inherited + * automatic memory management of objects and blocks + * blocks are refcounted and freed when their last owner releases them + * Speed + * aggressive caching all-around + * no gc pressure in calling methods after the first invocation + * fast, small embedded XML parser + + +## Limitations + +### FFI callback limitations + +Blocks, function callbacks and overriden methods are based on ffi callbacks +which come with some limitations: + + * can't access the vararg part of the function, for variadic functions/methods + * can't access the pass-by-value struct args or any arg after the first pass-by-value struct arg + * can't return structs by value + +To counter this, you can use [cbframe] as a workaround. Enable it with +`objc.debug.cbframe = true` and now all problem methods and blocks +will receive a single arg: a pointer to a [D_CPUSTATE] struct that you have +to pick up args from and write the return value into. Note that self +isn't passed in this case, the cpu state is the only arg. + +[D_CPUSTATE]: https://github.com/luapower/cbframe/blob/master/cbframe_x86_h.lua + +### Broken bridgesupport files in OSX 10.13+ + +`*.bridgesupport` files are required to get extra type information not covered +by the objc RTTI API. In OSX 10.13 and above, these files are broken, so you +need to deploy your own and put them in the `bridgesupport` directory. +Parsable files from OSX 10.12.6 are available at https://github.com/luapower/bridgesupport +(just use `mgit clone bridgesupport` if you're using mgit). + +## Quick Tutorial + +### Loading frameworks + +~~~{.lua} +--load a framework by name; `objc.searchpaths` says where the frameworks are. you can also use full paths. +--classes and protocols are loaded, but also C constants, enums, functions, structs and even macros. +objc.load'Foundation' + +--you can also load sub-frameworks like this: +objc.load'Carbon.HIToolbox' + +--which is the same as using relative paths: +objc.load'Carbon.framework/Versions/Current/Frameworks/HIToolbox' +~~~ + +### Creating and using objects + +~~~{.lua} +--instantiate a class. the resulting object is retained and released on gc. +--you can call `release()` on it too, for a more speedy destruction. +local str = objc.NSString:alloc():initWithUTF8String'wazza' + +--call methods with multiple arguments using underscores for ':'. last underscore is optional. +--C constants, enums and functions are in the objc namespace too. +local result = str:compare_options(otherStr, objc.NSLiteralSearch) +~~~ + +### Subclassing + +~~~{.lua} +--create a derived class. when creating a class, say which protocols you wish it conforms to, +--so that you don't have to deal with type encodings when implementing its methods. +objc.class('NSMainWindow', 'NSWindw ') + +--add methods to your class. the selector `windowWillClose` is from the `NSWindowDelegate` protocol +--so its type encoding is inferred from the protocol definition. +function objc.NSMainWindow:windowWillClose(notification) + ... +end + +--override existing methods. use `objc.callsuper` to call the supermethod. +function objc.NSMainWindow:update() + ... + return objc.callsuper(self, 'update') +end + +~~~ + +### Converting between Lua and Obj-C types + +~~~{.lua} +local str = objc.toobj'hello' --create a NSString from a Lua string +local num = objc.toobj(3.14) --create a NSNumber from a Lua number +local dic = objc.toobj{a = 1, b = 'hi'} --create a NSDictionary from a Lua table +local arr = objc.toobj{1, 2, 3} --create a NSArray from a Lua table + +local s = objc.tolua(str) +local n = objc.tolua(num) +local t1 = objc.tolua(dic) +local t2 = objc.tolua(arr) +~~~ + +### Adding Lua variables (luavars) + +~~~{.lua} +--add Lua variables to your objects - their lifetime is tied to the lifetime of the object. +--you can also add class variables - they will be accessible through the objects too. +objc.NSObject.myClassVar = 'I can live forever' +local obj = objc.NSObject:new() +obj.myInstanceVar = 'I live while obj lives' +obj.myClassVar = 5 --change the class var (same value for all objects) +~~~ + +### Adding Lua methods + +Lua methods are just Lua variables which happen to have a function-type value. +You can add them to a class or to an instance, but that doesn't make them +"class methods" or "instance methods" in OOP sense. Instead, this distinction +comes about when you call them: + +~~~{.lua} +function objc.NSObject:myMethod() end +local str = objc.toobj'hello' --create a NSString instance, which is a NSObject +str:myMethod() --instance method (str passed as self) +objc.NSString:myMethod() --class method (NSString passed as self) +~~~ + +As you can see, luavars attached to a class are also inherited. + +> If this looks like a lot of magic, it is. The indexing rules for class and instance +objects (i.e. getting and setting object and class fields) are pretty complex. +Have a look at the API sections "object fields" and "class fields" to learn more. + +### Accessing properties & ivars + +~~~{.lua} +--get and set class and instance properties using the dot notation. +local pr = objc.NSProgress:progressWithTotalUnitCount(123) +print(pr.totalUnitCount) --prints 123 +pr.totalUnitCount = 321 --sets it + +--get and set ivars using the dot notation. +local obj = objc.NSDocInfo:new() +obj.time = 123 +print(obj.time) --prints 123 +~~~ + +### Creating and using blocks + +~~~{.lua} +--blocks are created automatically when passing a Lua function where a block is expected. +--their lifetime is auto-managed, for both synchronous and asynchronous methods. +local str = objc.NSString:alloc():initWithUTF8String'line1\nline2\nline3' +str:enumerateLinesUsingBlock(function(line, stop) + print(line:UTF8String()) --'char *' return values are also converted to Lua strings automatically +end) + +--however, blocks are slow to create and use ffi callbacks which are very limited in number. +--create your blocks outside loops if possible, or call `collectgarbage()` every few hundred iterations. + +--create a block with its type signature inferred from usage. +--in this case, its type is that of arg#1 to NSString's `enumerateLinesUsingBlock` method. +local block = objc.toarg(objc.NSString, 'enumerateLinesUsingBlock', 1, function(line, stop) + print(line:UTF8String()) +end) +str:enumerateLinesUsingBlock(block) + +--create a block with its method type encoding given manaully. +--for type encodings see: +-- https://code.google.com/p/jscocoa/wiki/MethodEncoding +local block = objc.block(function(line, stop) + print(line:UTF8String()) +end, 'v@^B'}) --retval is 'v' (void), line is '@' (object), stop is '^B' (pointer to BOOL) +str:enumerateLinesUsingBlock(block) +~~~ + +### More goodies + +Look up anything in Cocoa by a Lua pattern: + + ./luajit objc_test.lua inspect_find foo + +Then inspect it: + + ./luajit objc_test.lua inspect_class PAFootprint + + +### Even more goodies + +Check out the unit test script, it also contains a few demos, not just tests. \ +Check out the undocumented `objc_inspect` module, it has a simple cmdline inspection API. + + +## Memory management + +Memory management in objc is automatic. Cocoa's reference counting system is +tied to the Lua's garbage collector so that you don't have to worry about +retain/release. The integration is not air-tight though, so you need to know +how it's put together to avoid some tricky situations. + +### Strong and weak references + +Ref. counting systems are fragile: they require that retain() and release() +calls on an object be perfectly balanced. If they're not, you're toast. +Thinking of object relationships in in terms of weak and strong references +can help a lot with that. + +A strong reference is a retained reference, guaranteed to be available until +released. A weak reference is not retained and its availability depends on +context. + +A strong reference has a finalizer that calls release() when collected. +A weak reference doesn't have a finalizer. + +Calling release() on a strong reference releases the reference, and removes +the finalizer, turning it into a weak reference. You should not call +release() on a weak reference. + +### Return values are strong + +Cocoa's rules say that if you alloc an object, you get a strong (retained) +reference on that object. Other method calls that return an object return +a weak (non-retained) reference to that object. Lua retains all object return +values so you always get a strong reference. This is required for the +alloc():init() sequence to work, and it's generally convenient. + +### Callback arguments are weak + +Object arguments passed to overriden methods (including the self argument), +blocks and function pointers, are weak references, not tied to Lua's garbage +collector. If you want to keep them around outside the scope of the callback, +you need to retain them: + +~~~{.lua} +local strong_ref +function MySubClass:overridenMethod() + strong_ref = self:retain() --self is a weak ref. it needs to be retained. +end +~~~ + +### Luavars and object ownership + +You should only use luavars on objects that you own. Luavars go away +when the last strong reference to an object goes away. Setting Lua vars +on an object with only weak references will leak those vars! Even worse, +those vars might show up as vars of other objects! + +### Strong/weak ambiguities + +If you create a `NSWindow`, you don't get an _unconditionally_ retained +reference to that window, contrary to Cocoa's rules, because if the user +closes the window, it is your reference that gets released. The binding +doesn't know about that and on gc it calls release again, giving you a crash +at an unpredictable time (`export NSZombieEnabled=YES` can help here). +To fix that you can either tell Cocoa that your ref is strong by calling +`win:setReleasedWhenClosed(false)`, or tell the gc that your ref is weak by +calling `ffi.gc(win, nil)`. If you chose the latter, remember that you can't +use luavars on that window! + + +## Main API + +----------------------------------------------------------- -------------------------------------------------------------- +__global objects__ + +`objc` namespace for loaded classes, C functions, + function aliases, enums, constants, and this API + +__frameworks__ + +`objc.load(name|path[, option])` load a framework given its name or its full path \ + option 'notypes': don't load bridgesupport file + +`objc.searchpaths = {path1, ...}` search paths for frameworks + +`objc.findframework(name|path) -> path, name` find a framework in searchpaths + +__classes__ + +`objc.class'name' -> cls` class by name (`objc.class'Foo'` == `objc.Foo`) + +`objc.class(obj) -> cls` class of instance + +`objc.class('Foo', 'SuperFoo ') -> cls` create a class which conforms to protocols + +`objc.class('Foo', 'SuperFoo', 'Protocol1', ...) -> cls` create a class (alternative way) + +`objc.classname(cls) -> s` class name + +`objc.isclass(x) -> true|false` check for Class type + +`objc.isobj(x) -> true|false` check for id type + +`objc.ismetaclass(cls) -> true|false` check if the class is a metaclass + +`objc.superclass(cls|obj) -> cls|nil` superclass + +`objc.metaclass(cls|obj) -> cls` metaclass + +`objc.isa(cls|obj, supercls) -> true|false` check the inheritance chain + +`objc.conforms(cls|obj, protocol) -> true|false` check if a class conforms to a protocol + +`objc.responds(cls, sel) -> true|false` check if instances of cls responds to a selector + +`objc.conform(cls, protocol) -> true|false` declare that a class conforms to a protocol + +__object fields__ + +`obj.field` \ access an instance field, i.e. try to get, in order: \ +`obj:method(args...)` - an instance luavar \ + - a readable instance property \ + - an ivar \ + - an instance method \ + - a class field (see below) + +`obj.field = val` \ set an instance field, i.e. try to set, in order: \ + - an existing instance luavar \ + - a writable instance property \ + - an ivar \ + - an existing class field (see below) \ + - a new instance luavar + +__class fields__ + +`cls.field` \ access a class field, i.e. try to get, in order: \ +`cls:method(args...)` - a class luavar \ + - a readable class property \ + - a class method \ + - a class luavar from a superclass + +`cls.field = val` \ set a class field, i.e. try to set, in order: \ +`function cls:method(args...) end` - 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 \ + - a new class luavar + +__type conversions__ + +`objc.tolua(x) -> luatype` convert a NSNumber, NSString, NSDictionary, NSArray + to a Lua number, string, table respectively. + anything else passes through. + +`objc.toobj(x) -> objtype` convert a Lua number, string, or table to a + NSNumber, NSString, NSDictionary, NSArray respectively. + anything else passes through. + +`objc.ipairs(arr) -> next, arr, 0` ipairs for NSarray. + +__overriding__ + +`objc.override(cls, sel, func[,mtype|ftype]) -> true|false` override an existing method, or add a method + which conforms to one of the conforming protocols. + returns true if the method was found and overriden. + +`objc.callsuper(obj, sel, args...) -> retval` call the method implementation of the superclass + of an object. + +`objc.swizle(cls, sel1, sel2[, func])` swap implementations between sel1 and sel2. + if sel2 is not an existing selector, func is required. + +__selectors__ + +`objc.SEL(name|sel) -> sel` create/find a selector by name + +`sel:name() -> s` selector name (same as tostring(sel)) + + +__blocks and callbacks__ + +`objc.toarg(cls, sel, argindex, x) -> objtype` convert a Lua value to an objc value - used specifically + to create blocks and function callbacks with an appropriate + type signature for a specific method argument. + +`objc.block(func, mtype|ftype) -> block` create a block with a specific type encoding. + +----------------------------------------------------------- -------------------------------------------------------------- + + +## Reflection API + +----------------------------------------------------------- -------------------------------------------------------------- +__protocols__ + +`objc.protocols() -> iter() -> proto` loaded protocols (formal or informal) + +`objc.protocol(name|proto) -> proto` get a protocol by name (formal or informal) + +`proto:name() -> s` protocol name (same as tostring(proto)) + +`proto:protocols() -> iter() -> proto` inherited protocols + +`proto:properties() -> iter() -> prop` get properties (inherited ones not included) + +`proto:property(proto, name, required, readonly) -> prop` find a property + +`proto:methods(proto, inst, req) -> iter() -> sel, mtype` get method names and raw, non-annotated type encodings + +`proto:mtype(proto, sel, inst, req) -> mtype` find a method and return its raw type encoding + +`proto:ctype(proto, sel, inst, req[, for_cb]) -> ctype` find a method and return its C type encoding + +__classes__ + +`objc.classes() -> iter() -> cls` loaded classes + +`objc.protocols(cls) -> iter() -> proto` protocols which a class conforms to (formal or informal) + +objc.properties(cls) -> iter() -> prop` instance properties \ + use metaclass(cls) to get class properties + +`objc.property(cls, name) -> prop` instance property by name (looks in superclasses too) + +`objc.methods(cls) -> iter() -> meth` instance methods \ + use metaclass(cls) to get class methods + +`objc.method(cls, name) -> meth` instance method by name (looks in superclasses too) + +`objc.ivars(cls) -> iter() -> ivar` ivars + +`objc.ivar(cls) -> ivar` ivar by name (looks in superclasses too) + +__properties__ + +`prop:name() -> s` property name (same as tostring(prop)) + +`prop:getter() -> s` getter name + +`prop:setter() -> s` setter name (if not readonly) + +`prop:stype() -> s` type encoding + +`prop:ctype() -> s` C type encoding + +`prop:readonly() -> true|false` readonly check + +`prop:ivar() -> s` ivar name + +__methods__ + +`meth:selector() -> sel` selector + +`meth:name() -> s` selector name (same as tostring(meth)) + +`meth:mtype() -> s` type encoding + +`meth:implementation() -> IMP` implementation (untyped) + +__ivars__ + +`ivar:name() -> s` name (same as tostring(ivar)) + +`ivar:stype() -> s` type encoding + +`ivar:ctype() -> s` C type encoding + +`ivar:offset() -> n` offset + +----------------------------------------------------------- -------------------------------------------------------------- + + +## Debug API + +----------------------------------------------------------- -------------------------------------------------------------- +__logging__ +`objc.debug.errors` (true) log errors to stderr +`objc.debug.printcdecl` (false) print C declarations on stdout +`objc.debug.logtopics= {topic = true}` (empty) enable logging on some topic (see source code) +`objc.debug.errcount = {topic = count}` error counts +__solving C name clashes__ +`objc.debug.rename.string.foo = bar` load a string constant under a different name +`objc.debug.rename.enum.foo = bar` load an enum under a different name +`objc.debug.rename.typedef.foo = bar` load a type under a different name +`objc.debug.rename.const.foo = bar` load a const under a different name +`objc.debug.rename.function.foo = bar` load a global function under a different name +__loading frameworks__ +`objc.debug.loadtypes` (true) load bridgesupport files +`objc.debug.loaddeps` (false) load dependencies per bridgesupport file (too many to be useful) +`objc.debug.lazyfuncs` (true) cdef functions on the first call instead of on load +`objc.debug.checkredef` (false) check incompatible redefinition attempts (makes parsing slower) +`objc.debug.usexpat` (false) use expat to parse bridgesupport files +__gc bridging__ +`objc.debug.noretain.foo = true` declare that method `foo` already retains the object it returns +----------------------------------------------------------- -------------------------------------------------------------- + +## Future developments + +> NOTE: I don't plan to work on these, except on requests with a use case. Patches/pull requests welcome. + +### Bridging + + * function-pointer args on function-pointer args (recorded but not used - need use cases) + * test for overriding a method that takes a function-pointer (not a block) arg and invoking that arg from the callback + * auto-coercion of types for functions/methods with format strings, eg. NSLog + * format string parser - apply to variadic functions and methods that have the `printf_format` attribute + * return pass-by-reference out parameters as multiple Lua return values + * record type modifiers O=out, N=inout + * auto-allocation of out arrays using array type annotations + * `c_array_length_in_result` - array length is the return value + * `c_array_length_in_arg` - array length is an arg + * `c_array_delimited_by_null` - vararg ends in null - doesn't luajit do that already? + * `c_array_of_variable_length` - ??? + * `c_array_of_fixed_length` - specifies array size? doesn't seem so + * `sel_of_type`, `sel_of_type64` - use cases? + * core foundation stuff + * `cftypes` xml node - use cases? + * `already_retained` flag + * operator overloading (need good use cases) + +### Inspection + + * list all frameworks in searchpaths + * find framework in searchpaths + * report conforming methods, where they come from and mark the required ones, especially required but not implemented + * inspection of instances + * print class, superclasses and protocols in one line + * print values of luavars, ivars, properties + * listing sections: ivars, properties, methods, with origin class/protocol for each + +### Type Cache + +The idea is to cache bridgesupport data into Lua files for faster loading of frameworks. + + * one Lua cache file for each framework to be loaded with standard 'require' + * dependencies also loaded using standard 'require' + * save dependency loading + * save cdecls - there's already a pretty printer and infrastructure for recording those + * save constants and enums + * save function wrappers + * save mtas (find a more compact format for annotations containing only {retval='B'} ?) + * save informal protocols diff --git a/objc/objc_dispatch.lua b/objc/objc_dispatch.lua new file mode 100644 index 0000000..907e8a3 --- /dev/null +++ b/objc/objc_dispatch.lua @@ -0,0 +1,260 @@ + +--Grand Central Dispatch binding by Fjölnir Ásgeirsson (c) 2012, MIT license. +--modified for luapower/objc by Cosmin Apreutesei, public domain. +--not used yet, thus not finished. the API will probably change in the future. + +local ffi = require'ffi' +local objc = require'objc' +local C = ffi.C +local dispatch = {} +objc.dispatch = dispatch + +ffi.cdef[[ +// base.h +typedef void *dispatch_object_t; +typedef void (*dispatch_function_t)(void *); + +// object.h +void dispatch_debug(dispatch_object_t object, const char *message, ...); +void dispatch_debugv(dispatch_object_t object, const char *message, va_list ap); +void dispatch_retain(dispatch_object_t object); +void dispatch_release(dispatch_object_t object); +void *dispatch_get_context(dispatch_object_t object); +void dispatch_set_context(dispatch_object_t object, void *context); +void dispatch_set_finalizer_f(dispatch_object_t object, dispatch_function_t finalizer); +void dispatch_suspend(dispatch_object_t object); +void dispatch_resume(dispatch_object_t object); + +// time.h +typedef uint64_t dispatch_time_t; +dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta); +dispatch_time_t dispatch_walltime(const struct timespec *when, int64_t delta); + +// queue.h +typedef struct dispatch_queue_s *dispatch_queue_t; +typedef struct dispatch_queue_attr_s *dispatch_queue_attr_t; +typedef long dispatch_queue_priority_t; + +struct dispatch_queue_s _dispatch_main_q; +struct dispatch_queue_attr_s _dispatch_queue_attr_concurrent; + +typedef id dispatch_block_t; +void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); +void dispatch_async(dispatch_queue_t queue, dispatch_block_t block); + +void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *context, void (*work)(void *, size_t)); +dispatch_queue_t dispatch_get_current_queue(void); +dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags); +dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); +const char *dispatch_queue_get_label(dispatch_queue_t queue); +void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue); +void dispatch_main(void); +void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_barrier_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_barrier_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor); +void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key); +void *dispatch_get_specific(const void *key); + +// source.h +typedef struct dispatch_source_s *dispatch_source_t; +typedef const struct dispatch_source_type_s *dispatch_source_type_t; +typedef unsigned long dispatch_source_mach_send_flags_t; +typedef unsigned long dispatch_source_proc_flags_t; +typedef unsigned long dispatch_source_vnode_flags_t; + +const struct dispatch_source_type_s _dispatch_source_type_data_add; +const struct dispatch_source_type_s _dispatch_source_type_data_or; +const struct dispatch_source_type_s _dispatch_source_type_mach_send; +const struct dispatch_source_type_s _dispatch_source_type_mach_recv; +const struct dispatch_source_type_s _dispatch_source_type_proc; +const struct dispatch_source_type_s _dispatch_source_type_read; +const struct dispatch_source_type_s _dispatch_source_type_signal; +const struct dispatch_source_type_s _dispatch_source_type_timer; +const struct dispatch_source_type_s _dispatch_source_type_vnode; +const struct dispatch_source_type_s _dispatch_source_type_write; + +dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, + unsigned long mask, dispatch_queue_t queue); +void dispatch_source_set_event_handler_f(dispatch_source_t source, dispatch_function_t handler); +void dispatch_source_set_cancel_handler_f(dispatch_source_t source, dispatch_function_t cancel_handler); +void dispatch_source_cancel(dispatch_source_t source); +long dispatch_source_testcancel(dispatch_source_t source); +uintptr_t dispatch_source_get_handle(dispatch_source_t source); +unsigned long dispatch_source_get_mask(dispatch_source_t source); +unsigned long dispatch_source_get_data(dispatch_source_t source); +void dispatch_source_merge_data(dispatch_source_t source, unsigned long value); +void +dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway); +void dispatch_source_set_registration_handler_f(dispatch_source_t source, dispatch_function_t registration_handler); + +// group.h +typedef struct dispatch_group_s *dispatch_group_t; + +dispatch_group_t dispatch_group_create(void); +void dispatch_group_async_f(dispatch_group_t group, dispatch_queue_t queue, void *context, dispatch_function_t work); +long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout); +void dispatch_group_notify_f(dispatch_group_t group, dispatch_queue_t queue, void *context, dispatch_function_t work); +void dispatch_group_enter(dispatch_group_t group); +void dispatch_group_leave(dispatch_group_t group); + +// semaphore.h +typedef struct dispatch_semaphore_s *dispatch_semaphore_t; + +dispatch_semaphore_t dispatch_semaphore_create(long value); +long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); +long dispatch_semaphore_signal(dispatch_semaphore_t dsema); + +// once.h +typedef long dispatch_once_t; + +void dispatch_once_f(dispatch_once_t *predicate, void *context, dispatch_function_t function); + +// data.h (Requires blocks for all of it's functionality, see http://github.com/aptiva/tlc if you need it) +typedef struct dispatch_data_s *dispatch_data_t; + +struct dispatch_data_s _dispatch_data_empty; + +// io.h (Requires blocks for all of it's functionality, see http://github.com/aptiva/tlc if you need it) +typedef int dispatch_fd_t; +]] + +-- Types +dispatch.object_t = ffi.typeof'dispatch_object_t' +dispatch.function_t = ffi.typeof'dispatch_function_t' +dispatch.time_t = ffi.typeof'dispatch_time_t' +dispatch.queue_t = ffi.typeof'dispatch_queue_t' +dispatch.queue_attr_t = ffi.typeof'dispatch_queue_attr_t' +dispatch.queue_priority_t = ffi.typeof'dispatch_queue_priority_t' +dispatch.source_t = ffi.typeof'dispatch_source_t' +dispatch.source_type_t = ffi.typeof'dispatch_source_type_t' +dispatch.source_mach_send_flags_t = ffi.typeof'dispatch_source_mach_send_flags_t' +dispatch.source_proc_flags_t = ffi.typeof'dispatch_source_proc_flags_t' +dispatch.source_vnode_flags_t = ffi.typeof'dispatch_source_vnode_flags_t' +dispatch.group_t = ffi.typeof'dispatch_group_t' +dispatch.semaphore_t = ffi.typeof'dispatch_semaphore_t' +dispatch.once_ = ffi.typeof'dispatch_once_t' +dispatch.data_t = ffi.typeof'dispatch_data_t' +dispatch.fd_t = ffi.typeof'dispatch_fd_t' + +if ffi.os ~= 'OSX' then + error('platform not OSX', 2) +end + +-- Contants +dispatch.emptyData = ffi.cast(dispatch.data_t, C._dispatch_data_empty) +dispatch.defaultDataDestructor = nil + +dispatch.nsecPerSec = 1000000000ULL +dispatch.nsecPerMsec = 1000000ULL +dispatch.usecPerSec = 1000000ULL +dispatch.nsecPerUsec = 1000ULL + +dispatch.timeNow = 0 +dispatch.timeForever = bit.bnot(0) + +objc.DISPATCH_QUEUE_PRIORITY_HIGH = ffi.cast(dispatch.queue_priority_t, 2) +objc.DISPATCH_QUEUE_PRIORITY_DEFAULT = ffi.cast(dispatch.queue_priority_t, 0) +objc.DISPATCH_QUEUE_PRIORITY_LOW = ffi.cast(dispatch.queue_priority_t, -2) +objc.DISPATCH_QUEUE_PRIORITY_BACKGROUND = ffi.cast(dispatch.queue_priority_t, -32768) -- INT16_MIN + +dispatch.main_queue = C._dispatch_main_q +dispatch.serialQueueAttr = nil +dispatch.queue_attr_concurrent = C._dispatch_queue_attr_concurrent +dispatch.defaultTargetQueue = nil + +dispatch.machSendDead = ffi.cast(dispatch.source_proc_flags_t, 0x1) +dispatch.procExit = ffi.cast(dispatch.source_proc_flags_t, 0x80000000) +dispatch.procFork = ffi.cast(dispatch.source_proc_flags_t, 0x40000000) +dispatch.procExec = ffi.cast(dispatch.source_proc_flags_t, 0x20000000) +dispatch.procSignal = ffi.cast(dispatch.source_proc_flags_t, 0x08000000) + +dispatch.sourceTypeDataAdd = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_data_add) +dispatch.sourceTypeDataOr = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_data_or) +dispatch.sourceTypeMachSend = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_mach_send) +dispatch.sourceTypeMachRecv = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_mach_recv) +dispatch.sourceTypeProc = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_proc) +dispatch.sourceTypeRead = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_read) +dispatch.sourceTypeSignal = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_signal) +dispatch.sourceTypeTimer = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_timer) +dispatch.sourceTypeVnode = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_vnode) +dispatch.sourceTypeWrite = ffi.cast(dispatch.source_type_t, C._dispatch_source_type_write) +dispatch.vNodeDelete = ffi.cast(dispatch.source_vnode_flags_t, 0x1) +dispatch.vNodeWrite = ffi.cast(dispatch.source_vnode_flags_t, 0x2) +dispatch.vNodeExtend = ffi.cast(dispatch.source_vnode_flags_t, 0x4) +dispatch.vNodeAttrib = ffi.cast(dispatch.source_vnode_flags_t, 0x8) +dispatch.vNodeLink = ffi.cast(dispatch.source_vnode_flags_t, 0x10) +dispatch.vNodeRename = ffi.cast(dispatch.source_vnode_flags_t, 0x20) +dispatch.vNodeRevoke = ffi.cast(dispatch.source_vnode_flags_t, 0x40) + + +-- Functions +dispatch.debug = C.dispatch_debug +dispatch.debugv = C.dispatch_debugv +dispatch.retain = C.dispatch_retain +dispatch.release = C.dispatch_release +dispatch.dispatch_get_context = C.dispatch_get_context +dispatch.set_context = C.dispatch_set_context +dispatch.set_finalizer = C.dispatch_set_finalizer_f +dispatch.suspend = C.dispatch_suspend +dispatch.resume = C.dispatch_resume + +dispatch.time = C.dispatch_time +dispatch.walltime = C.dispatch_walltime + +dispatch.async_f = C.dispatch_async_f +dispatch.sync_f = C.dispatch_sync_f +dispatch.apply = C.dispatch_apply_f +dispatch.get_current_queue = C.dispatch_get_current_queue +dispatch.get_global_queue = C.dispatch_get_global_queue +dispatch.queue_create = C.dispatch_queue_create +dispatch.dispatch_queue_get_label = C.dispatch_queue_get_label +dispatch.set_target_queue = C.dispatch_set_target_queue +dispatch.main = C.dispatch_main +dispatch.after = C.dispatch_after_f +dispatch.barrier_async = C.dispatch_barrier_async_f +dispatch.barrier_sync = C.dispatch_barrier_sync_f +dispatch.queue_set_specific = C.dispatch_queue_set_specific +dispatch.dispatch_queue_get_specific = C.dispatch_queue_get_specific +dispatch.dispatch_get_specific = C.dispatch_get_specific + +dispatch.source_create = C.dispatch_source_create +dispatch.source_set_event_handler = C.dispatch_source_set_event_handler_f +dispatch.source_set_cancel_handler = C.dispatch_source_set_cancel_handler_f +dispatch.source_cancel = C.dispatch_source_cancel +dispatch.source_testcancel = C.dispatch_source_testcancel +dispatch.source_get_handle = C.dispatch_source_get_handle +dispatch.source_get_mask = C.dispatch_source_get_mask +dispatch.source_get_data = C.dispatch_source_get_data +dispatch.source_merge_data = C.dispatch_source_merge_data +dispatch.source_set_timer = C.dispatch_source_set_timer +dispatch.source_set_registration_handler = C.dispatch_source_set_registration_handler_f + +dispatch.group_create = C.dispatch_group_create +dispatch.group_async = C.dispatch_group_async_f +dispatch.group_wait = C.dispatch_group_wait +dispatch.group_notify = C.dispatch_group_notify_f +dispatch.group_enter = C.dispatch_group_enter +dispatch.group_leave = C.dispatch_group_leave + +dispatch.semaphore_create = C.dispatch_semaphore_create +dispatch.semaphore_wait = C.dispatch_semaphore_wait +dispatch.semaphore_signal = C.dispatch_semaphore_signal + +dispatch.once = C.dispatch_once_f + +dispatch.DISPATCH_QUEUE_SERIAL = nil + +--note: do not use with queues that call back from a different thread! +function dispatch.async(queue, block) + C.dispatch_async(queue, objc.block(block)) +end +jit.off(dispatch.async) + +function dispatch.sync(queue, block) + C.dispatch_sync(queue, objc.block(block)) +end + +return dispatch diff --git a/objc/objc_inspect.lua b/objc/objc_inspect.lua new file mode 100644 index 0000000..726eb1e --- /dev/null +++ b/objc/objc_inspect.lua @@ -0,0 +1,246 @@ +local objc = require'objc' + +local inspect = {} +objc.inspect = inspect + +--pretty helpers + +local _ = string.format + +local function p(...) --formatted line + print(_(...)) +end + +local function isort(iter, method) --sort an iterator of object by a method + local t = {} + while true do + local v = iter() + if v == nil then break end + t[#t+1] = v + end + table.sort(t, function(a, b) return a[method](a) < b[method](b) end) + local i = 0 + return function() + i = i + 1 + if t[i] == nil then return end + return t[i] + end +end + +local function icount(iter) + local n = 0 + for _ in iter do + n = n + 1 + end + return n +end + +--class header + +local function protocols_spec(protocols) + local t = {} + for proto in isort(protocols, 'name') do + t[#t+1] = proto:name() .. protocols_spec(proto:protocols()) + end + return #t > 0 and _(' <%s>', table.concat(t, ', ')) or '' +end + +local function class_spec(cls, indent) + indent = indent or 1 + local super_spec = objc.superclass(cls) and + _('\n%s<- %s', (' '):rep(indent), class_spec(objc.superclass(cls), indent + 1)) or '' + return objc.classname(cls) .. protocols_spec(objc.protocols(cls)) .. super_spec +end + +local function protocol_spec(proto) + return proto:name() .. protocols_spec(proto:protocols()) +end + +function inspect.class_header(cls) + p('Class %s', class_spec(objc.class(cls))) +end + +--classes + +function inspect.classes() + for cls in objc.classes() do + p('%-50s protocols: %-5s properties: %-5s ivars: %-5s methods (i): %-5s methods (c): %-5s', + objc.classname(cls), + icount(objc.protocols(cls)), + icount(objc.properties(cls)), + icount(objc.ivars(cls)), + icount(objc.methods(cls)), + icount(objc.methods(objc.metaclass(cls)))) + end +end + +--protocols + +function inspect.protocols() + for proto in objc.protocols() do + p('%-50s %-10s protocols: %-5s properties: %-5s methods (i/o): %-5s methods (i/r): %-5s methods (c/o): %-5s methods (c/r): %-5s', + proto:name(), proto.formal and 'formal' or 'informal', + icount(proto:protocols()), + icount(proto:properties()), + icount(proto:methods(true, false)), + icount(proto:methods(true, true)), + icount(proto:methods(false, false)), + icount(proto:methods(false, true))) + end +end + +--properties + +local function inspect_properties(name, props) + for prop in isort(props, 'name') do + p('%-30s %-40s %-8s %-20s %-20s %-20s %-20s', + name, prop:name(), prop:readonly() and 'r/o' or 'r/w', + prop:ctype(), prop:ivar() or '', prop:getter(), prop:setter() or '') + end +end + +local function not_(arg, list, process, ...) + if arg then return end + for arg in list() do + process(arg, ...) + end + return true +end + +function inspect.protocol_properties(proto) + if not_(proto, objc.protocols, inspect.protocol_properties) then return end + proto = objc.protocol(proto) + inspect_properties(proto:name(), proto:properties()) +end + +function inspect.class_properties(cls) + if not_(cls, objc.classes, inspect.class_properties) then return end + cls = objc.class(cls) + inspect_properties(objc.classname(cls), objc.properties(cls)) +end + +--methods + +function inspect.class_methods(cls, inst) + if not_(cls, objc.classes, inspect.class_methods, inst) then return end + cls = inst and objc.class(cls) or objc.metaclass(cls) + for meth in isort(objc.methods(cls), 'name') do + p('%-40s %-50s %-50s %s', objc.classname(cls), meth:name(), + objc.ftype_ctype(objc.method_ftype(cls, meth:selector(), meth)), meth:mtype()) + end +end + +function inspect.protocol_methods(proto, inst, required) + if not_(proto, objc.protocols, inspect.protocol_methods, inst, required) then return end + proto = objc.protocol(proto) + for selname, mtype in proto:methods(inst or false, required or false) do + p('%-40s %-50s %-50s %s', proto:name(), selname, objc.ftype_ctype(objc.mtype_ftype(mtype)), mtype) + end +end + +--ivars + +function inspect.class_ivars(cls) + if not_(cls, objc.classes, inspect.ivars) then return end + cls = objc.class(cls) + for ivar in isort(objc.ivars(cls), 'name') do + p('%-40s %-50s %-50s %-5s %s', objc.classname(cls), ivar:name(), ivar:ctype(), + tonumber(ivar:offset()), ivar:stype()) + end +end + +--full class inspection + +function inspect.class(cls) + print'' + inspect.class_header(cls) + print'\nProperties:\n' + inspect.class_properties(cls) + print'\nIvars:\n' + inspect.class_ivars(cls) + print'\nMethods:\n' + inspect.class_methods(cls) +end + +function inspect.protocol(proto) + print'' + print(objc.protocol(proto):name()) + print'\nProperties:\n' + inspect.protocol_properties(proto) + print'\nMethods (i/o):\n' + inspect.protocol_methods(proto, true, false) + print'\nMethods (i/r):\n' + inspect.protocol_methods(proto, true, true) + print'\nMethods (c/o):\n' + inspect.protocol_methods(proto, false, false) + print'\nMethods (c/r):\n' + inspect.protocol_methods(proto, false, true) +end + +function inspect.find(patt) + --TODO: find framework / load all frameworks + local function find_in_class(prefix, cls) + for meth in objc.methods(cls) do + if meth:name():find(patt) then + p('%-20s [%s %s]', prefix..' method', objc.classname(cls), meth:name()) + end + end + for prop in objc.properties(cls) do + if prop:name():find(patt) then + p('%-20s %s.%s', prefix..' property', objc.classname(cls), prop:name()) + end + end + for ivar in objc.ivars(cls) do + if ivar:name():find(patt) then + p('%-20s %s.%s', prefix..' ivar', objc.classname(cls), ivar:name()) + end + end + end + for cls in objc.classes() do + if objc.classname(cls):find(patt) then + p('%-20s %s', 'class', objc.classname(cls)) + end + find_in_class('instance', cls) + find_in_class('class', cls) + end + local function find_proto_method(proto, postfix, inst, required) + for selname in proto:methods(inst, required) do + if selname:find(patt) then + p('%-20s [%s %s]', 'protocol method ('..postfix..')', proto:name(), selname) + end + end + end + for proto in objc.protocols() do + if proto:name():find(patt) then + p('%-20s %s', 'protocol', proto:name()) + end + find_proto_method(proto, 'i/o', true, false) + find_proto_method(proto, 'i/r', true, true) + find_proto_method(proto, 'c/o', false, false) + find_proto_method(proto, 'c/r', false, true) + for prop in proto:properties() do + if prop:name():find(patt) then + p('%-20s %s.%s', 'protocol property', proto:name(), prop:name()) + end + end + end + local function find_global(title, prefix, namespace) + for k in pairs(namespace) do + if type(k) == 'string' and k:find(patt) then + p('%-20s %s%s', title, prefix, k) + end + end + end + find_global('global', 'objc.', objc) + find_global('C global', 'objc.', objc.debug.cnames.global) + find_global('C struct', '', objc.debug.cnames.struct) +end + + +if not ... then + for k,v in pairs(inspect) do + print(_('%-10s %s', type(v), 'objc.inspect.'..k)) + end +end + +return inspect diff --git a/objc/objc_test.lua b/objc/objc_test.lua new file mode 100644 index 0000000..1fee702 --- /dev/null +++ b/objc/objc_test.lua @@ -0,0 +1,855 @@ +local glue = require'glue' +local objc = require'objc' +local ffi = require'ffi' +local pp = require'pp' + +io.stdout:setvbuf'no' +io.stderr:setvbuf'no' + +setmetatable(_G, {__index = objc}) + +--test options + +local subprocess = false --run each bridgesupport test in a subprocess +objc.debug.lazyfuncs = true +objc.debug.checkredef = false +objc.debug.printcdecl = false +objc.debug.loaddeps = false +objc.debug.loadtypes = true + +local bsdir = '_bridgesupport' --path where *.bridgesupport files are on Windows (tree or flat doesn't matter) +local luajit = ffi.os == 'Windows' and 'luajit' or './luajit' --luajit command for subprocess running + +if ffi.os == 'OSX' then + objc.load'Foundation' + pool = NSAutoreleasePool:new() +end + +--test helpers + +local function printf(...) + print(string.format(...)) +end + +local function hr() + print(('-'):rep(80)) +end + +local n = 0 +local function genname(prefix) + if not prefix then return genname'MyClass' end + n = n + 1 + return prefix..n +end + +local function errpcall(patt, ...) --pcall that should fail with a specific message + local ok, err = pcall(...) + assert(not ok) + assert(err:find(patt)) +end + +--test namespace + +local test = {} --{name = test_func} +local eyetest = {} --{name = test_func} +local demo = {} +local tests = {tests = test, ['eye tests'] = eyetest, demos = demo} + +function test.parsing() + assert(stype_ctype('[8^c]', 'arr') == 'char *arr[8]') --array of pointers + assert(stype_ctype('^[8c]', 'arr') == 'char (*arr)[8]') --pointer to array + assert(stype_ctype('[8[4c]]', 'arr') == 'char arr[8][4]') --multi-dim. array + assert(stype_ctype('[3^[8^c]]', 'arr') == 'char *(*arr[3])[8]') + assert(stype_ctype('{?="x"i"y"i""(?="ux"I"uy"I)}', nil, 'cdef') == + 'struct {\n\tint x;\n\tint y;\n\tunion {\n\t\tunsigned int ux;\n\t\tunsigned int uy;\n\t};\n}' + ) --nested unnamed anonymous structs + + local function mtype_ctype(mtype, ...) + return ftype_ctype(mtype_ftype(mtype), ...) + end + assert(mtype_ctype('@"Class"@:{_NSRect={_NSPoint=ff}{_NSSize=ff}}^{?}^?', 'globalFunction') == + 'id globalFunction (id, SEL, struct _NSRect, void *, void *)') --unseparated method args + assert(mtype_ctype('{_NSPoint=ff}iii', nil, true) == + 'void (*) (int, int, int)') --struct return value not supported + assert(mtype_ctype('iii{_NSPoint=ff}ii', nil, true) == + 'int (*) (int, int)') --pass-by-value struct not supported, stop at first encounter + assert(mtype_ctype('{_NSPoint=ff}ii{_NSPoint=ff}i', nil, true) == + 'void (*) (int, int)') --combined case +end + +function eyetest.indent() + --_NXEvent (test indent for nested unnamed anonymous structs) + print(stype_ctype('{?="type"i"location"{?="x"i"y"i}"time"Q"flags"i"window"I"service_id"Q"ext_pid"i"data"(?="mouse"{?="subx"C"suby"C"eventNum"s"click"i"pressure"C"buttonNumber"C"subType"C"reserved2"C"reserved3"i"tablet"(?="point"{_NXTabletPointData="x"i"y"i"z"i"buttons"S"pressure"S"tilt"{?="x"s"y"s}"rotation"S"tangentialPressure"s"deviceID"S"vendor1"s"vendor2"s"vendor3"s}"proximity"{_NXTabletProximityData="vendorID"S"tabletID"S"pointerID"S"deviceID"S"systemTabletID"S"vendorPointerType"S"pointerSerialNumber"I"uniqueID"Q"capabilityMask"I"pointerType"C"enterProximity"C"reserved1"s})}"mouseMove"{?="dx"i"dy"i"subx"C"suby"C"subType"C"reserved1"C"reserved2"i"tablet"(?="point"{_NXTabletPointData="x"i"y"i"z"i"buttons"S"pressure"S"tilt"{?="x"s"y"s}"rotation"S"tangentialPressure"s"deviceID"S"vendor1"s"vendor2"s"vendor3"s}"proximity"{_NXTabletProximityData="vendorID"S"tabletID"S"pointerID"S"deviceID"S"systemTabletID"S"vendorPointerType"S"pointerSerialNumber"I"uniqueID"Q"capabilityMask"I"pointerType"C"enterProximity"C"reserved1"s})}"key"{?="origCharSet"S"repeat"s"charSet"S"charCode"S"keyCode"S"origCharCode"S"reserved1"i"keyboardType"I"reserved2"i"reserved3"i"reserved4"i"reserved5"[4i]}"tracking"{?="reserved"s"eventNum"s"trackingNum"i"userData"i"reserved1"i"reserved2"i"reserved3"i"reserved4"i"reserved5"i"reserved6"[4i]}"scrollWheel"{?="deltaAxis1"s"deltaAxis2"s"deltaAxis3"s"reserved1"s"fixedDeltaAxis1"i"fixedDeltaAxis2"i"fixedDeltaAxis3"i"pointDeltaAxis1"i"pointDeltaAxis2"i"pointDeltaAxis3"i"reserved8"[4i]}"zoom"{?="deltaAxis1"s"deltaAxis2"s"deltaAxis3"s"reserved1"s"fixedDeltaAxis1"i"fixedDeltaAxis2"i"fixedDeltaAxis3"i"pointDeltaAxis1"i"pointDeltaAxis2"i"pointDeltaAxis3"i"reserved8"[4i]}"compound"{?="reserved"s"subType"s"misc"(?="F"[11f]"L"[11i]"S"[22s]"C"[44c])}"tablet"{?="x"i"y"i"z"i"buttons"S"pressure"S"tilt"{?="x"s"y"s}"rotation"S"tangentialPressure"s"deviceID"S"vendor1"s"vendor2"s"vendor3"s"reserved"[4i]}"proximity"{?="vendorID"S"tabletID"S"pointerID"S"deviceID"S"systemTabletID"S"vendorPointerType"S"pointerSerialNumber"I"uniqueID"Q"capabilityMask"I"pointerType"C"enterProximity"C"reserved1"s"reserved2"[4i]})}', nil, 'cdef')) +end + +function eyetest.tostring() + print(objc.NSNumber:numberWithDouble(0)) + print(objc.NSString:alloc():initWithUTF8String'') --empty string = NSFConstantString with hi address + print(objc.NSString:alloc():initWithUTF8String'asdjfah') --real object +end + +--test parsing of bridgesupport files. +--works on Windows too - just copy your bridgesupport files into whatever you set `bsdir` above. +function eyetest.bridgesupport(bsfile) + + local function list_func(cmd) + return function() + return coroutine.wrap(function() + local f = io.popen(cmd) + for s in f:lines() do + coroutine.yield(s) + end + f:close() + end) + end + end + + local bsfiles + if ffi.os == 'Windows' then + bsfiles = list_func('dir /B /S '..bsdir..'\\*.bridgesupport') + elseif ffi.os == 'OSX' then + bsfiles = list_func('find /System/Library/frameworks -name \'*.bridgesupport\'') + else + error'can\'t run on this OS' + end + + local loaded = {} + local n = 0 + + local objc_load = objc.debug.load_framework --keep it, we'll patch it + + function objc.debug.load_framework(path) --either `name.bridgesupport` or `name.framework` or `name.framework/name` + local name + if path:match'%.bridgesupport$' then + name = path:match'([^/\\]+)%.bridgesupport$' + else + name = path:match'/([^/]+)%.framework$' or path:match'([^/]+)$' + if ffi.os == 'Windows' then + path = bsdir..'\\'..name..'.bridgesupport' + else + path = path .. '/Resources/BridgeSupport/' .. name .. '.bridgesupport' + end + end + if loaded[name] then return end + loaded[name] = true + if glue.canopen(path) then + + if ffi.os == 'OSX' then + + --load the dylib first (needed for function aliases) + local dpath = path:gsub('Resources/BridgeSupport/.*$', name) + if glue.canopen(dpath) then + pcall(ffi.load, dpath, true) + end + + --load the dylib with inlines first (needed for function aliases) + local dpath = path:gsub('bridgesupport$', 'dylib') + if glue.canopen(dpath) then + pcall(ffi.load, dpath, true) + end + end + + objc.debug.load_bridgesupport(path) + n = n + 1 + --print(n, '', name) + else + print('! not found', name, path) + end + end + + local function status() + pp('errors', objc.debug.errcount) + print('globals: '..objc.debug.cnames.global[1]) + print('structs: '..objc.debug.cnames.struct[1]) + end + + if bsfile then + objc.debug.load_framework(bsfile) + else + for bsfile in bsfiles() do + if bsfile:match'Python' then + print('skipping '..bsfile) --python bridgesupport files are non-standard and deprecated + else + --print(); print(bsfile); print(('='):rep(80)) + if subprocess then + os.execute(luajit..' '..arg[0]..' bridgesupport '..bsfile) + else + objc.debug.load_framework(bsfile) + end + end + end + status() + end + + objc.debug.load_framework = objc_load --put it back +end + +function test.selectors() + assert(tostring(SEL'se_lec_tor') == 'se:lec:tor') + assert(tostring(SEL'se_lec_tor_') == 'se:lec:tor:') + assert(tostring(SEL'__se_lec_tor') == '__se:lec:tor') + assert(tostring(SEL'__se:lec:tor:') == '__se:lec:tor:') +end + +--class, superclass, metaclass, class protocols +function test.class() + --arg. checking + errpcall('already', class, 'NSObject', 'NSString') + errpcall('superclass', class, genname(), 'MyUnknownClass') + errpcall('protocol', class, genname(), 'NSObject ') + + --class overloaded constructors + local cls = class('MyClassX', false) --root class + assert(classname(cls) == 'MyClassX') + assert(not superclass(cls)) + + --derived class + local cls = class(genname(), 'NSArray') + assert(isa(cls, 'NSArray')) + + --derived + conforming + local cls = class(genname(), 'NSArray ') + assert(isa(cls, 'NSArray')) + + assert(conforms(cls, 'NSStreamDelegate')) + assert(conforms(cls, 'NSLocking')) + + local t = {0} + for proto in protocols(cls) do + t[proto:name()] = true + t[1] = t[1] + 1 + end + assert(t[1] == 2) + assert(t.NSStreamDelegate) + assert(t.NSLocking) + + --class hierarchy queries + assert(superclass(cls) == NSArray) + assert(metaclass(cls)) + assert(superclass(metaclass(cls)) == metaclass'NSArray') + assert(metaclass(superclass(cls)) == metaclass'NSArray') + assert(metaclass(metaclass(cls)) == nil) + assert(isa(cls, 'NSObject')) + assert(ismetaclass(metaclass(cls))) + assert(isclass(cls)) + assert(not ismetaclass(cls)) + assert(not isobj(cls)) + assert(isclass(metaclass(cls))) + + local obj = cls:new() + assert(isobj(obj)) + assert(not isclass(obj)) +end + +function test.refcount() + local cls = class(genname(), 'NSObject') + local inst, inst2, inst3 + + inst = cls:new() + assert(inst:retainCount() == 1) + + inst2 = inst:retain() --same class, new cdata, new reference + assert(inst:retainCount() == 2) + + inst3 = inst:retain() + assert(inst:retainCount() == 3) + + inst3 = nil --release() on gc + collectgarbage() + assert(inst:retainCount() == 2) + + inst3 = inst:retain() + assert(inst:retainCount() == 3) + + inst:release() --manual release() + assert(inst:retainCount() == 2) + + inst = nil --object already disowned by inst, refcount should not decrease + collectgarbage() + assert(inst2:retainCount() == 2) + + inst, inst2, inst3 = nil + collectgarbage() +end + +function test.luavars() + local cls = class(genname(), 'NSObject') + + --class vars + cls.myclassvar = 'doh1' + assert(cls.myclassvar == 'doh1') --intialized + cls.myclassvar = 'doh' + assert(cls.myclassvar == 'doh') --updated + + --inst vars + local inst = cls:new() + + inst.myinstvar = 'DOH1' + assert(inst.myinstvar == 'DOH1') --initialized + inst.myinstvar = 'DOH' + assert(inst.myinstvar == 'DOH') --updated + + --class vars from instances + assert(inst.myclassvar == 'doh') --class vars are readable from instances + inst.myclassvar = 'doh2' + assert(cls.myclassvar == 'doh2') --and they can be updated from instances + assert(inst.myclassvar == 'doh2') + + --soft ref counting + local inst2 = inst:retain() + assert(inst.myinstvar == 'DOH') --2 refs + inst = nil + collectgarbage() + assert(inst2.myinstvar == 'DOH') --1 ref + inst2:release() --0 refs; instance gone, vars gone (no way to test, memory was freed) + assert(cls.myclassvar == 'doh2') --class vars still there + + local i = 0 + function NSObject:myMethod() i = i + 1 end + local str = toobj'hello' --create a NSString instance, which is a NSObject + str:myMethod() --instance method (str passed as self) + objc.NSString:myMethod() --class method (NSString passed as self) + assert(i == 2) + + function NSObject:myMethod() i = i - 1 end --override + str:myMethod() --instance method (str passed as self) + objc.NSString:myMethod() --class method (NSString passed as self) + assert(i == 0) +end + +function test.override() + objc.debug.logtopics.addmethod = true + + local cls = class(genname(), 'NSObject') + local metacls = metaclass(cls) + local obj = cls:new() + local instdesc = 'hello-instance' + local classdesc = 'hello-class' + + function metacls:description() --override the class method + return classdesc --note: we can return the string directly. + end + + function cls:description() --override the instance method + return instdesc --note: we can return the string directly. + end + + assert(objc.tolua(cls:description()) == classdesc) --class method was overriden + assert(objc.tolua(obj:description()) == instdesc) --instance method was overriden and it's different + + --subclass and test again + + local cls2 = class(genname(), cls) + local metacls2 = metaclass(cls2) + local obj2 = cls2:new() + + function metacls2:description() --override the class method + return objc.callsuper(self, 'description'):UTF8String() .. '2' + end + + function cls2:description(callsuper) --override the instance method + return objc.callsuper(self, 'description'):UTF8String() .. '2' + end + + assert(objc.tolua(cls2:description()) == classdesc..'2') --class method was overriden + assert(objc.tolua(obj2:description()) == instdesc..'2') --instance method was overriden and it's different +end + +function test.ivars() + local obj = NSDocInfo:new() + + if ffi.abi'64bit' then + assert(ffi.typeof(obj.time) == ffi.typeof'long long') + else + assert(type(obj.time) == 'number') + end + assert(type(obj.mode) == 'number') --unsigned short + assert(ffi.typeof(obj.flags) == ffi.typeof(obj.flags)) --anonymous struct (assert that it was cached) + + obj.time = 123 + assert(obj.time == 123) + + assert(obj.flags.isDir == 0) + obj.flags.isDir = 3 --1 bit + assert(obj.flags.isDir == 1) --1 bit was set (so this is not a luavar or anything) +end + +test.properties = objc.with_properties(function() + --TODO: find another class with r/w properties. NSProgress is not public on 10.7. + local pr = NSProgress:progressWithTotalUnitCount(123) + assert(pr.totalUnitCount == 123) --as initialized + pr.totalUnitCount = 321 --read/write property + assert(pr.totalUnitCount == 321) + assert(not pcall(function() pr.indeterminate = true end)) --attempt to set read-only property + assert(pr.indeterminate == false) +end) + +local timebase, last_time +function timediff() + objc.load'System' + local time + time = mach_absolute_time() + if not timebase then + timebase = ffi.new'mach_timebase_info_data_t' + mach_timebase_info(timebase) + end + local d = tonumber(time - (last_time or 0)) * timebase.numer / timebase.denom / 10^9 + last_time = time + return d +end + +function test.blocks() + --objc.debug.logtopics.block = true + local times = 20000 + + timediff() + + --take 1: creating blocks in inner loops with automatic memory management of blocks. + local s = NSString:alloc():initWithUTF8String'line1\nline2\nline3' + for i=1,times do + local t = {} + --note: the signature of the block arg for enumerateLinesUsingBlock was taken from bridgesupport. + s:enumerateLinesUsingBlock(function(line, pstop) + t[#t+1] = line:UTF8String() + if #t == 2 then --stop at line 2 + pstop[0] = 1 + end + end) + assert(#t == 2) + assert(t[1] == 'line1') + assert(t[2] == 'line2') + --note: callbacks are slow, expensive to create, and limited in number. we have to release them often! + if i % 200 == 0 then + collectgarbage() + end + end + + printf('take 1: block in loop (%d times): %4.2fs', times, timediff()) + + --take 2: creating a single block in the outer loop (we must give its type). + local t + local blk = toarg(NSString, 'enumerateLinesUsingBlock', 1, function(line, pstop) + t[#t+1] = line:UTF8String() + if #t == 2 then --stop at line 2 + pstop[0] = 1 + end + end) + local s = NSString:alloc():initWithUTF8String'line1\nline2\nline3' + for i=1,times do + t = {} + s:enumerateLinesUsingBlock(blk) + assert(#t == 2) + assert(t[1] == 'line1') + assert(t[2] == 'line2') + end + + printf('take 2: single block (%d times): %4.2fs', times, timediff()) + +end + +function test.tolua() + local n = toobj(123.5) + assert(isa(n, 'NSNumber')) + assert(tolua(n) == 123.5) + + local s = toobj'hello' + assert(isa(s, 'NSString')) + assert(tolua(s) == 'hello') + + local a = {1,2,6,7} + local t = toobj(a) + assert(t:count() == #a) + for i=1,#a do + assert(t:objectAtIndex(i-1):doubleValue() == a[i]) + end + a = tolua(t) + assert(#a == 4) + assert(a[3] == 6) + + local d = {a = 1, b = 'baz', d = {1,2,3}, [{x=1}] = {y=2}} + local t = toobj(d) + assert(t:count() == 4) + assert(tolua(t:valueForKey(toobj'a')) == d.a) + assert(tolua(t:valueForKey(toobj'b')) == d.b) + assert(tolua(t:valueForKey(toobj'd'))[2] == 2) +end + +function test.args() + local s = NSString:alloc():initWithUTF8String'\xE2\x82\xAC' --euro symbol + --return string + assert(s:UTF8String() == '\xE2\x82\xAC') + --return boolean (doesn't work for methods) + assert(s:isAbsolutePath() == false) + --return null + assert(type(s:cStringUsingEncoding(NSASCIIStringEncoding)) == 'nil') + --selector arg + assert(s:respondsToSelector'methodForSelector:' == true) + --class arg + assert(NSArray:isSubclassOfClass'NSObject' == true) + assert(NSArray:isSubclassOfClass'XXX' == false) + --string arg + assert(NSString:alloc():initWithString('hey'):UTF8String() == 'hey') + --table arg for array + local a = NSArray:alloc():initWithArray{6,25,5} + assert(a:objectAtIndex(1):doubleValue() == 25) + --table arg for dictionary + local d = NSDictionary:alloc():initWithDictionary{a=5,b=7} + assert(d:valueForKey('b'):doubleValue() == 7) +end + +function demo.window() + objc.load'AppKit' + + local NSApp = class('NSApp', 'NSApplication ') + + --we need to add methods to the class before creating any objects! + --note: NSApplicationDelegate is an informal protocol brought from bridgesupport. + + function NSApp:applicationShouldTerminateAfterLastWindowClosed() + print'last window closed...' + collectgarbage() + return true + end + + function NSApp:applicationShouldTerminate() + print'terminating...' + return true + end + + local app = NSApp:sharedApplication() + app:setDelegate(app) + app:setActivationPolicy(NSApplicationActivationPolicyRegular) + + local NSWin = class('NSWin', 'NSWindow ') + + --we need to add methods to the class before creating any objects! + --note: NSWindowDelegate is a formal protocol brought from the runtime. + + function NSWin:windowWillClose() + print'window will close...' + end + + local style = bit.bor( + NSTitledWindowMask, + NSClosableWindowMask, + NSMiniaturizableWindowMask, + NSResizableWindowMask) + + local win = NSWin:alloc():initWithContentRect_styleMask_backing_defer( + NSMakeRect(300, 300, 500, 300), style, NSBackingStoreBuffered, false) + win:setDelegate(win) + win:setTitle"▀▄▀▄▀▄ [ Lua Rulez ] ▄▀▄▀▄▀" + + app:activateIgnoringOtherApps(true) + win:makeKeyAndOrderFront(nil) + + app:run() +end + +function demo.speech() + objc.load'AppKit' + local speech = NSSpeechSynthesizer:new() + voiceid = NSSpeechSynthesizer:availableVoices():objectAtIndex(11) + speech:setVoice(voiceid) + speech:startSpeakingString'Calm, fitter, healthier, and more productive; A pig. In a cage. On antibiotics.' + while speech:isSpeaking() do + os.execute'sleep 1' + end +end + +function demo.http() --what a dense word soup just to make a http request + objc.load'AppKit' + local app = NSApplication:sharedApplication() + + local post = NSString:stringWithFormat('firstName=%@&lastName=%@&eMail=%@&message=%@', + toobj'Dude', toobj'Edud', toobj'x@y.com', toobj'message') + local postData = post:dataUsingEncoding(NSUTF8StringEncoding) + local postLength = NSString:stringWithFormat('%ld', postData:length()) + NSLog('Post data: %@', post) + local request = NSMutableURLRequest:new() + request:setURL(NSURL:URLWithString'http://posttestserver.com/post.php') + request:setHTTPMethod'POST' + request:setValue_forHTTPHeaderField(postLength, 'Content-Length') + request:setValue_forHTTPHeaderField('application/x-www-form-urlencoded', 'Content-Type') + request:setHTTPBody(postData) + + NSLog('%@', request) + + local CD = class('ConnDelegate', 'NSObject ') + + function CD:connection_didReceiveData(conn, data) + self.webData:appendData(data) + NSLog'Connection received data' + end + + function CD:connection_didReceiveResponse(conn, response) + NSLog'Connection received response' + NSLog('%@', response:description()) + end + + function CD:connection_didFailWithError(conn, err) + NSLog('Connection error: %@', err:localizedDescription()) + app:terminate(nil) + end + + function CD:connectionDidFinishLoading(conn) + NSLog'Connection finished loading' + local html = NSString:alloc():initWithBytes_length_encoding(self.webData:mutableBytes(), + self.webData:length(), NSUTF8StringEncoding) + NSLog('OUTPUT:\n%@', html) + app:terminate(nil) + end + + local cd = ConnDelegate:new() + cd.webData = NSMutableData:new() + + local conn = NSURLConnection:alloc():initWithRequest_delegate_startImmediately(request, cd, false) + conn:start() + + app:run() +end + +function demo.http_gcd() + objc.load'AppKit' + local app = NSApplication:sharedApplication() + + local url = NSURL:URLWithString'http://posttestserver.com/post.php' + local req = NSURLRequest:requestWithURL(url) + + local queue = dispatch.main_queue --dispatch.get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + + objc.debug.logtopics.block = true + + local n = 0 + local blk = block(function() + n = n + 1 + print('called', n) + + local response = ffi.new'id[1]' + local err = ffi.new'id[1]' + local data = NSURLConnection:sendSynchronousRequest_returningResponse_error(req, response, err) + + print(tolua(NSString:alloc():initWithBytes_length_encoding( + data:mutableBytes(), data:length(), NSUTF8StringEncoding))) + + if n == 2 then + print'---- Done. Hit Ctrl+C twice ----' + end + end) + dispatch.async(queue, blk) --increase refcount + dispatch.async(queue, blk) --increase refcount + print'queued' + blk = nil; collectgarbage() --decrease refcount (stil queued) + print'released' + app:run() +end + +-- inspection ------------------------------------------------------------------------------------------------------------ + +local function load_many_frameworks() + objc.debug.loaddeps = true + for s in string.gmatch([[ +AGL +AVFoundation +AVKit +Accelerate +Accounts +AddressBook +AppKit +AppKitScripting +AppleScriptKit +AppleScriptObjC +AppleShareClientCore +ApplicationServices +AudioToolbox +AudioUnit +AudioVideoBridging +Automator +CFNetwork +CalendarStore +Carbon +Cocoa +Collaboration +CoreAudio +CoreAudioKit +CoreData +CoreFoundation +CoreGraphics +CoreLocation +CoreMIDI +CoreMedia +CoreMediaIO +CoreServices +CoreText +CoreVideo +CoreWLAN +DVComponentGlue +DVDPlayback +DirectoryService +DiscRecording +DiscRecordingUI +DiskArbitration +DrawSprocket +EventKit +ExceptionHandling +FWAUserLib +ForceFeedback +Foundation +GLKit +GLUT +GSS +GameController +GameKit +ICADevices +IMServicePlugIn +IOBluetooth +IOBluetoothUI +IOKit +IOSurface +ImageCaptureCore +ImageIO +InputMethodKit +InstallerPlugins +InstantMessage +JavaFrameEmbedding +JavaScriptCore +Kerberos +LDAP +LatentSemanticMapping +MapKit +MediaAccessibility +MediaLibrary +MediaToolbox +NetFS +OSAKit +OpenAL +OpenCL +OpenDirectory +OpenGL +PCSC +PreferencePanes +PubSub +QTKit +Quartz +QuartzCore +QuickLook +SceneKit +ScreenSaver +Scripting +ScriptingBridge +Security +SecurityFoundation +SecurityInterface +ServiceManagement +Social +SpriteKit +StoreKit +SyncServices +System +SystemConfiguration +TWAIN +Tcl +Tk +VideoDecodeAcceleration +VideoToolbox +WebKit +]], '([^\n\r]+)') do + pcall(objc.load, s) + end + objc.debug.loaddeps = false +end + +function eyetest.inspect_classes() + load_many_frameworks() + inspect.classes() +end + +function eyetest.inspect_protocols() + load_many_frameworks() + inspect.protocols() +end + +function eyetest.inspect_class_properties(cls) + load_many_frameworks() + inspect.class_properties(cls) +end + +function eyetest.inspect_protocol_properties(proto) + load_many_frameworks() + inspect.protocol_properties(proto) +end + +local function req(s) + return s and s ~= '' and s or nil +end + +function eyetest.inspect_class_methods(cls, inst) + load_many_frameworks() + inspect.class_methods(req(cls), inst == 'inst') +end + +function eyetest.inspect_protocol_methods(proto, inst, required) + load_many_frameworks() + inspect.protocol_methods(req(proto), inst == 'inst', required == 'required') +end + +function eyetest.inspect_class_ivars(cls) + load_many_frameworks() + inspect.class_ivars(req(cls)) +end + +function eyetest.inspect_class(cls) + load_many_frameworks() + inspect.class(cls) +end + +function eyetest.inspect_protocol(proto) + load_many_frameworks() + inspect.protocol(proto) +end + +function eyetest.inspect_find(patt) + load_many_frameworks() + inspect.find(patt) +end + +-------------- + +local function test_all(tests, ...) + for k,v in glue.sortedpairs(tests) do + if k ~= 'all' then + print(k) + hr() + tests[k](...) + end + end +end + +function test.all(...) + test_all(test) +end + +--cmdline interface + +local function run(...) + local testname = ... + if not testname then + print('Usage: '..luajit..' '..arg[0]..' ') + for k,t in glue.sortedpairs(tests) do + printf('%s:', k) + for k in glue.sortedpairs(t) do + print('', k) + end + end + else + local test = test[testname] or eyetest[testname] or demo[testname] + if not test then + printf('Invalid test "%s"', tostring(testname)) + os.exit(1) + end + test(select(2, ...)) + print'ok' + end +end + +run(...) diff --git a/player.lua b/player.lua new file mode 100644 index 0000000..912fca8 --- /dev/null +++ b/player.lua @@ -0,0 +1,25 @@ +player={} + +player.x=0 +player.y=0 +player.speed=0.2 +player.direction=0 +player.lastMoveTime=0 +player.weapon={} + +function player.update(dt) +local now=love.timer.getTime() +if (player.direction~=0) and (now-player.lastMoveTime>=player.speed) then +player.move(player.x+player.direction) +end -- check direction +end -- update + +function player.move(dx) +player.lastMoveTime=love.timer.getTime() +if dx<0 or dx>game.field.width then +-- boundary shit goes here +else +player.x=dx +love.audio.setPosition(player.x, player.y, 0) +end -- if boundary +end \ No newline at end of file diff --git a/soundgroups.lua b/soundgroups.lua new file mode 100644 index 0000000..83d405c --- /dev/null +++ b/soundgroups.lua @@ -0,0 +1,48 @@ +local fs=love.filesystem +group=object:extend() +function group:addDirectory(directory) files=fs.getDirectoryItems(directory) + for i,currentfile in ipairs(files) do + fullpath=directory .."/" ..currentfile + if not fs.isDirectory(fullpath) and not (currentfile:sub(1,1)==".") then + if fs.getInfo(fullpath, file) then + nameOnly=string.match(currentfile, "%P+") --%P matches all non punctuation characters + sound=love.audio.newSource(fullpath, "static") + if sound:typeOf("Source") then + --print("Loaded " .. nameOnly) + if sound:getChannelCount()==1 then sound:setRelative(true) end + self[nameOnly]=sound + self.sounds[nameOnly] =sound + end --if is a sound + end --if it's a file + end -- if it's not a dot + + end --for +end --addDirectory +function group:new(directory) + + self.sounds={} + self:addDirectory(directory) + +end + +function group:setEffect(effect) + --print("setting effects on group") + for i,currentsource in pairs(self) do + + if currentsource.typeOf~=nil and currentsource:typeOf("Source") then + if currentsource:setEffect(effect) then + --print("effect set on " ..i) + else + print("failed to set effect on " ..i) + end + end --if it's a source + end --for loop +end --setEffect function + + + + +t={ +a=2, +b=3 +} \ No newline at end of file diff --git a/tick.lua b/tick.lua new file mode 100644 index 0000000..6dd0c49 --- /dev/null +++ b/tick.lua @@ -0,0 +1,70 @@ +-- tick +-- https://github.com/bjornbytes/tick +-- MIT License + +local tick = { + framerate = nil, + rate = .03, + timescale = 1, + sleep = .001, + dt = 0, + accum = 0, + tick = 1, + frame = 1 +} + +local timer = love.timer +local graphics = love.graphics + +love.run = function() + if not timer then + error('love.timer is required for tick') + end + + if love.load then love.load(love.arg.parseGameArguments(arg), arg) end + timer.step() + local lastframe = 0 + + if love.update then love.update(0) end + + return function() + tick.dt = timer.step() * tick.timescale + tick.accum = tick.accum + tick.dt + while tick.accum >= tick.rate do + tick.accum = tick.accum - tick.rate + + if love.event then + love.event.pump() + for name, a, b, c, d, e, f in love.event.poll() do + if name == 'quit' then + if not love.quit or not love.quit() then + return a or 0 + end + end + + love.handlers[name](a, b, c, d, e, f) + end + end + + tick.tick = tick.tick + 1 + if love.update then love.update(tick.rate) end + end + + while tick.framerate and timer.getTime() - lastframe < 1 / tick.framerate do + timer.sleep(.0005) + end + + lastframe = timer.getTime() + if graphics and graphics.isActive() then + graphics.origin() + graphics.clear(graphics.getBackgroundColor()) + tick.frame = tick.frame + 1 + if love.draw then love.draw() end + graphics.present() + end + + timer.sleep(tick.sleep) + end +end + +return tick diff --git a/ticker.lua b/ticker.lua new file mode 100644 index 0000000..e6b4f50 --- /dev/null +++ b/ticker.lua @@ -0,0 +1,31 @@ +ticker=object:extend() + +function ticker:new(tickTime) +self.tickTime=tickTime +self.ticks=0 +self.lastTickTime=-1 +end + +function ticker:update(dt) +if self.lastTickTime==-1 then +return false +end +local now=love.timer.getTime() +local diff=now-self.lastTickTime +if diff>=self.tickTime then +self.lastTickTime=now+(self.tickTime-diff) +self.ticks=self.ticks+1 +self:tick() +return true +end +end + +function ticker:calibrate(time) +self.lastTickTime=time +self.ticks=1 -- can't be bothered to be elegant tbh +self.tick() +end + +function ticker:tick() +-- do stuff here +end \ No newline at end of file diff --git a/timeEvent.lua b/timeEvent.lua new file mode 100644 index 0000000..8323281 --- /dev/null +++ b/timeEvent.lua @@ -0,0 +1,16 @@ +timeEvent=object:extend() + +function timeEvent:new(i,func, recurring) +self.i=i -- either period or counter, depending on recurring +this.func=func +this.recurring=recurring or false +end + +function timeEvent:update() +if this.recurring + +else + +end +end + diff --git a/tolk.lua b/tolk.lua new file mode 100644 index 0000000..c9565df --- /dev/null +++ b/tolk.lua @@ -0,0 +1,71 @@ +local ffi = require "ffi" +local encoding = require "encoding" +ffi.cdef[[ +void Tolk_Load(); +void Tolk_Output(const char *s, bool interrupt); +void Tolk_Silence(); +void Tolk_Speak(const char *s, bool interrupt); +void Tolk_Braille(const char *s, bool interrupt); +void Tolk_TrySAPI(bool try); +void Tolk_PreferSAPI(bool prefer); +const wchar_t * Tolk_DetectScreenReader(); +bool Tolk_HasSpeech(); +bool Tolk_HasBraille(); +bool Tolk_IsSpeaking();]] +local tolk = ffi.load("Tolk") +tolk.Tolk_Load() +local function output(s, interrupt) + interrupt=interrupt or false + + tolk.Tolk_Output(encoding.to_utf16(s), interrupt) +end + +local function speak(s, interrupt) + interrupt=interrupt or false + + tolk.Tolk_Speak(encoding.to_utf16(s), interrupt) +end + +local function braille(s) + tolk.Tolk_Braille(encoding.to_utf16(s)) +end + +local function silence() +tolk.Tolk_Silence() +end + +local function trySAPI(try) + tolk.Tolk_TrySAPI(try) +end + +local function preferSAPI(prefer) + tolk.Tolk_PreferSAPI(prefer) +end +local function isSpeaking() + return tolk.Tolk_IsSpeaking() +end + +local function detectScreenReader() + --todo, need to convert the returned value to something Lua likes +end + +local function hasSpeech() + return tolk.Tolk_HasSpeech() +end + +local function hasBraille() + return tolk.Tolk_HasBraille() +end + + +return { + output=output, + speak=speak, + braille=braille, + silence=silence, + isSpeaking=isSpeaking, + trySAPI=trySAPI, + preferSAPI=preferSAPI, + detectScreenReader=detectScreenReader, + hasSpeech=hasSpeech, + hasBraille=hasBraille} diff --git a/track.lua b/track.lua new file mode 100644 index 0000000..3b52510 --- /dev/null +++ b/track.lua @@ -0,0 +1,7 @@ +track=object:extend() + +function track:new(info) +self.info=info +self.music=love.audio.newSource(info.path,"static") +self.timeStep=60/info.bpm/self.info.beatDivisions +end diff --git a/tracks/chacarron.lua b/tracks/chacarron.lua new file mode 100644 index 0000000..c75c3a3 --- /dev/null +++ b/tracks/chacarron.lua @@ -0,0 +1,9 @@ +local t={ +path="audio/tracks/clicky.flac", +bpm=60, +startTime=2, +beatDivisions=4, +volumeBase=1 +} + +return track(t) \ No newline at end of file diff --git a/tracks/gourmet.lua b/tracks/gourmet.lua new file mode 100644 index 0000000..003a39f --- /dev/null +++ b/tracks/gourmet.lua @@ -0,0 +1,9 @@ +local t={ +path="audio/tracks/gourmet.mp3", +bpm=160, +startTime=0.05, +beatDivisions=4, +volumeBase=0 +} + +return track(t) \ No newline at end of file diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..a4d448a --- /dev/null +++ b/utils.lua @@ -0,0 +1,12 @@ +utils={} + +function utils.accuracy(beatDivisions, time) +local ticksToPrevDiv=(game.ticker.ticks-1)%beatDivisions +local ticksToNextDiv=beatDivisions-ticksToPrevDiv +local prevDivTime=game.ticker.lastTickTime-ticksToPrevDiv*game.ticker.tickTime +local nextDivTime=game.ticker.lastTickTime+ticksToNextDiv*game.ticker.tickTime +local closest=math.min(math.abs(time-prevDivTime), math.abs(time-nextDivTime)) +local halfDivision=beatDivisions/2*game.ticker.tickTime +-- print(math.abs(time-nextDivTime),"to next beat, ",math.abs(time-prevDivTime)," to previous beat. Half division is ",halfDivision,". Closest is ",closest,". The current time is ",time,", and the next division is at ", nextDivTime,", last tick happened at ",game.ticker.lastTickTime," and is tick number ",game.ticker.ticks) +return (halfDivision-closest)/halfDivision -- Math is hard +end \ No newline at end of file