設定ファイルとして使うためのLua組み込みあれこれ

Cで書いたアプリケーションにLuaを組み込む機会があったので、メモを残しておく。具体的な組み込み方(LuaのC APIの呼び方など)はネットに出回っているし、本も何冊か出版されているので、それ以外について書き記そうと思う。

タイトル名の通り、設定ファイルとして使うために組み込んだので、その方面の内容が中心だ。ちなみに組み込み先はUnixアプリで、よくあるxxx.conf的な設定ファイルの代替に使用した。なので、設定ファイル機能は読み込み専用だ。変更した設定を上書き保存する機能はない。

最近ではTOMLも悪くない選択かもしれないが、仕様・実装ともにまだ枯れていないようだからなあ。

設定ファイルにLuaを使うメリット

たかだか設定ファイルごときに言語処理系を組み込むだなんて過剰な気がしないでもないが(だって別に、Luaで書いた関数をアプリから実行するわけでもないのだし……)、しかしそれなりに利点はある。

出来合いのパーサである

WindowsMac OS X(+ iOS)にはプラットフォーム固有の設定ファイル機能*1があるが、LinuxC/C++でアプリを書くときにはその手の標準機能が見当たらない(見落としているだけかもしれないが)。

使用しているプラットフォーム/フレームワークに設定ファイル用のAPIが用意されていないなら、自分で設定ファイルのフォーマットを考えて実装するか、出来合いのパーサを組み込んで使用するか、選択を迫られることになる。

ここで、「出来合いのパーサ」の候補として、XMLなどと並んでLuaが出てきても変ではないだろう。20年を超える歴史があり、ユーザは多くて情報も出回っているし、実績もある。それに、他の言語を組み込むよりは大掛かりにならずに済むはずだ。

ビルド済み共有ライブラリがある

Linuxでは、各ディストリビューションにて共有ライブラリ(liblua.so)が提供されていることが多い。FreeBSDもpkg(かつてのportとpackage)に用意されていたはず。プラットフォームのパッケージ管理システムのコマンド一発で導入できる可能性は高い。

あと、LuaBinariesのように有志が提供しているビルド済みライブラリもある。*2

という訳で、自分でソースからビルドせずとも使える環境が多い。

ライセンスが緩め

MIT License。Luaを組み込んだアプリを公開するときは「著作権表示」と「MIT Licenseの全文(もしくはライセンス文へのリンク)」をどこかに記載する必要があるが、しかしそれだけで済むとも言える。

文法の仕様が一般に公開されている

さすがに標準規格にはなっていないが、公式のドキュメント日本語訳も存在するし、入門書も出回っている。

独自フォーマットの設定ファイルの場合、文法の仕様が明確になっていないことがあるが、Luaではその心配はないだろう。

シンプルかつそこそこ高機能である

Luaは3つの意味でシンプルだ。

まず実装がシンプルだ。gccとmakeに慣れている人なら、自力でのビルドは簡単だ。依存関係も、単に静的ライブラリ(liblua.a)を使用するだけなら、実質的にlibdlだけで済む*3。各ディストリビューションの共有ライブラリを使用してもよいかもしれない。

次に、アプリ組み込み用のC APIもシンプルだ。単に設定ファイルとして使うだけなら、CとLuaとの間のやり取りは容易に定型化できる。

最後に、Lua自体の文法がシンプルだ。例えるなら「XMLに対するJSON」のようにシンプルで、設定ファイルに意外と向いている。

このようにシンプルなLuaだが、設定ファイルにてテーブルを使ってパラメータを構造化することや関数を記述・利用することが可能であるし、プリミティブではあるが標準ライブラリが付いているし、「設定ファイル用の定数(的なもの)」を比較的楽に用意できたりと、そこそこ高機能でもある。

(条件付だが)クロスプラットフォームである

Windows(というかCP932)のことを考えなければ、複数のプラットフォームで簡単に使用できる。Windowsも含める場合は、少し工夫が必要になる。

設定ファイルにLuaを使うときに不向きなこと

設定を書き換えて保存すること

Luaはあくまでも組み込み言語なので、設定ファイル用のライブラリのような「値を書き換えて保存」という機能を持たない。必要なら、自分で実装しなくてはならない(既に誰かコードを書いて公開しているかもしれないが)。

Unix環境のアプリの設定ファイル(*.confみたいな名前のファイル)のように読み込み専用なら問題ないのだが……。

Windowsでの採用

WindowsアプリへのLua組み込みは、ダメではないが、ちょっと難がある。

Shift_JIS(CP932)のダメ文字問題があるので、設定ファイル中にASCIIではない文字を記述したい場合、次のいずれかの対応が必要となる。

  • Shift_JISスクリプトを書いた上で、有志が公開しているShift_JIS対応版のLuaを組み込む。
  • Shift_JISスクリプトを書いた上で、シングルクォートないしダブルクォートによる文字列リテラルでは、文字化けする文字(2byte目が0x5C `\' である文字)の直後に\を書く。
    • 「[ [」などで始まる文字列リテラルないしブロックコメントを書く場合は、2byte目が0x5D `]' である文字の直後に気をつける。*4
  • UTF-8あたりでスクリプトを書いた上で、C/C++側にてlua_tostring()やlua_tolstring()で取得した文字列の文字コードを変換して使用する。

もうひとつ。LuaのC APIでは文字列としてchar型を使用しているので、マルチバイト文字を使う設定のプロジェクトにするか、LuaのC APIが絡む部分は明示的にマルチバイト用のAPIを呼ぶように実装する必要がある。

自分でビルドする場合のポイント

今回は公式プロジェクトのLua 5.2.3をビルドした。アーカイブを解凍して、lua-5.2.3/src直下のMakefileでビルドする。このMakefileは、liblua.a・lua・luacを一度に全てビルドする。

Linux用のバイナリは「make linux」で、Mac OS X用は「make macosx」でビルドできる。ちなみに、これでビルドできるのは静的ライブラリ(liblua.a)だ。共有ライブラリ(liblua.soなど)が欲しい場合はMakefileを書き換える必要があるようだが、今回は試していない。

Mac OS Xでのビルドは一手間いるかもしれない。Xcodeはインストール済みだがCommand Line Toolsは未インストールである場合、マクロCC・AR・RANLIBに設定するコマンドをxcrun経由で呼ぶ必要がある。SDKROOTも設定しておいた方がよさそうだ。

--- Makefile	2013-11-11 20:45:49.764621900 +0900
+++ Makefile.mod	2014-11-09 22:15:12.066445600 +0900
@@ -105,8 +105,11 @@
 linux:
 	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -ldl -lreadline"
 
+macosx: sdkroot = $(shell xcodebuild -version -sdk macosx10.9 | sed -n '/^Path: /s///p')
 macosx:
-	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC=cc
+	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC="xcrun cc" \
+	AR="xcrun ar rcu" RANLIB="xcrun ranlib" \
+	SDKROOT="$(sdkroot)" CPPFLAGS="-isysroot \"$(sdkroot)\""

mingw:
	$(MAKE) "LUA_A=lua52.dll" "LUA_T=lua.exe" \

また、Universal Binaryのアプリに組み込みたいのなら、TARGET_ARCHを設定する必要もあるだろう(次の例では、liblua.aはUniversal Binaryになるがluaとluacはならないので注意)。

--- Makefile	2013-11-11 20:45:49.764621900 +0900
+++ Makefile.mod	2014-11-09 22:15:12.066445600 +0900
@@ -105,8 +105,9 @@
 linux:
 	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -ldl -lreadline"
 
 macosx:
-	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC=cc
+	$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC=cc \
+	TARGET_ARCH="-arch i386 -arch x86_64"

mingw:
	$(MAKE) "LUA_A=lua52.dll" "LUA_T=lua.exe" \

ちなみに、Makefileを見ているとlibreadlineが必要な気がしてくるが、実際のところliblua.aをビルドするだけなら不要だ。libreadlineはコマンドluaでしか使用していない。libreadlineが用意されていない環境では、liblua.aのビルドに成功した後、luaをビルドしようとして失敗する。

自作アプリケーションのMakefileにliblua.aのビルドを含めたい場合、liblua.aのビルドの成否に関係なくLuaMakefileが失敗する、という前提で組み込んだ方が無難だろう。GNU MakeやNetBSD makeなら、LuaMakefileを実行するコマンド行に「-」修飾子を付けてエラーを無視することができる。万が一liblua.aのビルドに失敗した場合は、liblua.aをリンクする工程で失敗するはずだ。

LUADIR := /path/to/lua-5.2.3/src

$(LUADIR)/liblua.a:
	- cd "$(LUADIR)"; make linux

Windowsでビルドしたい場合だが、Visual Studio付属のMicrosoft C/C++ Compilerでよければ、NMAKE用のMakefileを書いた。

設定内容をC言語から参照する

大まかには、次の作業を設定項目分だけ繰り返せばよい。ちょっとした工夫で、構造体配列を使用したテーブルと、それを舐めるループで実装できる。

  1. lua_getglobal()で設定値を取得する。値はスタックに詰まれる。
  2. スタックに詰まれた値の型をチェックする。
    • 設定値としてnilを認めていないのにlua_isnil()が真だった場合、該当する設定項目が設定ファイルに書かれていないか、値にnilが設定されている。
    • 想定していた型ではなかった場合は、設定値として誤った内容が書かれている可能性が高い(この辺は、設定項目の設計に依存?)。
  3. 値が想定していた型の場合は、値を取り出し、更なる検証を行い、適切な値だったら使用する。
      • 取得した値を保持する必要がある場合、特に文字列に関しては、コピーを作成して保持するようにしておくこと。
  4. lua_pop()でスタックに詰まれた設定値を削除する。

詳細は公式のリファレンスマニュアルや日本語訳などを参照のこと。

設定ファイル用の定数(もどき)を提供する

例えばC/C++側で次のような定数を使用していたとする。

typedef enum {
	FOO_MODE_A,
	FOO_MODE_B,
	FOO_MODE_C
} foo_mode_t;

これを設定ファイル側にも公開したい。

-- Fooモードのデフォルト設定
foo_mode = FOO_MODE_B

Lua側では定数ではなく変数になってしまうが、ひとまず目を瞑るとする。

どうすればよいか? lua_Stateに対してlua_setglobal()で値を設定した後で、設定ファイルを読み込んでlua_Stateに適用すればよい。

lua_State *lua;

/* 中略 */

/* 本来はループ化するべきだが、単なる例なのでしない */
lua_pushinteger(lua, (lua_Integer) FOO_MODE_A);
lua_setglobal(lua, "FOO_MODE_A");
lua_pushinteger(lua, (lua_Integer) FOO_MODE_B);
lua_setglobal(lua, "FOO_MODE_B");
lua_pushinteger(lua, (lua_Integer) FOO_MODE_C);
lua_setglobal(lua, "FOO_MODE_C");

/* ↑で値を設定した後で、設定ファイルを読み込む */
if (luaL_dofile(lua, conf_file_name)) {
	/* 設定ファイルのロードに失敗 */
}

こうすれば、設定ファイルを読み込んで解釈する時点でFOO_MODE_A〜FOO_MODE_Cが既に定義されているので、誤ってfoo_modeにnilが設定されることはない。

設定ファイルの再読み込み

設定ファイルを読み直す場合、次の要件を満たしたいことが多いと思う。

  • 前回読み込んだ内容を一旦クリアしたうえで、新しい内容を適用したい。
  • しかし読み直しに失敗した場合は、前回読み込んだ内容のままにしたい。

この要件を満たすには、次のようにすればよい。

  1. 新しいlua_Stateを生成する。
  2. 設定ファイルを読み込み、(1) で生成したlua_Stateに適用する。
    • 適用に失敗したら、(1) で生成したlua_Stateを破棄する。
  3. 古いlua_Stateを破棄し、新しいlua_Stateにすげ替える。

lua_Stateは意外と軽量なので、複数作成してもあまり問題にならないだろう。

マルチスレッド

lua_Stateがマルチスレッド環境下での複数コンテクストからの同時アクセスに対応しているか否か不明だが、何となく対応していないようだ。

この場合、アプローチは2つだ。

  • ミューテックスやイベントループなどを使用して、lua_Stateへのアクセスを逐次化する。
  • スレッドごとにlua_Stateを作成し、使用する。

どちらを採用するべきか? アプリケーション全体の設計次第だろう。ただ、前項にも書いたが、lua_Stateは意外と軽量であり、複数作成してもあまり問題にならないことが多いはずだ、という点は押さえておくべきだろう。つまり、スレッドごとにlua_Stateを作成する方法をことさら忌避しなくてもよい。

まとめ

考慮すべき点はあるものの、ポータブルな読み込み専用の設定ファイルとしてLuaを使用するのは悪くないと思う。

個人的に、「シンプルな文法でポータブルな設定ファイル」の仕様とライブラリの有無は悩みの種だ。

XMLは仕様が標準化されているが、XMLの上でのフォーマット(設定情報をどのように表現するか?)を設計する必要がある。得てして良いフォーマットを考えることは難しい。XML元来の特性もあり、大抵は冗長な代物になってしまうものだ。

JSONは仕様が標準化されているだけでなく、文法がシンプルだ。しかしJavaScriptベースなだけに、CやC++に変換した後のデータ表現に不安がある。TOMLもデータ表現について同じ不安がある上に、ライブラリの実装がまだ新しい点が気にかかる。

CSVや「SQLite組み込み!」な方法は、設定ファイルというよりレコードファイル向けだろうし……。

QtのQSettingsでは、ポータブルな設定ファイルのフォーマットとしてINIファイルを使っているようだが、さてはて。

*1:Windows APIのINIファイル、.NET Frameworkのアプリケーション構成ファイル、Objective-CのNSUserDefaultsなど。

*2:もっともWindowsでVisual C++を使用するアプリに組み込む場合は、LuaBinariesのDLLは使えないのだが。

*3:luaやluacが欲しい場合はlibreadlineが必要。

*4:例えば「[ [」で開始している場合、「望]」がブロックの終了だと解釈されてしまう。