Lua-Module

Laufzeitmodule erweitern die Lua-Umgebung um neue Funktionalität. Module können deterministische Hilfsfunktionen, E/A-Operationen oder asynchrone Befehle bereitstellen, die an externe Systeme abgeben.

Die Lua-Runtime-Implementierung kann sich in zukünftigen Versionen ändern.

Modul-Definition

Jedes Modul verwendet luaapi.ModuleDef:

var Module = &luaapi.ModuleDef{
    Name:        "mymodule",
    Description: "My custom module",
    Class:       []string{luaapi.ClassDeterministic},
    Types:       ModuleTypes,  // Typ-Definitionen für Tooling
    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
    },
}

Die Build-Funktion gibt zurück:

  • Modul-Tabelle mit exportierten Funktionen
  • Liste von Yield-Typen für asynchrone Operationen (oder nil)

Modul-Tabellen werden einmal erstellt und für Wiederverwendung über alle Lua-States gecacht.

Modul-Klassifikation

Das Class-Feld bestimmt wo das Modul verwendet werden kann:

Klasse Beschreibung
ClassDeterministic Selbe Eingabe produziert immer selbe Ausgabe
ClassNondeterministic Ausgabe variiert (Zeit, Zufall)
ClassIO Externe E/A-Operationen
ClassNetwork Netzwerkoperationen
ClassEncoding Serialisierung und Kodierung
ClassTime Zugriff auf Uhr und Timer
ClassProcess Prozesssteuerung
ClassSecurity Sicherheitskontext und Tokens
ClassStorage Datenpersistenz
ClassWorkflow Workflow-sichere Operationen

Workflow-Prozesse werden mit ClassDeterministic und ClassWorkflow als erlaubten Klassen kompiliert: Ein Modul steht Workflows zur Verfügung, wenn es mindestens eine davon trägt, andernfalls ist es auf Funktionen und Prozesse beschränkt.

Funktionen exponieren

Funktionen haben Signatur func(l *lua.LState) int wobei der Rückgabewert die Anzahl auf den Stack gepushter Werte ist:

func greetFunc(l *lua.LState) int {
    name := l.CheckString(1)           // Erforderliches Argument
    greeting := l.OptString(2, "Hello") // Optional mit Default

    l.Push(lua.LString(greeting + ", " + name + "!"))
    return 1
}
Methode Beschreibung
l.CheckString(n) Erforderlicher String an Position n
l.CheckInt(n) Erforderliche Ganzzahl
l.CheckNumber(n) Erforderliche Zahl
l.CheckTable(n) Erforderliche Tabelle
l.OptString(n, def) Optionaler String mit Default
l.OptInt(n, def) Optionale Ganzzahl mit Default

Tabellen

Tabellen, die zwischen Go und Lua übergeben werden, sind standardmäßig mutable. Modul-Export-Tabellen sollten als immutable markiert werden:

mod := lua.CreateTable(0, 5)
mod.RawSetString("func1", lua.LGoFunc(func1))
mod.Immutable = true  // Verhindert dass Lua Exports modifiziert

Daten-Tabellen bleiben für normale Nutzung mutable:

result := l.CreateTable(0, 3)
result.RawSetString("name", lua.LString("value"))
result.RawSetString("count", lua.LNumber(42))
l.Push(result)

Typsystem

Module verwenden zwei separate aber komplementäre Typisierungsmechanismen.

Typ-Definitionen (Tooling)

Das Types-Feld stellt Typsignaturen für IDE-Support und Dokumentation bereit. Typen werden mit den Fluent-Buildern des typ-Pakets erstellt:

import (
    "github.com/wippyai/go-lua/types/io"
    "github.com/wippyai/go-lua/types/typ"
)

func ModuleTypes() *io.Manifest {
    m := io.NewManifest("mymodule")

    objectType := typ.NewInterface("mymodule.Object", []typ.Method{
        {Name: "get_value", Type: typ.Func().Param("self", typ.Self).
            Returns(typ.String, typ.NewOptional(typ.LuaError)).Build()},
        {Name: "set_value", Type: typ.Func().Param("self", typ.Self).
            Param("value", typ.String).Returns(typ.NewOptional(typ.LuaError)).Build()},
    })

    m.DefineType("Object", objectType)
    m.SetExport(objectType)
    return m
}

Verfügbare Typkonstrukte:

Typ Beschreibung
typ.String String-Primitiv
typ.Number Numerischer Wert
typ.Integer Ganzzahliger Wert
typ.Boolean Boolean-Wert
typ.Any Jeder Lua-Wert
typ.Self Empfängertyp für Methoden
typ.LuaError Fehlertyp
typ.NewOptional(t) Optionaler Wert vom Typ t
typ.NewInterface(name, methods) Objekt mit Methoden
typ.Func() Builder für Funktionssignaturen
typ.NewRecord() Builder für struct-ähnliche Typen (Felder über .Field/.OptField)
typ.NewArray(t) Array mit Elementtyp t
typ.NewMap(k, v) Map mit Key-/Value-Typen

Funktions-Builder verketten Param, OptParam, Variadic und Returns:

// (string, ...any) -> (string, error?)
typ.Func().
    Param("first", typ.String).
    Variadic(typ.Any).
    Returns(typ.String, typ.NewOptional(typ.LuaError)).
    Build()

Records deklarieren Felder mit Field (erforderlich) und OptField (optional):

typ.NewRecord().
    Field("key", typ.String).
    Field("value", typ.Any).
    OptField("ttl", typ.Number).
    Build()

Siehe das typ-Paket in go-lua für das vollständige Typsystem.

UserData-Bindings (Runtime)

RegisterTypeMethods erstellt die tatsächlichen Go-zu-Lua-Bindings:

func init() {
    value.RegisterTypeMethods(nil, "mymodule.Object",
        map[string]lua.LGoFunc{
            "__tostring": objectToString,  // Metamethoden
        },
        map[string]lua.LGoFunc{
            "get_value": objectGetValue,   // Reguläre Methoden
            "set_value": objectSetValue,
        },
    )
}

Metatables sind immutable und global gecacht für thread-sichere Wiederverwendung.

System Zweck Definiert
Typ-Definitionen IDE, Docs, Type-Checking Signaturen
UserData-Bindings Runtime-Methodenaufrufe Ausführbare Funktionen

Asynchrone Operationen

Für Operationen die auf externe Systeme warten, geben Sie einen Yield statt eines Ergebnisses zurück. Der Yield wird an einen Go-Handler dispatcht und der Prozess wird fortgesetzt wenn der Handler abschließt.

Yields definieren

Deklarieren Sie Yield-Typen in der Build-Funktion des Moduls:

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 erstellen

Geben Sie -1 zurück um einen Yield statt normaler Rückgabewerte zu signalisieren:

func fetchFunc(l *lua.LState) int {
    url := l.CheckString(1)

    yield := AcquireFetchYield()
    yield.URL = url

    l.Push(yield)
    return -1  // Signalisiert Yield, nicht Stack-Anzahl
}

Yield-Implementierung

Yields verbinden Lua-Werte und Dispatcher-Commands:

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}
}

Der Dispatcher routet den Command an einen Handler. Siehe Command-Dispatch für Handler-Implementierung.

Fehlerbehandlung

Geben Sie Fehler als zweiten Wert mit strukturierten Fehlern zurück:

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
}

Sicherheit

Prüfen Sie Berechtigungen vor sensiblen Operationen:

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
    }

    // Mit Operation fortfahren
}

Testen

Einfache Modul-Tests verifizieren Struktur und synchrone Funktionen:

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)
    }
}

Module mit Yields testen

Um Lua-Code zu testen der yielding Funktionen verwendet, erstellen Sie einen minimalen Scheduler mit den erforderlichen Dispatchern:

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()

    // Dispatcher für Yields registrieren die Ihr Modul verwendet
    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
}

// Stop umhüllt Scheduler.Stop, das einen Context benötigt.
func (ts *testScheduler) Stop() {
    ts.Scheduler.Stop(context.Background())
}

// OnStart erfüllt process.Lifecycle neben OnComplete.
func (ts *testScheduler) OnStart(context.Context, pid.PID, process.Process) error { return nil }

func (ts *testScheduler) OnComplete(_ context.Context, p pid.PID, result *runtime.Result) {
    ts.mu.Lock()
    ch, ok := ts.pending[p.UniqID]
    delete(ts.pending, p.UniqID)
    ts.mu.Unlock()
    if ok {
        ch <- result
    }
}

func (ts *testScheduler) Execute(ctx context.Context, p pid.PID, proc process.Process,
    method string, input payload.Payloads) (*runtime.Result, error) {
    resultCh := make(chan *runtime.Result, 1)
    ts.mu.Lock()
    ts.pending[p.UniqID] = resultCh
    ts.mu.Unlock()

    _, err := ts.Scheduler.Submit(ctx, p, proc, method, input)
    if err != nil {
        return nil, err
    }

    select {
    case result := <-resultCh:
        return result, nil
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

Erstellen Sie Prozesse aus Lua-Scripts mit den Modulen die Sie testen:

func bindMyModule(l *lua.LState) error {
    tbl, _ := mymodule.Module.Build()
    l.SetGlobal(mymodule.Module.Name, tbl)
    return nil
}

func newLuaProcess(script string) *engine.Process {
    proto, _ := lua.CompileString(script, "test.lua")
    proc, _ := engine.NewProcess(
        engine.WithProto(proto),
        engine.WithModuleBinder(bindMyModule),
    )
    return proc
}

func TestMyModuleYields(t *testing.T) {
    sched := newTestScheduler()
    sched.Start()
    defer sched.Stop()

    script := `
        local result = mymodule.fetch("http://example.com")
        return result.status
    `

    ctx, _ := ctxapi.OpenFrameContext(context.Background())
    proc := newLuaProcess(script)

    result, err := sched.Execute(ctx, pid.PID{UniqID: "test"}, proc, "", nil)
    if err != nil {
        t.Fatal(err)
    }
    // Auf result assertieren
}

Siehe runtime/lua/modules/time/integration_test.go für ein vollständiges Beispiel.

Siehe auch