Módulos Lua
Los módulos de runtime extienden el entorno Lua con nueva funcionalidad. Los módulos pueden proveer utilidades determinísticas, operaciones I/O o comandos asíncronos que hacen yield a sistemas externos.
La implementación del runtime Lua puede cambiar en futuras versiones.
Definición de Módulo
Cada módulo usa luaapi.ModuleDef:
var Module = &luaapi.ModuleDef{
Name: "mymodule",
Description: "My custom module",
Class: []string{luaapi.ClassDeterministic},
Types: ModuleTypes, // Type definitions for 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
},
}
La función Build devuelve:
- Tabla del módulo con funciones exportadas
- Lista de tipos de yield para operaciones asíncronas (o nil)
Las tablas de los módulos se construyen una vez y se almacenan en caché para su reutilización en todos los estados Lua.
Clasificación de Módulos
El campo Class determina dónde se puede usar el módulo:
| Clase | Descripción |
|---|---|
ClassDeterministic |
La misma entrada siempre produce la misma salida |
ClassNondeterministic |
La salida varía (tiempo, aleatoriedad) |
ClassIO |
Operaciones de I/O externas |
ClassNetwork |
Operaciones de red |
ClassEncoding |
Serialización y codificación |
ClassTime |
Acceso a reloj y temporizadores |
ClassProcess |
Control de procesos |
ClassSecurity |
Contexto de seguridad y tokens |
ClassStorage |
Persistencia de datos |
ClassWorkflow |
Operaciones seguras para workflows |
Los procesos de workflow se compilan con ClassDeterministic y ClassWorkflow como clases permitidas: un módulo está disponible para workflows si lleva al menos una de ellas; en caso contrario queda restringido a funciones y procesos.
Exponer Funciones
Las funciones tienen la firma func(l *lua.LState) int, donde el valor de retorno es el número de valores apilados en la pila:
func greetFunc(l *lua.LState) int {
name := l.CheckString(1) // Required argument
greeting := l.OptString(2, "Hello") // Optional with default
l.Push(lua.LString(greeting + ", " + name + "!"))
return 1
}
| Método | Descripción |
|---|---|
l.CheckString(n) |
String requerido en la posición n |
l.CheckInt(n) |
Entero requerido |
l.CheckNumber(n) |
Número requerido |
l.CheckTable(n) |
Tabla requerida |
l.OptString(n, def) |
String opcional con valor por defecto |
l.OptInt(n, def) |
Entero opcional con valor por defecto |
Tablas
Las tablas que se pasan entre Go y Lua son mutables por defecto. Las tablas de exportación de módulos deben marcarse como inmutables:
mod := lua.CreateTable(0, 5)
mod.RawSetString("func1", lua.LGoFunc(func1))
mod.Immutable = true // Prevent Lua from modifying exports
Las tablas de datos permanecen mutables para su uso normal:
result := l.CreateTable(0, 3)
result.RawSetString("name", lua.LString("value"))
result.RawSetString("count", lua.LNumber(42))
l.Push(result)
Sistema de Tipos
Los módulos usan dos mecanismos de tipado separados pero complementarios.
Definiciones de Tipos (Herramientas)
El campo Types proporciona firmas de tipo para soporte en IDE y documentación. Los tipos se construyen con los constructores fluidos del paquete typ:
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
}
Constructores de tipo disponibles:
| Tipo | Descripción |
|---|---|
typ.String |
Primitivo string |
typ.Number |
Valor numérico |
typ.Integer |
Valor entero |
typ.Boolean |
Valor booleano |
typ.Any |
Cualquier valor Lua |
typ.Self |
Tipo receptor para métodos |
typ.LuaError |
Tipo error |
typ.NewOptional(t) |
Valor opcional de tipo t |
typ.NewInterface(name, methods) |
Objeto con métodos |
typ.Func() |
Constructor de firmas de función |
typ.NewRecord() |
Constructor de tipos tipo struct (campos vía .Field/.OptField) |
typ.NewArray(t) |
Array de elementos de tipo t |
typ.NewMap(k, v) |
Mapa con tipos de clave/valor |
Los constructores de función encadenan Param, OptParam, Variadic y Returns:
// (string, ...any) -> (string, error?)
typ.Func().
Param("first", typ.String).
Variadic(typ.Any).
Returns(typ.String, typ.NewOptional(typ.LuaError)).
Build()
Los records declaran campos con Field (obligatorio) y OptField (opcional):
typ.NewRecord().
Field("key", typ.String).
Field("value", typ.Any).
OptField("ttl", typ.Number).
Build()
Consulte el paquete typ en go-lua para el sistema de tipos completo.
Bindings UserData (Runtime)
RegisterTypeMethods crea los bindings reales de Go a Lua:
func init() {
value.RegisterTypeMethods(nil, "mymodule.Object",
map[string]lua.LGoFunc{
"__tostring": objectToString, // Metamethods
},
map[string]lua.LGoFunc{
"get_value": objectGetValue, // Regular methods
"set_value": objectSetValue,
},
)
}
Las metatablas son inmutables y se cachean globalmente para una reutilización segura entre hilos.
| Sistema | Propósito | Define |
|---|---|---|
| Definiciones de Tipos | IDE, docs, verificación de tipos | Firmas |
| Bindings UserData | Llamadas a métodos en runtime | Funciones ejecutables |
Operaciones Asíncronas
Para operaciones que esperan en sistemas externos, devuelva un yield en lugar de un resultado. El yield se despacha a un handler Go y el proceso se reanuda cuando el handler termina.
Definir Yields
Declare los tipos de yield en la función Build del módulo:
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
}
Crear un Yield
Devuelva -1 para indicar un yield en lugar de valores de retorno normales:
func fetchFunc(l *lua.LState) int {
url := l.CheckString(1)
yield := AcquireFetchYield()
yield.URL = url
l.Push(yield)
return -1 // Signal yield, not stack count
}
Implementación del Yield
Los yields conectan los valores Lua con los comandos del dispatcher:
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}
}
El dispatcher enruta el comando a un handler. Consulte Despacho de Comandos para implementar handlers.
Manejo de Errores
Devuelva errores como segundo valor usando errores estructurados:
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
}
Seguridad
Verifique los permisos antes de realizar operaciones sensibles:
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
}
// Proceed with operation
}
Pruebas
Las pruebas básicas de módulo verifican la estructura y las funciones síncronas:
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)
}
}
Probar Módulos con Yields
Para probar código Lua que usa funciones con yield, cree un planificador mínimo con los dispatchers requeridos:
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()
// Register dispatchers for yields your module uses
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 envuelve Scheduler.Stop, que requiere un context.
func (ts *testScheduler) Stop() {
ts.Scheduler.Stop(context.Background())
}
// OnStart satisface process.Lifecycle junto con 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()
}
}
Cree procesos desde scripts Lua con los módulos que esté probando:
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)
}
// Assert on result
}
Consulte runtime/lua/modules/time/integration_test.go para un ejemplo completo.
Véase También
- Despacho de Comandos - Manejo de comandos de yield
- Planificador - Ejecución de procesos