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をポインタ等で保持したりできなかったりしますが。