-
Notifications
You must be signed in to change notification settings - Fork 1
導入のために再び「Hello World」として、C++ のホストコードから、Lua のコードを呼び出す簡単な例を示す。ここでは Lua そのものよりもプログラム全体のビルドの手順が分かりやすいように、コンパイルやリンクの手順を GNU make の Makefile で表した。
~~include_code("samples/helloagain/Makefile")
冒頭の LUASRCS
という変数は、Lua の処理系を起動するのに必要な C のソースコードが列挙されている。これらのファイルは Lua の配布パッケージに含まれている。ただしよく見るとわかるように、lua.c と luac.c は、ここでは必要ないので除外している。これらはそれぞれ lua コマンドと luac コマンド(バイトコードコンパイラ)として main
関数を含むので、組み込みの際には省かなければいけない。
それ以降は、一般的な C や C++ コードのコンパイルとリンク手順が示してある。
プログラム実行時に読み込む Lua のファイル(hello.lua と名付ける)は次の通りである。
~~include_code("samples/helloagain/hello.lua")
ここで greeting
という関数を定義していて、それを実行すると「Hello, World!」と画面に出力する。
そして、この Lua スクリプトを読み込んで関数を呼び出す C++ のコード(helloagain.cpp)は次の通りである。
~~include_code("samples/helloagain/helloagain.cpp")
詳しくは後の章で述べるが、コードをよく見ると greeting
という関数を取得して呼び出しているのが分かる。
次に Lua インタプリタ本体のソースコードを取得する。Lua の公式ウェブサイトのダウンロードページ(http://www.lua.org/download.html)から、5.2 以降の最新版(執筆時点では 5.2.2)の tar ボール(.tar.gz ファイル)を取得する。
これを展開すると、lua-5.2.2 というディレクトリができて、その中に src というディレクトリがある。この中に Lua のソースコードが入っている。ここでは、lua-5.2.2 というディレクトリをそのまま Makefile と同じディレクトリに移動する。
ここまでできたら make
コマンドを実行してみよう。それぞれのソースファイルがコンパイルされて、最後に次のように出力されていれば成功である。
./helloagain
Hello, Again!
途中でエラーが発生した場合は、Lua のソースファイルのディレクトリやそれぞれのファイルの名前等をもう一度確認する。
ここでは Mac で Xcode のプロジェクトに取り込む方法を示す。基本的には、単にソースコードをプロジェクトに足し、本体のプログラムのどこかで Lua のインタプリタを作成して初期化するだけである。
あらかじめ Lua のコードをコンパイルし、スタティックライブラリや共有ライブラリ、DLL 等の形で保存することもできるが、ここでは、組み込む先(ホスト)のプログラムに直接埋め込んで一緒にリンクしてしまう方法を紹介する。
この方法の良いところは、コンパイルオプションをホストプログラムと同じ条件に設定できるので、デバッガを使ったデバッグなどがしやすいことなどがあげられるが、それ以上に簡単で分かりやすいという利点がある。また、このような方法は、Visual Studio や Qt Creator 等の IDE でも全く同じように使うことができ、クロスコンパイル等の特殊な条件でも何ら特別な操作を必要としない。
Xcode のプロジェクトに Lua のソースコード一式をプロジェクトにドラッグアンドドロップする。あるいは、「Add Files to "ProjectName"...」というメニューから選択して、Lua のソースコード一式を選択しても良い。
ここでも、main
関数を含む lua.c、luac.c だけは必要ないので除外する。
最初に最低限の手間で Lua のインタプリタを使えるようにする手順を紹介し、次の項で補足の事柄を述べることにする。
まずホストのソースファイルにヘッダファイルをインクルードする。
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
Lua はソースファイルのサフィックスがすべて .c なので、Visual C++ や Xcode を使った場合は C のコードとしてコンパイルされる。従って、正しくリンクできるように extern "C"
を使って C のリンケージで関数が呼び出せるようにする必要がある。
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
じつはこれと全く同じ内容が lua.hpp というファイルにも書いてあるので、3 個のファイルを同時にインクルードするなら単純に次のようにしても良い。
#include "lua.hpp"
Lua は C++ としても正しくコンパイルできるように書かれているので、C++ コンパイラでコンパイルすれば上の extern "C"
は必要ない。
Objective-C の場合は、#include
の代わりに #import
を使っても良い。
#import "lua.h"
#import "lualib.h"
#import "lauxlib.h"
Lua のインタプリタを新しく作るには luaL_newstate()
を使う。
lua_State *luaL_newstate (void);
この関数が返す lua_State
という構造体に、Lua の実行に必要な状態がすべて保存されている。次のようにして構造体へのポインタを取得する。
lua_State *L = luaL_newstate();
この L
はプログラムの終了まで使い続けるので、大事にとっておく。なお、Lua のマニュアルなどのドキュメントでは慣習的にこの lua_State
構造体へのポインタは L
という名前となっていることが多いので、ここでもそれに従う。その方がコードをコピーしたりする時に便利である。
次に、luaL_openlibs()
を呼び出して Lua の標準ライブラリ関数をインタプリタに登録する。
void luaL_openlibs (lua_State *L);
ここで、L
は先ほど取得した L
を渡せば良い。
luaL_openlibs(L);
Xcode を使って新しいプロジェクトを作り、Lua のソースコードを追加した後、main.c を次のように書き換える。なお、ここではプロジェクトのテンプレートに Command Line Tool を選んだ。
#include <stdio.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
int main(int argc, const char * argv[])
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_getglobal(L, "print");
lua_pushstring(L, "Hello Lua!");
lua_call(L, 1, 0);
return 0;
}
うまくいけば、下の図のように Lua インタプリタが実行され、コンソールに「Hello Lua!」と表示されるはずである。
どうしてヘッダファイルが 3 個に分かれているかというと、内容は以下のように種類ごとに分類されているからである。
lua.h には Lua インタプリタを使用するのに最低限必要な関数や構造体が宣言されている。このファイルは Lua インタプリタを使う場合には必ずインクルードしなくてはいけない。
lualib.h は、Lua の標準ライブラリ(文字列処理やテーブル処理など)を、Lua のコード中で使用するのに必要な関数が宣言されている。最初に Lua のインタプリタを生成して、標準ライブラリ関数を登録する時に参照するので、それ以降のコードでは必要ない。
lauxlib.h は、luaL_
で始まる C の補助 API が宣言されている。
ただし、lualib.h と lauxlib.h には関数と数個の補助的な構造体が宣言されているだけなので、ヘッダファイルからインクルードする必要はない。コンパイルにかかる時間を節約するためなどで、必要最低限のインクルードで済ませたい場合は、lua.h だけをインクルードしておけば良い。
Lua のインタプリタは C のグローバルな環境をいっさい書き換えないので、必要ならばいくつでもインタプリタを生成してよい。
ただし、Lua のコルーチンを使うとあたかも複数のインタプリタが協調して動いているような処理ができるので、実際に複数のインタプリタを生成する必要はあまりない。例外は、マルチ CPU(コア)の環境で、複数のインタプリタを並列に実行させたい場合である。残念ながら Lua にはプリエンプティブなマルチスレッドの機能がないので、別々の CPU に処理を振り分けたい場合は、あらかじめ CPU の数だけスレッドを作っておいて、それらの上でインタプリタを生成する必要がある。
luaL_openlibs()
は、文字列処理やファイル IO なども含むすべての標準ライブラリをインタプリタに登録する。もしこれらの機能のうちの一部が不要であれば、luaL_requiref()
を使って機能ごとに個別にライブラリ関数を登録することもできる。
void luaL_requiref (lua_State *L, const char *modname,
lua_CFunction openf, int glb);
ファイルシステムのないプラットフォームなどの特殊な環境で動かしたい場合や、リンク後のバイナリのサイズが気になるようであれば試してみると良いかもしれないが、筆者自身は試したことがない。具体的な使用例は linit.c で定義されている luaL_openlibs()
の内容を見てみると良い。
C と Lua の間で関数を呼び出すとき、必要な情報は仮想スタック(virtual stack)を使って受け渡しする。ここでいうスタックは、データ構造とアルゴリズムの教科書によく出てくるスタックの構造を持ったデータである。ただし、厳密な意味でのスタックと違い、たとえばトップ以外の位置にある値を参照したりすることもできる。
なお、Lua のリファレンスマニュアルでもこのスタックこのことを単に the stack と呼ぶので、本書でも単にスタックといった場合はこのスタックを指すことにし、それ以外の一般的な意味でのスタックについて言及する必要があるときは、その都度但し書きをすることにする。
+-------------
4 | 14
+-------------
3 | (Lua のテーブル)
+-------------
2 | "how"
+-------------
1 | (Lua の関数)
+-------------
スタックのインデックスは、底(最初に積まれた要素)が 1 で、後から積まれた要素は順番に 2、3、4、…… となる。また、マイナスのインデックスを使うことで、トップから数えることも出来る。たとえば -1 はスタックのトップを表し、-3 はトップから数えて 3 番目である。
+-------------
-1 | 14
+-------------
-2 | (Lua のテーブル)
+-------------
-3 | "how"
+-------------
-4 | (Lua の関数)
+-------------
スタックには Lua の値であれば何でもしまっておくことができる。このスタックは Lua そのものの関数呼び出しスタックとは別物であるので、好きなように書き換えてよいし、後片付けもしなくてよい。
Lua の関数を C から呼ぶのに必要な操作は、関数と引数をスタックに積んで、この状態で、lua_call()
を呼び出すだけである。
void lua_call (lua_State *L, int nargs, int nresults);
ここに nargs
は引数の数、nresults
は期待する戻り値の数である。
しかし、理屈は簡単だが実際の操作はかなり煩雑になるので、以下では例をみながら説明する。
print "Hello Lua!"
に相当する関数呼び出しをするには、次のような手順を実行する。
-
print
というシンボルをグローバル変数のテーブルから引く -
"Hello Lua!"
という文字列をスタックに積む -
lua_call
を呼び出す
これを C で書くと次のようになる。
lua_getglobal(L, "print");
lua_pushstring(L, "Hello Lua!");
lua_call(L, 1, 0);
lua_getglobal
は、グローバルに定義された変数の値を取得する関数である。
void lua_getglobal (lua_State *L, const char *name);
取得した値は再びスタックにプッシュされる。print
は関数を格納したグローバルな変数なので、この関数がスタックにプッシュされることになる。
いま、スタックは次の図のような内容になっている。
+-------------
1 | print 関数
+-------------
次に lua_pushstring
で文字列をスタックにプッシュしている。
+-------------
2 | "Hello Lua!"
+-------------
1 | print 関数
+-------------
最後に lua_call
を呼び出す。lua_call
は呼び出したい関数に渡す引数の個数と、関数から受け取る戻り値の個数を指定する。
void lua_call (lua_State *L, int nargs, int nresults);
ここでは、nargs
に 1、nresults
に 0 を指定している。lua_call
を実行すると、引数と関数はスタックから取り除かれる。従って、最終的にはスタックの内容は空っぽになる。そして関数の副作用として画面に Hello Lua! と出力されるはずである。
Lua のリファレンスマニュアルの lua_call
の項から例を引いてみよう。Lua で次のように書く内容を考える。
a = f("how", t.x, 14)
よく見ると、第 2 引数は t
というテーブルの x
というキーを参照していることに注意すること。まず、目指すべきスタックの状態は、次のようなものである。
+-------------
4 | 14
+-------------
3 | t.x の値
+-------------
2 | "how"
+-------------
1 | f 関数
+-------------
このなかで厄介なのは t.x
の部分である。まずは、他の項目は脇へ置いて、この値をスタックへプッシュすることを考える。まずマニュアルから当該部分に例示されたコードを見てみよう。以下ではコメントも含めてコードを引用する。
lua_getglobal(L, "t"); /* table to be indexed */
lua_getfield(L, -1, "x"); /* push result of t.x (2nd arg) */
lua_remove(L, -2); /* remove 't' from the stack */
まず最初に t
というテーブルを取得するために、lua_getglobal()
を呼び出す。すると、t
という変数に結びつけられた値(ここではテーブル)がスタックにプッシュされる。
+-------------
1 (-1) | t テーブル
+-------------
次に、テーブルから x
という名前のキーに割り当てられた値をとりだす。これは lua_getfield()
という関数を使う。
void lua_getfield (lua_State *L, int index, const char *k);
この関数に与える第 2 引数は、スタックの中のテーブルのインデックスである。いま、テーブルはスタックのトップに積まれているので、インデックスは -1 となる。この結果、t.x
の値がスタックにプッシュされる。
+-------------
2 (-1) | t.x の値
+-------------
1 (-2) | t テーブル
+-------------
ここで、元のテーブルはもう要らないので、lua_remove()
で削除する。
void lua_remove (lua_State *L, int index);
いま、当該のテーブルはスタックのトップから数えて 2 番目にあるので、インデックスとして -2 を指定する。なお、lua_remove()
は、スタックの途中から値を削除した場合、隙間を埋めるようにそれより上に積まれている値が順次下にシフトする。
+-------------
1 (-1) | t.x の値
+-------------
これで t.x
の値をスタックにプッシュすることができた。
さて、ここまで、スタックのインデックスにはトップから数えたマイナスの値を使っていたが、これはどうしてだろうか? テーブルが入っているインデックスは 1 だと分かっていれば、lua_getfield()
の時も lua_remove()
の時も、両方インデックスに 1 を指定すれば良いのに。
しかし実際のスタック操作では、空っぽの状態のスタックを扱うことはあまりないので、スタックの底から数えるよりもトップから数えた方が間違いがないのだ。このことをみるために、今度は実際のスタックに近い内容を考える。
いま、スタックには f
という関数と "how"
という文字列をプッシュするとする。
lua_getglobal(L, "f"); /* function to be called */
lua_pushstring(L, "how"); /* 1st argument */
+-------------
2 (-1) | "how"
+-------------
1 (-2) | f 関数
+-------------
ここで、lua_getglobal(L, "t")
を呼び出すと、スタックは次のようになる。
+-------------
3 (-1) | t テーブル
+-------------
2 (-2) | "how"
+-------------
1 (-3) | f 関数
+-------------
テーブルのインデックスは 3 になった。でも、スタックのトップは -1 というインデックスで参照できるので、lua_getfield(L, -1, "x")
という関数呼び出しは、うまく動作する。
+-------------
4 (-1) | t.x の値
+-------------
3 (-2) | t テーブル
+-------------
2 (-3) | "how"
+-------------
1 (-4) | f 関数
+-------------
次の lua_remove(L, -2)
も同様である。
+-------------
3 (-1) | t.x の値
+-------------
2 (-2) | "how"
+-------------
1 (-3) | f 関数
+-------------
最後に整数の 14 をプッシュする。
lua_pushinteger(L, 14); /* 3rd argument */
lua_pushinteger()
は整数をプッシュする関数である。
void lua_pushinteger (lua_State *L, lua_Integer n);
ただし、Lua の内部では数値はすべて lua_Number
(double
)に変換される。これでようやく当初の目的を達成した。
+-------------
4 (-1) | 14
+-------------
3 (-2) | t.x の値
+-------------
2 (-3) | "how"
+-------------
1 (-4) | f 関数
+-------------
さてここで、f
という関数は引数を 3 個とり値を 1 個返すので、次のようにして lua_call()
を呼び出す。
lua_call(L, 3, 1); /* call 'f' with 3 arguments and 1 result */
lua_call()
は、関数とその引数をスタックから消去し、代わりに関数からの戻り値をスタックにプッシュする。
+-------------
1 (-1) | 返り値
+-------------
最後にもう一仕事残っている。
a = f("how", t.x, 14)
この Lua のコードと同等の動作をするために、最後に a
というグローバル変数に値を代入する。
lua_setglobal(L, "a"); /* set global 'a' */
lua_setglobal()
はスタックのトップから値を取り出し(ポップ)、その値を引数で与えた名前の変数に代入する。
void lua_setglobal (lua_State *L, const char *name);
そして最終的にスタックは空っぽになる。
Lua 関数からの戻り値は、再びスタックに積まれる。いくつの値を戻すかは、呼び出し側が決めることになっているので、スタックに積まれたデータの数が期待と異なることはない。また、関数が実際に返した値の数と、この期待する返り値の数が異なっていた場合は、多値の代入と同じように 調整(adjustment) が行われる。 [http://www.lua.org/manual/5.2/manual.html#3.3.3]
Lua の関数の実行中にエラーが発生したときに、そのエラーを C 側で捕捉するには lua_pcall()
という関数を使う。
int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
nargs
と nresults
の意味は lua_pcall
と同じで、最後の引数として msgh
という値をとるところが違う。この引数には、エラーメッセージを処理する関数のインデックスを指定することができるが、特に特別な処理をする必要がなくて単にエラーが捕捉できさえすれば良いという場合は 0 を指定する。
また lua_call
は値を返さないのに比べ、lua_pcall
は正常に関数が実行されれば 0 を、途中でエラーが発生すれば 0 以外の値を返す。エラーが発生して 0 以外の値を返すときには、同時にスタックのトップにエラーメッセージもプッシュするので、C の呼び出し側はこのメッセージを読み取れば良い。
最も手軽な例は、次のようになる。
if (lua_pcall(L, 1, 0, 0)) {
fprintf(stderr, "Lua Error: %s", lua_tostring(L, -1));
}
lua_pcall()
が 0 以外の値を返す時はエラーメッセージの文字列がスタックのトップにあるので、これを lua_tostring()
を使って取得し、標準エラー出力に出力している。ここから C++ の例外を投げたりしても良い。
また、エラーについてもっと詳細な情報を得たければ、第 4 引数にエラーハンドラを指定することもできる。ただし、Lua のコード中でもエラーハンドリングは可能で、その方が簡単である。第 4 引数の msgh
の正確な意味と、lua_pcall
の返り値の意味について詳しくはマニュアルを参照すること。
http://www.lua.org/manual/5.2/manual.html#lua_pcall