LuaJIT 拡張機能と FFI ライブラリの詳細解説

LuaJIT の互換性と拡張概要

LuaJIT は Lua 5.1 に対して完全な上位互換性を提供します。標準 Lua ライブラリ関数および Lua/C API 関数のすべてを利用可能です。さらに、リンカーおよび動的ローダーレベルにおいて Lua 5.1 と ABI 互換性があるため、標準 Lua 用ヘッダファイルでコンパイルした C モジュールを、Lua または LuaJIT どちらからも同じ共有ライブラリとしてロードできます。

標準 Lua VM を拡張し、いくつかの追加モジュールを導入していますが、ここで説明するのは性能向上(最適化 VM や JIT コンパイラなど)ではなく、機能面の拡張についてです。

組み込み拡張モジュール

LuaJIT には以下の組み込み拡張モジュールが含まれています。

bit.* - ビット演算

Lua BitOp で定義されたすべてのビット演算をサポートしています。このモジュールは内置されており、別途インストールする必要はありません。使用前にモジュールをrequireしてください。

local bitops = require("bit")
-- 使用例:bitops.band, bitops.bor など

インストール済みの Lua BitOp モジュールがある場合、LuaJIT はそれを無視します。これにより、共有環境で Lua と LuaJIT の両方でビット操作を利用可能です。

ffi.* - FFI ライブラリ

純粋な Lua コードから外部 C 関数の呼び出しや C データ構造の利用を可能にします。

jit.* - JIT コンパイラ制御

JIT コンパイラエンジンの挙動を制御する関群です。

C API 拡張

Lua/C API に対して追加機能いくつか提供されています。

標準ライブラリ関数の強化

  • xpcall(f, err [,args...]): 標準 Lua 5.1 と異なり、エラー関数の後の引数を保護コンテキストで呼び出される関数に渡せます。
  • loadfile() 等: UTF-8 ソースコードを透過的に処理します。識別子や文字列に UTF-8 文字を使用可能で、ソース先頭の UTF-8 BOM はスキップされます。
  • tostring() 等: 非有限数値の変換をプラットフォーム問わず統一します。NaN は "nan", 正の無限大は "inf", 負の無限大は "-inf" になります。
  • tonumber() 等: 組み込みの文字列から数値への変換を使用します。locale に依存せず IEEE-754 標準に基づいた全精度変換を行い、十六進浮動小数点数(例:0x1.5p-3)もサポートします。
  • string.dump(f [,strip]): 追加引数でデバッグ情報を含まない「ストリップ」バイトコードを生成可能です。生成されたバイトコードは LuaJIT がサポートするアーキテクチャ間で移植可能です。
  • math.random(): 周期 2^223 の Tausworthe PRNG を使用し、標準 Lua の ANSI rand() より高品質な乱数を生成します。シードから生成される序列はプラットフォーム間で同一です。
  • io.* 関数: 64 ビットファイルオフセットを処理します。2GB を超えるファイルの操作やシークが可能です。
  • debug.* 関数: 呼び出されたメタメソッドに関する情報を返します。namewhat フィールドは "metamethod" になり、name フィールドにはメタメソッド名(例:"__index")が入ります。

完全な VM 再開可能性

LuaJIT VM は完全に再開可能です。標準 Lua 5.1 VM では不可能だった、コルーチンからの yield をコンテキスト跨いで行えます。例えば、pcall() や xpcall()、イテレータ、メタメソッドを跨いで yield することが可能です。

Lua 5.2 拡張機能

LuaJIT は Lua 5.2 の言語およびライブラリ拡張の一部をサポートしています。既存コードを壊さない機能は無条件で有効です。

  • goto と ::labels::
  • 十六進エスケープ文字列 '\x3F' と '\*'
  • load(), loadstring(), loadfile() の拡張
  • math.log(x [,base])
  • string.rep(s, n [,sep])
  • string.format() の %q, %s, %a, %A 拡張
  • io.read("*L"), io.lines() の拡張
  • os.exit(status|true|false [,close])
  • package.searchpath, package.loadlib
  • debug.getinfo, debug.getlocal, debug.getupvalue 等の拡張

さらに、-DLUAJIT_ENABLE_LUA52COMPAT でビルドされた場合、以下の機能が有効になります。

  • goto がキーワード化
  • break の配置制限緩和
  • __lt, __le の混合型呼び出し
  • __len のテーブル対応と rawlen()
  • __pairs, __ipairs のチェック
  • coroutine.running() の二値返却
  • table.pack(), table.unpack()
  • io.write(), file:write() のファイルハンドル返却
  • os.execute(), pipe:close() の詳細な終了状態返却
  • debug.setmetatable(), debug.getuservalue(), debug.setuservalue()
  • math.mod(), string.gfind() の削除

注意:これは言語および Lua ライブラリレベルでの部分的な互換性です。Lua/C API および ABI 互換性を壊す機能(例:_ENV)は実装されていません。

C++ 例外相互運用性

LuaJIT は C++ 例外との相互運用性を内置サポートしています。機能範囲はターゲットプラットフォームとツールチェーンに依存します。

プラットフォームコンパイラ相互運用性
POSIX/x64, DWARF2 展開GCC 4.3+完全
その他プラットフォーム, DWARF2 展開GCC制限付き
Windows/x64MSVC完全
Windows/x86任意なし
その他プラットフォームその他コンパイラなし

完全な相互運用性とは、Lua 側で C++ 例外を pcall() 等で捕捉可能、C++ 側で Lua エラーを catch(...) で捕捉可能、C++ デストラクタ呼び出しが保証される状態を指します。

FFI ライブラリ详解

FFI ライブラリを使用すると、煩雑な手動 Lua/C バインディング作成を大幅に削減できます。バインディング言語を学ぶ必要はなく、単純な C 宣言を解析するだけです。

使用例:外部 C 関数の呼び出し

外部 C ライブラリ関数の呼び出しは以下のように記述します。

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
-- 標準 C ライブラリ名前空間 ffi.C を使用
ffi.C.printf("Status Code: %d\n", 200)

① FFI ライブラリのロード、② C 宣言の追加、③ 名前付き C 関数の呼び出し、という手順です。ffi.C は標準 C ライブラリ名前空間にバインドされており、引数は自動的に Lua オブジェクトから対応する C 型へ変換されます。

使用例:C データ構造の利用

FFI は C データ構造の作成とアクセスも可能です。大量の小さなテーブルを含む大きなテーブルを実装する代わりに、固定構造の配列を使用することでメモリオーバーヘッドを削減できます。

local ffi = require("ffi")
ffi.cdef[[
typedef struct { uint8_t r, g, b, a; } pixel_t;
]]

local function create_buffer(count)
  -- 可変長配列の生成
  local buf = ffi.new("pixel_t[?]", count)
  for i = 0, count - 1 do
    buf[i].g = i % 256
    buf[i].a = 255
  end
  return buf
end

local function process_buffer(buf, count)
  for i = 0, count - 1 do
    local luminance = 0.3 * buf[i].r + 0.59 * buf[i].g + 0.11 * buf[i].b
    buf[i].r = luminance
    buf[i].g = luminance
    buf[i].b = luminance
  end
end

local SIZE = 400 * 400
local image = create_buffer(SIZE)
-- 処理ループ
for i = 1, 1000 do
  process_buffer(image, SIZE)
end

この例では、メモリ消費を大幅に削減し、パフォーマンスを向上させています。ffi.new() は配列をゼロフィルするため、必要なフィールドのみ設定すれば済みます。

FFI チュートリアル

FFI ライブラリのロード

FFI ライブラリは内置されていますが、初期化は自動ではありません。各 Lua ファイルの先頭で以下のように require します。

local ffi = require("ffi")

標準システム関数へのアクセス

OS に依存しない sleep 関数の実装例です。

local ffi = require("ffi")
ffi.cdef[[
void Sleep(int ms);
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
]]

local sleep_func
if ffi.os == "Windows" then
  function sleep_func(s)
    ffi.C.Sleep(s * 1000)
  end
else
  function sleep_func(s)
    ffi.C.poll(nil, 0, s * 1000)
  end
end

-- 使用例
for i = 1, 10 do
  io.write("."); io.flush()
  sleep_func(0.05)
end
io.write("\n")

zlib 圧縮ライブラリの利用

文字列の圧縮・解圧ラッパー関数です。

local ffi = require("ffi")
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,
              const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,
               const uint8_t *source, unsigned long sourceLen);
]]
local zlib_lib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")

local function deflate(data)
  local max_len = zlib_lib.compressBound(#data)
  local buffer = ffi.new("uint8_t[?]", max_len)
  local len_ptr = ffi.new("unsigned long[1]", max_len)
  local res = zlib_lib.compress2(buffer, len_ptr, data, #data, 9)
  assert(res == 0)
  return ffi.string(buffer, len_ptr[0])
end

local function inflate(comp_data, orig_len)
  local buffer = ffi.new("uint8_t[?]", orig_len)
  local len_ptr = ffi.new("unsigned long[1]", orig_len)
  local res = zlib_lib.uncompress(buffer, len_ptr, comp_data, #comp_data)
  assert(res == 0)
  return ffi.string(buffer, len_ptr[0])
end

C 型へのメタメソッド定義

C 型に対して Lua の演算子オーバーロードを定義できます。

local ffi = require("ffi")
ffi.cdef[[
typedef struct { double x, y; } vector_t;
]]

local vector_ctor
local mt = {
  __add = function(a, b) return vector_ctor(a.x + b.x, a.y + b.y) end,
  __len = function(a) return math.sqrt(a.x * a.x + a.y * a.y) end,
  __index = {
    magnitude_sq = function(a) return a.x * a.x + a.y * a.y end,
  },
}
vector_ctor = ffi.metatype("vector_t", mt)

local v1 = vector_ctor(3, 4)
print(#v1)        -- 5
print(v1:magnitude_sq()) -- 25
local v2 = v1 + vector_ctor(1, 1)

ffi.* API 関数リファレンス

  • ffi.cdef(def): 型または外部シンボルの C 宣言を追加します。
  • ffi.C: デフォルトの C ライブラリ名前空間です。
  • ffi.load(name [,global]): 動的ライブラリをロードし、シンボルにバインドされた新しい C ライブラリ名前空間を返します。
  • ffi.new(ct [,nelem] [,init...]): 指定された型の cdata オブジェクトを作成します。
  • ffi.typeof(ct): 指定された型の ctype オブジェクトを作成します。
  • ffi.cast(ct, init): 指定された型のスカラー cdata オブジェクトを作成し、キャスト変換で初期化します。
  • ffi.metatype(ct, metatable): ctype オブジェクトを作成し、メタテーブルに関連付けます。
  • ffi.gc(cdata, finalizer): ポインタまたは集約 cdata オブジェクトにファイナライザを関連付けます。
  • ffi.sizeof(ct [,nelem]): 型のサイズ(バイト)を返します。
  • ffi.string(ptr [,len]): ptr が指すデータから Lua 文字列を作成します。
  • ffi.copy(dst, src, len): src のデータを dst にコピーします。
  • ffi.fill(dst, len [,c]): dst のデータを c で埋めます。
  • ffi.abi(param): ターゲット ABI に関する情報を返します。
  • ffi.os / ffi.arch: ターゲット OS およびアーキテクチャ名を含みます。

FFI 语义

FFI ライブラリは C 言語の语义に可能な限り従います。C 解析器は C99 標準およびいくつかの拡張(GCC/MSVC 固有の属性など)をサポートしますが、プリアンプリプロセスは行われません。

型変換規則

C 型から Lua オブジェクトへ、Lua オブジェクトから C 型への変換規則が定義されています。例えば、int8_t は符号拡張されて double 経由で number になり、int64_t はボックス化された cdata として扱われます。

コールバック

Lua 関数を C 関数ポインタに変換すると、自動的にコールバック関数が生成されます。コールバックはリソースを消費するため、不要になったら cb:free() で解放する必要があります。パフォーマンス上の理由から、頻繁に呼び出される数値計算などでコールバックを使用するのは避けるべきです。

jit.* ライブラリ

この内置モジュールの関数は JIT コンパイラエンジンの挙動を制御します。

  • jit.on() / jit.off(): JIT コンパイラ全体をオンまたはオフにします。
  • jit.flush(): 编译済みコードのキャッシュをフラッシュします。
  • jit.on(func) / jit.off(func): 特定の Lua 関数に対して JIT 编译を有効化または無効化します。
  • jit.status(): JIT コンパイラの現在の状態を返します。
  • jit.version: LuaJIT のバージョン文字列を含みます。

jit.opt.* サブモジュールを使用すると、-O コマンドラインオプションと同様の最適化制御をプログラムから行えます。

Lua/C API 拡張

LuaJIT は標準 Lua/C API に拡張を追加しています。使用する際には luajit.h または lua.hpp をインクルードする必要があります。

luaJIT_setmode(L, idx, mode)

C コードから VM を制御するための API 拡張です。

LUA_API int luaJIT_setmode(lua_State *L, int idx, int mode);

mode 引数には以下のフラグを組み合わせます。

  • LUAJIT_MODE_ENGINE: JIT 编译器全体をオン/オフまたはキャッシュをフラッシュします。
  • LUAJIT_MODE_FUNC: 指定された関数の JIT 编译設定を変更します。
  • LUAJIT_MODE_TRACE: 指定されたトレースをフラッシュします。
  • LUAJIT_MODE_WRAPCFUNC: C 関数呼び出しに対するラッパー関数を定義します。例外捕捉や変換に利用可能です。

ラッパー関数を使用する例:

static int wrap_exceptions(lua_State *L, lua_CFunction f) {
  try {
    return f(L);
  } catch (const char *s) {
    lua_pushstring(L, s);
  } catch (...) {
    lua_pushliteral(L, "unknown error");
  }
  return lua_error(L);
}

// 初期化処理
lua_pushlightuserdata(L, (void *)wrap_exceptions);
luaJIT_setmode(L, -1, LUAJIT_MODE_WRAPCFUNC | LUAJIT_MODE_ON);

タグ: LuaJIT FFI Lua5.1 JIT Compilation C Interface

6月30日 22:34 投稿