PythonからLuaを呼ぼう
Luaは組み込み目的で作られた言語ということですが、どれだけ組み込みやすいか、PythonからLuaの組み込みをやってみることに。
luaのライブラリを使う場合、ふつうcで操作するのですが、pythonでは以前spidermonkeyでやったようにctypesを使って呼び出してみます。
初期化と終了
import ctypes import ctypes.util # load liblua5.1.so liblua_path = ctypes.util.find_library("lua5.1") liblua = ctypes.CDLL(liblua_path) # create state state = liblua.luaL_newstate() # load standard libraries liblua.luaL_openlibs(state) # ...このなかでコードを呼び出す... # finish state liblua.lua_close(state)
luaのstateというのは、ランタイム環境一式を保持してるようです。luaはスタックマシン構造で、そのスタックも保持してます。
lua.hにもこの構造の定義は含まず、利用者が行うのはポインタ保持だけで、ライブラリ外では直接構造体をいじらない仕様になってます。これはspidermonkeyと比べるとかなり扱いやすい部分になってます。
luaL_openlibsを呼ぶことでprint()等の標準の機能が使えるようになります。標準ライブラリを読み込まず、相当するものをC関数として自作し、stateに登録していくことも可能なようです。
ここ以下のコードは、openlibsとcloseの間に埋め込む部分だけを記述しています。
スクリプト実行
Hello Worldです。
# load script: returns 0 if success script = 'print("Hello " .. "World")\n' liblua.luaL_loadbuffer(state, script, len(script), "<snip>") # call script: args: state, numargs, numresults, returns 0 if success liblua.lua_call(state, 0, 0)
スクリプトは、luaL_loadbufferで登録できます。その時点ではパーズされ関数のような形で保持されます。lua_callを使って実行させることができます。callの引数は
- state: 実行環境
- nargs: 引数の数
- nresults: 戻り値の数
エラー処理を行う場合(スタックトレースをとる場合など)は、あらかじめerror処理関数をstateにpushし、lua_callの代わりに、lua_pcallを使います。
luaL_loadbufferもlua_callも(lua_pcallも)成功時0を返し、失敗時は0以外の値になります。詳しくはドキュメントにあります。
戻り値を取り出す。
スクリプトで計算させ、その結果をpythonで表示させて見ます。
ロードするスクリプトでは、return文を書きます:
# call calc script = 'return 2 + 3\n' liblua.luaL_loadbuffer(state, script, len(script), "<snip>") # call with nresults liblua.lua_call(state, 0, 1) # get result print liblua.lua_tointeger(state, liblua.lua_gettop(state)) # result stack liblua.lua_settop(state, -liblua.lua_gettop(state) - 1)
lua_callでは戻り値がひとつあるので、第三引数を1にします。
呼び出し直後は戻り値はスタックトップ(にあたる部分)にあるので、lua_gettopでスタックトップのアドレスを取り出し、lua_tointegerでCの整数値として参照します。luaの変数値を参照する場合は、型に応じたlua_to〜関数を使います。
(tableの場合は、フィールドアクセスしてから参照する)
スタック上の変数はもう使用しないので、スタックデータをすべてpopします。
ドキュメントではlua_popは関数風になってますが、マクロで定義されています。そのため、同等のものになるようlua_settopを使っています。-1してるのは、luaが1から始まるインデックスを採用してるからでしょうか。
#define lua_pop(L,n) lua_settop(L, -(n)-1)
グローバル変数に値をセットする
定数をスクリプトに埋め込むのではなく、環境上のグローバル変数としてpythonからセットしてみます。
ロードするスクリプトでは、変数を使うコードにします:
# call with global vars (= global table's field) LUA_GLOBALSINDEX = -10002 # stack index of grobal table, see lua.h script = 'return a ^ b\n' liblua.luaL_loadbuffer(state, script, len(script), "<snip>") # set a liblua.lua_pushinteger(state, 2); liblua.lua_setfield(state, LUA_GLOBALSINDEX, "a") # set b liblua.lua_pushinteger(state, 10); liblua.lua_setfield(state, LUA_GLOBALSINDEX, "b") # call liblua.lua_call(state, 0, 1) print liblua.lua_tointeger(state, liblua.lua_gettop(state)) liblua.lua_settop(state, -1 - liblua.lua_gettop(state))
グローバル変数は、事前に用意されているグローバルテーブルのフィールドメンバーになっています。グローバルテーブルの(仮想の)アドレスは、lua.hで定義されています。
#define LUA_GLOBALSINDEX (-10002)
テーブルのフィールドへのセットは、先にセットする値をスタックにpushしlua_setfieldでテーブルとフィールド名を指定して呼び出すことで、可能です。
引数を持つ関数の呼び出し
グローバル変数を使うのではなく、関数としてスクリプトを書き、その関数を呼び出して戻り値を参照することにします。
ロードするスクリプトでは関数をreturnさせるようにします:
# call function script = ''' return function (x, y) return x ^ y end ''' liblua.luaL_loadbuffer(state, script, len(script), "<snip>") # load func on stack liblua.lua_call(state, 0, 1) # push args liblua.lua_pushinteger(state, 2) liblua.lua_pushinteger(state, 3) # call with nargs and nresults liblua.lua_call(state, 2, 1) print liblua.lua_tointeger(state, liblua.lua_gettop(state)) liblua.lua_settop(state, -1 - liblua.lua_gettop(state))
最初にスクリプトを実行し、スタックトップに関数をおきます。
次に引数に使う値を左から順にpushしていきます。intの場合はlua_pushintegerを使います。
引数をつみ終えたら、引数と戻り値の数をそれぞれ指定してlua_call呼び出します。
Pythonで書いた関数を呼び出す
# register function and call it # define lua_CFunction and lua_pushcfunction macro lua_CFunction = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p) liblua.lua_pushcfunction = ( lambda state, func: liblua.lua_pushcclosure(state, func, 0)) # external cfunction defined by python def luacfunc_sum(state): print("SUM") top = liblua.lua_gettop(state) # number of params # get params: index started from 1 args = [liblua.lua_tointeger(state, i + 1) for i in range(top)] ret = sum(args) # push results liblua.lua_pushinteger(state, ret) return 1 # returns number of result # wrap lua_CFunction and pushcfunction func = lua_CFunction(luacfunc_sum) # IMPORTANT: hold as local var liblua.lua_pushcfunction(state, func) # push args for i in range(10): liblua.lua_pushinteger(state, i + 1) # call liblua.lua_call(state, 10, 1) # get result print(liblua.lua_tointeger(state, liblua.lua_gettop(state)))
注意点は、lua_CFunctionのラッパーをローカル変数などにいれて、使うときまでgcされないようにすることです。これをしないと、Segmentation Faultがでます。
def pyfunc(state): print "Hello World" return 0 # BAD: lua_CFunction is removed after the statement liblua.lua_pushcfunction(state, lua_CFunction(pyfunc)) liblua.lua_call(state, 0, 0) # !Segmentation Fault!
感想
luaが組み込みやすいのは、構造体を隠蔽してる点が大きいのだろう。特にちょっとした使いかたをしたい場合、こういった類の違いは大きな割合になるだろうし。その一方で、データ構造が隠蔽されてるため、直接tableをポインタ等で保持したりできなかったりしますが。