なんだか暑さと眠さで気だるくてしょうがないので気分転換になんか書いてみる。
突然だけど Lua を知ったのは2年くらい前。某NodeのPaaS開発のプロジェクトに参加した時に、カーネル内でパケットを解析して動的にL7ルーティング出来ないもんかと色々調べてた時に見つけたのが始まり。その時は実験用のサーバにとりあえずインストールして実行したらカーネルパニックで落ちました(テヘペロ)ってな感じで終わったんだけど。
その後、個人的に触ってみたらびっくりするくらい文法がシンプルな上に高速で省メモリで久しぶりに好きになった言語なのだけれど、Web系での情報があまりないのが悲しいトコ。
基本的に 組み込み言語 として紹介されてるので、その時点でスルーされてるのかもしれないけど、Nginx 上で Lua を動かすモジュールとして開発された OpenResty (CloudFlareの中の人が開発)やその上で使うフレームワークとして Lapis とか Gin なんてのがあったり、NodeJS っぽく使える Luvit (Rackspaceの中の人が開発)なんてのがあったりもするので、個人的にはそろそろ流行ってくれるといいなぁって思うんだけど。
でも、やっぱりモジュールが圧倒的に少ないし、例えばちょっと話題になったプロダクトを使ってみようと思ってもこんな風 Calling Aerospike from Lua にライブラリが用意されてなくて解決策は「 C の API あるから Lua から呼んだら良いよb」なんて事が普通にある。
そんなわけで、Lua 使いが増えると嬉しい+モジュールが増えるのも嬉しいので Lua バインディングの作り方を書いてみる。
LuaRocks をインストール。
当たり前だけど、普通に Lua のバインディングを書いた場合は Makefile も書かないといけないんだけど LuaRocks という Lua のパッケージマネージャーを使う事で特別なコンパイル処理を走らせる必要がない限りは Makefile を書かなくて良いのでオススメです。もちろん Makefie が必要な時にもビルドコマンド走らせることも出来し、Lua だけで書いたモジュールの管理も出来る。
インストールは LuaRocks Wiki: Download に各プラットフォーム毎の方法が載ってるので参照してください。OSX で Homebrew 使ってる人は brew install luarocks
でインストール出来る。
最低限の下準備
まずディレクトリ構成は以下にする。
.
├── mymodule-scm-1.rockspec
├── src
│ └── mymodule.c
└── test
└── test.lua
LuaRocks は rockspec
という設定(仕様?)ファイルを元にコンパイルとインストールをするので以下のように最低限の設定を mymodule-scm-1.rockspec
に書く。
package = "mymodule"
version = "scm-1"
source = {
url = "git://example.com/your/mymodule.git"
}
description = {
summary = "my first lua binding module",
homepage = "https://example.com/your/mymodule",
license = "MIT/X11",
maintainer = "Your Name"
}
dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
mymodule = {
sources = {
"src/mymodule.c"
}
}
}
}
package
項目がモジュール名で version
項目がそのまんまバージョン情報なんだけど、rockspec
のファイル名は必ず package-version.rockspec
にしないといけない。なので、この rockspec
の内容ならファイル名は必ず mymodule-scm-1.rockspec
ということになる。
基本的にモジュールのビルドに必要な情報は build
テーブルで、build.type
を builtin
にすることで LuaRocks がコンパイルしてくれる。それから build.modules
テーブルにはビルドするモジュールの設定情報を書くお約束になっている。
C のソースをコンパイルするので mymodule.sources
に mymodule
のソースファイルのパスを列挙する。
スタックを使って C と Lua でデータをやりとりする
C と Lua 間でのデータのやりとりには スタック
という概念を使うんだけど、先にコード見たほうが分かりやすいかもしれないので、テキトーに受け取った文字列の頭に Hello
という文字を付けて返す関数を書いてみる。
# include <lua.h>
# include <lauxlib.h>
static int hello_lua( lua_State *L )
{
const char *str = luaL_checkstring( L, 1 );
lua_pushfstring( L, "Hello %s", str );
return 1;
}
LUALIB_API int luaopen_mymodule( lua_State *L )
{
lua_pushcfunction( L, hello_lua );
return 1;
}
スクリプト側はモジュールを読み込んで、引数付きで関数を呼び出すコードにする。
local mymodule = require('mymodule');
-- C の関数 hello_lua を呼ぶ。
print( mymodule('World') );
関数名(シンボル名)のルール
LUALIB_API int luaopen_mymodule( lua_State *L )
まずこの箇所は Lua スクリプトから require('mymodule')
としてモジュールを読み込む時に呼ばれる。これはお約束として必ず luaopen_
という名前で始まりモジュール名で終わらないといけない。
複数のサブモジュールで構成されたモジュールの場合は、luaopen_mymodule_submodule
という風に _
(アンダースコア)で名前を区切ることでスクリプト側で require('mymodule.submodule')
として読み込む時に .
(ドット)区切り文字が _
に解釈されてサブモジュールが検索される。
戻り値をスタックに積む
次にスタックに C の関数を積んで return 1;
で積んだ数を返す。
lua_pushcfunction( L, hello_lua );
return 1;
関数を1つだけスタックに積んで 1
を返したので スクリプト側に 1
つ値が戻ってくるので;
local mymodule = require('mymodule');
とすることで mymodule
に hello_lua
という C の関数が代入されるってわけだね。
当たり前だけど、Lua は 多値
をサポートしてるので複数の値をスタックに積めば複数の値を返すこともできる。
引数をスタックから受け取る
こうして返ってきた関数をスクリプト側で mymodule('World')
として呼ぶことで hello_lua
関数が実行される。
const char *str = luaL_checkstring( L, 1 );
渡された引数 'World'
は以下のようにで受け取る。この 1
はスタックに積まれた1つ目の値を 変換可能な文字列
かを確認して受け取るという意味になる。ロジック上、絶対に 文字列
を受け取りたい場合は以下のように型を確認して確実に受け取るようにすればオッケーです。
const char *str = NULL;
// 文字列じゃないなら意図的に引数エラーを発生させる
if( lua_type( L, 1 ) != LUA_TSTRING ){
lua_checktype( L, 1, LUA_TSTRING );
// ここには決して到達しないけど、clang の静的アナライザに怒られるので return しとく。
return 1;
}
// 文字列を取り出す
str = lua_tostring( L, 1 );
引数が何個積まれているのかは int lua_gettop(L)
で一番上のインデックス番号を知る事で何個積まれているのか把握できるけど、これは 今の状態のスタック
に積まれた数を返すものなのでスタックに値を積むたびに変化する。そのため引数の数がロジック上で意味をなす場合は、関数の最初の行で const int argc = lua_gettop(L);
なんて保存しておくのがベター。
lua_pushfstring( L, "Hello %s", str );
return 1;
それから、 lua_pushfstring
で printf
関数のようにフォーマットした文字列をスタックに1つ積んで return 1;
で返す。このフォーマットされた文字列のメモリは Lua で管理されて GC
の対象になるので自分で free
を呼ぶ必要はない。
print( mymodule('World') );
最後にスクリプト側で print( mymodule('World') );
で返ってきたフォーマットされた文字列 Hello World
を出力して終了する。
こんな感じで C と Lua 間でのデータは基本的にスタックでやりとりするだけなのでとっても簡単。
ちなみに、スタックのインデックスは負の値でもよい。
例えば、スタックに3つ値が積まれている場合は 1
2
3
とすることで下から1番目〜3番目を表していて、これを -1
-2
-3
とすることで上から1番目〜3番目と表してる。
こんな感じってことだね。
- 1 == -3
- 2 == -2
- 3 == -1
TIPS: テーブルデータの操作
Lua にはいくつかのデータ型があるけど、言語自体が用意しててデータ構造として使えるのは テーブル型
(以下、テーブル)だけで、このテーブル(配列とハッシュテーブルがいっしょくたになった)データ構造の理解は、普通にデータ構造としても、OOPっぽい表現でモジュールを使わせる場合にも必要になるのでその使い方をちょっとだけ解説。
ハッシュテーブルとしての操作
ハッシュテーブルの操作はキーを元にデータの出し入れをすることなのだけれど、例えば APRのハッシュテーブル のような API は残念ながら用意されていないので、これもスタック操作で取り出す事になる。
例えばスタックの1番目にテーブルが入っていて key
フィールドのデータを取り出したいときは;
// テーブルの中身:{ key = 'value' }
const char *val = NULL;
// キーを積む
lua_pushstring( L, "key" );
// テーブルのあるスタックインデックスは1番目
lua_rawget( L, 1 );
// 中身が文字列である事を確認
if( lua_isstring( L, -1 ) ){
// 文字列としてとりだす
val = lua_tostring( L, -1 );
}
printf( "val: %s\n", val );
てな感じでとりだす。ネストしたテーブルも同じように繰り返すだけでオッケーです。
配列としての操作
配列としての操作はハッシュテーブルのキーが整数になっただけなので、lua_rawgeti
API を使うだけです。
// テーブルの中身:{ 'value' }
const char *val = NULL;
// テーブルのあるスタックインデックスは1番目、配列の1番目の要素を取り出す
lua_rawgeti( L, 1, 1 );
// 中身が文字列である事を確認
if( lua_isstring( L, -1 ) ){
// 文字列としてとりだす
val = lua_tostring( L, -1 );
}
printf( "val: %s\n", val );
さらにもう一つ lua_gettable
という API があって、これは __index
メタメソッドを呼ぶことがあるので、メタメソッドが呼ばれる必要がある場合はこれを使うと良い。
テーブルの中身を全部取り出したい
キーと値を全部取り出したい時は lua_next
という API を使う。
// nil を積む
lua_pushnil( L );
// テーブルのあるスタックインデックスは1番目
// lua_next でスタックインデックスの -1 に値、-2 にキーが積まれる
while( lua_next( L, -2 ) )
{
// キーを表示
switch( lua_type( L, -2 ) ){
case LUA_TNUMBER:
printf("key: %td, ", lua_tointeger( L, -2 ) );
break;
case LUA_TSTRING:
printf("key: %s, ", lua_tostring( L, -2 ) );
break;
}
// 値を表示
switch( lua_type( L, -1 ) ){
case LUA_TNUMBER:
printf("val: %f\n", lua_tonumber( L, -1 ) );
break;
case LUA_TSTRING:
printf("val: %s\n", lua_tostring( L, -1 ) );
break;
case LUA_TBOOLEAN:
printf("val: %s\n", lua_toboolean( L, -1 ) );
break;
// それ以外は型名を表示
default:
printf("val: %s\n", lua_typename( L, lua_type( L, -1 ) ) );
}
// スタックトップから1つ取り除く=値を取り除く
lua_pop( L, 1 );
}
複雑なテーブル構造を C で受け取ってパースしたいという事はあまりないだろうけど、例えば Aerospike という KVS には Complex Data Types として List と Map という JSON と(たぶん)等価なデータを入れることが出来るんだけど、そうゆう時に Lua のテーブルをこういったデータ構造に変換する時に使ったりします。こんな風に。
モジュールに OOP っぽくメソッドを生やす
そもそも Lua がオブジェクト指向な言語ではないので、メタテーブル
を使うんだけど・・そろそろ飽きてきたのでかけ足で(ぉ
メタテーブルの定義
基本的にはモジュールが呼ばれた時に定義しておくのが定石なので luaopen_mymodule
関数内で定義します。
# define MODULE_MT "mymodule"
LUALIB_API int luaopen_mymodule( lua_State *L )
{
struct luaL_Reg method_mt[] = {
{ "__gc", gc_lua },
{ "__tostring", tostring_lua },
{ NULL, NULL }
};
struct luaL_Reg method[] = {
{ "say", say_lua },
{ NULL, NULL }
};
struct luaL_Reg *ptr = NULL;
// MODULE_MT という名前でメタテーブルを作成してスタックに積む
luaL_newmetatable( L, MODULE_MT );
// メタテーブルにキーをメタメソッッド名、値を C 関数として追加する
ptr = method_mt;
do {
lua_pushstring( L, ptr->name );
lua_pushcfunction( L, ptr->func );
lua_rawset( L, -3 );
ptr++;
} while( ptr->name );
// __index メタメソッド名を積む
lua_pushstring( L, "__index" );
// 値としてテーブルを積む
lua_newtable( L );
// テーブルにメソッドを追加する
ptr = method;
do {
lua_pushstring( L, ptr->name );
lua_pushcfunction( L, ptr->func );
lua_rawset( L, -3 );
ptr++;
} while( ptr->name );
// メタテーブルにキーを __index 、値をテーブルとして追加する
lua_rawset( L, -3 );
// スタックからメタテーブルを取り除く
lua_pop( L, 1 );
// テーブルを作って alloc_lua を new 関数として追加
lua_newtable( L );
lua_pushstring( L, "new" );
lua_pushcfunction( L, new_lua );
lua_rawset( L, -3 );
// テーブルを返す
return 1;
}
これでスクリプト側で require('mymodule')
とした時に new
というフィールドに new_lua
という C の関数を持ったテーブルが返される。
local mymodule = require('mymodule');
--[[ 中身はこゆの
{
new = "function: 0x7f9be1c0ddf0"
}
--]]
ちなみに struct luaL_Reg
という構造体は関数をまとめて登録する時に使える便利な構造体って程度の認識でオッケーです。
メソッドを定義する
上のコードにない関数 new_lua
と say_lua
を定義していく。
typedef struct {
char *mydata;
} mymodule_t;
static int new_lua( lua_State *L )
{
const char *str = luaL_checkstring( L, 1 );
// Lua のメモリマネージャーからメモリを確保して、スタックに積む
mymodule_t *m = lua_newuserdata( L, sizeof( mymodule_t ) );
// メモリが確保できたら、asprintfで独自にメモリを確保する
if( m && asprintf( &m->mydata, "Hello %s", str ) != -1 ){
// 作成済みメタテーブルを積む
luaL_getmetatable( L, MODULE_MT );
// m にメタテーブルを設定する
lua_setmetatable( L, -2 );
return 1;
}
// 失敗したので nil とエラーメッセージを返す
lua_pushnil( L );
lua_pushstring( L, strerror( errno ) );
return 2;
}
GC の動作もわかった方が良いはずなので mymodule_t
という構造体を定義して、それを Lua のメモリマネージャーからメモリを確保する。これにメタテーブルを設定することで、GC 時 __gc
メタメソッドが呼ばれる=ファイナライザ的に使えるようになる。
static int say_lua( lua_State *L )
{
// 第1引数が MODULE_MT のデータかどうかチェックする
mymodule_t *m = luaL_checkudata( L, 1, MODULE_MT );
// 初期化時に作成した文字列を積む
lua_pushstring( L, m->mydata );
return 1;
}
say_lua
は単純に mymodule_t
の mydata
を返すだけの処理。スクリプト側で使う時には以下のようにメソッドを :
(コロン)付きで呼ぶ。
local mymodule = require('mymodule');
local obj = mymodule.new('World!');
print( obj:say() ); -- Hello World! と出力される
メタメソッドを定義する
あまり関係ないけど、Lua でテーブルを print( {} )
として表示させてみると table: 0x7fb373d08d10
なんて風に表示される。これってなんのデータなのか一目で分かって気に入ってるので、同じように表示させるために __tostring
メタメソッドを定義する。
// スクリプト側で tostring を呼び出された時の処理
static int tostring_lua( lua_State *L )
{
lua_pushfstring( L, MODULE_MT ": %p", lua_touserdata( L, 1 ) );
return 1;
}
これで print( obj )
として表示させてみると mymodule: 0x7f9aead06058
なんて風に表示される。
最後に GC 時の処理。
// GC が呼ばれた時の処理
static int gc_lua( lua_State *L )
{
mymodule_t *m = (mymodule_t*)lua_touserdata( L, 1 );
printf("[collect gabage]\n");
// 確保してあったメモリを解放する
free( m->mydata );
return 0;
}
まぁ、コメントに書いてある通りです。そんなわけでまとめると以下のようなソースになる。
# include <stdlib.h>
# include <string.h>
# include <errno.h>
# include <lua.h>
# include <lauxlib.h>
# define MODULE_MT "mymodule"
typedef struct {
char *mydata;
} mymodule_t;
static int say_lua( lua_State *L )
{
// 第1引数が MODULE_MT のデータかどうかチェックする
mymodule_t *m = luaL_checkudata( L, 1, MODULE_MT );
// 初期化時に作成した文字列を積む
lua_pushstring( L, m->mydata );
return 1;
}
static int new_lua( lua_State *L )
{
const char *str = luaL_checkstring( L, 1 );
// Lua のメモリマネージャーからメモリを確保する
mymodule_t *m = lua_newuserdata( L, sizeof( mymodule_t ) );
// メモリが確保できたら、asprintfで独自にメモリを確保する
if( m && asprintf( &m->mydata, "Hello %s", str ) != -1 ){
// 作成済みメタテーブルを積む
luaL_getmetatable( L, MODULE_MT );
// m にメタテーブルを設定する
lua_setmetatable( L, -2 );
return 1;
}
// 失敗したので nil とエラーメッセージを返す
lua_pushnil( L );
lua_pushstring( L, strerror( errno ) );
return 2;
}
// スクリプト側で tostring を呼び出された時の処理
static int tostring_lua( lua_State *L )
{
lua_pushfstring( L, MODULE_MT ": %p", lua_touserdata( L, 1 ) );
return 1;
}
// GC が呼ばれた時の処理
static int gc_lua( lua_State *L )
{
mymodule_t *m = (mymodule_t*)lua_touserdata( L, 1 );
printf("[collect gabage]\n");
// 確保してあったメモリを解放する
free( m->mydata );
return 0;
}
LUALIB_API int luaopen_mymodule( lua_State *L )
{
struct luaL_Reg method_mt[] = {
{ "__gc", gc_lua },
{ "__tostring", tostring_lua },
{ NULL, NULL }
};
struct luaL_Reg method[] = {
{ "say", say_lua },
{ NULL, NULL }
};
struct luaL_Reg *ptr = NULL;
// MODULE_MT という名前でメタテーブルを作成してスタックに積む
luaL_newmetatable( L, MODULE_MT );
// メタテーブルにキーをメタメソッッド名、値を C 関数として追加する
ptr = method_mt;
do {
lua_pushstring( L, ptr->name );
lua_pushcfunction( L, ptr->func );
lua_rawset( L, -3 );
ptr++;
} while( ptr->name );
// __index メタメソッド名を積む
lua_pushstring( L, "__index" );
// 値としてテーブルを積む
lua_newtable( L );
// テーブルにメソッドを追加する
ptr = method;
do {
lua_pushstring( L, ptr->name );
lua_pushcfunction( L, ptr->func );
lua_rawset( L, -3 );
ptr++;
} while( ptr->name );
// メタテーブルにキーを __index 、値をテーブルとして追加する
lua_rawset( L, -3 );
// スタックからメタテーブルを取り除く
lua_pop( L, 1 );
// テーブルを作って alloc_lua を new 関数として追加
lua_newtable( L );
lua_pushstring( L, "new" );
lua_pushcfunction( L, new_lua );
lua_rawset( L, -3 );
// テーブルを返す
return 1;
}
まとめ
スタックで Lua とデータやりとりするトコ以外は普通の C プログラミングなので、ライブラリのバインディング書くとか、もちろん C/C++ で書かれたアプリに組み込むのもすごく簡単にできるのでオススメですよって話でした。
でも、ここまで書いておいてなんだけど、Lua を使う人は増えそうに無い気が・・・。
追記
重要なこと忘れてた。モジュールのコンパイルとインストールは rockspec
ファイルのあるディレクトリで luarocks make
ですb