luaはじめてみました
組込言語は専ら自作しておりましたが、いい加減ポピュラーなものを使おうということで、luaをさわってみました。
これはこれで、自作の自由度と比べると不自由な点が結構あって大変ですが、速度と機能で補って余る感じであります。
luaのいいところは何と言ってもVM(仮想マシン)の複数生成と、その生成が早い点でしょうか。
個人的には「オブジェクト単位で仮想マシンを使いたいので文法はそこそこあればいいよ」という使い方をしたいので、仮想マシン生成のコストがとにかく低いのが非常に魅力的です。
とはいっても、1VM生成に大体0.1msかかるようなので、一秒間に100個以上の生成は避けなければなりません。
単なる計算だけに使うなら良いのですが、もうちょっと機能を使いたいということで標準ライブラリ(luaL_openlibs)などを組み込むと、一気に読み込み負荷が跳ね上がり、20個ほどの生成が限界となります。
VM間でソースを共有、又は高速VMクローン、なんて機能が有ればいいのですが、ここは種類別にVMを作っておいてコルーチンなんかで数を凌ぐのが一般的なようです。
と、所(初)感はこんな所でしょうか。
とりあえず、最初にハマっちゃった点をちょこちょこっと書き留めておきます。
luaL_dofileとluaL_loadfile/luaL_loadbufferの挙動の違い
毎度おなじみ「luaL_dofile」ですが、オンメモリのコードをロード&実行したい場合には「luaL_loadbuffer」を使用することでしょう。
この時、それぞれで挙動が違う店を注意しなければなりません。
cからlua上のコードを呼び出す場合、
// c lua_open luaL_dofile //※ココ top = lua_gettop lua_getglobal([関数名]) lua_pcall [返値取得] lua_settop = top
は正しく動作しますが、「luaL_dofile」を「luaL_loadfile」にするだけでは上手く動きません。
「attempt to call global 'sample_function' 」というエラーが出ます。
というのも、loadfile/bufferはロードするだけで、関数や変数のセットアップはしませんので、一度グローバルなcallをしてやる必要があります。
// c lua_open luaL_loadfile lua_pcall(vm, 0, 0, 0); //追加 〜以下同じ〜
loadとsetupが分かれていると言うことは、ガンガンloadでコード(モジュールとして)を追加できるのかなーと思ったらそうでもないみたい。暗黙的にやってもいいんでないかなーと思ったりします。
luaからcの関数を呼ぶ場合のスタックについて
luaからcの関数を呼ぶ場合、c側のサンプルコードは
// c int lua_c_add(lua_state *vm) //cdecl { p1 = lua_tonumber(vm,1); //第1引数 p2 = lua_tonumber(vm,2); //第2引数 lua_pushnumber(vm,p1+p2); return 1; }
こんな感じのをよくみかえるとおもいますが、よくよく考えると、
lua_tonumberは値を参照するだけでpopしないし、おまけに返値はlua_pushnumberで更にスタックに値を積み上げているので、どんどんスタックが増えるふえるよね?
あと、lua_tonumberの引数の(1,2)は絶対位置だから、相対位置(-2,-1)にしないとおかしいよね?
という疑問が湧くかと思いますが、コレの解決は以下にあります。
http://sugarpot.sakura.ne.jp/yuno/html/lua51_manual_ja.html#lua_CFunction
「呼び出しごとにまっさらなスタック用意するし、使い終わったら返値だけ取って破棄するよ」ってことなので問題ないそうです。
コルーチンをいっぱい作りたい
仮想VMの生成も(コード読み込み負荷などで)限界があるので、高速化にはオブジェクトごとにコルーチンを使うって事も十分視野に入ると思います。
そんなわけで、オブジェクトの数だけ、c(アプリ)からコルーチンをいっぱい生成しようとします。
すると、14個ほどでエラー(スタックオーバーフロー)が出ることでしょう。なんということでしょう。
リファレンスによれば「コルーチンは生成するとスタックに積まれるよ」ということなので、スタックサイズを最大まで増やすことで2000個ぐらいには可能になりますが、あまり良い方法とは言い難いでしょう。
これを回避するには、1個生成するごとにどこかに(ガベコレ対象のところ)待避させて、スタックから消してやる(popしてやる)ことが一般的なようで、この方法であればかなりの数のコルーチン生成が可能になります。
//生成 for{ top = lua_gettop(vm); co[i] := lua_newthread(vm); //コルーチン生成。lua_stateを返す ci[i] := luaL_ref(vm,LUA_REGISTRYINDEX); //cから操作できるテーブルにぶち込む。登録index値を返す lua_settop(vm, top); //pop数考えるのめんどうだし〜 } 〜 //削除・解放 for{ luaL_unref(vm,LUA_REGISTRYINDEX,ci[i]); //指定されたIndex値にぶち込まれたものを削除 }
LUA_REGISTRYINDEXはcからのみアクセスできる場所とか何とかです。
ガベコレ対象ならグローバルでもよさそうですが、あちらはlua側で使用しますから、有効な資源は活用しましょうって事で。
文字列をコードとして実行する
指定した文字列をコードを、現在の状態やコードを維持したまま、命令として実行させるにはloadstringをつかいます。
こういうのは組込言語の柔軟性というか、華ですね。
assert(loadstring("文字列"))();
ですが、ただ指定された命令を実行するだけではなく、関数呼び出し(call)で実行されるという点に注意が必要です。
なので、以下の例はエラーです。
local x=3; -- x = x * 2; assert(loadstring("x = x * 2"))();
修正点は2つ。
1つめは、callされるためローカル変数「x」がloadstringの実行からは見えなくなるので、値の内容を文字列として渡して上げる必要があること。
2つめは、「x」に値を代入できないので、計算結果をreturnでかえしてあげる(loadstringの外に出す)必要があることです
local x=3; x = assert(loadstring( string.format("return %d * 2",x) ))();
値を渡す手段は文字列に直接変換値を埋め込むほかにも、スタックに乗せる方法とか、値を渡すため専用のグローバル変数を設けるなどの方法もあるかと思います。
値の受け渡しはまどろっこしいと思いますので、出来るだけ避ける方が良いような気がします。
文字列を実行は、かなり特殊な場合にしか使いませんが、変数テーブルの保存と復元なんかに重宝したりします。(保存で「{a=10,b=20}」といったテーブル宣言コードを文字列で出力しておき、復元はその文字列コードを直接実行するだけ)
cからテーブルの値を取得・設定したい
グローバル変数にあるテーブルの値をcから取るには、lua側に専用の命令を作っておくと便利です。
そんなの変数名が固定的になるじゃねーか!って思いますが、そこは先ほどのloadstringの出番です。
-- arg=1 , result=1 function getvalue(f) --fはフィールド指定文字列(例:"ObjectData[3].Position.x") return assert(loadstring( string.format("return %s",f) ))(); end
あとは、cから引数に「objectdata[2].position.x」とかなんとかを設定してこの関数を呼んで、スタック上の返値を拾ってあげれば完了です。
設定もほぼ同手段で
-- arg=2 , result=0 function setvalue(f,v) --fはフィールド指定文字列 vは代入する値 assert(loadstring( string.format("%s=%s",f,v) ))(); end
cから呼ぶ際、pcallに指定する引数の数(arg)、返値の数(result)あたりを間違わないように。(自動化方法も考えたけど、そもそもcから呼ぶことは少ないから手動でいいや)
あぁ、Flashとの連携で昔こんなことやったような記憶が。