Luaモジュール
ランタイムモジュールはLua環境を新しい機能で拡張します。モジュールは決定論的ユーティリティ、I/O操作、または外部システムにyieldする非同期コマンドを提供できます。
Luaランタイムの実装は将来のバージョンで変更される可能性があります。
モジュール定義
すべてのモジュールはluaapi.ModuleDefを使用:
var Module = &luaapi.ModuleDef{
Name: "mymodule",
Description: "My custom module",
Class: []string{luaapi.ClassDeterministic},
Types: ModuleTypes, // ツーリング用型定義
Build: func() (*lua.LTable, []luaapi.YieldType) {
mod := lua.CreateTable(0, 2)
mod.RawSetString("hello", lua.LGoFunc(helloFunc))
mod.RawSetString("greet", lua.LGoFunc(greetFunc))
mod.Immutable = true
return mod, nil
},
}
Build関数は以下を返す:
- エクスポートされた関数を持つモジュールテーブル
- 非同期操作用のyieldタイプのリスト(またはnil)
モジュールテーブルは一度構築され、すべてのLua状態で再利用するためにキャッシュ。
モジュール分類
Classフィールドはモジュールを使用できる場所を決定:
| クラス | 説明 |
|---|---|
ClassDeterministic |
同じ入力は常に同じ出力を生成 |
ClassNondeterministic |
出力が変動(時間、乱数) |
ClassIO |
外部I/O操作 |
ClassNetwork |
ネットワーク操作 |
ClassStorage |
データ永続化 |
ClassWorkflow |
ワークフロー安全な操作 |
ClassDeterministicのみでタグ付けされたモジュールはワークフロー安全。I/Oやネットワーククラスを追加すると、モジュールは関数とプロセスに制限。
関数の公開
関数はfunc(l *lua.LState) intのシグネチャを持ち、戻り値はスタックにプッシュされた値の数:
func greetFunc(l *lua.LState) int {
name := l.CheckString(1) // 必須引数
greeting := l.OptString(2, "Hello") // デフォルト付きオプション
l.Push(lua.LString(greeting + ", " + name + "!"))
return 1
}
| メソッド | 説明 |
|---|---|
l.CheckString(n) |
位置nの必須文字列 |
l.CheckInt(n) |
必須整数 |
l.CheckNumber(n) |
必須数値 |
l.CheckTable(n) |
必須テーブル |
l.OptString(n, def) |
デフォルト付きオプション文字列 |
l.OptInt(n, def) |
オプション整数 |
テーブル
GoとLua間で渡されるテーブルはデフォルトで可変。モジュールエクスポートテーブルはイミュータブルにマークすべき:
mod := lua.CreateTable(0, 5)
mod.RawSetString("func1", lua.LGoFunc(func1))
mod.Immutable = true // Luaがエクスポートを変更するのを防止
データテーブルは通常使用のため可変のまま:
result := l.CreateTable(0, 3)
result.RawSetString("name", lua.LString("value"))
result.RawSetString("count", lua.LNumber(42))
l.Push(result)
型システム
モジュールは2つの別々だが補完的な型付けメカニズムを使用。
型定義(ツーリング)
TypesフィールドはIDEサポートとドキュメント用の型シグネチャを提供:
func ModuleTypes() *types.TypeManifest {
m := types.NewManifest("mymodule")
objectType := &types.InterfaceType{
Name: "mymodule.Object",
Methods: map[string]*types.FunctionType{
"get_value": types.NewFunction(nil, []types.Type{types.String}),
"set_value": types.NewFunction([]types.Type{types.String}, nil),
},
}
m.DefineType("Object", objectType)
m.SetExport(moduleType)
return m
}
利用可能な型構成:
| 型 | 説明 |
|---|---|
types.String |
文字列プリミティブ |
types.Number |
数値 |
types.Boolean |
ブール値 |
types.Any |
任意のLua値 |
types.LuaError |
エラー型 |
types.Optional(t) |
型tのオプション値 |
types.InterfaceType |
メソッドを持つオブジェクト |
types.FunctionType |
パラメータ/戻り値を持つ関数シグネチャ |
types.RecordType |
構造体的な型(フィールド付き) |
types.TableType |
キー/値型を持つテーブル |
関数シグネチャは可変長パラメータをサポート:
// (string, ...any) -> (string, error?)
types.FunctionType{
Params: []types.Type{types.String},
Variadic: types.Any,
Returns: []types.Type{types.String, types.Optional(types.LuaError)},
}
完全な型システムについてはgo-luaのtypesパッケージを参照。
UserDataバインディング(ランタイム)
RegisterTypeMethodsは実際のGoからLuaへのバインディングを作成:
func init() {
value.RegisterTypeMethods(nil, "mymodule.Object",
map[string]lua.LGoFunc{
"__tostring": objectToString, // メタメソッド
},
map[string]lua.LGoFunc{
"get_value": objectGetValue, // 通常メソッド
"set_value": objectSetValue,
},
)
}
メタテーブルはイミュータブルでスレッドセーフな再利用のためにグローバルにキャッシュ。
| システム | 目的 | 定義 |
|---|---|---|
| 型定義 | IDE、ドキュメント、型チェック | シグネチャ |
| UserDataバインディング | ランタイムメソッド呼び出し | 実行可能関数 |
非同期操作
外部システムを待つ操作には、結果の代わりにyieldを返す。yieldはGoハンドラにディスパッチされ、ハンドラ完了時にプロセスが再開。
Yieldの定義
モジュールのBuild関数でyieldタイプを宣言:
Build: func() (*lua.LTable, []luaapi.YieldType) {
mod := lua.CreateTable(0, 1)
mod.RawSetString("fetch", lua.LGoFunc(fetchFunc))
mod.Immutable = true
yields := []luaapi.YieldType{
{Sample: &FetchYield{}, CmdID: myapi.FetchCommand},
}
return mod, yields
}
Yieldの作成
通常の戻り値の代わりにyieldをシグナルするには-1を返す:
func fetchFunc(l *lua.LState) int {
url := l.CheckString(1)
yield := AcquireFetchYield()
yield.URL = url
l.Push(yield)
return -1 // yieldをシグナル、スタックカウントではない
}
Yield実装
YieldはLua値とディスパッチャコマンドをブリッジ:
type FetchYield struct {
*myapi.FetchCmd
}
func (y *FetchYield) String() string { return "<fetch_yield>" }
func (y *FetchYield) Type() lua.LValueType { return lua.LTUserData }
func (y *FetchYield) CmdID() dispatcher.CommandID { return myapi.FetchCommand }
func (y *FetchYield) ToCommand() dispatcher.Command { return y.FetchCmd }
func (y *FetchYield) Release() { releaseFetchYield(y) }
func (y *FetchYield) HandleResult(l *lua.LState, data any, err error) []lua.LValue {
if err != nil {
return []lua.LValue{lua.LNil, lua.NewLuaError(l, err.Error())}
}
resp := data.(*myapi.FetchResponse)
return []lua.LValue{lua.LString(resp.Body), lua.LNil}
}
ディスパッチャはコマンドをハンドラにルーティング。ハンドラの実装についてはコマンドディスパッチを参照。
エラー処理
構造化エラーを使用してエラーを2番目の値として返す:
func myFunc(l *lua.LState) int {
result, err := doSomething()
if err != nil {
lerr := lua.NewLuaError(l, err.Error()).
WithKind(lua.Internal).
WithRetryable(true)
l.Push(lua.LNil)
l.Push(lerr)
return 2
}
l.Push(lua.LString(result))
l.Push(lua.LNil)
return 2
}
セキュリティ
センシティブな操作を実行する前に権限をチェック:
func myFunc(l *lua.LState) int {
ctx := l.Context()
if !security.IsAllowed(ctx, "mymodule.action", resource, nil) {
l.Push(lua.LNil)
l.Push(lua.NewLuaError(l, "permission denied").WithKind(lua.PermissionDenied))
return 2
}
// 操作を続行
}
テスト
基本的なモジュールテストは構造と同期関数を検証:
func TestModule(t *testing.T) {
l := lua.NewState()
defer l.Close()
mod, _ := Module.Build()
l.SetGlobal("mymodule", mod)
err := l.DoString(`
local m = mymodule
assert(m.hello() == "Hello, World!")
`)
if err != nil {
t.Fatal(err)
}
}
Yield付きモジュールのテスト
yielding関数を使用するLuaコードをテストするには、必要なディスパッチャを持つ最小限のスケジューラを作成:
type testScheduler struct {
*actor.Scheduler
clock *clock.Dispatcher
mu sync.Mutex
pending map[string]chan *runtime.Result
}
func newTestScheduler() *testScheduler {
ts := &testScheduler{pending: make(map[string]chan *runtime.Result)}
reg := scheduler.NewRegistry()
// モジュールが使用するyield用のディスパッチャを登録
clockSvc := clock.NewDispatcher()
clockSvc.RegisterAll(func(id dispatcher.CommandID, h dispatcher.Handler) {
reg.Register(id, h)
})
ts.clock = clockSvc
ts.Scheduler = actor.NewScheduler(reg, actor.WithWorkers(4), actor.WithLifecycle(ts))
return ts
}
完全な例についてはruntime/lua/modules/time/integration_test.goを参照。
関連項目
- コマンドディスパッチ - yieldコマンドの処理
- スケジューラ - プロセス実行