この文書では、Luaのモジュールの仕組みと、チャンクについて解説する。
モジュールを書く側の話に関しては、この文書ではLuaで書く場合を主に解説し、Cでの書き方は解説しない。
Luaのモジュールシステムは Lua 5.1 と Lua 5.2 以降で色々変化があった。この記事ではその違いにも留意して解説する。なお、 LuaJIT は基本的に Lua 5.1 と互換なので、 LuaJIT の場合は Lua 5.1 向けの説明を読んでほしい。
なお、ここで解説する仕様とベストプラクティスはLuaの標準的な話であって、他のソフトウェアに組み込まれたLuaの場合は組み込む側の都合によって色々変更されている可能性がある。
require
の仕組みとカスタマイズ方法
まず、 require
を呼び出した時に何が起こるかを確認しておこう。読み込みたいモジュール名(require
の引数)は modname
とする。
- 当該モジュールが読み込み済みかを検査する。
-
package.loaded
テーブルの当該キー(つまりpackage.loaded[modname]
)に値があるかを検査する。あれば、その値を返す。
-
- 当該モジュールが読み込み済みでない場合、ローダーと呼ばれる関数を以下の手順で探す(この手順は
package.searchers
1テーブルをいじることでカスタマイズできる。以下の手順は、package.searchers
がカスタマイズされていないと仮定した場合のデフォルト値)-
package.preload[modname]
を見る。ここに値が入っていれば、それがローダーである。 -
package.path
に基づいて Lua モジュールを探す。 -
package.cpath
に基づいて C モジュールを探す。 - オールインワンローダーを探す。(詳細略)
-
- ローダーを、「モジュールの名前」を引数として呼び出す。(ローダーがファイルから見つかった場合は、引数としてモジュール名の他にファイル名が渡される。)
- 次回同じモジュールを
require
した場合のために、package.loaded[modname]
を適宜設定する。- ローダーが
nil
でない値を返した場合、その値をpackage.loaded[modname]
に設定する。 - ローダーが
nil
を返した場合(ローダーが値を返さなかった場合も含む)、- ローダー自身が
package.loaded[modname]
を設定していれば、それで良い。 - そうでなければ、
package.loaded[modname]
にtrue
を設定する。
- ローダー自身が
- ローダーが
- 最終的な
package.loaded[modname]
の値を返す。
ここの動作はLua 5.1からLua 5.3までそんなに変わっていない。
というわけで、 require
の挙動をカスタマイズしたい場合(組み込みモジュールを追加したい、モジュールの探索場所を変えたい、等)には、大雑把に
-
package.loaded
をいじる -
package.preload
をいじる -
package.path
やpackage.cpath
をいじる(環境変数から設定する場合はLUA_PATH
やLUA_CPATH
をいじる) -
package.searchers
をいじる
という4つの手段があることになる。以下、どういう場合にどの方法を使うと良いか、見ていこう。
Luaを組み込んだ実行環境が新たな組み込みモジュールを増やしたい(例えば、LuaFileSystem lfs
を内蔵したい)場合は、ローダー(LuaFileSystemの場合は、Cで書かれた luaopen_lfs
)を呼び出して package.loaded
にその返り値を設定するのが適切である2。Luaコードからそのモジュールを使いたい場合は、通常通り require
を使うことができる。
(ちなみに、Lua標準のモジュール(string
とか table
とか os
とか)も require
で取得できる:local string = require "string"
など)
ローダーを事前に実行するのではなく、 require
が呼ばれたタイミングで実行したい場合は、 package.preload
にローダーを設定しておくと良い。
LuaRocksのように、パッケージマネージャーが管理するモジュール置き場があってそこを使えるようにしたいという場合は、 package.path
/package.cpath
(あるいは環境変数 LUA_PATH
/LUA_CPATH
)をいじるのが適当である。
Lua以外のエコシステム(例:TeX)の仕組みに基づいてモジュールを読み込みたい、という場合は package.searchers
をいじるのが適当である。
モジュールを書く側のベストプラクティス
ローダーに期待される動作は、
- モジュールに相当する値(典型的にはテーブル)を返す
- モジュールに相当する値(典型的にはテーブル)を
package.loaded[modname]
に設定する
のいずれかである(両方行っても良い)。
モジュールと同名のグローバル変数を設定するかどうかは require
の動作には影響しないし、 Lua 5.2 以降は同名のグローバル変数は設定しないのが一般的である。
Luaでモジュールを書く場合は、「値を返す」のが一般的である。つまり、
local foo = {}
foo.greet = function()
print("Hello world!")
end
return foo
や
local function greet()
print("Hello world!")
end
return {
greet = greet,
}
と書く。この書き方は Lua 5.1〜5.3 いずれでも動作する。
Lua 5.1 時代は Lua でモジュールを書く際に module
という関数を使うのが一般的だったが、これはもはや黒歴史であり、忘れて良い。
モジュールを定義する際にうっかり local
を書き忘れてしまうと、不用意にグローバル変数を定義してしまうことになり、よろしくない。対処方法は後述する。
自身のモジュール名を取得したい場合は、チャンクの引数(後述)が利用できる。例:
local modname = ... -- チャンクの引数
local function greet()
print("Hello from " .. modname)
end
return {
greet = greet,
}
モジュールを使う側のベストプラクティス
require
はモジュールを表す値(典型的にはテーブル)を返すので、それをローカル変数で受けて利用すれば良い。
例えば、 foo
という名前のモジュールを使う際は、
local foo = require "foo"
foo.greet() -- モジュールを使う
とする。この書き方は Lua 5.1〜5.3 のいずれでも通用する。
Lua 5.1 時代(〜2011年)は、慣習として、ローダー自身がグローバル変数を設定することが多かったので
require "foo"
foo.greet()
という書き方もできたが、 Lua 5.2 時代(2012年〜)からはローダーがグローバル変数を設定することは少なくなった。そのため、 Lua 5.1 向けのLuaコードであっても、最新版のライブラリーを使う場合は後者の書き方(グローバル変数が設定されていることを期待する)はできない。
例:LuaFileSystemのローダーは、以前は lfs
という名前のグローバル変数を設定していたが、今はそうではない(当該コミット)。筆者が作っているluaexifも、Lua 5.2対応に合わせてグローバル変数を設定しないように仕様変更した。
なお、Lua標準のインタープリター lua
の -l
オプションで読み込まれたモジュールに関しては、 Lua 5.2 以降でも(Luaインタープリター側の処理によって)同名のグローバル変数が設定される。
チャンク:ソースコードと関数の関係
Luaでファイルを(モジュールとしてではなく、単に)実行するには dofile
関数を使う。この「ファイルを実行する」という処理は、
- 当該ファイルを読み取り、バイトコードへコンパイルする
- コンパイルしたバイトコードを実行する
の2つの処理から成ると考えられる。
Luaにおいてはこの2つ(コンパイルと実行)を別々に行うことができる。そして、「コンパイルしてできるもの」は普通の関数である。(この「関数」は、バイトコードと環境 _ENV
3 の組である。環境 _ENV
の初期値は load
の引数で与えることができる)
ファイルに対してコンパイルのみを行う関数は loadfile
であり、dofile
を loadfile
を使って実装するなら次のようになる:
function dofile(filename)
local chunk, err = loadfile(filename)
if chunk ~= nil then -- 正常に読み込めた:
return chunk() -- 実行
else -- エラー:
return nil, err
end
end
ソースコードをファイル以外(例:文字列リテラル)から読み込むことも当然可能である。その場合は loadfile
の代わりに load
(Lua 5.2以降)または loadstring
(Lua 5.1)を使う。
他の言語での eval
関数みたいな処理(文字列をその場でコンパイルし、実行する)は、Luaでは
assert(load("local a = 1 + 1; print(a)"))()
-- Lua 5.1の場合は
-- assert(loadstring("local a = 1 + 1; print(a)"))()
となる。
Luaのソースコードのひとまとまり(ファイルや文字列)、またはそれらをコンパイルしてできた関数はチャンクと呼ばれる。トップレベルの local
で宣言した変数のスコープは一つのチャンクで完結するため、異なるチャンクの間では共有されない。
例:Luaの対話環境(REPL)においては1行4が1つのチャンクに相当する。そのため、先の行で定義したローカル変数は後の行で参照できない。
> local a = 123 -- 先の行で定義したローカル変数は
> print(a) -- 後の行で参照できない
nil
例:LuaTeXの \directlua
の中で定義したローカル変数は後の \directlua
から参照できない。1つの \directlua
呼び出しが1つのチャンクに相当するからである。
\directlua{local a = 123; tex.print(tostring(a))} % --> 123
\directlua{tex.print(tostring(a))} % --> nil
\bye
チャンクの引数と返り値
Luaのチャンクが普通の関数だということは、チャンクも引数と返り値を持てるということである。
チャンクの返り値は「モジュールを書く側のベストプラクティス」ですでに登場している。return
を書けば良い。
チャンクの引数は、可変長引数である。Luaで function(...) ほにゃらら end
と書いた場合と同等である。
例:Lua標準のインタープリターでLuaファイルを実行した場合、チャンクの引数としてコマンドライン引数が渡される。
local arg1, arg2, arg3 = ...
print(arg1, arg2, arg3)
local args_table = table.pack(...)
print("Number of arguments: ", #args_table)
$ lua hoge.lua foo bar
foo bar nil
Number of arguments: 2
$ lua hoge.lua a b c d
a b c
Number of arguments: 4
ただし、コマンドライン引数は arg
という名前のグローバル変数としても渡されるので、普通にプログラムを書くときは arg
を使うことが多いかもしれない。
例:require
がLuaモジュールのローダーを呼び出す際には、モジュール名(とファイル名)が渡される。
local modname = ... -- チャンクの引数
local function greet()
print("Hello from " .. modname)
end
return {
greet = greet,
}
local foo = require "foo"
foo.greet() -- Hello from foo
チャンクと関数
というわけで、
-- すごい処理
print("Hello world!")
という内容のチャンク(をコンパイルして得られる関数)は
function(...)
-- すごい処理
print("Hello world!")
end
という関数とだいたい等価である。
細かいことを言うと、Lua 5.2 でグローバル変数(local
で束縛されていない変数、と言った方が適切かもしれない)へのアクセス方法が変わった関係で、 Lua 5.2 以降での相当する擬似コードは
(function()
local _ENV = <グローバル変数に相当するテーブル>
return function(...)
-- すごい処理
print("Hello world!") -- この print は _ENV.print と等価
end
end)()
となる。
おまけ:不用意にグローバル変数を触っていないかチェックする
モジュールを書く際にうっかり local
を書き忘れると、グローバル変数へのアクセスが発生してグローバル環境を汚染してしまう。そのため、意図しないグローバル変数へのアクセスが発生していないか検査する方法があると良い。
Lua 5.2 では、グローバル変数(というか、束縛されていない変数)へのアクセスは _ENV
という名前の変数を介するようになったので、 _ENV
に nil
でも代入しておけば不用意にグローバル環境を汚染することは避けられる。
local _G = _G -- グローバル変数が入ったテーブル
_ENV = nil
local bar = {}
bar.greet = function()
_G.print("Hello world!") -- OK
print("Goodbye world!") -- エラー! attempt to index a nil value (upvalue '_ENV')
end
return bar
グローバル変数への意図しないアクセスを実行せずに検出したい、という場合は、 luac -l
を使うと良い(luac
というのは与えられたチャンクをバイトコードにコンパイルするコマンドであり、 -l
オプションはコンパイル後のバイトコードをテキスト形式で印字してくれる)。
$ luac -l bar.lua
main <bar.lua:0,0> (8 instructions at 0x7fec42500080)
0+ params, 3 slots, 1 upvalue, 2 locals, 2 constants, 1 function
1 [1] GETTABUP 0 0 -1 ; _ENV "_G"
2 [2] LOADNIL 1 0
3 [2] SETUPVAL 1 0 ; _ENV
4 [3] NEWTABLE 1 0 0
5 [7] CLOSURE 2 0 ; 0x7fec42500230
6 [7] SETTABLE 1 -2 2 ; "greet" -
7 [8] RETURN 1 2
8 [8] RETURN 0 1
function <bar.lua:4,7> (7 instructions at 0x7fec42500230)
0 params, 2 slots, 2 upvalues, 0 locals, 3 constants, 0 functions
1 [5] GETTABUP 0 0 -1 ; _G "print"
2 [5] LOADK 1 -2 ; "Hello world!"
3 [5] CALL 0 2 1
4 [6] GETTABUP 0 1 -1 ; _ENV "print"
5 [6] LOADK 1 -3 ; "Goodbye world!"
6 [6] CALL 0 2 1
7 [7] RETURN 0 1
グローバル変数へのアクセスは _ENV
に対する GETTABUP
命令および SETTABUP
命令として印字される。後は、アクセスしている変数名が意図したものかどうかをチェックすれば良い。目視で検査するなら luac -l bar.lua | grep _ENV
を実行すると良いだろう。
筆者がLuaで書いている cluttex というプログラムでは、 luac
を使ってグローバル変数へのアクセスを検査するスクリプトを用意している。コード→ checkglobal.lua
-
Lua 5.1においては、
package.searchers
ではなくpackage.loaders
という名前だった。 ↩ -
余談だが、LuajitTeXはこの部分に不備があり、LuaFileSystemを組み込んでいる(グローバル変数
lfs
が存在する)にも関わらずrequire "lfs"
が動作しない。対処法は、require "lfs"
を呼ぶ前にif lfs and package.loaded["lfs"] then package.loaded["lfs"] = lfs end
を実行しておくことである。 ↩ -
関数の環境が
_ENV
と呼ばれる上位値になったのは Lua 5.2 以降の話で、 Lua 5.1 時代は同様のものがgetfenv
/setfenv
で取得・設定できた。 ↩ -
入力が1行で完結しない場合は、複数行。 ↩