Luaのモジュール徹底解説(Lua 5.1〜5.3対応)


この文書では、Luaのモジュールの仕組みと、チャンクについて解説する。

モジュールを書く側の話に関しては、この文書ではLuaで書く場合を主に解説し、Cでの書き方は解説しない。

Luaのモジュールシステムは Lua 5.1 と Lua 5.2 以降で色々変化があった。この記事ではその違いにも留意して解説する。なお、 LuaJIT は基本的に Lua 5.1 と互換なので、 LuaJIT の場合は Lua 5.1 向けの説明を読んでほしい。

なお、ここで解説する仕様とベストプラクティスはLuaの標準的な話であって、他のソフトウェアに組み込まれたLuaの場合は組み込む側の都合によって色々変更されている可能性がある。

require の仕組みとカスタマイズ方法

まず、 require を呼び出した時に何が起こるかを確認しておこう。読み込みたいモジュール名(require の引数)は modname とする。

  1. 当該モジュールが読み込み済みかを検査する。
    1. package.loaded テーブルの当該キー(つまり package.loaded[modname])に値があるかを検査する。あれば、その値を返す。
  2. 当該モジュールが読み込み済みでない場合、ローダーと呼ばれる関数を以下の手順で探す(この手順は package.searchers 1テーブルをいじることでカスタマイズできる。以下の手順は、 package.searchers がカスタマイズされていないと仮定した場合のデフォルト値)
    1. package.preload[modname] を見る。ここに値が入っていれば、それがローダーである。
    2. package.path に基づいて Lua モジュールを探す。
    3. package.cpath に基づいて C モジュールを探す。
    4. オールインワンローダーを探す。(詳細略)
  3. ローダーを、「モジュールの名前」を引数として呼び出す。(ローダーがファイルから見つかった場合は、引数としてモジュール名の他にファイル名が渡される。)
  4. 次回同じモジュールを require した場合のために、 package.loaded[modname] を適宜設定する。
    1. ローダーが nil でない値を返した場合、その値を package.loaded[modname] に設定する。
    2. ローダーが nil を返した場合(ローダーが値を返さなかった場合も含む)、
      1. ローダー自身が package.loaded[modname] を設定していれば、それで良い。
      2. そうでなければ、 package.loaded[modname]true を設定する。
  5. 最終的な package.loaded[modname] の値を返す。

ここの動作はLua 5.1からLua 5.3までそんなに変わっていない。

というわけで、 require の挙動をカスタマイズしたい場合(組み込みモジュールを追加したい、モジュールの探索場所を変えたい、等)には、大雑把に

  1. package.loaded をいじる
  2. package.preload をいじる
  3. package.pathpackage.cpath をいじる(環境変数から設定する場合は LUA_PATHLUA_CPATH をいじる)
  4. 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 をいじるのが適当である。

モジュールを書く側のベストプラクティス

ローダーに期待される動作は、

  1. モジュールに相当する値(典型的にはテーブル)を返す
  2. モジュールに相当する値(典型的にはテーブル)を package.loaded[modname] に設定する

のいずれかである(両方行っても良い)。

モジュールと同名のグローバル変数を設定するかどうかは require の動作には影響しないし、 Lua 5.2 以降は同名のグローバル変数は設定しないのが一般的である。

Luaでモジュールを書く場合は、「値を返す」のが一般的である。つまり、

foo.lua
local foo = {}
foo.greet = function()
  print("Hello world!")
end
return foo

foo.lua
local function greet()
  print("Hello world!")
end
return {
  greet = greet,
}

と書く。この書き方は Lua 5.1〜5.3 いずれでも動作する。

Lua 5.1 時代は Lua でモジュールを書く際に module という関数を使うのが一般的だったが、これはもはや黒歴史であり、忘れて良い。

モジュールを定義する際にうっかり local を書き忘れてしまうと、不用意にグローバル変数を定義してしまうことになり、よろしくない。対処方法は後述する。

自身のモジュール名を取得したい場合は、チャンクの引数(後述)が利用できる。例:

foo.lua
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 関数を使う。この「ファイルを実行する」という処理は、

  1. 当該ファイルを読み取り、バイトコードへコンパイルする
  2. コンパイルしたバイトコードを実行する

の2つの処理から成ると考えられる。

Luaにおいてはこの2つ(コンパイルと実行)を別々に行うことができる。そして、「コンパイルしてできるもの」は普通の関数である。(この「関数」は、バイトコードと環境 _ENV3 の組である。環境 _ENV の初期値は load の引数で与えることができる)

ファイルに対してコンパイルのみを行う関数は loadfile であり、dofileloadfile を使って実装するなら次のようになる:

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つのチャンクに相当する。そのため、先の行で定義したローカル変数は後の行で参照できない。

Luaの対話環境
> 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ファイルを実行した場合、チャンクの引数としてコマンドライン引数が渡される。

hoge.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モジュールのローダーを呼び出す際には、モジュール名(とファイル名)が渡される。

foo.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 という名前の変数を介するようになったので、 _ENVnil でも代入しておけば不用意にグローバル環境を汚染することは避けられる。

bar.lua
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


  1. Lua 5.1においては、 package.searchers ではなく package.loaders という名前だった。 

  2. 余談だが、LuajitTeXはこの部分に不備があり、LuaFileSystemを組み込んでいる(グローバル変数 lfs が存在する)にも関わらず require "lfs" が動作しない。対処法は、 require "lfs" を呼ぶ前に if lfs and package.loaded["lfs"] then package.loaded["lfs"] = lfs end を実行しておくことである。 

  3. 関数の環境が _ENV と呼ばれる上位値になったのは Lua 5.2 以降の話で、 Lua 5.1 時代は同様のものが getfenv/setfenv で取得・設定できた。 

  4. 入力が1行で完結しない場合は、複数行。