From 054ed312a195e891c6ebc555c9435b842def9b52 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 17 Nov 2025 12:28:25 +0000 Subject: [PATCH 01/64] feat: high-level `lua` module API Extremely barebones implementation. Includes memory allocator, high-level state wrapper, and basic string and table types. The core idea of all types are going to be based around `LuaValue` (inspired by mlua-rs). `LuaValue` is an interface which all of the Lua type structs will implement, and a consumer can leverage the Go type system to assert down to or match against types. For example: ```go import "github.com/CompeyDev/lei/lua" state := lua.New() table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("world") table.Set(&key, &value) roundtripValue := table.Get(&key) // We can assert the type: println(roundtripValue.(*lua.LuaString)) // If unsure about the type, we can switch for it: switch roundtripValue.(type) { case *lua.LuaString: println(value.ToString()) case *lua.LuaTable: println("The value was a table?") default: panic("Unexpected type, expected string!") } ``` --- doc.go | 3 ++ lua/allocator.c | 50 +++++++++++++++++++ lua/memory.go | 81 ++++++++++++++++++++++++++++++ lua/state.go | 95 +++++++++++++++++++++++++++++++++++ lua/stdlib.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++++ lua/string.go | 34 +++++++++++++ lua/table.go | 43 ++++++++++++++++ lua/value.go | 15 ++++++ 8 files changed, 451 insertions(+) create mode 100644 doc.go create mode 100644 lua/allocator.c create mode 100644 lua/memory.go create mode 100644 lua/state.go create mode 100644 lua/stdlib.go create mode 100644 lua/string.go create mode 100644 lua/table.go create mode 100644 lua/value.go diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..79fb550 --- /dev/null +++ b/doc.go @@ -0,0 +1,3 @@ +// Root package. Use `ffi` or `lua` modules for low-level bindings +// or high-level APIs respectively. +package main diff --git a/lua/allocator.c b/lua/allocator.c new file mode 100644 index 0000000..c7a55a1 --- /dev/null +++ b/lua/allocator.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include + +typedef struct MemoryState MemoryState; + +void* allocator(void* ud, void* ptr, size_t osize, size_t nsize) { + MemoryState* mem = (MemoryState*)ud; + + // Convert void* to Go struct pointer using uintptr arithmetic + // SAFETY: unsafe, must keep pointer alive in Go! + int* usedMemory = (int*)((uintptr_t)mem + 0); + int* memoryLimit = (int*)((uintptr_t)mem + sizeof(int)); + bool* ignoreLimit = (bool*)((uintptr_t)mem + 2*sizeof(int)); + bool* limitReached = (bool*)((uintptr_t)mem + 2*sizeof(int) + sizeof(bool)); + + *limitReached = false; + + if (nsize == 0) { + if (ptr != NULL) { + free(ptr); + *usedMemory -= (int)osize; + } + return NULL; + } + + if (nsize > ((size_t)-1) >> 1) return NULL; + + int memDiff = ptr ? (int)nsize - (int)osize : (int)nsize; + int newUsed = *usedMemory + memDiff; + + if (*memoryLimit > 0 && newUsed > *memoryLimit && !*ignoreLimit) { + *limitReached = true; + return NULL; + } + + *usedMemory = newUsed; + + void* newPtr; + if (ptr == NULL) { + newPtr = malloc(nsize); + if (!newPtr) abort(); + } else { + newPtr = realloc(ptr, nsize); + if (!newPtr) abort(); + } + + return newPtr; +} \ No newline at end of file diff --git a/lua/memory.go b/lua/memory.go new file mode 100644 index 0000000..eba9829 --- /dev/null +++ b/lua/memory.go @@ -0,0 +1,81 @@ +package lua + +/* +#cgo CFLAGS: -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include +#include + +void* allocator(void* ud, void* ptr, size_t osize, size_t nsize); +*/ +import "C" +import ( + "runtime" + "unsafe" + + "github.com/CompeyDev/lei/ffi" +) + +const SYS_MIN_ALIGN = unsafe.Sizeof(uintptr(0)) * 2 + +type MemoryState struct { + usedMemory int + memoryLimit int + ignoreLimit bool + limitReached bool +} + +func newMemoryState() *MemoryState { + return &MemoryState{ + usedMemory: 0, + memoryLimit: 0, + ignoreLimit: false, + limitReached: false, + } +} + +func GetMemoryState(state *ffi.LuaState) *MemoryState { + var memState unsafe.Pointer + ffi.GetAllocF(state, &memState) + + if memState == nil { + panic("Luau state has no allocator userdata") + } + + return (*MemoryState)(memState) +} + +func (m *MemoryState) UsedMemory() int { + return m.usedMemory +} + +func (m *MemoryState) MemoryLimit() int { + return m.memoryLimit +} + +func (m *MemoryState) SetMemoryLimit(limit int) int { + prevLimit := m.memoryLimit + m.memoryLimit = limit + return prevLimit +} + +func RelaxLimitWith(state *ffi.LuaState, f func()) { + memState := GetMemoryState(state) + if memState != nil { + memState.ignoreLimit = true + f() + memState.ignoreLimit = false + } else { + f() + } +} + +func LimitReached(state *ffi.LuaState) bool { + return GetMemoryState(state).limitReached +} + +func newStateWithAllocator() *ffi.LuaState { + memState := newMemoryState() + state := ffi.NewState(C.allocator, unsafe.Pointer(memState)) + runtime.KeepAlive(memState) + + return state +} diff --git a/lua/state.go b/lua/state.go new file mode 100644 index 0000000..a2fe7a2 --- /dev/null +++ b/lua/state.go @@ -0,0 +1,95 @@ +package lua + +import ( + "unsafe" + + "github.com/CompeyDev/lei/ffi" +) + +type LuaOptions struct { + collectGarbage bool + isSafe bool +} + +type Lua struct { + state *ffi.LuaState + options LuaOptions +} + +func (l *Lua) RawState() *ffi.LuaState { + return l.state +} + +func (l *Lua) CreateTable() LuaTable { + state := l.state + ffi.NewTable(state) + + return LuaTable{ + lua: l, + index: int(ffi.GetTop(state)), + } +} + +func (l *Lua) CreateString(str string) LuaString { + state := l.state + + ffi.PushString(state, str) + index := ffi.GetTop(state) + return LuaString{lua: l, index: int(index)} +} + +func New() *Lua { + return NewWith(StdLibALLSAFE, LuaOptions{ + collectGarbage: true, + isSafe: true, + }) +} + +func NewWith(libs StdLib, options LuaOptions) *Lua { + if libs.Contains(StdLibPACKAGE) { + // TODO: disable c modules for package lib + } + + state := newStateWithAllocator() + if state == nil { + panic("Failed to create Lua state") + } + + ffi.RequireLib(state, "_G", unsafe.Pointer(ffi.BaseOpener()), true) + ffi.Pop(state, 1) + + // TODO: luau jit stuff + + type Library struct { + lib StdLib + name string + } + + luaLibs := map[Library]unsafe.Pointer{ + {StdLibCOROUTINE, ffi.LUA_COLIBNAME}: unsafe.Pointer(ffi.CoroutineOpener()), + {StdLibTABLE, ffi.LUA_TABLIBNAME}: unsafe.Pointer(ffi.TableOpener()), + {StdLibOS, ffi.LUA_OSLIBNAME}: unsafe.Pointer(ffi.OsOpener()), + {StdLibSTRING, ffi.LUA_STRLIBNAME}: unsafe.Pointer(ffi.StringOpener()), + {StdLibUTF8, ffi.LUA_UTF8LIBNAME}: unsafe.Pointer(ffi.Utf8Opener()), + {StdLibBIT, ffi.LUA_BITLIBNAME}: unsafe.Pointer(ffi.Bit32Opener()), + {StdLibBUFFER, ffi.LUA_BUFFERLIBNAME}: unsafe.Pointer(ffi.BufferOpener()), + // TODO: vector lib + {StdLibMATH, ffi.LUA_MATHLIBNAME}: unsafe.Pointer(ffi.MathOpener()), + {StdLibBUFFER, ffi.LUA_DBLIBNAME}: unsafe.Pointer(ffi.DebugOpener()), + // TODO: package lib + } + + for library, open := range luaLibs { + // FIXME: check safety here maybe? + + if libs.Contains(library.lib) { + ffi.RequireLib(state, library.name, unsafe.Pointer(open), true) + } + } + + // TODO: set finalizer to collect garbage if collectGarbage = true + return &Lua{ + state: state, + options: options, + } +} diff --git a/lua/stdlib.go b/lua/stdlib.go new file mode 100644 index 0000000..0e7789c --- /dev/null +++ b/lua/stdlib.go @@ -0,0 +1,130 @@ +package lua + +// StdLib represents flags describing the set of Lua standard libraries to load. +type StdLib uint32 + +const ( + // COROUTINE library + // https://www.lua.org/manual/5.4/manual.html#6.2 + StdLibCOROUTINE StdLib = 1 << 0 + + // TABLE library + // https://www.lua.org/manual/5.4/manual.html#6.6 + StdLibTABLE StdLib = 1 << 1 + + // OS library + // https://www.lua.org/manual/5.4/manual.html#6.9 + StdLibOS StdLib = 1 << 3 + + // STRING library + // https://www.lua.org/manual/5.4/manual.html#6.4 + StdLibSTRING StdLib = 1 << 4 + + // UTF8 library + // https://www.lua.org/manual/5.4/manual.html#6.5 + StdLibUTF8 StdLib = 1 << 5 + + // BIT library + // https://www.lua.org/manual/5.2/manual.html#6.7 + StdLibBIT StdLib = 1 << 6 + + // MATH library + // https://www.lua.org/manual/5.4/manual.html#6.7 + StdLibMATH StdLib = 1 << 7 + + // PACKAGE library + // https://www.lua.org/manual/5.4/manual.html#6.3 + StdLibPACKAGE StdLib = 1 << 8 + + // BUFFER library (Luau) + // https://luau.org/library#buffer-library + StdLibBUFFER StdLib = 1 << 9 + + // VECTOR library (Luau) + // https://luau.org/library#vector-library + StdLibVECTOR StdLib = 1 << 10 + + // DEBUG library (unsafe) + // https://www.lua.org/manual/5.4/manual.html#6.10 + StdLibDEBUG StdLib = 1 << 31 + + // StdLibNONE represents no libraries + StdLibNONE StdLib = 0 + + // StdLibALL represents all standard libraries (unsafe) + StdLibALL StdLib = ^StdLib(0) // equivalent to uint32 max + + // StdLibALLSAFE represents the safe subset of standard libraries + StdLibALLSAFE StdLib = (1 << 30) - 1 +) + +func (s StdLib) Contains(lib StdLib) bool { + return (s & lib) != 0 +} + +func (s StdLib) And(lib StdLib) StdLib { + return s & lib +} + +func (s StdLib) Or(lib StdLib) StdLib { + return s | lib +} + +func (s StdLib) Xor(lib StdLib) StdLib { + return s ^ lib +} + +func (s *StdLib) Add(lib StdLib) { + *s |= lib +} + +func (s *StdLib) Remove(lib StdLib) { + *s &^= lib +} + +func (s *StdLib) Toggle(lib StdLib) { + *s ^= lib +} + +func (s StdLib) String() string { + if s == StdLibNONE { + return "NONE" + } + if s == StdLibALL { + return "ALL" + } + + var libs []string + flags := map[StdLib]string{ + StdLibCOROUTINE: "COROUTINE", + StdLibTABLE: "TABLE", + StdLibOS: "OS", + StdLibSTRING: "STRING", + StdLibUTF8: "UTF8", + StdLibBIT: "BIT", + StdLibMATH: "MATH", + StdLibPACKAGE: "PACKAGE", + StdLibBUFFER: "BUFFER", + StdLibVECTOR: "VECTOR", + StdLibDEBUG: "DEBUG", + } + + for flag, name := range flags { + if s.Contains(flag) { + libs = append(libs, name) + } + } + + if len(libs) == 0 { + return "NONE" + } + + result := "" + for i, lib := range libs { + if i > 0 { + result += "|" + } + result += lib + } + return result +} diff --git a/lua/string.go b/lua/string.go new file mode 100644 index 0000000..c93805b --- /dev/null +++ b/lua/string.go @@ -0,0 +1,34 @@ +package lua + +import ( + "unsafe" + + "github.com/CompeyDev/lei/ffi" +) + +type LuaString struct { + lua *Lua + index int +} + +func (s *LuaString) ToString() string { + state := s.lua.state + return ffi.ToString(state, int32(s.index)) +} + +func (s *LuaString) ToPointer() unsafe.Pointer { + state := s.lua.state + return ffi.ToPointer(state, int32(s.index)) +} + +// +// LuaValue Implementation +// + +func (s *LuaString) luaState() *Lua { + return s.lua +} + +func (s *LuaString) stackIndex() int { + return s.index +} diff --git a/lua/table.go b/lua/table.go new file mode 100644 index 0000000..923a089 --- /dev/null +++ b/lua/table.go @@ -0,0 +1,43 @@ +package lua + +import "github.com/CompeyDev/lei/ffi" + +type LuaTable struct { + lua *Lua + index int +} + +func (t *LuaTable) Set(key LuaValue, value LuaValue) { + state := t.lua.state + + ffi.PushValue(state, int32(key.stackIndex())) + ffi.PushValue(state, int32(value.stackIndex())) + ffi.SetTable(state, int32(t.index)) +} + +func (t *LuaTable) Get(key LuaValue) LuaValue { + state := t.lua.state + + ffi.PushValue(state, int32(key.stackIndex())) + valueType := ffi.GetTable(state, int32(t.index)) + + switch valueType { + // TODO: other types + case ffi.LUA_TSTRING: + return &LuaString{lua: t.lua, index: int(ffi.GetTop(state))} + default: + panic("Unknown type") + } +} + +// +// LuaValue Implementation +// + +func (s *LuaTable) luaState() *Lua { + return s.lua +} + +func (s *LuaTable) stackIndex() int { + return s.index +} diff --git a/lua/value.go b/lua/value.go new file mode 100644 index 0000000..a58c5f1 --- /dev/null +++ b/lua/value.go @@ -0,0 +1,15 @@ +package lua + +import ( + "github.com/CompeyDev/lei/ffi" +) + +type LuaValue interface { + luaState() *Lua + stackIndex() int +} + +func TypeName(val LuaValue) string { + lua := val.luaState() + return ffi.TypeName(lua.state, ffi.Type(lua.state, int32(val.stackIndex()))) +} From ca1fe203c87d56d7df0704d9a8dc4bae6cf0389f Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 17 Nov 2025 12:30:28 +0000 Subject: [PATCH 02/64] chore: remove references to old `internal` module --- .gitmodules | 3 --- ffi/vector4.go | 2 +- main.go | 29 +++++++++++++---------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.gitmodules b/.gitmodules index 2ea0cb6..1fcd3e2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "internal/luau"] - path = internal/luau - url = https://github.com/luau-lang/luau [submodule "ffi/luau"] path = ffi/luau url = https://github.com/luau-lang/luau.git diff --git a/ffi/vector4.go b/ffi/vector4.go index 45bd0dc..e86e2a1 100644 --- a/ffi/vector4.go +++ b/ffi/vector4.go @@ -1,6 +1,6 @@ //go:build LUAU_VECTOR4 -package internal +package ffi /* #cgo CFLAGS: -Iluau/VM/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/include -DLUA_VECTOR_SIZE=4 diff --git a/main.go b/main.go index ebc32e3..4ef808b 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,21 @@ package main -import lualib "github.com/CompeyDev/lei/ffi" +import ( + "fmt" -func main() { - lua := lualib.LNewState() - println("Lua VM Address: ", lua) + "github.com/CompeyDev/lei/lua" +) - lualib.PushCFunction(lua, func(L *lualib.LuaState) int32 { - println("hi from closure?") - return 0 - }) +func main() { + state := lua.New() + memState := lua.GetMemoryState(state.RawState()) + memState.SetMemoryLimit(1) // FIXME: this no workie? - lualib.PushString(lua, "123") - lualib.PushNumber(lua, lualib.ToNumber(lua, 2)) + table := state.CreateTable() + key, value := state.CreateString("hello"), state.CreateString("world") + table.Set(&key, &value) - if !lualib.IsCFunction(lua, 1) { - panic("CFunction was not correctly pushed onto stack") - } + fmt.Printf("Used: %d, Limit: %d\n", memState.UsedMemory(), memState.MemoryLimit()) - if !lualib.IsNumber(lua, 3) { - panic("Number was not correctly pushed onto stack") - } + fmt.Println(key.ToString(), table.Get(&key).(*lua.LuaString).ToString()) } From aa33c3d819837cf15bb1e6b32549eaf8fe3fa67e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 18 Nov 2025 06:25:38 +0000 Subject: [PATCH 03/64] refactor(lua): rewrite allocator in go & more * Rewrote the allocator purely in Go and eliminated the C implementation for correctness. Also fixes limits not being respected due to improper decoding of the Go `MemoryState` struct in C * Memory state options (such as limits) must be specified before the creation of the Lua state, using the `NewWith` constructor. A `InitMemoryState` field was added to the options, and the `NewMemoryState` function was exported for this purpose * All unsafe methods which required or provided access to the underlying Lua state have been privated (`GetMemoryState`, `Lua.RawState`) * Removed redundant naming for `MemoryState` methods which included "memory" in the name itself * Introduced `StateWithMemory` holder struct which includes a `Pinner` that prevents GC or movement of the underlying `MemoryState` struct, which would otherwise cause memory corruption * Exported all unexported fields in `LuaOptions` and stopped storing the options in the `Lua` struct for no reason * The `Lua` struct is now only a thin wrapper around the unsafe Lua state * Renamed some private fields to be nicer and suit well with their methods. Also renamed the `luaState` method for the `LuaValue` interface to be `lua` instead * Added a cleanup finalizer for `Lua` which closes the unsafe Lua state and optionally triggers a GC in case `CollectGarbage` is set to true --- lua/allocator.c | 50 ------------------ lua/memory.go | 132 ++++++++++++++++++++++++++++++++++++++++-------- lua/state.go | 56 ++++++++++++-------- lua/string.go | 10 ++-- lua/table.go | 12 ++--- lua/value.go | 10 ++-- main.go | 8 +-- 7 files changed, 164 insertions(+), 114 deletions(-) delete mode 100644 lua/allocator.c diff --git a/lua/allocator.c b/lua/allocator.c deleted file mode 100644 index c7a55a1..0000000 --- a/lua/allocator.c +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include -#include -#include - -typedef struct MemoryState MemoryState; - -void* allocator(void* ud, void* ptr, size_t osize, size_t nsize) { - MemoryState* mem = (MemoryState*)ud; - - // Convert void* to Go struct pointer using uintptr arithmetic - // SAFETY: unsafe, must keep pointer alive in Go! - int* usedMemory = (int*)((uintptr_t)mem + 0); - int* memoryLimit = (int*)((uintptr_t)mem + sizeof(int)); - bool* ignoreLimit = (bool*)((uintptr_t)mem + 2*sizeof(int)); - bool* limitReached = (bool*)((uintptr_t)mem + 2*sizeof(int) + sizeof(bool)); - - *limitReached = false; - - if (nsize == 0) { - if (ptr != NULL) { - free(ptr); - *usedMemory -= (int)osize; - } - return NULL; - } - - if (nsize > ((size_t)-1) >> 1) return NULL; - - int memDiff = ptr ? (int)nsize - (int)osize : (int)nsize; - int newUsed = *usedMemory + memDiff; - - if (*memoryLimit > 0 && newUsed > *memoryLimit && !*ignoreLimit) { - *limitReached = true; - return NULL; - } - - *usedMemory = newUsed; - - void* newPtr; - if (ptr == NULL) { - newPtr = malloc(nsize); - if (!newPtr) abort(); - } else { - newPtr = realloc(ptr, nsize); - if (!newPtr) abort(); - } - - return newPtr; -} \ No newline at end of file diff --git a/lua/memory.go b/lua/memory.go index eba9829..64dbf99 100644 --- a/lua/memory.go +++ b/lua/memory.go @@ -3,8 +3,9 @@ package lua /* #cgo CFLAGS: -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include #include +#include -void* allocator(void* ud, void* ptr, size_t osize, size_t nsize); +extern void* allocator(void* ud, void* ptr, size_t osize, size_t nsize); */ import "C" import ( @@ -23,7 +24,7 @@ type MemoryState struct { limitReached bool } -func newMemoryState() *MemoryState { +func NewMemoryState() *MemoryState { return &MemoryState{ usedMemory: 0, memoryLimit: 0, @@ -32,33 +33,22 @@ func newMemoryState() *MemoryState { } } -func GetMemoryState(state *ffi.LuaState) *MemoryState { - var memState unsafe.Pointer - ffi.GetAllocF(state, &memState) - - if memState == nil { - panic("Luau state has no allocator userdata") - } - - return (*MemoryState)(memState) -} - -func (m *MemoryState) UsedMemory() int { +func (m *MemoryState) Used() int { return m.usedMemory } -func (m *MemoryState) MemoryLimit() int { +func (m *MemoryState) Limit() int { return m.memoryLimit } -func (m *MemoryState) SetMemoryLimit(limit int) int { +func (m *MemoryState) SetLimit(limit int) int { prevLimit := m.memoryLimit m.memoryLimit = limit return prevLimit } func RelaxLimitWith(state *ffi.LuaState, f func()) { - memState := GetMemoryState(state) + memState := getMemoryState(state) if memState != nil { memState.ignoreLimit = true f() @@ -69,13 +59,111 @@ func RelaxLimitWith(state *ffi.LuaState, f func()) { } func LimitReached(state *ffi.LuaState) bool { - return GetMemoryState(state).limitReached + return getMemoryState(state).limitReached } -func newStateWithAllocator() *ffi.LuaState { - memState := newMemoryState() - state := ffi.NewState(C.allocator, unsafe.Pointer(memState)) +func getMemoryState(state *ffi.LuaState) *MemoryState { + var memState unsafe.Pointer + ffi.GetAllocF(state, &memState) + + if memState == nil { + panic("Lua state has no allocator userdata") + } + + return (*MemoryState)(memState) +} + +//export allocator +func allocator(ud, ptr unsafe.Pointer, osize, nsize C.size_t) unsafe.Pointer { + memState := (*MemoryState)(ud) + + // Avoid GC of pointer for this call period runtime.KeepAlive(memState) + memState.limitReached = false + + // Free memory + if nsize == 0 { + if ptr != nil { + C.free(ptr) + memState.usedMemory -= int(osize) + } + return nil + } + + if nsize > C.size_t(^uint(0)>>1) { + return nil + } + + var memDiff int + if ptr != nil { + memDiff = int(nsize) - int(osize) + } else { + memDiff = int(nsize) + } + + memLimit := memState.memoryLimit + newUsedMemory := memState.usedMemory + memDiff + if memLimit > 0 && newUsedMemory > memLimit && !memState.ignoreLimit { + memState.limitReached = true + panic("allocations exceeded set limit for memory") + } + memState.usedMemory = newUsedMemory + + var newPtr unsafe.Pointer + if ptr == nil { + newPtr = C.malloc(nsize) + if newPtr == nil { + panic("memory allocation failed") + } + } else { + newPtr = C.realloc(ptr, nsize) + if newPtr == nil { + panic("memory reallocation failed") + } + } + + return newPtr +} + +type StateWithMemory struct { + luaState *ffi.LuaState + memState *MemoryState + pinner *runtime.Pinner +} + +func newStateWithAllocator(initState *MemoryState) *StateWithMemory { + var memState *MemoryState + if initState != nil { + memState = initState + } else { + memState = NewMemoryState() + } + + // Pin the memory state to prevent GC from moving it + pinner := &runtime.Pinner{} + pinner.Pin(memState) + + state := ffi.NewState(C.allocator, unsafe.Pointer(memState)) + + return &StateWithMemory{ + luaState: state, + memState: memState, + pinner: pinner, + } +} + +func (s *StateWithMemory) LuaState() *ffi.LuaState { + return s.luaState +} + +func (s *StateWithMemory) MemState() *MemoryState { + return s.memState +} + +func (s *StateWithMemory) Close() { + if s.pinner != nil { + s.pinner.Unpin() + } - return state + ffi.LuaClose(s.luaState) } diff --git a/lua/state.go b/lua/state.go index a2fe7a2..7d26552 100644 --- a/lua/state.go +++ b/lua/state.go @@ -1,47 +1,56 @@ package lua import ( + "runtime" "unsafe" "github.com/CompeyDev/lei/ffi" ) type LuaOptions struct { - collectGarbage bool - isSafe bool + InitMemoryState *MemoryState + CollectGarbage bool + IsSafe bool } type Lua struct { - state *ffi.LuaState - options LuaOptions + inner *StateWithMemory } -func (l *Lua) RawState() *ffi.LuaState { - return l.state +func (l *Lua) Memory() *MemoryState { + return l.inner.MemState() } func (l *Lua) CreateTable() LuaTable { - state := l.state + state := l.inner.luaState ffi.NewTable(state) return LuaTable{ - lua: l, + vm: l, index: int(ffi.GetTop(state)), } } func (l *Lua) CreateString(str string) LuaString { - state := l.state + state := l.inner.luaState ffi.PushString(state, str) index := ffi.GetTop(state) - return LuaString{lua: l, index: int(index)} + return LuaString{vm: l, index: int(index)} +} + +func (l *Lua) Close() { + l.inner.Close() +} + +func (l *Lua) state() *ffi.LuaState { + return l.inner.luaState } func New() *Lua { return NewWith(StdLibALLSAFE, LuaOptions{ - collectGarbage: true, - isSafe: true, + CollectGarbage: true, + IsSafe: true, }) } @@ -50,13 +59,13 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { // TODO: disable c modules for package lib } - state := newStateWithAllocator() + state := newStateWithAllocator(options.InitMemoryState) if state == nil { panic("Failed to create Lua state") } - ffi.RequireLib(state, "_G", unsafe.Pointer(ffi.BaseOpener()), true) - ffi.Pop(state, 1) + ffi.RequireLib(state.luaState, "_G", unsafe.Pointer(ffi.BaseOpener()), true) + ffi.Pop(state.luaState, 1) // TODO: luau jit stuff @@ -83,13 +92,18 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { // FIXME: check safety here maybe? if libs.Contains(library.lib) { - ffi.RequireLib(state, library.name, unsafe.Pointer(open), true) + ffi.RequireLib(state.luaState, library.name, unsafe.Pointer(open), true) } } - // TODO: set finalizer to collect garbage if collectGarbage = true - return &Lua{ - state: state, - options: options, - } + lua := &Lua{inner: state} + runtime.SetFinalizer(lua, func(l *Lua) { + if options.CollectGarbage { + ffi.LuaGc(l.state(), ffi.LUA_GCCOLLECT, 0) + } + + l.Close() + }) + + return lua } diff --git a/lua/string.go b/lua/string.go index c93805b..1956cbf 100644 --- a/lua/string.go +++ b/lua/string.go @@ -7,17 +7,17 @@ import ( ) type LuaString struct { - lua *Lua + vm *Lua index int } func (s *LuaString) ToString() string { - state := s.lua.state + state := s.vm.state() return ffi.ToString(state, int32(s.index)) } func (s *LuaString) ToPointer() unsafe.Pointer { - state := s.lua.state + state := s.vm.state() return ffi.ToPointer(state, int32(s.index)) } @@ -25,8 +25,8 @@ func (s *LuaString) ToPointer() unsafe.Pointer { // LuaValue Implementation // -func (s *LuaString) luaState() *Lua { - return s.lua +func (s *LuaString) lua() *Lua { + return s.vm } func (s *LuaString) stackIndex() int { diff --git a/lua/table.go b/lua/table.go index 923a089..93f55fb 100644 --- a/lua/table.go +++ b/lua/table.go @@ -3,12 +3,12 @@ package lua import "github.com/CompeyDev/lei/ffi" type LuaTable struct { - lua *Lua + vm *Lua index int } func (t *LuaTable) Set(key LuaValue, value LuaValue) { - state := t.lua.state + state := t.vm.state() ffi.PushValue(state, int32(key.stackIndex())) ffi.PushValue(state, int32(value.stackIndex())) @@ -16,7 +16,7 @@ func (t *LuaTable) Set(key LuaValue, value LuaValue) { } func (t *LuaTable) Get(key LuaValue) LuaValue { - state := t.lua.state + state := t.vm.state() ffi.PushValue(state, int32(key.stackIndex())) valueType := ffi.GetTable(state, int32(t.index)) @@ -24,7 +24,7 @@ func (t *LuaTable) Get(key LuaValue) LuaValue { switch valueType { // TODO: other types case ffi.LUA_TSTRING: - return &LuaString{lua: t.lua, index: int(ffi.GetTop(state))} + return &LuaString{vm: t.vm, index: int(ffi.GetTop(state))} default: panic("Unknown type") } @@ -34,8 +34,8 @@ func (t *LuaTable) Get(key LuaValue) LuaValue { // LuaValue Implementation // -func (s *LuaTable) luaState() *Lua { - return s.lua +func (s *LuaTable) lua() *Lua { + return s.vm } func (s *LuaTable) stackIndex() int { diff --git a/lua/value.go b/lua/value.go index a58c5f1..16cfa02 100644 --- a/lua/value.go +++ b/lua/value.go @@ -1,15 +1,13 @@ package lua -import ( - "github.com/CompeyDev/lei/ffi" -) +import "github.com/CompeyDev/lei/ffi" type LuaValue interface { - luaState() *Lua + lua() *Lua stackIndex() int } func TypeName(val LuaValue) string { - lua := val.luaState() - return ffi.TypeName(lua.state, ffi.Type(lua.state, int32(val.stackIndex()))) + lua := val.lua().state() + return ffi.TypeName(lua, ffi.Type(lua, int32(val.stackIndex()))) } diff --git a/main.go b/main.go index 4ef808b..5f20bda 100644 --- a/main.go +++ b/main.go @@ -7,15 +7,15 @@ import ( ) func main() { - state := lua.New() - memState := lua.GetMemoryState(state.RawState()) - memState.SetMemoryLimit(1) // FIXME: this no workie? + mem := lua.NewMemoryState() + mem.SetLimit(250 * 1024) // 250KB max + state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem}) table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("world") table.Set(&key, &value) - fmt.Printf("Used: %d, Limit: %d\n", memState.UsedMemory(), memState.MemoryLimit()) + fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) fmt.Println(key.ToString(), table.Get(&key).(*lua.LuaString).ToString()) } From 7e911f1da8700014a8f838f432e34a89d46a1aed Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 20 Nov 2025 07:15:09 +0000 Subject: [PATCH 04/64] fix(ffi/lua): `LuauLoad` segfault due to use-after-free We were defering a free of the bytecode too early. `luau_load` expects the bytecode to be held for as long as it is executing. Also handled ahe case for if the size of the bytecode is zero, for which case we send a NULL pointer to `luau_load` instead. --- ffi/lua.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index f8884c3..3692dda 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -538,12 +538,19 @@ func Setfenv(L *LuaState, idx int32) int32 { // ========================= // -func LuauLoad(L *LuaState, chunkname string, data string, size uint64, env int32) int32 { +func LuauLoad(L *LuaState, chunkname string, data []byte, size uint64, env int32) int32 { cchunkname := C.CString(chunkname) defer C.free(unsafe.Pointer(cchunkname)) - cdata := C.CString(data) - defer C.free(unsafe.Pointer(cdata)) + var cdata *C.char + if size == 0 { + // NULL for empty slices + cdata = (*C.char)(C.NULL) + } else { + cdata = (*C.char)(unsafe.Pointer(&data[0])) + } + + // NOTE: We don't free the bytecode after it's loaded return int32(C.luau_load(L, cchunkname, cdata, C.size_t(size), C.int(env))) } From 2b15b5b82a4364ef39ce759c66bbcf0af6dfcb39 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 20 Nov 2025 07:17:45 +0000 Subject: [PATCH 05/64] feat(ffi): add `luacode` compiler bindings --- ffi/luacode.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 ffi/luacode.go diff --git a/ffi/luacode.go b/ffi/luacode.go new file mode 100644 index 0000000..5060020 --- /dev/null +++ b/ffi/luacode.go @@ -0,0 +1,161 @@ +package ffi + +/* +#cgo CFLAGS: -Iluau/Compiler/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include +#cgo LDFLAGS: -Lluau/cmake -lLuau.Compiler -lLuau.Ast -lm -lstdc++ +#include +#include +*/ +import "C" +import "unsafe" + +type CompileConstant *C.void + +type CompileOptions struct { + OptimizationLevel int + DebugLevel int + TypeInfoLevel int + CoverageLevel int + + VectorLib string + VectorCtor string + VectorType string + + MutableGlobals []string + UserdataTypes []string + + LibrariesWithKnownMembers []string + LibraryMemberTypeCb unsafe.Pointer + LibraryMemberConstantCb unsafe.Pointer + + DisabledBuiltins []string +} + +func LuauCompile(source string, size int, options *CompileOptions, outsize *int) []byte { + var goArrToC = func(goArr []string) **C.char { + if len(goArr) == 0 { + return nil + } + + // Allocate space for N+1 pointers (extra for NULL terminator) + arr := C.malloc(C.size_t(len(goArr)+1) * C.size_t(unsafe.Sizeof(uintptr(0)))) + slice := (*[1 << 30]*C.char)(arr)[: len(goArr)+1 : len(goArr)+1] + + for i, s := range goArr { + slice[i] = C.CString(s) + } + slice[len(goArr)] = nil // NULL terminator + return (**C.char)(arr) + } + + var freeCArr = func(arr **C.char) { + if arr == nil { + return + } + // Free strings until we hit NULL + for i := 0; ; i++ { + ptr := *(**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(arr)) + uintptr(i)*unsafe.Sizeof(uintptr(0)))) + if ptr == nil { + break + } + C.free(unsafe.Pointer(ptr)) + } + C.free(unsafe.Pointer(arr)) + } + + csource := C.CString(source) + coutsize := C.size_t(*outsize) + coptions := (*C.lua_CompileOptions)(C.malloc(C.sizeof_lua_CompileOptions)) + + coptions.optimizationLevel = C.int(options.OptimizationLevel) + coptions.debugLevel = C.int(options.DebugLevel) + coptions.typeInfoLevel = C.int(options.TypeInfoLevel) + coptions.coverageLevel = C.int(options.CoverageLevel) + + coptions.vectorLib = C.CString(options.VectorLib) + coptions.vectorCtor = C.CString(options.VectorCtor) + coptions.vectorType = C.CString(options.VectorType) + + coptions.mutableGlobals = goArrToC(options.MutableGlobals) + coptions.userdataTypes = goArrToC(options.UserdataTypes) + coptions.librariesWithKnownMembers = goArrToC(options.LibrariesWithKnownMembers) + + coptions.libraryMemberTypeCb = C.lua_LibraryMemberTypeCallback(options.LibraryMemberTypeCb) + coptions.libraryMemberConstantCb = C.lua_LibraryMemberConstantCallback(options.LibraryMemberConstantCb) + + coptions.disabledBuiltins = goArrToC(options.DisabledBuiltins) + + defer C.free(unsafe.Pointer(csource)) + defer C.free(unsafe.Pointer(coptions.vectorLib)) + defer C.free(unsafe.Pointer(coptions.vectorCtor)) + defer C.free(unsafe.Pointer(coptions.vectorType)) + defer C.free(unsafe.Pointer(coptions)) + + defer freeCArr(coptions.mutableGlobals) + defer freeCArr(coptions.userdataTypes) + defer freeCArr(coptions.librariesWithKnownMembers) + defer freeCArr(coptions.disabledBuiltins) + + bytecode := C.luau_compile(csource, C.size_t(size), coptions, &coutsize) + defer C.free(unsafe.Pointer(bytecode)) + + *outsize = int(coutsize) + result := make([]byte, coutsize) + + copy(result, (*[1 << 30]byte)(unsafe.Pointer(bytecode))[:coutsize:coutsize]) + + return result +} + +func LuauSetCompileConstantNil(constant unsafe.Pointer) { + C.luau_set_compile_constant_nil((*C.lua_CompileConstant)(constant)) +} + +func LuauSetCompileConstantBoolean(constant unsafe.Pointer, b bool) { + var cBool C.int + if b { + cBool = 1 + } else { + cBool = 0 + } + C.luau_set_compile_constant_boolean((*C.lua_CompileConstant)(constant), cBool) +} + +func LuauSetCompileConstantNumber(constant unsafe.Pointer, n float64) { + C.luau_set_compile_constant_number((*C.lua_CompileConstant)(constant), C.double(n)) +} + +func LuauSetCompileConstantVector(constant unsafe.Pointer, x, y, z, w float32) { + C.luau_set_compile_constant_vector( + (*C.lua_CompileConstant)(constant), + C.float(x), + C.float(y), + C.float(z), + C.float(w), + ) +} + +func LuauSetCompileConstantString(constant unsafe.Pointer, s string) { + if len(s) == 0 { + C.luau_set_compile_constant_string((*C.lua_CompileConstant)(constant), nil, 0) + return + } + + bytes := []byte(s) + ptr := (*C.char)(unsafe.Pointer(&bytes[0])) + size := C.size_t(len(s)) + + C.luau_set_compile_constant_string((*C.lua_CompileConstant)(constant), ptr, size) +} + +func LuauSetCompileConstantStringBytes(constant unsafe.Pointer, data []byte) { + if len(data) == 0 { + C.luau_set_compile_constant_string((*C.lua_CompileConstant)(constant), nil, 0) + return + } + + ptr := (*C.char)(unsafe.Pointer(&data[0])) + size := C.size_t(len(data)) + + C.luau_set_compile_constant_string((*C.lua_CompileConstant)(constant), ptr, size) +} From 65e206e05f60b6c04bb7970e31c20be98a9a9605 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 20 Nov 2025 07:18:11 +0000 Subject: [PATCH 06/64] chore(ffi): include tests for luacode compiler bindings --- ffi/luacode_test.go | 151 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 ffi/luacode_test.go diff --git a/ffi/luacode_test.go b/ffi/luacode_test.go new file mode 100644 index 0000000..b46e267 --- /dev/null +++ b/ffi/luacode_test.go @@ -0,0 +1,151 @@ +package ffi_test + +import ( + "slices" + "testing" + + "github.com/CompeyDev/lei/ffi" +) + +func TestLuauCompile_Basic(t *testing.T) { + source := ` + local function add(a, b) + return a + b + end + return add(1, 2) + ` + + outsize := 0 + options := &ffi.CompileOptions{ + OptimizationLevel: 1, + DebugLevel: 1, + TypeInfoLevel: 0, + CoverageLevel: 0, + } + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + + if bytecode == nil { + t.Fatal("LuauCompile returned nil") + } + + if outsize == 0 { + t.Fatal("Output size is 0") + } + + if len(bytecode) != outsize { + t.Errorf("Expected bytecode length %d, got %d", outsize, len(bytecode)) + } + + t.Logf("Compiled successfully: %d bytes", outsize) +} + +func TestLuauCompile_SyntaxError(t *testing.T) { + source := ` + local function broken( + -- missing closing parenthesis and end + ` + + outsize := 0 + options := &ffi.CompileOptions{ + OptimizationLevel: 1, + DebugLevel: 1, + } + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + + // The function should still return bytecode containing the error + if bytecode == nil { + t.Fatal("LuauCompile returned nil even for error case") + } + + t.Logf("Error bytecode: %d bytes", outsize) +} + +func TestLuauCompile_WithOptions(t *testing.T) { + source := ` + local x = vector.create(1, 2, 3) + return x + ` + + outsize := 0 + options := &ffi.CompileOptions{ + OptimizationLevel: 2, + DebugLevel: 2, + TypeInfoLevel: 1, + CoverageLevel: 1, + VectorLib: "vector", + VectorCtor: "create", + VectorType: "vector", + MutableGlobals: []string{"_G"}, + UserdataTypes: []string{"MyUserdata"}, + DisabledBuiltins: []string{"math.random"}, + } + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + + if bytecode == nil { + t.Fatal("LuauCompile returned nil") + } + + if outsize == 0 { + t.Fatal("Output size is 0") + } + + t.Logf("Compiled with options: %d bytes", outsize) +} + +func TestLuauCompile_EmptySource(t *testing.T) { + source := "" + outsize := 0 + options := &ffi.CompileOptions{ + OptimizationLevel: 1, + DebugLevel: 1, + } + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + + if bytecode == nil { + t.Fatal("LuauCompile returned nil for empty source") + } + + t.Logf("Empty source compiled: %d bytes", outsize) +} + +func TestLuauCompile_BinaryDataIntegrity(t *testing.T) { + source := `return "test"` + outsize := 0 + options := &ffi.CompileOptions{ + OptimizationLevel: 1, + DebugLevel: 1, + } + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + hasNullByte := slices.Contains(bytecode, 0) + + t.Logf("Bytecode contains null bytes: %v", hasNullByte) + t.Logf("Bytecode length: %d, outsize: %d", len(bytecode), outsize) + + if len(bytecode) != outsize { + t.Errorf("Bytecode length mismatch: expected %d, got %d", outsize, len(bytecode)) + } +} + +func TestLuauCompile_ExecuteBytecode(t *testing.T) { + source := `return 42` + outsize := 0 + options := &ffi.CompileOptions{OptimizationLevel: 1, DebugLevel: 1} + + bytecode := ffi.LuauCompile(source, len(source), options, &outsize) + + L := ffi.LNewState() + defer ffi.LuaClose(L) + + ffi.LuauLoad(L, "test", bytecode, uint64(outsize), 0) + ffi.Pcall(L, 0, 1, 0) + + result := ffi.ToInteger(L, -1) + if result != 42 { + t.Error("Executed result did not match") + } +} From 1f30e0894ac15277c4cec95e81e2dd18f339f8cb Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 22 Nov 2025 08:17:33 +0000 Subject: [PATCH 07/64] fix(ffi/lua): zero-indexed values for `lua_Status` and `lua_CoStatus` --- ffi/lua.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index 3692dda..afa3c6d 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -48,7 +48,7 @@ const ( // const ( - LUA_OK = iota + 1 + LUA_OK = iota LUA_YIELD LUA_ERRRUN LUA_ERRSYNTAX @@ -64,7 +64,7 @@ const ( // const ( - LUA_CORUN = iota + 1 + LUA_CORUN = iota LUA_COSUS LUA_CONOR LUA_COFIN From a1144d33dbbd1c02314ba3622af060d52caabb3c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 22 Nov 2025 08:18:31 +0000 Subject: [PATCH 08/64] feat(ffi): minimal `Bytecode.h` bindings exporting version consts --- ffi/bytecode.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 ffi/bytecode.go diff --git a/ffi/bytecode.go b/ffi/bytecode.go new file mode 100644 index 0000000..1940fee --- /dev/null +++ b/ffi/bytecode.go @@ -0,0 +1,23 @@ +package ffi + +/* +#cgo CFLAGS: -Iluau/Common/include +#include "Luau/Bytecode.h" + +enum LuauBytecodeTag LuauBytecodeTag; +*/ +import "C" + +// +// Version Constants +// + +const ( + LBC_VERSION_MIN = C.LBC_VERSION_MIN + LBC_VERSION_MAX = C.LBC_VERSION_MAX + LBC_VERSION_TARGET = C.LBC_VERSION_TARGET + + LBC_TYPE_VERSION_MIN = C.LBC_TYPE_VERSION_MIN + LBC_TYPE_VERSION_MAX = C.LBC_TYPE_VERSION_MAX + LBC_TYPE_VERSION_TARGET = C.LBC_TYPE_VERSION_TARGET +) From 99593104a07a71dd2d961b0fc6b8fba588d2ac11 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 22 Nov 2025 08:29:03 +0000 Subject: [PATCH 09/64] feat(lua): add high-level execution API * Added `Compiler` struct as a safe abstraction around the Luau bytecode compiler. Returns structured `SyntaxError`s from `Compiler.Compile` * Allowed specifying a specific `Compiler` for `Lua` to use using state creation, and using `Lua.SetCompiler` * Added `Lua.Execute` to load and run a chunk of bytecode or source code and return its results, if any. Returns structured `LoadError`s --- lua/compiler.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ lua/errors.go | 38 +++++++++++++++++ lua/state.go | 84 +++++++++++++++++++++++++++++++++++- main.go | 11 ++++- 4 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 lua/compiler.go create mode 100644 lua/errors.go diff --git a/lua/compiler.go b/lua/compiler.go new file mode 100644 index 0000000..fbab52d --- /dev/null +++ b/lua/compiler.go @@ -0,0 +1,111 @@ +package lua + +import "C" +import ( + "github.com/CompeyDev/lei/ffi" +) + +type Compiler struct{ options *ffi.CompileOptions } + +func (c *Compiler) WithOptimizationLevel(lvl int) *Compiler { + c.options.OptimizationLevel = lvl + return c +} + +func (c *Compiler) WithDebugLevel(lvl int) *Compiler { + c.options.DebugLevel = lvl + return c +} + +func (c *Compiler) WithTypeInfoLevel(lvl int) *Compiler { + c.options.TypeInfoLevel = lvl + return c +} + +func (c *Compiler) WithCoverageLevel(lvl int) *Compiler { + c.options.CoverageLevel = lvl + return c +} + +func (c *Compiler) WithMutableGlobals(globals []string) *Compiler { + c.options.MutableGlobals = append(c.options.MutableGlobals, globals...) + return c +} + +func (c *Compiler) WithUserdataTypes(types []string) *Compiler { + c.options.UserdataTypes = append(c.options.UserdataTypes, types...) + return c +} + +func (c *Compiler) WithConstantLibraries(libs []string) *Compiler { + c.options.LibrariesWithKnownMembers = append(c.options.LibrariesWithKnownMembers, libs...) + return c +} + +func (c *Compiler) WithDisabledBuiltins(builtins []string) *Compiler { + c.options.DisabledBuiltins = append(c.options.DisabledBuiltins, builtins...) + return c +} + +func (c *Compiler) Compile(source string) ([]byte, error) { + outsize := 0 + bytecode := ffi.LuauCompile(source, len(source), c.options, &outsize) + + // Check for compilation error + // If bytecode starts with 0, the rest is an error message starting with ':' + // See https://github.com/luau-lang/luau/blob/0.671/Compiler/src/Compiler.cpp#L4410 + if outsize > 0 && bytecode[0] == 0 { + // Extract error message (skip the 0 byte and ':' character) + message := "" + if outsize > 2 { + message = string(bytecode[2:]) + } + + // Check if input is incomplete (ends with ) + incompleteInput := len(message) > 0 && + (len(message) >= 5 && message[len(message)-5:] == "") + + return nil, &SyntaxError{ + IncompleteInput: incompleteInput, + Message: message, + } + } + + return bytecode, nil +} + +func DefaultCompiler() *Compiler { + return &Compiler{options: &ffi.CompileOptions{ + OptimizationLevel: 1, + DebugLevel: 1, + TypeInfoLevel: 0, + CoverageLevel: 0, + MutableGlobals: make([]string, 0), + }} +} + +type SyntaxError struct { + IncompleteInput bool + Message string +} + +func (e *SyntaxError) Error() string { + if e.IncompleteInput { + return "incomplete input: " + e.Message + } + + return "syntax error: " + e.Message +} + +func isBytecode(data []byte) bool { + // Luau bytecode starts with a version byte (currently 0-5 range) + // See: https://github.com/luau-lang/luau/blob/0.671/Compiler/src/BytecodeBuilder.cpp#L13 + if len(data) == 0 { + return false + } + + // Check if the first byte is within the bytecode versionByte range (source code starting with + // these bytes would be extremely rare) + versionByte := data[0] + return versionByte >= ffi.LBC_VERSION_MIN && versionByte <= ffi.LBC_VERSION_MAX +} diff --git a/lua/errors.go b/lua/errors.go new file mode 100644 index 0000000..d367d4c --- /dev/null +++ b/lua/errors.go @@ -0,0 +1,38 @@ +package lua + +import ( + "fmt" + + "github.com/CompeyDev/lei/ffi" +) + +type LoadError struct { + Code int + Message string +} + +func (e *LoadError) Error() string { + switch e.Code { + case ffi.LUA_ERRSYNTAX: + return "syntax error: " + e.Message + case ffi.LUA_ERRMEM: + return "memory allocation error: " + e.Message + case ffi.LUA_ERRERR: + return "error handler error: " + e.Message + default: + return fmt.Sprintf("load error (code %d): %s", e.Code, e.Message) + } +} + +func newLoadError(state *ffi.LuaState, code int) *LoadError { + if code != ffi.LUA_OK { + message := ffi.ToString(state, -1) + err := &LoadError{Code: code, Message: message} + + ffi.Pop(state, 1) + + return err + } + + return nil +} diff --git a/lua/state.go b/lua/state.go index 7d26552..87df022 100644 --- a/lua/state.go +++ b/lua/state.go @@ -11,10 +11,80 @@ type LuaOptions struct { InitMemoryState *MemoryState CollectGarbage bool IsSafe bool + Compiler *Compiler } type Lua struct { - inner *StateWithMemory + inner *StateWithMemory + compiler *Compiler +} + +func (l *Lua) Execute(name string, input []byte) ([]any, error) { + // TODO: create a load function which doesnt execute + + state := l.inner.luaState + initialStack := ffi.GetTop(state) // Track initial stack size + + if !isBytecode(input) { + bytecode, err := l.compiler.Compile(string(input)) + if err != nil { + return nil, err + } + + input = bytecode + } + + loadResult := ffi.LuauLoad(state, name, input, uint64(len(input)), 0) + loadErr := newLoadError(state, int(loadResult)) + + if loadErr != nil { + return nil, loadErr + } + + execResult := ffi.Pcall(state, 0, -1, 0) + execErr := newLoadError(state, int(execResult)) + + if execErr != nil { + return nil, execErr + } + + stackNow := ffi.GetTop(state) + resultsCount := stackNow - initialStack + + if resultsCount == 0 { + return nil, nil + } + + // TODO: contemplate whether to return LuaValues or go values + results := make([]any, resultsCount) + for i := range resultsCount { + // The stack has grown by the number of returns of the chunk from the + // initial value tracked at the beginning. We add one to that due to + // Lua's 1-based indexing system + stackIndex := int32(initialStack + i + 1) + luaType := ffi.Type(state, stackIndex) + + switch luaType { + case ffi.LUA_TNIL: + results[i] = nil + case ffi.LUA_TBOOLEAN: + results[i] = ffi.ToBoolean(state, stackIndex) + case ffi.LUA_TNUMBER: + results[i] = ffi.ToNumber(state, stackIndex) + case ffi.LUA_TSTRING: + results[i] = ffi.ToString(state, stackIndex) + case ffi.LUA_TTABLE: + results[i] = "" + case ffi.LUA_TFUNCTION: + results[i] = "" + default: + results[i] = "" + } + } + + ffi.Pop(state, resultsCount) + + return results, nil } func (l *Lua) Memory() *MemoryState { @@ -39,6 +109,10 @@ func (l *Lua) CreateString(str string) LuaString { return LuaString{vm: l, index: int(index)} } +func (l *Lua) SetCompiler(compiler *Compiler) { + l.compiler = compiler +} + func (l *Lua) Close() { l.inner.Close() } @@ -51,6 +125,7 @@ func New() *Lua { return NewWith(StdLibALLSAFE, LuaOptions{ CollectGarbage: true, IsSafe: true, + Compiler: DefaultCompiler(), }) } @@ -96,7 +171,12 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { } } - lua := &Lua{inner: state} + compiler := options.Compiler + if compiler == nil { + compiler = DefaultCompiler() + } + + lua := &Lua{inner: state, compiler: compiler} runtime.SetFinalizer(lua, func(l *Lua) { if options.CollectGarbage { ffi.LuaGc(l.state(), ffi.LUA_GCCOLLECT, 0) diff --git a/main.go b/main.go index 5f20bda..212931e 100644 --- a/main.go +++ b/main.go @@ -8,14 +8,21 @@ import ( func main() { mem := lua.NewMemoryState() - mem.SetLimit(250 * 1024) // 250KB max + // mem.SetLimit(250 * 1024) // 250KB max state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem}) table := state.CreateTable() - key, value := state.CreateString("hello"), state.CreateString("world") + key, value := state.CreateString("hello"), state.CreateString("lei") table.Set(&key, &value) fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) fmt.Println(key.ToString(), table.Get(&key).(*lua.LuaString).ToString()) + + values, err := state.Execute("main", []byte("print('hello, lei!'); return 1, 2, 3")) + if err != nil { + fmt.Println(err) + } + + fmt.Println(values...) } From cdced2637d2a38d159205cca27c5ad9d4eed0408 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 22 Nov 2025 08:48:45 +0000 Subject: [PATCH 10/64] fix(lua/compiler): initialize missed empty arrays --- lua/compiler.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lua/compiler.go b/lua/compiler.go index fbab52d..1edb9cb 100644 --- a/lua/compiler.go +++ b/lua/compiler.go @@ -76,11 +76,14 @@ func (c *Compiler) Compile(source string) ([]byte, error) { func DefaultCompiler() *Compiler { return &Compiler{options: &ffi.CompileOptions{ - OptimizationLevel: 1, - DebugLevel: 1, - TypeInfoLevel: 0, - CoverageLevel: 0, - MutableGlobals: make([]string, 0), + OptimizationLevel: 1, + DebugLevel: 1, + TypeInfoLevel: 0, + CoverageLevel: 0, + MutableGlobals: make([]string, 0), + UserdataTypes: make([]string, 0), + LibrariesWithKnownMembers: make([]string, 0), + DisabledBuiltins: make([]string, 0), }} } From b539fd3851d597485c34d3e49e42384390b0d8ca Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 25 Nov 2025 12:58:03 +0000 Subject: [PATCH 11/64] refactor(ffi/lua): use cgo exports for pseudo indices --- ffi/lua.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index afa3c6d..3eb7e51 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -36,9 +36,9 @@ const LUA_MULTRET = -1 // const ( - LUA_REGISTRYINDEX = -LUAI_MAXCSTACK - 2000 - LUA_ENVIRONINDEX = -LUAI_MAXCSTACK - 2001 - LUA_GLOBALSINDEX = -LUAI_MAXCSTACK - 2002 + LUA_REGISTRYINDEX = C.LUA_REGISTRYINDEX + LUA_ENVIRONINDEX = C.LUA_ENVIRONINDEX + LUA_GLOBALSINDEX = C.LUA_GLOBALSINDEX ) // From 00de5eba2c913902209451881da0bf57c88d5785 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 25 Nov 2025 13:01:49 +0000 Subject: [PATCH 12/64] feat(lua): use registry to safely store index handles for types * Implement nil type with opaque handle to 0 * Add `deref` method to `LuaValue` interface, which supposed to be the index to type located stack after pushing it from the registry * Make `Lua.Execute` return `LuaValue`s * Make `Create`* functions in `Lua` return pointers (so that we can have finalizers that remove them from the registry in the future) * Implement `LuaTable.Iterable()` to convert a table into a iterable `map[LuaValue]LuaValue` * Implement `As[T]` to convert a `LuaValue` into a go type (currently supporting tables to structs or maps, nil, and strings) --- lua/nil.go | 11 +++++ lua/state.go | 45 ++++++++----------- lua/string.go | 23 ++++++---- lua/table.go | 53 +++++++++++++++-------- lua/value.go | 117 ++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 40 ++++++++++++++--- 6 files changed, 230 insertions(+), 59 deletions(-) create mode 100644 lua/nil.go diff --git a/lua/nil.go b/lua/nil.go new file mode 100644 index 0000000..e69b3ff --- /dev/null +++ b/lua/nil.go @@ -0,0 +1,11 @@ +package lua + +type LuaNil struct{ vm *Lua } + +// +// LuaValue Implementation +// + +func (n *LuaNil) lua() *Lua { return n.vm } +func (n *LuaNil) ref() int { return 0 } +func (n *LuaNil) deref() int { return 0 } diff --git a/lua/state.go b/lua/state.go index 87df022..975eb0b 100644 --- a/lua/state.go +++ b/lua/state.go @@ -19,7 +19,7 @@ type Lua struct { compiler *Compiler } -func (l *Lua) Execute(name string, input []byte) ([]any, error) { +func (l *Lua) Execute(name string, input []byte) ([]LuaValue, error) { // TODO: create a load function which doesnt execute state := l.inner.luaState @@ -56,30 +56,13 @@ func (l *Lua) Execute(name string, input []byte) ([]any, error) { } // TODO: contemplate whether to return LuaValues or go values - results := make([]any, resultsCount) + results := make([]LuaValue, resultsCount) for i := range resultsCount { // The stack has grown by the number of returns of the chunk from the // initial value tracked at the beginning. We add one to that due to // Lua's 1-based indexing system stackIndex := int32(initialStack + i + 1) - luaType := ffi.Type(state, stackIndex) - - switch luaType { - case ffi.LUA_TNIL: - results[i] = nil - case ffi.LUA_TBOOLEAN: - results[i] = ffi.ToBoolean(state, stackIndex) - case ffi.LUA_TNUMBER: - results[i] = ffi.ToNumber(state, stackIndex) - case ffi.LUA_TSTRING: - results[i] = ffi.ToString(state, stackIndex) - case ffi.LUA_TTABLE: - results[i] = "
" - case ffi.LUA_TFUNCTION: - results[i] = "" - default: - results[i] = "" - } + results[i] = intoLuaValue(l, stackIndex) } ffi.Pop(state, resultsCount) @@ -91,22 +74,32 @@ func (l *Lua) Memory() *MemoryState { return l.inner.MemState() } -func (l *Lua) CreateTable() LuaTable { +func (l *Lua) CreateTable() *LuaTable { state := l.inner.luaState + ffi.NewTable(state) + index := ffi.Ref(state, -1) - return LuaTable{ + t := &LuaTable{ vm: l, - index: int(ffi.GetTop(state)), + index: int(index), } + + return t } -func (l *Lua) CreateString(str string) LuaString { +func (l *Lua) CreateString(str string) *LuaString { state := l.inner.luaState ffi.PushString(state, str) - index := ffi.GetTop(state) - return LuaString{vm: l, index: int(index)} + + index := ffi.Ref(state, -1) + ffi.RawGetI(state, ffi.LUA_REGISTRYINDEX, int32(index)) + + ffi.Pop(state, 1) + + s := &LuaString{vm: l, index: int(index)} + return s } func (l *Lua) SetCompiler(compiler *Compiler) { diff --git a/lua/string.go b/lua/string.go index 1956cbf..b4a0b20 100644 --- a/lua/string.go +++ b/lua/string.go @@ -13,22 +13,29 @@ type LuaString struct { func (s *LuaString) ToString() string { state := s.vm.state() - return ffi.ToString(state, int32(s.index)) + + s.deref() + defer ffi.Pop(state, 1) + + return ffi.ToString(state, -1) } func (s *LuaString) ToPointer() unsafe.Pointer { state := s.vm.state() - return ffi.ToPointer(state, int32(s.index)) + + s.deref() + defer ffi.Pop(state, 1) + + return ffi.ToPointer(state, -1) } // -// LuaValue Implementation +// LuaValue implementation // -func (s *LuaString) lua() *Lua { - return s.vm -} +func (s *LuaString) lua() *Lua { return s.vm } +func (s *LuaString) ref() int { return s.index } -func (s *LuaString) stackIndex() int { - return s.index +func (s *LuaString) deref() int { + return int(ffi.RawGetI(s.lua().state(), ffi.LUA_REGISTRYINDEX, int32(s.ref()))) } diff --git a/lua/table.go b/lua/table.go index 93f55fb..2a862b3 100644 --- a/lua/table.go +++ b/lua/table.go @@ -10,34 +10,53 @@ type LuaTable struct { func (t *LuaTable) Set(key LuaValue, value LuaValue) { state := t.vm.state() - ffi.PushValue(state, int32(key.stackIndex())) - ffi.PushValue(state, int32(value.stackIndex())) - ffi.SetTable(state, int32(t.index)) + t.deref() // table (-3) + key.deref() // key (-2) + value.deref() // value (-1) + + ffi.SetTable(state, -3) + ffi.Pop(state, 1) } func (t *LuaTable) Get(key LuaValue) LuaValue { state := t.vm.state() - ffi.PushValue(state, int32(key.stackIndex())) - valueType := ffi.GetTable(state, int32(t.index)) + t.deref() //////////////////// table (-3) + key.deref() //////////////////// key (-2) + ffi.GetTable(state, -2) + + val := intoLuaValue(t.vm, -1) // value (-1) + ffi.Pop(state, 2) + + return val +} + +func (t *LuaTable) Iterable() map[LuaValue]LuaValue { + state := t.vm.state() + + t.deref() + tableIndex := ffi.GetTop(state) + ffi.PushNil(state) + + obj := make(map[LuaValue]LuaValue) + for ffi.Next(state, tableIndex) != 0 { + key, value := intoLuaValue(t.vm, -2), intoLuaValue(t.vm, -1) + obj[key] = value - switch valueType { - // TODO: other types - case ffi.LUA_TSTRING: - return &LuaString{vm: t.vm, index: int(ffi.GetTop(state))} - default: - panic("Unknown type") + ffi.Pop(state, 1) // only pop value, leave key in place } + + ffi.Pop(state, 1) + return obj } // -// LuaValue Implementation +// LuaValue implementation // -func (s *LuaTable) lua() *Lua { - return s.vm -} +func (t *LuaTable) lua() *Lua { return t.vm } +func (t *LuaTable) ref() int { return t.index } -func (s *LuaTable) stackIndex() int { - return s.index +func (t *LuaTable) deref() int { + return int(ffi.RawGetI(t.lua().state(), ffi.LUA_REGISTRYINDEX, int32(t.ref()))) } diff --git a/lua/value.go b/lua/value.go index 16cfa02..7c13fd4 100644 --- a/lua/value.go +++ b/lua/value.go @@ -1,13 +1,124 @@ package lua -import "github.com/CompeyDev/lei/ffi" +import ( + "fmt" + "reflect" + + "github.com/CompeyDev/lei/ffi" +) type LuaValue interface { lua() *Lua - stackIndex() int + ref() int + deref() int } func TypeName(val LuaValue) string { lua := val.lua().state() - return ffi.TypeName(lua, ffi.Type(lua, int32(val.stackIndex()))) + return ffi.TypeName(lua, ffi.Type(lua, int32(val.ref()))) +} + +func As[T any](v LuaValue) (T, error) { + var zero T + + targetType := reflect.TypeOf(zero) + reflectValue, err := asReflectValue(v, targetType) + + return reflectValue.Interface().(T), err +} + +func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { + // TODO: allow annotations to override field names + + zero := reflect.Zero(t) + + switch val := v.(type) { + case *LuaString: + if t.Kind() == reflect.String { + str := reflect.ValueOf(val.ToString()).Convert(t) + return str, nil + } + + case *LuaTable: + switch t.Kind() { + case reflect.Map: + res := reflect.MakeMap(t) + for key, value := range val.Iterable() { + var kVal reflect.Value + var vVal reflect.Value + var err error + + // Key conversion + if t.Key() == reflect.TypeOf((*LuaValue)(nil)).Elem() { + kVal = reflect.ValueOf(key) + } else { + kVal, err = asReflectValue(key, t.Key()) + if err != nil { + return zero, err + } + } + + // Value conversion + if t.Elem() == reflect.TypeOf((*LuaValue)(nil)).Elem() { + vVal = reflect.ValueOf(value) + } else { + vVal, err = asReflectValue(value, t.Elem()) + if err != nil { + return zero, err + } + } + + res.SetMapIndex(kVal, vVal) + } + + return res, nil + + case reflect.Struct: + res := reflect.New(t).Elem() + for key, value := range val.Iterable() { + keyStr, ok := key.(*LuaString) + if !ok { + continue + } + + field := res.FieldByName(keyStr.ToString()) + if !field.IsValid() || !field.CanSet() { + continue + } + + vVal, err := asReflectValue(value, field.Type()) + if err != nil { + return zero, err + } + + field.Set(vVal) + } + + return res, nil + + } + + case *LuaNil: + return zero, nil + + } + + return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero) +} + +func intoLuaValue(lua *Lua, index int32) LuaValue { + state := lua.state() + + switch ffi.Type(state, index) { + case ffi.LUA_TSTRING: + ref := ffi.Ref(state, index) + return &LuaString{vm: lua, index: int(ref)} + case ffi.LUA_TTABLE: + ref := ffi.Ref(state, index) + return &LuaTable{vm: lua, index: int(ref)} + case ffi.LUA_TNIL: + return &LuaNil{} + default: + panic("unsupported Lua type") + } } diff --git a/main.go b/main.go index 212931e..22d5dd6 100644 --- a/main.go +++ b/main.go @@ -13,16 +13,46 @@ func main() { table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("lei") - table.Set(&key, &value) + table.Set(key, value) fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) - fmt.Println(key.ToString(), table.Get(&key).(*lua.LuaString).ToString()) - - values, err := state.Execute("main", []byte("print('hello, lei!'); return 1, 2, 3")) + fmt.Println(key.ToString(), table.Get(key).(*lua.LuaString).ToString()) + values, err := state.Execute("main", []byte("print('hello, lei!'); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) if err != nil { fmt.Println(err) + return + } + + for i, value := range values { + fmt.Print(i, ": ") + + switch v := value.(type) { + case *lua.LuaString: + fmt.Println(v.ToString()) + case *lua.LuaTable: + fmt.Println() + + for key, val := range v.Iterable() { + k, kErr := lua.As[string](key) + v, vErr := lua.As[string](val) + + if kErr != nil || vErr != nil { + fmt.Println(" (non-string key or value)") + } + + fmt.Printf(" %v: %v\n", k, v) + } + } } - fmt.Println(values...) + iterable, iterErr := lua.As[map[string]string](table) + if iterErr != nil { + fmt.Println(iterErr) + return + } + + for k, v := range iterable { // or, we can use `.Iterable` + fmt.Printf("%s %s\n", k, v) + } } From 0a533305ee21d5abad47109942fb3c47cc2d5155 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 26 Nov 2025 06:48:27 +0000 Subject: [PATCH 13/64] feat(lua/value): add annotation and exported case normalization * Annotations of the form `lua:"field_name"` can be used on struct fields to override the corresponding key name in Lua tables * If a field that is exported using PascalCase syntax cannot be found in the table, reflection falls back to trying to look for a corresponding camelCase name --- lua/value.go | 53 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/lua/value.go b/lua/value.go index 7c13fd4..b8d54aa 100644 --- a/lua/value.go +++ b/lua/value.go @@ -3,6 +3,7 @@ package lua import ( "fmt" "reflect" + "strings" "github.com/CompeyDev/lei/ffi" ) @@ -18,6 +19,10 @@ func TypeName(val LuaValue) string { return ffi.TypeName(lua, ffi.Type(lua, int32(val.ref()))) } +// +// Lua<->Go Type Conversion +// + func As[T any](v LuaValue) (T, error) { var zero T @@ -28,8 +33,6 @@ func As[T any](v LuaValue) (T, error) { } func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { - // TODO: allow annotations to override field names - zero := reflect.Zero(t) switch val := v.(type) { @@ -81,17 +84,47 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { continue } - field := res.FieldByName(keyStr.ToString()) - if !field.IsValid() || !field.CanSet() { - continue - } + luaKey := keyStr.ToString() + var field reflect.Value + var found bool + + for i := 0; i < t.NumField(); i++ { + // Annotation-based field name overrides (eg: `lua:"field_name"`) + structField := t.Field(i) + tagVal, ok := structField.Tag.Lookup("lua") + if ok && tagVal == luaKey { + field = res.Field(i) + found = true + break + } + + // Exact matches + if structField.Name == luaKey { + field = res.Field(i) + found = true + break + } - vVal, err := asReflectValue(value, field.Type()) - if err != nil { - return zero, err + // If field is exported, try also using lowercase first character + if name := structField.Name; structField.IsExported() { + lower := strings.ToLower(name[:1]) + name[1:] + if lower == luaKey { + field = res.Field(i) + found = true + break + } + } } - field.Set(vVal) + if found && field.IsValid() && field.CanSet() { + // Recursively convert value to a reflect value + vVal, err := asReflectValue(value, field.Type()) + if err != nil { + return zero, err + } + + field.Set(vVal) + } } return res, nil From 1a5e3a18ece62e738658c1997f2ef0f70451d320 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 26 Nov 2025 06:50:10 +0000 Subject: [PATCH 14/64] chore(lua): include Lua to Go reflection tests --- lua/value_test.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 lua/value_test.go diff --git a/lua/value_test.go b/lua/value_test.go new file mode 100644 index 0000000..3b9fed0 --- /dev/null +++ b/lua/value_test.go @@ -0,0 +1,163 @@ +package lua_test + +import ( + "testing" + + "github.com/CompeyDev/lei/lua" +) + +func TestAs(t *testing.T) { + state := lua.New() + + // 1. Tag match + t.Run("Tag match", func(t *testing.T) { + type Person struct { + Name string `lua:"username"` + } + + table := state.CreateTable() + table.Set(state.CreateString("username"), state.CreateString("Alice")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Name != "Alice" { + t.Fatalf("expected Alice, got %v", res.Name) + } + }) + + // 2. Exact field name match + t.Run("Exact match", func(t *testing.T) { + type Person struct { + Age string // TODO: make this int once we have numbers + } + + table := state.CreateTable() + table.Set(state.CreateString("Age"), state.CreateString("30")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Age != "30" { + t.Fatalf("expected '30', got %v", res.Age) + } + }) + + // 3. Lowercase-first-letter fallback + t.Run("Lowercase fallback", func(t *testing.T) { + type Person struct{ Country string } + + table := state.CreateTable() + table.Set(state.CreateString("country"), state.CreateString("Germany")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Country != "Germany" { + t.Fatalf("expected 'Germany', got %v", res.Country) + } + }) + + // 4. Unexported field ignored + t.Run("Unexported field", func(t *testing.T) { + type Box struct{ secret string } + + table := state.CreateTable() + table.Set(state.CreateString("secret"), state.CreateString("trains r cool")) + + res, err := lua.As[Box](table) + if err != nil { + t.Fatal(err) + } + + if res.secret != "" { + t.Fatalf("expected empty, got %v", res.secret) + } + }) + + // 5. Mixed fields + t.Run("Mixed fields", func(t *testing.T) { + type Person struct { + Name string `lua:"username"` + Age string // TODO: use int once LuaNumber is implemented + Email string + } + + table := state.CreateTable() + table.Set(state.CreateString("username"), state.CreateString("Bob")) + table.Set(state.CreateString("Age"), state.CreateString("25")) + table.Set(state.CreateString("email"), state.CreateString("bobby@example.com")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Name != "Bob" || res.Age != "25" || res.Email != "bobby@example.com" { + t.Fatalf("unexpected result: %+v", res) + } + }) + + // 6. Missing Lua key + t.Run("Missing key", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + table := state.CreateTable() + table.Set(state.CreateString("Name"), state.CreateString("Johnny")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Name != "Johnny" || res.Age != 0 { + t.Fatalf("unexpected result: %+v", res) + } + }) + + // 7. Extra Lua key ignored + t.Run("Extra key ignored", func(t *testing.T) { + type Person struct{ Name string } + + table := state.CreateTable() + table.Set(state.CreateString("unknown"), state.CreateTable()) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Name != "" { + t.Fatalf("expected Name empty, got %v", res.Name) + } + }) + + // 8. Tag overrides lowercase fallback + t.Run("Tag overrides fallback", func(t *testing.T) { + type Person struct { + Name string `lua:"user"` + } + + table := state.CreateTable() + table.Set(state.CreateString("name"), state.CreateString("Dave")) + table.Set(state.CreateString("user"), state.CreateString("Eve")) + + res, err := lua.As[Person](table) + if err != nil { + t.Fatal(err) + } + + if res.Name != "Eve" { + t.Fatalf("expected 'Eve', got %v", res.Name) + } + }) +} From 9996bd29b8e6f20e906e748f15bc96e646eab190 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sun, 7 Dec 2025 08:21:00 +0000 Subject: [PATCH 15/64] feat(ffi/lua): accept ptr for `PushCClosureK` for `debugname` * i.e., this allows for providing a nil pointer which is allowed as per the API * Now also accepts an empty string for providing no debug name * Remove hardcoded GCC headers include path --- ffi/lauxlib.go | 2 +- ffi/lua.go | 20 ++++++++++++-------- ffi/luacode.go | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ffi/lauxlib.go b/ffi/lauxlib.go index 08a5a3c..59ec95e 100644 --- a/ffi/lauxlib.go +++ b/ffi/lauxlib.go @@ -1,7 +1,7 @@ package ffi /* -#cgo CFLAGS: -Iluau/VM/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/include +#cgo CFLAGS: -Iluau/VM/include #cgo LDFLAGS: -Lluau/cmake -lLuau.VM -lm -lstdc++ #include #include diff --git a/ffi/lua.go b/ffi/lua.go index 3eb7e51..72f6f64 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -1,7 +1,7 @@ package ffi /* -#cgo CFLAGS: -Iluau/VM/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include +#cgo CFLAGS: -Iluau/VM/include #cgo LDFLAGS: -Lluau/cmake -lLuau.VM -lm -lstdc++ #include #include @@ -398,9 +398,13 @@ func PushString(L *LuaState, s string) { // arguments from Go->C isn't something that is possible. // func PushFStringL(L *lua_State, fmt string) {} -func PushCClosureK(L *LuaState, f unsafe.Pointer, debugname string, nup int32, cont unsafe.Pointer) { - cdebugname := C.CString(debugname) - defer C.free(unsafe.Pointer(cdebugname)) +func PushCClosureK(L *LuaState, f unsafe.Pointer, debugname *string, nup int32, cont unsafe.Pointer) { + var cdebugname *C.char + if debugname != nil && *debugname != "" { + cdebugname = C.CString(*debugname) + defer C.free(unsafe.Pointer(cdebugname)) + } + C.clua_pushcclosurek(L, f, cdebugname, C.int(nup), cont) } @@ -964,18 +968,18 @@ func PushLiteral(L *LuaState, s string) { } func PushCFunction(L *LuaState, f unsafe.Pointer) { - PushCClosureK(L, f, *new(string), 0, nil) + PushCClosureK(L, f, new(string), 0, nil) } -func PushCFunctionD(L *LuaState, f unsafe.Pointer, debugname string) { +func PushCFunctionD(L *LuaState, f unsafe.Pointer, debugname *string) { PushCClosureK(L, f, debugname, 0, nil) } func PushCClosure(L *LuaState, f unsafe.Pointer, nup int32) { - PushCClosureK(L, f, *new(string), nup, nil) + PushCClosureK(L, f, new(string), nup, nil) } -func PushCClosureD(L *LuaState, f unsafe.Pointer, debugname string, nup int32) { +func PushCClosureD(L *LuaState, f unsafe.Pointer, debugname *string, nup int32) { PushCClosureK(L, f, debugname, nup, nil) } diff --git a/ffi/luacode.go b/ffi/luacode.go index 5060020..e63d12b 100644 --- a/ffi/luacode.go +++ b/ffi/luacode.go @@ -1,7 +1,7 @@ package ffi /* -#cgo CFLAGS: -Iluau/Compiler/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include +#cgo CFLAGS: -Iluau/Compiler/include #cgo LDFLAGS: -Lluau/cmake -lLuau.Compiler -lLuau.Ast -lm -lstdc++ #include #include From 942da1aca242512b64121077276cf1c18f0cf2b3 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 9 Dec 2025 10:07:57 +0000 Subject: [PATCH 16/64] fix(ffi/lua): `luau_load` returns a boolean, not just an int --- ffi/lua.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index 72f6f64..7a02552 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -542,7 +542,7 @@ func Setfenv(L *LuaState, idx int32) int32 { // ========================= // -func LuauLoad(L *LuaState, chunkname string, data []byte, size uint64, env int32) int32 { +func LuauLoad(L *LuaState, chunkname string, data []byte, size uint64, env int32) bool { cchunkname := C.CString(chunkname) defer C.free(unsafe.Pointer(cchunkname)) @@ -556,7 +556,7 @@ func LuauLoad(L *LuaState, chunkname string, data []byte, size uint64, env int32 // NOTE: We don't free the bytecode after it's loaded - return int32(C.luau_load(L, cchunkname, cdata, C.size_t(size), C.int(env))) + return C.luau_load(L, cchunkname, cdata, C.size_t(size), C.int(env)) == 0 } func Call(L *LuaState, nargs int32, nresults int32) { From 08013bae623e98d11b1105ed168d6988a2d7817c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 9 Dec 2025 10:09:14 +0000 Subject: [PATCH 17/64] fix(ffi/lauxlib): use-after-free due to Go-side free calls --- ffi/lauxlib.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ffi/lauxlib.go b/ffi/lauxlib.go index 59ec95e..81585ba 100644 --- a/ffi/lauxlib.go +++ b/ffi/lauxlib.go @@ -66,8 +66,6 @@ func LArgError(L *LuaState, narg int32, extramsg string) { func LCheckLString(L *LuaState, narg int32, l *uint64) string { p := C.luaL_checklstring(L, C.int(narg), (*C.size_t)(l)) - defer C.free(unsafe.Pointer(p)) - return C.GoString(p) } @@ -76,8 +74,6 @@ func LOptLString(L *LuaState, narg int32, def string, l *uint64) string { defer C.free(unsafe.Pointer(cdef)) p := C.luaL_optlstring(L, C.int(narg), cdef, (*C.ulong)(l)) - defer C.free(unsafe.Pointer(p)) - return C.GoString(p) } From 39ffca19c96965186130ce88afbe183dfbb39be6 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 9 Dec 2025 10:11:14 +0000 Subject: [PATCH 18/64] feat(lua): implement `LuaChunk` for callable bodies * Renamed `LoadError` to `LuaError` * Implement ability to construct Lua-callable functions from Go functions. This required implementing a registry on the Go side and a small wrapper around a cgo generated Go trampoline which received an opaque identifier which would be passed to the Go side, which would finally execute the required function * Fixed memory leak due to no cleanup finalizer for `LuaString` TODO: cleanup unused functions in the registry with no references TODO: allow safe function input for `Lua.CreateFunction` instead of expecting a `lua_CFunction` --- lua.rs | 942 ++++++++++++++++++++++++++++++++++++++++++++++++ lua/chunk.go | 61 ++++ lua/compiler.go | 1 - lua/errors.go | 8 +- lua/memory.go | 1 - lua/registry.c | 10 + lua/registry.go | 67 ++++ lua/state.go | 82 ++--- lua/value.go | 8 +- main.go | 26 +- 10 files changed, 1146 insertions(+), 60 deletions(-) create mode 100644 lua.rs create mode 100644 lua/chunk.go create mode 100644 lua/registry.c create mode 100644 lua/registry.go diff --git a/lua.rs b/lua.rs new file mode 100644 index 0000000..c88f389 --- /dev/null +++ b/lua.rs @@ -0,0 +1,942 @@ +//! [`lua.h`][h] - The Luau VM. +//! +//! [h]: https://github.com/luau-lang/luau/blob/master/VM/include/lua.h + +use std::ffi::{c_char, c_double, c_float, c_int, c_uint, c_void}; +use std::ptr; + +use crate::luaconf; + +/// Used to get all the results from a function call in [`lua_call`] and [`lua_pcall`]. +pub const LUA_MULTRET: c_int = -1; + +/// Pseudo-index for the registry. +pub const LUA_REGISTRYINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2000; +/// Pseudo-index for the environment of the running C function. +pub const LUA_ENVIRONINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2001; +/// Pseudo-index for the thread-environment. +pub const LUA_GLOBALSINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2002; + +/// OK status. +pub const LUA_OK: c_int = 0; +/// The thread is suspended. +pub const LUA_YIELD: c_int = 1; +/// Runtime error. +pub const LUA_ERRRUN: c_int = 2; +/// Legacy error code, preserved for compatibility. +pub const LUA_ERRSYNTAX: c_int = 3; +/// Memory allocation error. +pub const LUA_ERRMEM: c_int = 4; +/// Error while running running the error handler function. +pub const LUA_ERRERR: c_int = 5; +/// Yielded for a debug breakpoint. +pub const LUA_BREAK: c_int = 6; + +/// Coroutine is running. +pub const LUA_CORUN: c_int = 0; +/// Coroutine is suspended. +pub const LUA_COSUS: c_int = 1; +/// Coroutine is 'normal' (it resumed another coroutine) +pub const LUA_CONOR: c_int = 2; +/// Coroutine has finished. +pub const LUA_COFIN: c_int = 3; +/// Coorutine finished with an error. +pub const LUA_COERR: c_int = 4; + +/// Type for an "empty" stack position. +pub const LUA_TNONE: c_int = -1; +/// Type `nil` +pub const LUA_TNIL: c_int = 0; +/// Type `boolean` +pub const LUA_TBOOLEAN: c_int = 1; +/// Type `lightuserdata` +pub const LUA_TLIGHTUSERDATA: c_int = 2; +/// Type `number` +pub const LUA_TNUMBER: c_int = 3; +/// Type `vector` +pub const LUA_TVECTOR: c_int = 4; +/// Type `string` +pub const LUA_TSTRING: c_int = 5; +/// Type `table` +pub const LUA_TTABLE: c_int = 6; +/// Type `function` +pub const LUA_TFUNCTION: c_int = 7; +/// Type `userdata` +pub const LUA_TUSERDATA: c_int = 8; +/// Type `thread` +pub const LUA_TTHREAD: c_int = 9; +/// Type `buffer` +pub const LUA_TBUFFER: c_int = 10; + +/// Internal tag for GC objects. +pub const LUA_TPROTO: c_int = 11; +/// Internal tag for GC objects. +pub const LUA_TUPVAL: c_int = 12; +/// Internal tag for GC objects. +pub const LUA_TDEADKEY: c_int = 13; + +/// The number of types that exist. +pub const LUA_T_COUNT: c_int = 11; + +/// Stop garbage collection. +pub const LUA_GCSTOP: c_int = 0; +/// Resume garbage collection. +pub const LUA_GCRESTART: c_int = 1; +/// Run a full GC cycle. Not recommended for latency sensitive applications. +pub const LUA_GCCOLLECT: c_int = 2; +/// Returns the current amount of memory used in KB. +pub const LUA_GCCOUNT: c_int = 3; +/// Returns the remainder in bytes of the current amount of memory used. +/// +/// This is the remainder after dividing the total amount of bytes by 1024. +pub const LUA_GCCOUNTB: c_int = 4; +/// Returns 1 if the GC is active (not stopped). +/// +/// The GC may not be actively collecting even if it's running. +pub const LUA_GCISRUNNING: c_int = 5; +/// Performs an explicit GC step, with the step size specified in KB. +pub const LUA_GCSTEP: c_int = 6; +/// Set the goal GC parameter. +pub const LUA_GCSETGOAL: c_int = 7; +/// Set the step multiplier GC parameter +pub const LUA_GCSETSTEPMUL: c_int = 8; +/// Set the step size GC parameter. +pub const LUA_GCSETSTEPSIZE: c_int = 9; + +/// Sentinel value indicating the absence of a registry reference. +pub const LUA_NOREF: c_int = -1; +/// Special reference indicating a `nil` value. +pub const LUA_REFNIL: c_int = 0; + +/// Type of Luau numbers. +pub type lua_Number = c_double; +/// Type for integer functions. +pub type lua_Integer = c_int; +/// Unsigned integer type. +pub type lua_Unsigned = c_uint; + +/// Type for C functions. +/// +/// When called, the arguments passed to the function are available in the stack, with the first +/// argument at index 1 and the last argument at the top of the stack ([`lua_gettop`]). Return +/// values should be pushed onto the stack (the first result is pushed first), and the number of +/// return values should be returned. +pub type lua_CFunction = unsafe extern "C-unwind" fn(*mut lua_State) -> c_int; + +/// Type for a continuation function. +/// +/// See [`lua_pushcclosurek`]. +pub type lua_Continuation = unsafe extern "C-unwind" fn(*mut lua_State, c_int) -> c_int; + +/// Type for a memory allocation function. +/// +/// This function is called with the `ud` passed to [`lua_newstate`], a pointer to the block being +/// allocated/reallocated/freed, the original size of the block, and the requested new size of the +/// block. +/// +/// If the given new size is non-zero, a pointer to a block with the requested size should be +/// returned. If the request cannot be fulfilled or the requested size is zero, null should be +/// returned. If the given original size is not zero, the given block should be freed. It is +/// assumed this never fails if the requested size is smaller than the original size. +pub type lua_Alloc = + unsafe extern "C-unwind" fn(*mut c_void, *mut c_void, usize, usize) -> *mut c_void; + +/// Destructor function for a userdata. Called before the userdata is garbage collected. +pub type lua_Destructor = unsafe extern "C-unwind" fn(L: *mut lua_State, userdata: *mut c_void); + +/// Functions to be called by the debugger in specific events. +pub type lua_Hook = unsafe extern "C-unwind" fn(L: *mut lua_State, ar: *mut lua_Debug); + +/// Callback function for [`lua_getcoverage`]. Receives the coverage information. +pub type lua_Coverage = unsafe extern "C-unwind" fn( + context: *mut c_void, + function: *const c_char, + linedefined: c_int, + depth: c_int, + hits: *const c_int, + size: usize, +); + +/// A Luau thread. +/// +/// This is an opaque type and always exists behind a pointer (like `*mut lua_State`). +#[repr(C)] +pub struct lua_State { + _data: (), + _marker: core::marker::PhantomData, +} + +/// Activation record. Contains debug information. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct lua_Debug { + /// Name of the function. + pub name: *const c_char, + /// One of `Lua`, `C`, `main`, or `tail`. + pub what: *const c_char, + /// The source. Usually the filename. + pub source: *const c_char, + /// Short chunk identifier. + pub short_src: *const c_char, + /// The line where the function is defined. + pub linedefined: c_int, + /// The current line. + pub currentline: c_int, + /// The number of upvalues. + pub nupvals: c_uint, + /// The number of parameters. + pub nparams: c_uint, + /// Whether or not the function is variadic. + pub isvararg: c_char, + /// Userdata. + pub userdata: *mut c_void, + /// Buffer for `short_src`. + pub ssbuf: [c_char; luaconf::LUA_IDSIZE], +} + +impl lua_Debug { + pub fn new() -> lua_Debug { + lua_Debug { + name: ptr::null(), + what: ptr::null(), + source: ptr::null(), + short_src: ptr::null(), + linedefined: 0, + currentline: 0, + nupvals: 0, + nparams: 0, + isvararg: 0, + userdata: ptr::null_mut(), + ssbuf: [0; luaconf::LUA_IDSIZE], + } + } +} + +impl Default for lua_Debug { + fn default() -> lua_Debug { + lua_Debug::new() + } +} + +/// Callbacks that can be used to reconfigure behavior of the VM dynamically. +/// +/// This can be retrieved using [`lua_callbacks`]. +/// +/// **Note:** `interrupt` is safe to set from an arbitrary thread. All other callbacks should only +/// be changed when the VM is not running any code. +#[repr(C)] +#[derive(Debug, Default, Clone, Copy)] +pub struct lua_Callbacks { + /// Arbitrary userdata pointer. Never overwritten by Luau. + pub userdata: *mut c_void, + + /// Called at safepoints (e.g. loops, calls/returns, garbage collection). + pub interrupt: Option, + /// Called when an unprotected error is raised (if longjmp is used). + pub panic: Option, + + /// Called when `L` is created (`LP` is the parent), or destroyed (`LP` is null). + pub userthread: Option, + /// Called when a string is created. Returned atom can be retrieved via [`lua_tostringatom`]. + pub useratom: Option i16>, + + /// Called when a `BREAK` instruction is encountered. + pub debugbreak: Option, + /// Called after each instruction in single step mode. + pub debugstep: Option, + /// Called when thread execution is interrupted by break in another thread. + pub debuginterrupt: Option, + /// Called when a protected call results in an error. + pub debugprotectederror: Option, + + /// Called when memory is allocated. + pub onallocate: + Option, +} + +unsafe extern "C-unwind" { + /// Creates a new independent state using the given allocation function and `ud`. + pub unsafe fn lua_newstate(f: lua_Alloc, ud: *mut c_void) -> *mut lua_State; + /// Closes the given state. + /// + /// Destroys all objects in the state and frees all dynamic memory used by the state. + pub unsafe fn lua_close(L: *mut lua_State); + /// Pushes a new thread to the stack. + /// + /// Returns a pointer to a [`lua_State`] that represents the created thread. The new state + /// shared all global objects (such as tables) with the original state, but has an + /// independent execution stack. + pub unsafe fn lua_newthread(L: *mut lua_State) -> *mut lua_State; + /// Returns a pointer to a [`lua_State`], which represents the main thread of the given thread. + pub unsafe fn lua_mainthread(L: *mut lua_State) -> *mut lua_State; + /// Resets the given thread to its original state. + /// + /// Clears all the thread state, call frames, and the stack. + pub unsafe fn lua_resetthread(L: *mut lua_State); + /// Returns true if the given thread is in its original state. + pub unsafe fn lua_isthreadreset(L: *mut lua_State) -> c_int; + + /// Converts the given index into an absolute index. + pub unsafe fn lua_absindex(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the index of the top element in the stack. + /// + /// This is equal to the number of elements in the stack. + pub unsafe fn lua_gettop(L: *mut lua_State) -> c_int; + /// Sets the top of the stack to the given index. + /// + /// If the new top is greater than the old one, new elements are filled with `nil`. If the new + /// top is smaller, elements will be removed from the old top. + pub unsafe fn lua_settop(L: *mut lua_State, idx: c_int); + /// Pushes a copy of the element at the given index onto the stack. + pub unsafe fn lua_pushvalue(L: *mut lua_State, idx: c_int); + /// Removes the element at the given index from the stack. + /// + /// Elements above the given index are shifted down to fill the gap. This function cannot be + /// called with a pseudo-index. + pub unsafe fn lua_remove(L: *mut lua_State, idx: c_int); + /// Moves the top element into the given index. + /// + /// Elements above the given index are shifted up to make space. This function cannot be called + /// with a pseudo-index. + pub unsafe fn lua_insert(L: *mut lua_State, idx: c_int); + /// Replaces the element at the given index with the top element. + /// + /// The top element will be popped. + pub unsafe fn lua_replace(L: *mut lua_State, idx: c_int); + /// Ensures the stack has space for the given number of additional elements. + /// + /// Returns `0` if the request cannot be fulfilled, either because it would be bigger than the + /// maximum stack size, or because it cannot allocate memory for the extra space. + pub unsafe fn lua_checkstack(L: *mut lua_State, sz: c_int) -> c_int; + /// Like [`lua_checkstack`], but allows for an infinite amount of stack frames. + pub unsafe fn lua_rawcheckstack(L: *mut lua_State, sz: c_int); + + /// Move values between different threads of the same state. + /// + /// Pops `n` values from the stack of `from`, and pushes them onto the stack of `to`. + pub unsafe fn lua_xmove(from: *mut lua_State, to: *mut lua_State, n: c_int); + /// Copy a value to a different thread of the same state. + /// + /// The element at the given index is copied from `from`, and pushed onto the stack of `to`. + pub unsafe fn lua_xpush(from: *mut lua_State, to: *mut lua_State, idx: c_int); + + /// Checks if a value can be converted to a number. + /// + /// Returns `1` if the value at the given index is a number, or a string that can be converted + /// to a number, and `0` otherwise. + pub unsafe fn lua_isnumber(L: *mut lua_State, idx: c_int) -> c_int; + /// Checks if a value can be converted to a string. + /// + /// Returns `1` if the value at the given index is a string or a number, and `0` otherwise. + pub unsafe fn lua_isstring(L: *mut lua_State, idx: c_int) -> c_int; + /// Checks if a value is a C function. + /// + /// Returns `1` if the value at the given index is a C function, and `0` otherwise. + pub unsafe fn lua_iscfunction(L: *mut lua_State, idx: c_int) -> c_int; + /// Checks if a value is a Luau function. + /// + /// Returns `1` if the value at the given index is a Luau function, and `0` otherwise. + pub unsafe fn lua_isLfunction(L: *mut lua_State, idx: c_int) -> c_int; + /// Checks if a value is a userdata. + /// + /// Returns `1` if the value at the given index is a userdata, and `0` otherwise. + pub unsafe fn lua_isuserdata(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the type of the value at the given index. + pub unsafe fn lua_type(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the name of the given type. + pub unsafe fn lua_typename(L: *mut lua_State, tp: c_int) -> *const c_char; + + /// Checks if two values are equal. + /// + /// Returns `1` if the two values in the given indices are equal, and `0` otherwise. This + /// follows the behavior of the `==` operator, and may call metamethods. + pub unsafe fn lua_equal(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; + /// Like [`lua_equal`], but will not invoke metamethods. + pub unsafe fn lua_rawequal(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; + /// Checks if one value is less than another value. + /// + /// Returns `1` if the value at `idx1` is smaller than the value at `idx2`, and `0` otherwise. + /// This follows the behavior of the `<` operator, and may call metamethods. + pub unsafe fn lua_lessthan(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; + + /// Checks if the given value can be converted to a number, and returns the number. + /// + /// If the value at the given index is a number, or a string that can be converted to one, the + /// number will be returned and `isnum` will be `1`. Otherwise, `0` will be returned and `isnum` + /// will be `0`. + pub unsafe fn lua_tonumberx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Number; + /// Checks if the given value can be converted to an integer, and returns the integer. + /// + /// If the value at the given index is a number, or a string that can be converted to one, the + /// number will be truncated and returned and `isnum` will be `1`. Otherwise, `0` will be + /// returned and `isnum` will be `0`. + pub unsafe fn lua_tointegerx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Integer; + /// Checks if the given value can be converted to an unsigned integer, and returns it. + /// + /// If the value at the given index is a number, or a string that can be converted to one, the + /// number will be truncated and returned and `isnum` will be `1`. Otherwise, `0` will be + /// returned and `isnum` will be `0`. + /// + /// If the integer is negative, it will be forced into an unsigned integer in an unspecified + /// way. + pub unsafe fn lua_tounsignedx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) + -> lua_Unsigned; + /// Get the vector in the given value. + /// + /// If the value at the given index is a vetor, returns a pointer to the first component of + /// that vector (`x`), otherwise returns null. + pub unsafe fn lua_tovector(L: *mut lua_State, idx: c_int) -> *const c_float; + /// Get the boolean in the given value. + /// + /// Returns `1` if the given value is truthy, and `0` if it is falsy. A value is falsy if it is + /// `false` or `nil`. + pub unsafe fn lua_toboolean(L: *mut lua_State, idx: c_int) -> c_int; + /// Converts the given value to a string. + /// + /// The value at the given index is converted into a string and `len` will be the length of the + /// string. The value must be a string or number, otherwise null is returned. If it is a + /// number, the actual value in the stack will be changed to a string. + /// + /// An aligned pointer to the string within the Luau state is returned. This string is null + /// terminated, but may also contain nulls within its body. If the string has been removed from + /// the stack, it may get garbage collected causing the pointer to dangle. + pub unsafe fn lua_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; + /// Like [`lua_tolstringatom`] but doesn't get the length. + pub unsafe fn lua_tostringatom( + L: *mut lua_State, + idx: c_int, + atom: *mut c_int, + ) -> *const c_char; + /// Get a string and its atom. + /// + /// Returns an aligned pointer to the string within the Luau state. If the value is not a + /// string, null is returned, no conversion is performed for non-strings. The string is null + /// terminated, but may also contain nulls within its body. If the string has been removed from + /// the stack, it may get garbage collected causing the pointer to dangle. + /// + /// The `atom` will be the result of [`lua_Callbacks::useratom`] or -1 if not set. The same + /// string will have the same atom. + pub unsafe fn lua_tolstringatom( + L: *mut lua_State, + idx: c_int, + len: *mut usize, + atom: *mut c_int, + ) -> *const c_char; + /// Gets the current namecall string and its atom. + /// + /// When called during a namecall, a pointer to the string containing the name of the method + /// that was called is returned, otherwise null is returned. See [`lua_tolstringatom`] for more + /// information. + pub unsafe fn lua_namecallatom(L: *mut lua_State, atom: *mut c_int) -> *const c_char; + /// Get the length of the given value. + /// + /// Returns the "length" of the value at the given index. For strings, it is the length of the + /// string. For tables, it is the result of the `#` operator. For userdata, this is the size of + /// the block of memory allocated for the userdata. For other values, it is `0`. + pub unsafe fn lua_objlen(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the C function in the given value. + /// + /// Returns the C function in the value at the given index, otherwise returns null. + pub unsafe fn lua_tocfunction(L: *mut lua_State, idx: c_int) -> lua_CFunction; + /// Gets the light userdata in the given value. + /// + /// Returns the light userdata in the value at the given index, otherwise returns null. + pub unsafe fn lua_tolightuserdata(L: *mut lua_State, idx: c_int) -> *mut c_void; + /// Gets the light userdata with the given tag in the given value. + /// + /// Returns the light userdata in the value at the given index if its tag matches the given + /// tag, otherwise returns null. + pub unsafe fn lua_tolightuserdatatagged( + L: *mut lua_State, + idx: c_int, + tag: c_int, + ) -> *mut c_void; + /// Gets the userdata in the given value. + /// + /// Returns the userdata in the value at the given index, otherwise returns null. + pub unsafe fn lua_touserdata(L: *mut lua_State, idx: c_int) -> *mut c_void; + /// Gets the userdata with the given tag in the given value. + /// + /// Returns the userdata in the value at the given index if its tag matches the given tag, + /// otherwise returns null. + pub unsafe fn lua_touserdatatagged(L: *mut lua_State, idx: c_int, tag: c_int) -> *mut c_void; + /// Gets the tag of the userdata in the given value. + /// + /// Returns the tag of the userdata in the value at the given index, or `-1` if the value is + /// not a userdata. + pub unsafe fn lua_userdatatag(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the tag of the light userdata in the given value. + /// + /// Returns the tag of the light userdata in the value at the given index, or `-1` if the value + /// is not a userdata. + pub unsafe fn lua_lightuserdatatag(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the thread in the given value. + /// + /// Returns a pointer to a [`lua_State`] that represents the thread in the value at the given + /// index, or null if the value is not a thread. + pub unsafe fn lua_tothread(L: *mut lua_State, idx: c_int) -> *mut lua_State; + /// Gets the buffer in the given value. + /// + /// Returns a pointer to the first byte of the buffer in the value at the given index, or null + /// if the value is not a buffer. The `len` will be the length of the buffer. + pub unsafe fn lua_tobuffer(L: *mut lua_State, idx: c_int, len: *mut usize) -> *mut c_void; + /// Converts the given value into a generic pointer. + /// + /// Returns a pointer that represents the given value. The value can be a userdata, a table, a + /// thread or a function; otherwise, null is returned. Different objects give different + /// pointers. There is no way to convert a pointer back into its original value. + /// + /// This function is typically used for debugging purposes. + pub unsafe fn lua_topointer(L: *mut lua_State, idx: c_int) -> *const c_void; + + /// Pushes `nil` value onto the stack. + pub unsafe fn lua_pushnil(L: *mut lua_State); + /// Pushes the given number onto the stack. + pub unsafe fn lua_pushnumber(L: *mut lua_State, n: c_double); + /// Pushes the given integer onto the stack. + pub unsafe fn lua_pushinteger(L: *mut lua_State, n: c_int); + /// Pushes the given unsigned integer onto the stack. + pub unsafe fn lua_pushunsigned(L: *mut lua_State, n: c_uint); + + /// Pushes the given vector onto the stack. + #[cfg(not(feature = "vector4"))] + pub unsafe fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float); + /// Pushes the given vector onto the stack. + #[cfg(feature = "vector4")] + pub unsafe fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float, w: c_float); + + /// Pushes the pointed-to string with the given length onto the stack. + pub unsafe fn lua_pushlstring(L: *mut lua_State, s: *const c_char, l: usize); + /// Pushes the given null-terminated string onto the stack. + pub unsafe fn lua_pushstring(L: *mut lua_State, s: *const c_char); + /// Pushes a formatted string onto the stack. + /// + /// The `fmt` string can contain the following specifiers: + /// - `%%`: inserts a literal `%`. + /// - `%s`: inserts a zero terminated string. + /// - `%f`: inserts a [`lua_Number`]. + /// - `%p`: inserts a pointer as a hexadecimal numeral. + /// - `%d`: inserts an int. + /// - `%c`: inserts an int as a character. + pub unsafe fn lua_pushfstringL(L: *mut lua_State, fmt: *const c_char, ...) -> *const c_char; + /// Pushes the given C closure onto the stack, with a continuation function. + /// + /// Takes the C function, the function name for debugging, the number of upvalues and + /// an optional continuation function. + /// + /// The upvalues will be popped from the stack. The upvalues can then be retrieved within the + /// function by subtracting the index of the upvalue (starting from 1) from [`LUA_GLOBALSINDEX`]. + /// For example, to get the first upvalue you would do `LUA_GLOBALSINDEX - 1`. + /// + /// See `examples/continuations.rs` for more information about the continutation function. + pub unsafe fn lua_pushcclosurek( + L: *mut lua_State, + fn_: lua_CFunction, + debugname: *const c_char, + nup: c_int, + cont: Option, + ); + /// Pushes the given boolean onto the stack. + pub unsafe fn lua_pushboolean(L: *mut lua_State, b: c_int); + /// Pushes the current thread onto the stack. + /// + /// Returns `1` if this thread is the main thread of its state. + pub unsafe fn lua_pushthread(L: *mut lua_State) -> c_int; + + /// Pushes a tagged light userdata onto the stack. + pub unsafe fn lua_pushlightuserdatatagged(L: *mut lua_State, p: *mut c_void, tag: c_int); + /// Allocates a new tagged userdata and pushes it onto the stack. + /// + /// Returns a pointer to the allocated block of memory with the given size. + pub unsafe fn lua_newuserdatatagged(L: *mut lua_State, sz: usize, tag: c_int) -> *mut c_void; + /// Allocates a new tagged userdata with the metatable and pushes it onto the stack. + /// + /// The userdata will have the given tag. The metatable will be set to the metatable that has + /// been set using [`lua_setuserdatametatable`]. + pub unsafe fn lua_newuserdatataggedwithmetatable( + L: *mut lua_State, + sz: usize, + tag: c_int, + ) -> *mut c_void; + /// Allocates a new userdata with the given destructor and pushes it onto the stack. + /// + /// The specified destructor function will be called before the userdata is garbage collected. + pub unsafe fn lua_newuserdatadtor( + L: *mut lua_State, + sz: usize, + dtor: unsafe extern "C-unwind" fn(*mut c_void), + ) -> *mut c_void; + + /// Allocates a new Luau buffer and pushes it onto the stack. + /// + /// Returns a pointer to the allocated block of memory with the given size. + pub unsafe fn lua_newbuffer(L: *mut lua_State, sz: usize) -> *mut c_void; + + /// Gets an element from the given table and pushes it onto the stack. + /// + /// Pushes the value `t[k]` onto the stack, where `t` is the table at the given index and `k` + /// is the value at the top of the stack. The key is popped from the stack. This may call the + /// `__index` metamethod. + /// + /// Returns the type of the pushed value. + pub unsafe fn lua_gettable(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets a field from the given table and pushes it onto the stack. + /// + /// Pushes the value `t[k]` onto the stack, where `t` is the table at the given index and `k` + /// is the given string. This may call the `__index` metamethod. + /// + /// Returns the type of the pushed value. + pub unsafe fn lua_getfield(L: *mut lua_State, idx: c_int, k: *const c_char) -> c_int; + /// Gets a field from the given table without invoking metamethods. + /// + /// Like [`lua_getfield`], but does not invoke metamethods. + pub unsafe fn lua_rawgetfield(L: *mut lua_State, idx: c_int, k: *const c_char) -> c_int; + /// Gets an element from the given table without invoking metamethods. + /// + /// Like [`lua_gettable`], but does not invoke metamethods. + pub unsafe fn lua_rawget(L: *mut lua_State, idx: c_int) -> c_int; + /// Gets the element at the given index in the given table without invoking metamethods. + /// + /// Pushes the value `t[n]` onto the stack, where `t` is the table at the given index and `n` + /// is the given index. + /// + /// Returns the type of the pushed value. + pub unsafe fn lua_rawgeti(L: *mut lua_State, idx: c_int, n: c_int) -> c_int; + /// Pushes a new empty table with the given size hints onto the stack. + /// + /// `narr` is a hint for how many elements the array portion the table will contain, `nrec` is + /// a hint for how many other elements it will contain. The hints may be used to preallocate + /// the memory for the table. + /// + /// If you do not know the number of elements the table will contain in advance, you may use + /// [`lua_newtable`] instead. + pub unsafe fn lua_createtable(L: *mut lua_State, narr: c_int, nrec: c_int); + + /// Sets the `readonly`` flag of the given table. + pub unsafe fn lua_setreadonly(L: *mut lua_State, idx: c_int, enabled: c_int); + /// Gets the `readonly` flag of the given table. + pub unsafe fn lua_getreadonly(L: *mut lua_State, idx: c_int) -> c_int; + /// Sets the `safeenv` flag of the given table. + pub unsafe fn lua_setsafeenv(L: *mut lua_State, idx: c_int, enabled: c_int); + + /// Pushes the metatable of the value at the given index onto the stack. + /// + /// If the index is not valid or if it doesn't have a metatable, zero is returned. + pub unsafe fn lua_getmetatable(L: *mut lua_State, objindex: c_int) -> c_int; + /// Pushes the environment table of the value at the given index onto the stack. + pub unsafe fn lua_getfenv(L: *mut lua_State, idx: c_int); + + /// Sets an element in the given table. + /// + /// Does the equivalent of `t[k] = v`, where `t` is the value at the given index, `v` is the + /// value at the top of the stack, and `k` is the value just below the top. The key and value + /// will be popped from the stack. This may call the `__newindex` metamethod. + pub unsafe fn lua_settable(L: *mut lua_State, idx: c_int); + /// Sets a field in the given table. + /// + /// Does the equivalent of `t[k] = v`, where `t` is the value at the given index, `v` is the + /// value at the top of the stack, and `k` is the given string. The value will be popped from + /// the stack. This may call the `__newindex` metamethod. + pub unsafe fn lua_setfield(L: *mut lua_State, idx: c_int, k: *const c_char); + /// Sets a field in the given table without invoking metamethods. + /// + /// Like [`lua_setfield`], but does not invoke metamethods. + pub unsafe fn lua_rawsetfield(L: *mut lua_State, idx: c_int, k: *const c_char); + /// Sets an element in the given table without invoking metamethods. + /// + /// Like [`lua_settable`], but does not invoke metamethods. + pub unsafe fn lua_rawset(L: *mut lua_State, idx: c_int); + /// Sets the element at the given index in the given table without invoking metamethods. + /// + /// Does the equivalent of `t[n] = v` onto the stack, where `t` is the table at the given index + /// and `v` is the value at the top of the stack. + pub unsafe fn lua_rawseti(L: *mut lua_State, idx: c_int, n: c_int); + /// Sets the metatable of the given value. + /// + /// Pops a table from the stack and sets it as the metatable for the value at the given index. + /// + /// Always returns 1 for historical reasons. + pub unsafe fn lua_setmetatable(L: *mut lua_State, objindex: c_int) -> c_int; + /// Sets the environment for the given value. + /// + /// Pops a table from the stack and sets it as the new environment for the value at the given + /// index. Returns 1 if the value is a function, thread, or userdata. Otherwise, returns 0. + pub unsafe fn lua_setfenv(L: *mut lua_State, idx: c_int) -> c_int; + + /// Loads the given Luau chunk and pushes it as a function onto the stack. + /// + /// Takes the name of the chunk for debugging, a pointer to the bytecode, the size of the + /// bytecode, and a value representing the environment of the chunk. If `env` is 0, the current + /// environment will be used, otherwise the table on the stack at index `env` will be used. + /// + /// Returns 1 if there was an error, 0 otherwise. The error message will be pushed onto the + /// stack if there was an error. + pub unsafe fn luau_load( + L: *mut lua_State, + chunkname: *const c_char, + data: *const c_char, + size: usize, + env: c_int, + ) -> c_int; + /// Calls the given function. + /// + /// This will pop `nargs` values from the stack, which will be passed into the function as + /// arguments. The first argument is the first value that got pushed. The results of the + /// function are pushed onto the stack. The number of results will be adjusted to `nresults`, + /// unless `nresults` is [`LUA_MULTRET`], in which case all results are pushed. The first + /// result is pushed first, the last result will be at the top of the stack. + pub unsafe fn lua_call(L: *mut lua_State, nargs: c_int, nresults: c_int); + /// Calls the given function in protected mode. + /// + /// Like [`lua_call`]. However, if there is an error, it will get catched, the error message + /// will be pushed onto the stack, and the error code will be returned. + /// + /// If `errfunc` is zero, the error message pushed on the stack will not be modified. Otherwise, + /// `errfunc` is the stack index of an *error handler function*. In case of an error, this + /// function will be called with the error message and its return value will be the error + /// messaged pushed onto the stack. + /// + /// Returns zero if successful, otherwise returns one of [`LUA_ERRRUN`], [`LUA_ERRMEM`], or + /// [`LUA_ERRERR`]. + pub unsafe fn lua_pcall( + L: *mut lua_State, + nargs: c_int, + nresults: c_int, + errfunc: c_int, + ) -> c_int; + + /// Yields the current coroutine. + /// + /// This function should be called as the return of a C function. + pub unsafe fn lua_yield(L: *mut lua_State, nresults: c_int) -> c_int; + /// Breaks execution, as if a debug breakpoint has been reached. + /// + /// This function should be called as the return of a C function. + pub unsafe fn lua_break(L: *mut lua_State) -> c_int; + /// Starts and resumes a coroutine in the given thread `L`. + /// + /// To start a coroutine, push the main function plus any arguments onto the stack, then call + /// this function with `nargs` being the number of arguments. Once the coroutine suspends or + /// finishes, the results of the resumption will be pushed onto the stack. The first result + /// will be pushed first, the last result will be at the top of the stack. If the coroutine + /// yielded, [`LUA_YIELD`] is returned. If there was an error an error code is returned, and + /// the error value will be pushed onto the stack. Otherwise, [`LUA_OK`] is returned. + /// + /// To resume a coroutine, push values to be returned from the `yield` call onto the stack, and + /// call this function. Make sure to remove the results pushed from your previous resume call + /// before resuming again. + /// + /// The `from` parameter represents the coroutine that is resuming `L`. This may be null if + /// there is no such coroutine. + pub unsafe fn lua_resume(L: *mut lua_State, from: *mut lua_State, narg: c_int) -> c_int; + /// Error a coroutine in the given thread `L`. + /// + /// Like [`lua_resume`], but this will pop a single value from the stack which will be used as + /// the error value. If the coroutine is currently inside of a `pcall`, the error will be + /// catched by that `pcall`. Otherwise, this call will return an error code and the error value + /// will be pushed onto the stack. + pub unsafe fn lua_resumeerror(L: *mut lua_State, from: *mut lua_State) -> c_int; + /// Gets the status of the given thread `L`. + /// + /// The status can be [`LUA_OK`] for a normal thread, an error code if the thread finished + /// execution of a `lua_resume` with an error, or [`LUA_YIELD`] if the thread is suspended. + /// + /// You can only call functions in threads that are [`LUA_OK`]. You can resume threads that are + /// [`LUA_OK`] or [`LUA_YIELD`]. + pub unsafe fn lua_status(L: *mut lua_State) -> c_int; + /// Returns 1 if the given coroutine can yield, and 0 otherwise. + pub unsafe fn lua_isyieldable(L: *mut lua_State) -> c_int; + /// Gets the thread data of the given thread. + /// + /// The thread data can be set with [`lua_setthreaddata`]. + pub unsafe fn lua_getthreaddata(L: *mut lua_State) -> *mut c_void; + /// Sets the thread data of the given thread. + /// + /// The thread data can be retrieved afterwards with [`lua_getthreaddata`]. + pub unsafe fn lua_setthreaddata(L: *mut lua_State, data: *mut c_void); + /// Gets the status of the given coroutine `co`. + pub unsafe fn lua_costatus(L: *mut lua_State, co: *mut lua_State) -> c_int; + + /// Controls the garbage collector. + /// + /// Takes a GC operation `what` and data to use for the operation. The different operations are + /// documented inside [lua.h]. + /// + /// [lua.h]: https://github.com/luau-lang/luau/blob/master/VM/include/lua.h#L249 + pub unsafe fn lua_gc(L: *mut lua_State, what: c_int, data: c_int) -> c_int; + + /// Set the memory category used for memory statistics. + pub unsafe fn lua_setmemcat(L: *mut lua_State, category: c_int); + /// Get the total amount of memory used, in bytes. + /// + /// If `category < 0`, then the total amount of memory is returned. + pub unsafe fn lua_totalbytes(L: *mut lua_State, category: c_int) -> usize; + + /// Throws a Luau error. + /// + /// The error value must be at the top of the stack. + pub unsafe fn lua_error(L: *mut lua_State) -> !; + + /// Gets the next key-value pair in the given table. + /// + /// This will pop a key from the stack, and pushes the next key-value pair onto the stack of + /// the table at the given index. If the end of the table has been reached, nothing is pushed + /// and zero is returned. If a non-zero value is returned, then the key is at stack index `-2`, + /// and the value is at `-1`. + /// + /// While traversing a table, avoid calling [`lua_tolstring`] directly on a key, unless you + /// know the key is actually a string. [`lua_tolstring`] will change the value to a string if + /// it's a number, which will confuse [`lua_next`]. + pub unsafe fn lua_next(L: *mut lua_State, idx: c_int) -> c_int; + /// Iterate over the given table. + /// + /// This function should be repeatedly called, where `iter` is the previous return value, or `0` + /// on the first iteration. If `-1` is returned, the end of the table has been reached. + /// Otherwise, the key-value pair is pushed onto the stack. The key will be at index `-2` and + /// the value at `-1`. + /// + /// This function is similar to [`lua_next`], however the order of items from this function + /// matches the order from a normal `for i, v in t do` loop (array portion first, then the + /// remaining keys). + pub unsafe fn lua_rawiter(L: *mut lua_State, idx: c_int, iter: c_int) -> c_int; + + /// Pops `n` values from the stack and concatenates them. + /// + /// The concatenated string is pushed onto the stack. If `n` is zero, the result is an empty + /// string. Follows the same semantics as the `..` operator in Luau. + pub unsafe fn lua_concat(L: *mut lua_State, n: c_int); + + /// Obfuscates the given pointer. + /// + /// The same `p` will return the same value on the same state. + pub unsafe fn lua_encodepointer(L: *mut lua_State, p: usize) -> usize; + + /// Returns a high-precision timestamp (in seconds). + /// + /// This is the same as `os.clock()` from the Luau standard library. This can be used to + /// measure a duration with sub-microsecond precision. + pub unsafe fn lua_clock() -> c_double; + + /// Sets the tag of the userdata at the given index. + pub unsafe fn lua_setuserdatatag(L: *mut lua_State, idx: c_int, tag: c_int); + + /// Set the destructor function for userdata objects with the given tag. + pub unsafe fn lua_setuserdatadtor(L: *mut lua_State, tag: c_int, dtor: Option); + /// Gets the destructor function for userdata objects with the given tag. + pub unsafe fn lua_getuserdatadtor(L: *mut lua_State, tag: c_int) -> lua_Destructor; + + /// Sets the metatable for userdata objects with the given tag. + /// + /// This will pop a table from the top of the stack, and use it as the metatable for any + /// userdata objects created with the given tag. This cannot be called on a tag that already + /// has a metatable set. + pub unsafe fn lua_setuserdatametatable(L: *mut lua_State, tag: c_int); + /// Gets the metatable for userdata objects with the given tag. + pub unsafe fn lua_getuserdatametatable(L: *mut lua_State, tag: c_int); + + /// Sets the name for light userdata objects with the given tag. + /// + /// This cannot be called on a tag that already has a name set. + pub unsafe fn lua_setlightuserdataname(L: *mut lua_State, tag: c_int, name: *const c_char); + /// Gets the name for light userdata objects wiwth the given tag. + pub unsafe fn lua_getlightuserdataname(L: *mut lua_State, tag: c_int) -> *const c_char; + + /// Pushes a clone of the function at the given index onto the stack. + pub unsafe fn lua_clonefunction(L: *mut lua_State, idx: c_int); + + /// Clears the table at the given index. + pub unsafe fn lua_cleartable(L: *mut lua_State, idx: c_int); + /// Pushes a clone of the table at the given index onto the stack. + pub unsafe fn lua_clonetable(L: *mut lua_State, idx: c_int); + + /// Gets the allocator function of the given state. + /// + /// Will update `ud` to be the value passed to [`lua_newstate`]. + pub unsafe fn lua_getallocf(L: *mut lua_State, ud: *mut *mut c_void) -> lua_Alloc; + + /// Stores the given value in the registry and gets a reference to it. + /// + /// This will store a copy of the value at the given index into the registry. The reference, + /// which is an integer that identifies this value, is returned. A reference can then be + /// removed from the registry using [`lua_unref`], allowing it to be garbage collected. + /// References can be reused once freed. + /// + /// If the given value is `nil`, the special reference [`LUA_REFNIL`] is returned. The sentinel + /// reference [`LUA_NOREF`] will never be returned by this function. + pub unsafe fn lua_ref(L: *mut lua_State, idx: c_int) -> c_int; + /// Frees a reference created by [`lua_ref`] from the registry. + pub unsafe fn lua_unref(L: *mut lua_State, ref_: c_int); + + /// Gets the depth of the call stack. + pub unsafe fn lua_stackdepth(L: *mut lua_State) -> c_int; + /// Gets information about a specific function or function invocation. + /// + /// If `level < 0`, it is assumed to be a stack index, and information about the function at + /// that index is retrieved. Otherwise, it is assumed to be a level in the call stack, and + /// information about the invocation at that level is retrieved. + /// + /// The `ar` argument will be filled with the values requested in `what`. Each character in + /// `what` selects some fields in `ar` to be filled. + /// + /// - `s`: fills `source`, `what`, `linedefined`, and `short_src` + /// - `l`: fills `currentline` + /// - `u`: fills `nupvals` + /// - `a`: fills `isvararg` and `nparams` + /// - `n`: fills `name` + /// - `f`: pushes the function onto the stack + /// + /// Returns `1` if `ar` was updated, `0` otherwise. + pub unsafe fn lua_getinfo( + L: *mut lua_State, + level: c_int, + what: *const c_char, + ar: *mut lua_Debug, + ) -> c_int; + /// Pushes a copy of the `n`th argument at the given level onto the stack. + /// + /// Returns `1` if a value was pushed, `0` otherwise. Always returns `0` for invocations to + /// native functions. + pub unsafe fn lua_getargument(L: *mut lua_State, level: c_int, n: c_int) -> c_int; + /// Pushes a copy of the `n`th local variable at the given level onto the stack. + /// + /// Returns a pointer to the name of the variable. If no value was pushed, null is returned. + /// Always returns null for invocations to native functions. + pub unsafe fn lua_getlocal(L: *mut lua_State, level: c_int, n: c_int) -> *const c_char; + /// Pops a value from the stack and sets it as the `n`th local variable at the given level. + /// + /// Returns a pointer to the name of the variable. If no variable was set, null is returned. + /// Always returns null for invocations to native functions. + pub unsafe fn lua_setlocal(L: *mut lua_State, level: c_int, n: c_int) -> *const c_char; + /// Pushes a copy of the `n`th upvalue of the function at the given index onto the stack. + pub unsafe fn lua_getupvalue(L: *mut lua_State, funcindex: c_int, n: c_int) -> *const c_char; + /// Pops a value from the stack and sets it as the `n`th upvalue of the given function. + pub unsafe fn lua_setupvalue(L: *mut lua_State, funcindex: c_int, n: c_int) -> *const c_char; + + /// Enables single stepping for the given thread. + pub unsafe fn lua_singlestep(L: *mut lua_State, enabled: c_int); + /// Sets a breakpoint for the given function at the given line. + pub unsafe fn lua_breakpoint( + L: *mut lua_State, + funcindex: c_int, + line: c_int, + enabled: c_int, + ) -> c_int; + + /// Collects coverage information for the given function. + pub unsafe fn lua_getcoverage( + L: *mut lua_State, + funcindex: c_int, + context: *mut c_void, + callback: lua_Coverage, + ); + + /// Returns a string representation of the stack trace for debugging. + /// + /// This is **NOT thread-safe**, the result is stored in a shared global buffer. + pub unsafe fn lua_debugtrace(L: *mut lua_State) -> *const c_char; + + /// Gets the callbacks used by the given state. + /// + /// These are shared between all coroutines. + pub unsafe fn lua_callbacks(L: *mut lua_State) -> *mut lua_Callbacks; +} diff --git a/lua/chunk.go b/lua/chunk.go new file mode 100644 index 0000000..002b01a --- /dev/null +++ b/lua/chunk.go @@ -0,0 +1,61 @@ +package lua + +import "github.com/CompeyDev/lei/ffi" + +// NOTE: `bytecode` and `index` are expected to be mutually exclusive + +type LuaChunk struct { + vm *Lua + + name string + bytecode []byte + + index int + funcID uintptr +} + +func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { + state := c.vm.state() + + initialStack := ffi.GetTop(state) // Track initial stack size + + if c.bytecode != nil { + hasLoaded := ffi.LuauLoad(state, c.name, c.bytecode, uint64(len(c.bytecode)), 0) + if !hasLoaded { + // Miscellaneous error is denoted with a -1 code + return nil, &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} + } + } else { + // Push function onto the stack + ffi.RawGetI(state, ffi.LUA_REGISTRYINDEX, int32(c.index)) + + // Push all arguments onto the stack (deref) + for _, arg := range args { + arg.deref() + } + } + + status := ffi.Pcall(state, int32(len(args)), -1, 0) + if status != ffi.LUA_OK { + return nil, newLuaError(state, int(status)) + } + + stackNow := ffi.GetTop(state) + resultsCount := stackNow - initialStack + + if resultsCount == 0 { + return nil, nil + } + + // TODO: contemplate whether to return LuaValues or go values + results := make([]LuaValue, resultsCount) + for i := range resultsCount { + // The stack has grown by the number of returns of the chunk from the + // initial value tracked at the beginning. We add one to that due to + // Lua's 1-based indexing system + stackIndex := int32(initialStack + i + 1) + results[i] = intoLuaValue(c.vm, stackIndex) + } + + return results, nil +} diff --git a/lua/compiler.go b/lua/compiler.go index 1edb9cb..55b16b8 100644 --- a/lua/compiler.go +++ b/lua/compiler.go @@ -1,6 +1,5 @@ package lua -import "C" import ( "github.com/CompeyDev/lei/ffi" ) diff --git a/lua/errors.go b/lua/errors.go index d367d4c..dcbcd83 100644 --- a/lua/errors.go +++ b/lua/errors.go @@ -6,12 +6,12 @@ import ( "github.com/CompeyDev/lei/ffi" ) -type LoadError struct { +type LuaError struct { Code int Message string } -func (e *LoadError) Error() string { +func (e *LuaError) Error() string { switch e.Code { case ffi.LUA_ERRSYNTAX: return "syntax error: " + e.Message @@ -24,10 +24,10 @@ func (e *LoadError) Error() string { } } -func newLoadError(state *ffi.LuaState, code int) *LoadError { +func newLuaError(state *ffi.LuaState, code int) *LuaError { if code != ffi.LUA_OK { message := ffi.ToString(state, -1) - err := &LoadError{Code: code, Message: message} + err := &LuaError{Code: code, Message: message} ffi.Pop(state, 1) diff --git a/lua/memory.go b/lua/memory.go index 64dbf99..2b63507 100644 --- a/lua/memory.go +++ b/lua/memory.go @@ -1,7 +1,6 @@ package lua /* -#cgo CFLAGS: -I/usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include #include #include diff --git a/lua/registry.c b/lua/registry.c new file mode 100644 index 0000000..bfedb8f --- /dev/null +++ b/lua/registry.c @@ -0,0 +1,10 @@ +#include +#include +#include +#include <_cgo_export.h> + +int registryTrampoline(lua_State* L) { + uintptr_t registry_ptr = (uintptr_t)lua_touserdata(L, lua_upvalueindex(1)); + uintptr_t func_id = (uintptr_t)lua_touserdata(L, lua_upvalueindex(2)); + return registryTrampolineImpl(L, registry_ptr, func_id); +} diff --git a/lua/registry.go b/lua/registry.go new file mode 100644 index 0000000..61dcb1f --- /dev/null +++ b/lua/registry.go @@ -0,0 +1,67 @@ +package lua + +import ( + "runtime/cgo" + + "github.com/CompeyDev/lei/ffi" +) + +/* +#cgo CFLAGS: -I../ffi/luau/VM/include +#cgo LDFLAGS: -L../ffi/luau/cmake -lLuau.VM -lm -lstdc++ + +#include +#include +#include + +int registryTrampoline(lua_State* L); +*/ +import "C" + +var registryTrampoline = C.registryTrampoline + +//export registryTrampolineImpl +func registryTrampolineImpl(lua *C.lua_State, registryPtr uintptr, funcID uintptr) C.int { + handle := cgo.Handle(registryPtr) + reg := handle.Value().(*functionRegistry) + state := (*ffi.LuaState)(lua) + + fn, ok := reg.get(funcID) + if !ok { + ffi.PushString(state, "function not found in registry") + ffi.Error(state) + return 0 + } + + return C.int(fn(state)) +} + +type functionRegistry struct { + functions map[uintptr]ffi.LuaCFunction + nextID uintptr +} + +func newFunctionRegistry() *functionRegistry { + return &functionRegistry{ + functions: make(map[uintptr]ffi.LuaCFunction), + } +} + +func (fr *functionRegistry) register(fn ffi.LuaCFunction) uintptr { + fr.nextID++ + id := fr.nextID + fr.functions[id] = fn + return id +} + +func (fr *functionRegistry) get(id uintptr) (ffi.LuaCFunction, bool) { + fn, ok := fr.functions[id] + return fn, ok +} + +// FIXME: there is a memory leak of function entries here; we need to unregister +// once they are no longer used by Go or Lua. the issue here is that we cannot know +// when Lua is done with the function, so we need some kind of finalizer on the +// Lua side to notify us when it's done. the typical solution would be to use a full +// userdata instead of lightuserdata and set a dtor for that which calls back into Go +// to unregister diff --git a/lua/state.go b/lua/state.go index 975eb0b..6389fbb 100644 --- a/lua/state.go +++ b/lua/state.go @@ -2,6 +2,7 @@ package lua import ( "runtime" + "runtime/cgo" "unsafe" "github.com/CompeyDev/lei/ffi" @@ -15,59 +16,23 @@ type LuaOptions struct { } type Lua struct { - inner *StateWithMemory - compiler *Compiler + inner *StateWithMemory + compiler *Compiler + fnRegistry *functionRegistry } -func (l *Lua) Execute(name string, input []byte) ([]LuaValue, error) { - // TODO: create a load function which doesnt execute - - state := l.inner.luaState - initialStack := ffi.GetTop(state) // Track initial stack size - +func (l *Lua) Load(name string, input []byte) (*LuaChunk, error) { + chunk := &LuaChunk{vm: l, bytecode: input} if !isBytecode(input) { bytecode, err := l.compiler.Compile(string(input)) if err != nil { return nil, err } - input = bytecode - } - - loadResult := ffi.LuauLoad(state, name, input, uint64(len(input)), 0) - loadErr := newLoadError(state, int(loadResult)) - - if loadErr != nil { - return nil, loadErr - } - - execResult := ffi.Pcall(state, 0, -1, 0) - execErr := newLoadError(state, int(execResult)) - - if execErr != nil { - return nil, execErr - } - - stackNow := ffi.GetTop(state) - resultsCount := stackNow - initialStack - - if resultsCount == 0 { - return nil, nil - } - - // TODO: contemplate whether to return LuaValues or go values - results := make([]LuaValue, resultsCount) - for i := range resultsCount { - // The stack has grown by the number of returns of the chunk from the - // initial value tracked at the beginning. We add one to that due to - // Lua's 1-based indexing system - stackIndex := int32(initialStack + i + 1) - results[i] = intoLuaValue(l, stackIndex) + chunk.bytecode = bytecode } - ffi.Pop(state, resultsCount) - - return results, nil + return chunk, nil } func (l *Lua) Memory() *MemoryState { @@ -80,10 +45,8 @@ func (l *Lua) CreateTable() *LuaTable { ffi.NewTable(state) index := ffi.Ref(state, -1) - t := &LuaTable{ - vm: l, - index: int(index), - } + t := &LuaTable{vm: l, index: int(index)} + runtime.SetFinalizer(t, valueUnrefer[*LuaTable](t.lua())) return t } @@ -92,16 +55,31 @@ func (l *Lua) CreateString(str string) *LuaString { state := l.inner.luaState ffi.PushString(state, str) - index := ffi.Ref(state, -1) - ffi.RawGetI(state, ffi.LUA_REGISTRYINDEX, int32(index)) - - ffi.Pop(state, 1) s := &LuaString{vm: l, index: int(index)} + runtime.SetFinalizer(s, valueUnrefer[*LuaString](s.lua())) + return s } +func (l *Lua) CreateFunction(fn ffi.LuaCFunction) *LuaChunk { + state := l.state() + + entry := l.fnRegistry.register(fn) + registryHandle := uintptr(cgo.NewHandle(l.fnRegistry)) + + ffi.PushLightUserdata(state, unsafe.Pointer(registryHandle)) + ffi.PushLightUserdata(state, unsafe.Pointer(entry)) + ffi.PushCClosureK(state, registryTrampoline, nil, 2, nil) + + index := ffi.Ref(state, -1) + c := &LuaChunk{vm: l, index: int(index), funcID: entry} + runtime.SetFinalizer(c, func(c *LuaChunk) { ffi.Unref(state, index) }) + + return c +} + func (l *Lua) SetCompiler(compiler *Compiler) { l.compiler = compiler } @@ -169,7 +147,7 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { compiler = DefaultCompiler() } - lua := &Lua{inner: state, compiler: compiler} + lua := &Lua{inner: state, compiler: compiler, fnRegistry: newFunctionRegistry()} runtime.SetFinalizer(lua, func(l *Lua) { if options.CollectGarbage { ffi.LuaGc(l.state(), ffi.LUA_GCCOLLECT, 0) diff --git a/lua/value.go b/lua/value.go index b8d54aa..854b0ac 100644 --- a/lua/value.go +++ b/lua/value.go @@ -150,8 +150,14 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { ref := ffi.Ref(state, index) return &LuaTable{vm: lua, index: int(ref)} case ffi.LUA_TNIL: - return &LuaNil{} + return &LuaNil{vm: lua} default: panic("unsupported Lua type") } } + +func valueUnrefer[T LuaValue](lua *Lua) func(T) { + return func(value T) { + ffi.Unref(lua.state(), int32(value.ref())) + } +} diff --git a/main.go b/main.go index 22d5dd6..c115041 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/CompeyDev/lei/ffi" "github.com/CompeyDev/lei/lua" ) @@ -18,12 +19,18 @@ func main() { fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) fmt.Println(key.ToString(), table.Get(key).(*lua.LuaString).ToString()) - values, err := state.Execute("main", []byte("print('hello, lei!'); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) + chunk, err := state.Load("main", []byte("print('hello, lei!!!!'); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) if err != nil { fmt.Println(err) return } + values, returnErr := chunk.Call() + if returnErr != nil { + fmt.Println(returnErr) + return + } + for i, value := range values { fmt.Print(i, ": ") @@ -55,4 +62,21 @@ func main() { for k, v := range iterable { // or, we can use `.Iterable` fmt.Printf("%s %s\n", k, v) } + + cFnChunk := state.CreateFunction(func(L *ffi.LuaState) int32 { + ffi.PushString(L, "Hello") + ffi.PushString(L, "from") + ffi.PushString(L, fmt.Sprintf("Go, %s!", ffi.LCheckString(L, 1))) + return 3 + }) + + returns, callErr := cFnChunk.Call(state.CreateString("Lua")) + if callErr != nil { + fmt.Println(callErr) + return + } + + for i, ret := range returns { + fmt.Printf("Return %d: %s\n", i+1, ret.(*lua.LuaString).ToString()) + } } From cadff3b0c6d12e08079329a061eb9c6c36f15a0e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 9 Dec 2025 10:46:43 +0000 Subject: [PATCH 19/64] fix(lua): address noted func registry memory leak * Refactored `functionRegistry.get` to return `functionEntry` which holds a reference to its parent registry and the ID of the created function * Pass this entry to the C-side wrapper for the dtor trampoline and have it pass it back to the Go-side dtor using `cgo.Handle`s for safety. Finally, the dtor cleans up the function referred to by the ID from the registry it belongs to --- lua/chunk.go | 3 +-- lua/registry.c | 10 +++++++--- lua/registry.go | 32 +++++++++++++++++++------------- lua/state.go | 11 ++++++----- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/lua/chunk.go b/lua/chunk.go index 002b01a..33bf3f3 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -10,8 +10,7 @@ type LuaChunk struct { name string bytecode []byte - index int - funcID uintptr + index int } func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { diff --git a/lua/registry.c b/lua/registry.c index bfedb8f..1a31a25 100644 --- a/lua/registry.c +++ b/lua/registry.c @@ -4,7 +4,11 @@ #include <_cgo_export.h> int registryTrampoline(lua_State* L) { - uintptr_t registry_ptr = (uintptr_t)lua_touserdata(L, lua_upvalueindex(1)); - uintptr_t func_id = (uintptr_t)lua_touserdata(L, lua_upvalueindex(2)); - return registryTrampolineImpl(L, registry_ptr, func_id); + uintptr_t* handle_ptr = (uintptr_t*)lua_touserdata(L, lua_upvalueindex(1)); + return registryTrampolineImpl(L, *handle_ptr); +} + +void registryTrampolineDtor(lua_State* L) { + uintptr_t* handle_ptr = (uintptr_t*)lua_touserdata(L, lua_upvalueindex(1)); + registryTrampolineDtorImpl(L, *handle_ptr); } diff --git a/lua/registry.go b/lua/registry.go index 61dcb1f..f4f05c1 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -15,18 +15,19 @@ import ( #include int registryTrampoline(lua_State* L); +void registryTrampolineDtor(lua_State* L); */ import "C" var registryTrampoline = C.registryTrampoline +var registryTrampolineDtor = C.registryTrampolineDtor //export registryTrampolineImpl -func registryTrampolineImpl(lua *C.lua_State, registryPtr uintptr, funcID uintptr) C.int { - handle := cgo.Handle(registryPtr) - reg := handle.Value().(*functionRegistry) +func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { state := (*ffi.LuaState)(lua) + entry := cgo.Handle(handle).Value().(*functionEntry) - fn, ok := reg.get(funcID) + fn, ok := entry.registry.get(entry.id) if !ok { ffi.PushString(state, "function not found in registry") ffi.Error(state) @@ -36,32 +37,37 @@ func registryTrampolineImpl(lua *C.lua_State, registryPtr uintptr, funcID uintpt return C.int(fn(state)) } +//export registryTrampolineDtorImpl +func registryTrampolineDtorImpl(_ *C.lua_State, handle C.uintptr_t) { + entry := cgo.Handle(handle).Value().(*functionEntry) + delete(entry.registry.functions, entry.id) + cgo.Handle(handle).Delete() +} + type functionRegistry struct { functions map[uintptr]ffi.LuaCFunction nextID uintptr } +type functionEntry struct { + registry *functionRegistry + id uintptr +} + func newFunctionRegistry() *functionRegistry { return &functionRegistry{ functions: make(map[uintptr]ffi.LuaCFunction), } } -func (fr *functionRegistry) register(fn ffi.LuaCFunction) uintptr { +func (fr *functionRegistry) register(fn ffi.LuaCFunction) *functionEntry { fr.nextID++ id := fr.nextID fr.functions[id] = fn - return id + return &functionEntry{registry: fr, id: id} } func (fr *functionRegistry) get(id uintptr) (ffi.LuaCFunction, bool) { fn, ok := fr.functions[id] return fn, ok } - -// FIXME: there is a memory leak of function entries here; we need to unregister -// once they are no longer used by Go or Lua. the issue here is that we cannot know -// when Lua is done with the function, so we need some kind of finalizer on the -// Lua side to notify us when it's done. the typical solution would be to use a full -// userdata instead of lightuserdata and set a dtor for that which calls back into Go -// to unregister diff --git a/lua/state.go b/lua/state.go index 6389fbb..41e4cf0 100644 --- a/lua/state.go +++ b/lua/state.go @@ -67,14 +67,15 @@ func (l *Lua) CreateFunction(fn ffi.LuaCFunction) *LuaChunk { state := l.state() entry := l.fnRegistry.register(fn) - registryHandle := uintptr(cgo.NewHandle(l.fnRegistry)) + handle := cgo.NewHandle(entry) - ffi.PushLightUserdata(state, unsafe.Pointer(registryHandle)) - ffi.PushLightUserdata(state, unsafe.Pointer(entry)) - ffi.PushCClosureK(state, registryTrampoline, nil, 2, nil) + ud := (*uintptr)(ffi.NewUserdataDtor(state, uint64(unsafe.Sizeof(uintptr(0))), registryTrampolineDtor)) + *ud = uintptr(handle) + + ffi.PushCClosureK(state, registryTrampoline, nil, 1, nil) index := ffi.Ref(state, -1) - c := &LuaChunk{vm: l, index: int(index), funcID: entry} + c := &LuaChunk{vm: l, index: int(index)} runtime.SetFinalizer(c, func(c *LuaChunk) { ffi.Unref(state, index) }) return c From 480ee86baa81a17af3208e1d61b472eadeb4bd96 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 9 Dec 2025 11:27:18 +0000 Subject: [PATCH 20/64] feat(lua): `Lua.CreateFunction` functions accept `LuaValue` args Instead of handing the unsafe raw `lua_State` as we did in the incomplete implementation, we now supply a "cooked" `Lua` state along with an array of all `LuaValue` arguments passed and instead of pushing the returns onto the stack and returning the count, we now expect an array of `LuaValue`s and an optional error. --- lua.rs | 942 ------------------------------------------------ lua/chunk.go | 7 +- lua/registry.go | 46 ++- lua/state.go | 2 +- main.go | 12 +- 5 files changed, 50 insertions(+), 959 deletions(-) delete mode 100644 lua.rs diff --git a/lua.rs b/lua.rs deleted file mode 100644 index c88f389..0000000 --- a/lua.rs +++ /dev/null @@ -1,942 +0,0 @@ -//! [`lua.h`][h] - The Luau VM. -//! -//! [h]: https://github.com/luau-lang/luau/blob/master/VM/include/lua.h - -use std::ffi::{c_char, c_double, c_float, c_int, c_uint, c_void}; -use std::ptr; - -use crate::luaconf; - -/// Used to get all the results from a function call in [`lua_call`] and [`lua_pcall`]. -pub const LUA_MULTRET: c_int = -1; - -/// Pseudo-index for the registry. -pub const LUA_REGISTRYINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2000; -/// Pseudo-index for the environment of the running C function. -pub const LUA_ENVIRONINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2001; -/// Pseudo-index for the thread-environment. -pub const LUA_GLOBALSINDEX: c_int = -luaconf::LUAI_MAXCSTACK - 2002; - -/// OK status. -pub const LUA_OK: c_int = 0; -/// The thread is suspended. -pub const LUA_YIELD: c_int = 1; -/// Runtime error. -pub const LUA_ERRRUN: c_int = 2; -/// Legacy error code, preserved for compatibility. -pub const LUA_ERRSYNTAX: c_int = 3; -/// Memory allocation error. -pub const LUA_ERRMEM: c_int = 4; -/// Error while running running the error handler function. -pub const LUA_ERRERR: c_int = 5; -/// Yielded for a debug breakpoint. -pub const LUA_BREAK: c_int = 6; - -/// Coroutine is running. -pub const LUA_CORUN: c_int = 0; -/// Coroutine is suspended. -pub const LUA_COSUS: c_int = 1; -/// Coroutine is 'normal' (it resumed another coroutine) -pub const LUA_CONOR: c_int = 2; -/// Coroutine has finished. -pub const LUA_COFIN: c_int = 3; -/// Coorutine finished with an error. -pub const LUA_COERR: c_int = 4; - -/// Type for an "empty" stack position. -pub const LUA_TNONE: c_int = -1; -/// Type `nil` -pub const LUA_TNIL: c_int = 0; -/// Type `boolean` -pub const LUA_TBOOLEAN: c_int = 1; -/// Type `lightuserdata` -pub const LUA_TLIGHTUSERDATA: c_int = 2; -/// Type `number` -pub const LUA_TNUMBER: c_int = 3; -/// Type `vector` -pub const LUA_TVECTOR: c_int = 4; -/// Type `string` -pub const LUA_TSTRING: c_int = 5; -/// Type `table` -pub const LUA_TTABLE: c_int = 6; -/// Type `function` -pub const LUA_TFUNCTION: c_int = 7; -/// Type `userdata` -pub const LUA_TUSERDATA: c_int = 8; -/// Type `thread` -pub const LUA_TTHREAD: c_int = 9; -/// Type `buffer` -pub const LUA_TBUFFER: c_int = 10; - -/// Internal tag for GC objects. -pub const LUA_TPROTO: c_int = 11; -/// Internal tag for GC objects. -pub const LUA_TUPVAL: c_int = 12; -/// Internal tag for GC objects. -pub const LUA_TDEADKEY: c_int = 13; - -/// The number of types that exist. -pub const LUA_T_COUNT: c_int = 11; - -/// Stop garbage collection. -pub const LUA_GCSTOP: c_int = 0; -/// Resume garbage collection. -pub const LUA_GCRESTART: c_int = 1; -/// Run a full GC cycle. Not recommended for latency sensitive applications. -pub const LUA_GCCOLLECT: c_int = 2; -/// Returns the current amount of memory used in KB. -pub const LUA_GCCOUNT: c_int = 3; -/// Returns the remainder in bytes of the current amount of memory used. -/// -/// This is the remainder after dividing the total amount of bytes by 1024. -pub const LUA_GCCOUNTB: c_int = 4; -/// Returns 1 if the GC is active (not stopped). -/// -/// The GC may not be actively collecting even if it's running. -pub const LUA_GCISRUNNING: c_int = 5; -/// Performs an explicit GC step, with the step size specified in KB. -pub const LUA_GCSTEP: c_int = 6; -/// Set the goal GC parameter. -pub const LUA_GCSETGOAL: c_int = 7; -/// Set the step multiplier GC parameter -pub const LUA_GCSETSTEPMUL: c_int = 8; -/// Set the step size GC parameter. -pub const LUA_GCSETSTEPSIZE: c_int = 9; - -/// Sentinel value indicating the absence of a registry reference. -pub const LUA_NOREF: c_int = -1; -/// Special reference indicating a `nil` value. -pub const LUA_REFNIL: c_int = 0; - -/// Type of Luau numbers. -pub type lua_Number = c_double; -/// Type for integer functions. -pub type lua_Integer = c_int; -/// Unsigned integer type. -pub type lua_Unsigned = c_uint; - -/// Type for C functions. -/// -/// When called, the arguments passed to the function are available in the stack, with the first -/// argument at index 1 and the last argument at the top of the stack ([`lua_gettop`]). Return -/// values should be pushed onto the stack (the first result is pushed first), and the number of -/// return values should be returned. -pub type lua_CFunction = unsafe extern "C-unwind" fn(*mut lua_State) -> c_int; - -/// Type for a continuation function. -/// -/// See [`lua_pushcclosurek`]. -pub type lua_Continuation = unsafe extern "C-unwind" fn(*mut lua_State, c_int) -> c_int; - -/// Type for a memory allocation function. -/// -/// This function is called with the `ud` passed to [`lua_newstate`], a pointer to the block being -/// allocated/reallocated/freed, the original size of the block, and the requested new size of the -/// block. -/// -/// If the given new size is non-zero, a pointer to a block with the requested size should be -/// returned. If the request cannot be fulfilled or the requested size is zero, null should be -/// returned. If the given original size is not zero, the given block should be freed. It is -/// assumed this never fails if the requested size is smaller than the original size. -pub type lua_Alloc = - unsafe extern "C-unwind" fn(*mut c_void, *mut c_void, usize, usize) -> *mut c_void; - -/// Destructor function for a userdata. Called before the userdata is garbage collected. -pub type lua_Destructor = unsafe extern "C-unwind" fn(L: *mut lua_State, userdata: *mut c_void); - -/// Functions to be called by the debugger in specific events. -pub type lua_Hook = unsafe extern "C-unwind" fn(L: *mut lua_State, ar: *mut lua_Debug); - -/// Callback function for [`lua_getcoverage`]. Receives the coverage information. -pub type lua_Coverage = unsafe extern "C-unwind" fn( - context: *mut c_void, - function: *const c_char, - linedefined: c_int, - depth: c_int, - hits: *const c_int, - size: usize, -); - -/// A Luau thread. -/// -/// This is an opaque type and always exists behind a pointer (like `*mut lua_State`). -#[repr(C)] -pub struct lua_State { - _data: (), - _marker: core::marker::PhantomData, -} - -/// Activation record. Contains debug information. -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct lua_Debug { - /// Name of the function. - pub name: *const c_char, - /// One of `Lua`, `C`, `main`, or `tail`. - pub what: *const c_char, - /// The source. Usually the filename. - pub source: *const c_char, - /// Short chunk identifier. - pub short_src: *const c_char, - /// The line where the function is defined. - pub linedefined: c_int, - /// The current line. - pub currentline: c_int, - /// The number of upvalues. - pub nupvals: c_uint, - /// The number of parameters. - pub nparams: c_uint, - /// Whether or not the function is variadic. - pub isvararg: c_char, - /// Userdata. - pub userdata: *mut c_void, - /// Buffer for `short_src`. - pub ssbuf: [c_char; luaconf::LUA_IDSIZE], -} - -impl lua_Debug { - pub fn new() -> lua_Debug { - lua_Debug { - name: ptr::null(), - what: ptr::null(), - source: ptr::null(), - short_src: ptr::null(), - linedefined: 0, - currentline: 0, - nupvals: 0, - nparams: 0, - isvararg: 0, - userdata: ptr::null_mut(), - ssbuf: [0; luaconf::LUA_IDSIZE], - } - } -} - -impl Default for lua_Debug { - fn default() -> lua_Debug { - lua_Debug::new() - } -} - -/// Callbacks that can be used to reconfigure behavior of the VM dynamically. -/// -/// This can be retrieved using [`lua_callbacks`]. -/// -/// **Note:** `interrupt` is safe to set from an arbitrary thread. All other callbacks should only -/// be changed when the VM is not running any code. -#[repr(C)] -#[derive(Debug, Default, Clone, Copy)] -pub struct lua_Callbacks { - /// Arbitrary userdata pointer. Never overwritten by Luau. - pub userdata: *mut c_void, - - /// Called at safepoints (e.g. loops, calls/returns, garbage collection). - pub interrupt: Option, - /// Called when an unprotected error is raised (if longjmp is used). - pub panic: Option, - - /// Called when `L` is created (`LP` is the parent), or destroyed (`LP` is null). - pub userthread: Option, - /// Called when a string is created. Returned atom can be retrieved via [`lua_tostringatom`]. - pub useratom: Option i16>, - - /// Called when a `BREAK` instruction is encountered. - pub debugbreak: Option, - /// Called after each instruction in single step mode. - pub debugstep: Option, - /// Called when thread execution is interrupted by break in another thread. - pub debuginterrupt: Option, - /// Called when a protected call results in an error. - pub debugprotectederror: Option, - - /// Called when memory is allocated. - pub onallocate: - Option, -} - -unsafe extern "C-unwind" { - /// Creates a new independent state using the given allocation function and `ud`. - pub unsafe fn lua_newstate(f: lua_Alloc, ud: *mut c_void) -> *mut lua_State; - /// Closes the given state. - /// - /// Destroys all objects in the state and frees all dynamic memory used by the state. - pub unsafe fn lua_close(L: *mut lua_State); - /// Pushes a new thread to the stack. - /// - /// Returns a pointer to a [`lua_State`] that represents the created thread. The new state - /// shared all global objects (such as tables) with the original state, but has an - /// independent execution stack. - pub unsafe fn lua_newthread(L: *mut lua_State) -> *mut lua_State; - /// Returns a pointer to a [`lua_State`], which represents the main thread of the given thread. - pub unsafe fn lua_mainthread(L: *mut lua_State) -> *mut lua_State; - /// Resets the given thread to its original state. - /// - /// Clears all the thread state, call frames, and the stack. - pub unsafe fn lua_resetthread(L: *mut lua_State); - /// Returns true if the given thread is in its original state. - pub unsafe fn lua_isthreadreset(L: *mut lua_State) -> c_int; - - /// Converts the given index into an absolute index. - pub unsafe fn lua_absindex(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the index of the top element in the stack. - /// - /// This is equal to the number of elements in the stack. - pub unsafe fn lua_gettop(L: *mut lua_State) -> c_int; - /// Sets the top of the stack to the given index. - /// - /// If the new top is greater than the old one, new elements are filled with `nil`. If the new - /// top is smaller, elements will be removed from the old top. - pub unsafe fn lua_settop(L: *mut lua_State, idx: c_int); - /// Pushes a copy of the element at the given index onto the stack. - pub unsafe fn lua_pushvalue(L: *mut lua_State, idx: c_int); - /// Removes the element at the given index from the stack. - /// - /// Elements above the given index are shifted down to fill the gap. This function cannot be - /// called with a pseudo-index. - pub unsafe fn lua_remove(L: *mut lua_State, idx: c_int); - /// Moves the top element into the given index. - /// - /// Elements above the given index are shifted up to make space. This function cannot be called - /// with a pseudo-index. - pub unsafe fn lua_insert(L: *mut lua_State, idx: c_int); - /// Replaces the element at the given index with the top element. - /// - /// The top element will be popped. - pub unsafe fn lua_replace(L: *mut lua_State, idx: c_int); - /// Ensures the stack has space for the given number of additional elements. - /// - /// Returns `0` if the request cannot be fulfilled, either because it would be bigger than the - /// maximum stack size, or because it cannot allocate memory for the extra space. - pub unsafe fn lua_checkstack(L: *mut lua_State, sz: c_int) -> c_int; - /// Like [`lua_checkstack`], but allows for an infinite amount of stack frames. - pub unsafe fn lua_rawcheckstack(L: *mut lua_State, sz: c_int); - - /// Move values between different threads of the same state. - /// - /// Pops `n` values from the stack of `from`, and pushes them onto the stack of `to`. - pub unsafe fn lua_xmove(from: *mut lua_State, to: *mut lua_State, n: c_int); - /// Copy a value to a different thread of the same state. - /// - /// The element at the given index is copied from `from`, and pushed onto the stack of `to`. - pub unsafe fn lua_xpush(from: *mut lua_State, to: *mut lua_State, idx: c_int); - - /// Checks if a value can be converted to a number. - /// - /// Returns `1` if the value at the given index is a number, or a string that can be converted - /// to a number, and `0` otherwise. - pub unsafe fn lua_isnumber(L: *mut lua_State, idx: c_int) -> c_int; - /// Checks if a value can be converted to a string. - /// - /// Returns `1` if the value at the given index is a string or a number, and `0` otherwise. - pub unsafe fn lua_isstring(L: *mut lua_State, idx: c_int) -> c_int; - /// Checks if a value is a C function. - /// - /// Returns `1` if the value at the given index is a C function, and `0` otherwise. - pub unsafe fn lua_iscfunction(L: *mut lua_State, idx: c_int) -> c_int; - /// Checks if a value is a Luau function. - /// - /// Returns `1` if the value at the given index is a Luau function, and `0` otherwise. - pub unsafe fn lua_isLfunction(L: *mut lua_State, idx: c_int) -> c_int; - /// Checks if a value is a userdata. - /// - /// Returns `1` if the value at the given index is a userdata, and `0` otherwise. - pub unsafe fn lua_isuserdata(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the type of the value at the given index. - pub unsafe fn lua_type(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the name of the given type. - pub unsafe fn lua_typename(L: *mut lua_State, tp: c_int) -> *const c_char; - - /// Checks if two values are equal. - /// - /// Returns `1` if the two values in the given indices are equal, and `0` otherwise. This - /// follows the behavior of the `==` operator, and may call metamethods. - pub unsafe fn lua_equal(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; - /// Like [`lua_equal`], but will not invoke metamethods. - pub unsafe fn lua_rawequal(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; - /// Checks if one value is less than another value. - /// - /// Returns `1` if the value at `idx1` is smaller than the value at `idx2`, and `0` otherwise. - /// This follows the behavior of the `<` operator, and may call metamethods. - pub unsafe fn lua_lessthan(L: *mut lua_State, idx1: c_int, idx2: c_int) -> c_int; - - /// Checks if the given value can be converted to a number, and returns the number. - /// - /// If the value at the given index is a number, or a string that can be converted to one, the - /// number will be returned and `isnum` will be `1`. Otherwise, `0` will be returned and `isnum` - /// will be `0`. - pub unsafe fn lua_tonumberx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Number; - /// Checks if the given value can be converted to an integer, and returns the integer. - /// - /// If the value at the given index is a number, or a string that can be converted to one, the - /// number will be truncated and returned and `isnum` will be `1`. Otherwise, `0` will be - /// returned and `isnum` will be `0`. - pub unsafe fn lua_tointegerx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Integer; - /// Checks if the given value can be converted to an unsigned integer, and returns it. - /// - /// If the value at the given index is a number, or a string that can be converted to one, the - /// number will be truncated and returned and `isnum` will be `1`. Otherwise, `0` will be - /// returned and `isnum` will be `0`. - /// - /// If the integer is negative, it will be forced into an unsigned integer in an unspecified - /// way. - pub unsafe fn lua_tounsignedx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) - -> lua_Unsigned; - /// Get the vector in the given value. - /// - /// If the value at the given index is a vetor, returns a pointer to the first component of - /// that vector (`x`), otherwise returns null. - pub unsafe fn lua_tovector(L: *mut lua_State, idx: c_int) -> *const c_float; - /// Get the boolean in the given value. - /// - /// Returns `1` if the given value is truthy, and `0` if it is falsy. A value is falsy if it is - /// `false` or `nil`. - pub unsafe fn lua_toboolean(L: *mut lua_State, idx: c_int) -> c_int; - /// Converts the given value to a string. - /// - /// The value at the given index is converted into a string and `len` will be the length of the - /// string. The value must be a string or number, otherwise null is returned. If it is a - /// number, the actual value in the stack will be changed to a string. - /// - /// An aligned pointer to the string within the Luau state is returned. This string is null - /// terminated, but may also contain nulls within its body. If the string has been removed from - /// the stack, it may get garbage collected causing the pointer to dangle. - pub unsafe fn lua_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; - /// Like [`lua_tolstringatom`] but doesn't get the length. - pub unsafe fn lua_tostringatom( - L: *mut lua_State, - idx: c_int, - atom: *mut c_int, - ) -> *const c_char; - /// Get a string and its atom. - /// - /// Returns an aligned pointer to the string within the Luau state. If the value is not a - /// string, null is returned, no conversion is performed for non-strings. The string is null - /// terminated, but may also contain nulls within its body. If the string has been removed from - /// the stack, it may get garbage collected causing the pointer to dangle. - /// - /// The `atom` will be the result of [`lua_Callbacks::useratom`] or -1 if not set. The same - /// string will have the same atom. - pub unsafe fn lua_tolstringatom( - L: *mut lua_State, - idx: c_int, - len: *mut usize, - atom: *mut c_int, - ) -> *const c_char; - /// Gets the current namecall string and its atom. - /// - /// When called during a namecall, a pointer to the string containing the name of the method - /// that was called is returned, otherwise null is returned. See [`lua_tolstringatom`] for more - /// information. - pub unsafe fn lua_namecallatom(L: *mut lua_State, atom: *mut c_int) -> *const c_char; - /// Get the length of the given value. - /// - /// Returns the "length" of the value at the given index. For strings, it is the length of the - /// string. For tables, it is the result of the `#` operator. For userdata, this is the size of - /// the block of memory allocated for the userdata. For other values, it is `0`. - pub unsafe fn lua_objlen(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the C function in the given value. - /// - /// Returns the C function in the value at the given index, otherwise returns null. - pub unsafe fn lua_tocfunction(L: *mut lua_State, idx: c_int) -> lua_CFunction; - /// Gets the light userdata in the given value. - /// - /// Returns the light userdata in the value at the given index, otherwise returns null. - pub unsafe fn lua_tolightuserdata(L: *mut lua_State, idx: c_int) -> *mut c_void; - /// Gets the light userdata with the given tag in the given value. - /// - /// Returns the light userdata in the value at the given index if its tag matches the given - /// tag, otherwise returns null. - pub unsafe fn lua_tolightuserdatatagged( - L: *mut lua_State, - idx: c_int, - tag: c_int, - ) -> *mut c_void; - /// Gets the userdata in the given value. - /// - /// Returns the userdata in the value at the given index, otherwise returns null. - pub unsafe fn lua_touserdata(L: *mut lua_State, idx: c_int) -> *mut c_void; - /// Gets the userdata with the given tag in the given value. - /// - /// Returns the userdata in the value at the given index if its tag matches the given tag, - /// otherwise returns null. - pub unsafe fn lua_touserdatatagged(L: *mut lua_State, idx: c_int, tag: c_int) -> *mut c_void; - /// Gets the tag of the userdata in the given value. - /// - /// Returns the tag of the userdata in the value at the given index, or `-1` if the value is - /// not a userdata. - pub unsafe fn lua_userdatatag(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the tag of the light userdata in the given value. - /// - /// Returns the tag of the light userdata in the value at the given index, or `-1` if the value - /// is not a userdata. - pub unsafe fn lua_lightuserdatatag(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the thread in the given value. - /// - /// Returns a pointer to a [`lua_State`] that represents the thread in the value at the given - /// index, or null if the value is not a thread. - pub unsafe fn lua_tothread(L: *mut lua_State, idx: c_int) -> *mut lua_State; - /// Gets the buffer in the given value. - /// - /// Returns a pointer to the first byte of the buffer in the value at the given index, or null - /// if the value is not a buffer. The `len` will be the length of the buffer. - pub unsafe fn lua_tobuffer(L: *mut lua_State, idx: c_int, len: *mut usize) -> *mut c_void; - /// Converts the given value into a generic pointer. - /// - /// Returns a pointer that represents the given value. The value can be a userdata, a table, a - /// thread or a function; otherwise, null is returned. Different objects give different - /// pointers. There is no way to convert a pointer back into its original value. - /// - /// This function is typically used for debugging purposes. - pub unsafe fn lua_topointer(L: *mut lua_State, idx: c_int) -> *const c_void; - - /// Pushes `nil` value onto the stack. - pub unsafe fn lua_pushnil(L: *mut lua_State); - /// Pushes the given number onto the stack. - pub unsafe fn lua_pushnumber(L: *mut lua_State, n: c_double); - /// Pushes the given integer onto the stack. - pub unsafe fn lua_pushinteger(L: *mut lua_State, n: c_int); - /// Pushes the given unsigned integer onto the stack. - pub unsafe fn lua_pushunsigned(L: *mut lua_State, n: c_uint); - - /// Pushes the given vector onto the stack. - #[cfg(not(feature = "vector4"))] - pub unsafe fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float); - /// Pushes the given vector onto the stack. - #[cfg(feature = "vector4")] - pub unsafe fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float, w: c_float); - - /// Pushes the pointed-to string with the given length onto the stack. - pub unsafe fn lua_pushlstring(L: *mut lua_State, s: *const c_char, l: usize); - /// Pushes the given null-terminated string onto the stack. - pub unsafe fn lua_pushstring(L: *mut lua_State, s: *const c_char); - /// Pushes a formatted string onto the stack. - /// - /// The `fmt` string can contain the following specifiers: - /// - `%%`: inserts a literal `%`. - /// - `%s`: inserts a zero terminated string. - /// - `%f`: inserts a [`lua_Number`]. - /// - `%p`: inserts a pointer as a hexadecimal numeral. - /// - `%d`: inserts an int. - /// - `%c`: inserts an int as a character. - pub unsafe fn lua_pushfstringL(L: *mut lua_State, fmt: *const c_char, ...) -> *const c_char; - /// Pushes the given C closure onto the stack, with a continuation function. - /// - /// Takes the C function, the function name for debugging, the number of upvalues and - /// an optional continuation function. - /// - /// The upvalues will be popped from the stack. The upvalues can then be retrieved within the - /// function by subtracting the index of the upvalue (starting from 1) from [`LUA_GLOBALSINDEX`]. - /// For example, to get the first upvalue you would do `LUA_GLOBALSINDEX - 1`. - /// - /// See `examples/continuations.rs` for more information about the continutation function. - pub unsafe fn lua_pushcclosurek( - L: *mut lua_State, - fn_: lua_CFunction, - debugname: *const c_char, - nup: c_int, - cont: Option, - ); - /// Pushes the given boolean onto the stack. - pub unsafe fn lua_pushboolean(L: *mut lua_State, b: c_int); - /// Pushes the current thread onto the stack. - /// - /// Returns `1` if this thread is the main thread of its state. - pub unsafe fn lua_pushthread(L: *mut lua_State) -> c_int; - - /// Pushes a tagged light userdata onto the stack. - pub unsafe fn lua_pushlightuserdatatagged(L: *mut lua_State, p: *mut c_void, tag: c_int); - /// Allocates a new tagged userdata and pushes it onto the stack. - /// - /// Returns a pointer to the allocated block of memory with the given size. - pub unsafe fn lua_newuserdatatagged(L: *mut lua_State, sz: usize, tag: c_int) -> *mut c_void; - /// Allocates a new tagged userdata with the metatable and pushes it onto the stack. - /// - /// The userdata will have the given tag. The metatable will be set to the metatable that has - /// been set using [`lua_setuserdatametatable`]. - pub unsafe fn lua_newuserdatataggedwithmetatable( - L: *mut lua_State, - sz: usize, - tag: c_int, - ) -> *mut c_void; - /// Allocates a new userdata with the given destructor and pushes it onto the stack. - /// - /// The specified destructor function will be called before the userdata is garbage collected. - pub unsafe fn lua_newuserdatadtor( - L: *mut lua_State, - sz: usize, - dtor: unsafe extern "C-unwind" fn(*mut c_void), - ) -> *mut c_void; - - /// Allocates a new Luau buffer and pushes it onto the stack. - /// - /// Returns a pointer to the allocated block of memory with the given size. - pub unsafe fn lua_newbuffer(L: *mut lua_State, sz: usize) -> *mut c_void; - - /// Gets an element from the given table and pushes it onto the stack. - /// - /// Pushes the value `t[k]` onto the stack, where `t` is the table at the given index and `k` - /// is the value at the top of the stack. The key is popped from the stack. This may call the - /// `__index` metamethod. - /// - /// Returns the type of the pushed value. - pub unsafe fn lua_gettable(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets a field from the given table and pushes it onto the stack. - /// - /// Pushes the value `t[k]` onto the stack, where `t` is the table at the given index and `k` - /// is the given string. This may call the `__index` metamethod. - /// - /// Returns the type of the pushed value. - pub unsafe fn lua_getfield(L: *mut lua_State, idx: c_int, k: *const c_char) -> c_int; - /// Gets a field from the given table without invoking metamethods. - /// - /// Like [`lua_getfield`], but does not invoke metamethods. - pub unsafe fn lua_rawgetfield(L: *mut lua_State, idx: c_int, k: *const c_char) -> c_int; - /// Gets an element from the given table without invoking metamethods. - /// - /// Like [`lua_gettable`], but does not invoke metamethods. - pub unsafe fn lua_rawget(L: *mut lua_State, idx: c_int) -> c_int; - /// Gets the element at the given index in the given table without invoking metamethods. - /// - /// Pushes the value `t[n]` onto the stack, where `t` is the table at the given index and `n` - /// is the given index. - /// - /// Returns the type of the pushed value. - pub unsafe fn lua_rawgeti(L: *mut lua_State, idx: c_int, n: c_int) -> c_int; - /// Pushes a new empty table with the given size hints onto the stack. - /// - /// `narr` is a hint for how many elements the array portion the table will contain, `nrec` is - /// a hint for how many other elements it will contain. The hints may be used to preallocate - /// the memory for the table. - /// - /// If you do not know the number of elements the table will contain in advance, you may use - /// [`lua_newtable`] instead. - pub unsafe fn lua_createtable(L: *mut lua_State, narr: c_int, nrec: c_int); - - /// Sets the `readonly`` flag of the given table. - pub unsafe fn lua_setreadonly(L: *mut lua_State, idx: c_int, enabled: c_int); - /// Gets the `readonly` flag of the given table. - pub unsafe fn lua_getreadonly(L: *mut lua_State, idx: c_int) -> c_int; - /// Sets the `safeenv` flag of the given table. - pub unsafe fn lua_setsafeenv(L: *mut lua_State, idx: c_int, enabled: c_int); - - /// Pushes the metatable of the value at the given index onto the stack. - /// - /// If the index is not valid or if it doesn't have a metatable, zero is returned. - pub unsafe fn lua_getmetatable(L: *mut lua_State, objindex: c_int) -> c_int; - /// Pushes the environment table of the value at the given index onto the stack. - pub unsafe fn lua_getfenv(L: *mut lua_State, idx: c_int); - - /// Sets an element in the given table. - /// - /// Does the equivalent of `t[k] = v`, where `t` is the value at the given index, `v` is the - /// value at the top of the stack, and `k` is the value just below the top. The key and value - /// will be popped from the stack. This may call the `__newindex` metamethod. - pub unsafe fn lua_settable(L: *mut lua_State, idx: c_int); - /// Sets a field in the given table. - /// - /// Does the equivalent of `t[k] = v`, where `t` is the value at the given index, `v` is the - /// value at the top of the stack, and `k` is the given string. The value will be popped from - /// the stack. This may call the `__newindex` metamethod. - pub unsafe fn lua_setfield(L: *mut lua_State, idx: c_int, k: *const c_char); - /// Sets a field in the given table without invoking metamethods. - /// - /// Like [`lua_setfield`], but does not invoke metamethods. - pub unsafe fn lua_rawsetfield(L: *mut lua_State, idx: c_int, k: *const c_char); - /// Sets an element in the given table without invoking metamethods. - /// - /// Like [`lua_settable`], but does not invoke metamethods. - pub unsafe fn lua_rawset(L: *mut lua_State, idx: c_int); - /// Sets the element at the given index in the given table without invoking metamethods. - /// - /// Does the equivalent of `t[n] = v` onto the stack, where `t` is the table at the given index - /// and `v` is the value at the top of the stack. - pub unsafe fn lua_rawseti(L: *mut lua_State, idx: c_int, n: c_int); - /// Sets the metatable of the given value. - /// - /// Pops a table from the stack and sets it as the metatable for the value at the given index. - /// - /// Always returns 1 for historical reasons. - pub unsafe fn lua_setmetatable(L: *mut lua_State, objindex: c_int) -> c_int; - /// Sets the environment for the given value. - /// - /// Pops a table from the stack and sets it as the new environment for the value at the given - /// index. Returns 1 if the value is a function, thread, or userdata. Otherwise, returns 0. - pub unsafe fn lua_setfenv(L: *mut lua_State, idx: c_int) -> c_int; - - /// Loads the given Luau chunk and pushes it as a function onto the stack. - /// - /// Takes the name of the chunk for debugging, a pointer to the bytecode, the size of the - /// bytecode, and a value representing the environment of the chunk. If `env` is 0, the current - /// environment will be used, otherwise the table on the stack at index `env` will be used. - /// - /// Returns 1 if there was an error, 0 otherwise. The error message will be pushed onto the - /// stack if there was an error. - pub unsafe fn luau_load( - L: *mut lua_State, - chunkname: *const c_char, - data: *const c_char, - size: usize, - env: c_int, - ) -> c_int; - /// Calls the given function. - /// - /// This will pop `nargs` values from the stack, which will be passed into the function as - /// arguments. The first argument is the first value that got pushed. The results of the - /// function are pushed onto the stack. The number of results will be adjusted to `nresults`, - /// unless `nresults` is [`LUA_MULTRET`], in which case all results are pushed. The first - /// result is pushed first, the last result will be at the top of the stack. - pub unsafe fn lua_call(L: *mut lua_State, nargs: c_int, nresults: c_int); - /// Calls the given function in protected mode. - /// - /// Like [`lua_call`]. However, if there is an error, it will get catched, the error message - /// will be pushed onto the stack, and the error code will be returned. - /// - /// If `errfunc` is zero, the error message pushed on the stack will not be modified. Otherwise, - /// `errfunc` is the stack index of an *error handler function*. In case of an error, this - /// function will be called with the error message and its return value will be the error - /// messaged pushed onto the stack. - /// - /// Returns zero if successful, otherwise returns one of [`LUA_ERRRUN`], [`LUA_ERRMEM`], or - /// [`LUA_ERRERR`]. - pub unsafe fn lua_pcall( - L: *mut lua_State, - nargs: c_int, - nresults: c_int, - errfunc: c_int, - ) -> c_int; - - /// Yields the current coroutine. - /// - /// This function should be called as the return of a C function. - pub unsafe fn lua_yield(L: *mut lua_State, nresults: c_int) -> c_int; - /// Breaks execution, as if a debug breakpoint has been reached. - /// - /// This function should be called as the return of a C function. - pub unsafe fn lua_break(L: *mut lua_State) -> c_int; - /// Starts and resumes a coroutine in the given thread `L`. - /// - /// To start a coroutine, push the main function plus any arguments onto the stack, then call - /// this function with `nargs` being the number of arguments. Once the coroutine suspends or - /// finishes, the results of the resumption will be pushed onto the stack. The first result - /// will be pushed first, the last result will be at the top of the stack. If the coroutine - /// yielded, [`LUA_YIELD`] is returned. If there was an error an error code is returned, and - /// the error value will be pushed onto the stack. Otherwise, [`LUA_OK`] is returned. - /// - /// To resume a coroutine, push values to be returned from the `yield` call onto the stack, and - /// call this function. Make sure to remove the results pushed from your previous resume call - /// before resuming again. - /// - /// The `from` parameter represents the coroutine that is resuming `L`. This may be null if - /// there is no such coroutine. - pub unsafe fn lua_resume(L: *mut lua_State, from: *mut lua_State, narg: c_int) -> c_int; - /// Error a coroutine in the given thread `L`. - /// - /// Like [`lua_resume`], but this will pop a single value from the stack which will be used as - /// the error value. If the coroutine is currently inside of a `pcall`, the error will be - /// catched by that `pcall`. Otherwise, this call will return an error code and the error value - /// will be pushed onto the stack. - pub unsafe fn lua_resumeerror(L: *mut lua_State, from: *mut lua_State) -> c_int; - /// Gets the status of the given thread `L`. - /// - /// The status can be [`LUA_OK`] for a normal thread, an error code if the thread finished - /// execution of a `lua_resume` with an error, or [`LUA_YIELD`] if the thread is suspended. - /// - /// You can only call functions in threads that are [`LUA_OK`]. You can resume threads that are - /// [`LUA_OK`] or [`LUA_YIELD`]. - pub unsafe fn lua_status(L: *mut lua_State) -> c_int; - /// Returns 1 if the given coroutine can yield, and 0 otherwise. - pub unsafe fn lua_isyieldable(L: *mut lua_State) -> c_int; - /// Gets the thread data of the given thread. - /// - /// The thread data can be set with [`lua_setthreaddata`]. - pub unsafe fn lua_getthreaddata(L: *mut lua_State) -> *mut c_void; - /// Sets the thread data of the given thread. - /// - /// The thread data can be retrieved afterwards with [`lua_getthreaddata`]. - pub unsafe fn lua_setthreaddata(L: *mut lua_State, data: *mut c_void); - /// Gets the status of the given coroutine `co`. - pub unsafe fn lua_costatus(L: *mut lua_State, co: *mut lua_State) -> c_int; - - /// Controls the garbage collector. - /// - /// Takes a GC operation `what` and data to use for the operation. The different operations are - /// documented inside [lua.h]. - /// - /// [lua.h]: https://github.com/luau-lang/luau/blob/master/VM/include/lua.h#L249 - pub unsafe fn lua_gc(L: *mut lua_State, what: c_int, data: c_int) -> c_int; - - /// Set the memory category used for memory statistics. - pub unsafe fn lua_setmemcat(L: *mut lua_State, category: c_int); - /// Get the total amount of memory used, in bytes. - /// - /// If `category < 0`, then the total amount of memory is returned. - pub unsafe fn lua_totalbytes(L: *mut lua_State, category: c_int) -> usize; - - /// Throws a Luau error. - /// - /// The error value must be at the top of the stack. - pub unsafe fn lua_error(L: *mut lua_State) -> !; - - /// Gets the next key-value pair in the given table. - /// - /// This will pop a key from the stack, and pushes the next key-value pair onto the stack of - /// the table at the given index. If the end of the table has been reached, nothing is pushed - /// and zero is returned. If a non-zero value is returned, then the key is at stack index `-2`, - /// and the value is at `-1`. - /// - /// While traversing a table, avoid calling [`lua_tolstring`] directly on a key, unless you - /// know the key is actually a string. [`lua_tolstring`] will change the value to a string if - /// it's a number, which will confuse [`lua_next`]. - pub unsafe fn lua_next(L: *mut lua_State, idx: c_int) -> c_int; - /// Iterate over the given table. - /// - /// This function should be repeatedly called, where `iter` is the previous return value, or `0` - /// on the first iteration. If `-1` is returned, the end of the table has been reached. - /// Otherwise, the key-value pair is pushed onto the stack. The key will be at index `-2` and - /// the value at `-1`. - /// - /// This function is similar to [`lua_next`], however the order of items from this function - /// matches the order from a normal `for i, v in t do` loop (array portion first, then the - /// remaining keys). - pub unsafe fn lua_rawiter(L: *mut lua_State, idx: c_int, iter: c_int) -> c_int; - - /// Pops `n` values from the stack and concatenates them. - /// - /// The concatenated string is pushed onto the stack. If `n` is zero, the result is an empty - /// string. Follows the same semantics as the `..` operator in Luau. - pub unsafe fn lua_concat(L: *mut lua_State, n: c_int); - - /// Obfuscates the given pointer. - /// - /// The same `p` will return the same value on the same state. - pub unsafe fn lua_encodepointer(L: *mut lua_State, p: usize) -> usize; - - /// Returns a high-precision timestamp (in seconds). - /// - /// This is the same as `os.clock()` from the Luau standard library. This can be used to - /// measure a duration with sub-microsecond precision. - pub unsafe fn lua_clock() -> c_double; - - /// Sets the tag of the userdata at the given index. - pub unsafe fn lua_setuserdatatag(L: *mut lua_State, idx: c_int, tag: c_int); - - /// Set the destructor function for userdata objects with the given tag. - pub unsafe fn lua_setuserdatadtor(L: *mut lua_State, tag: c_int, dtor: Option); - /// Gets the destructor function for userdata objects with the given tag. - pub unsafe fn lua_getuserdatadtor(L: *mut lua_State, tag: c_int) -> lua_Destructor; - - /// Sets the metatable for userdata objects with the given tag. - /// - /// This will pop a table from the top of the stack, and use it as the metatable for any - /// userdata objects created with the given tag. This cannot be called on a tag that already - /// has a metatable set. - pub unsafe fn lua_setuserdatametatable(L: *mut lua_State, tag: c_int); - /// Gets the metatable for userdata objects with the given tag. - pub unsafe fn lua_getuserdatametatable(L: *mut lua_State, tag: c_int); - - /// Sets the name for light userdata objects with the given tag. - /// - /// This cannot be called on a tag that already has a name set. - pub unsafe fn lua_setlightuserdataname(L: *mut lua_State, tag: c_int, name: *const c_char); - /// Gets the name for light userdata objects wiwth the given tag. - pub unsafe fn lua_getlightuserdataname(L: *mut lua_State, tag: c_int) -> *const c_char; - - /// Pushes a clone of the function at the given index onto the stack. - pub unsafe fn lua_clonefunction(L: *mut lua_State, idx: c_int); - - /// Clears the table at the given index. - pub unsafe fn lua_cleartable(L: *mut lua_State, idx: c_int); - /// Pushes a clone of the table at the given index onto the stack. - pub unsafe fn lua_clonetable(L: *mut lua_State, idx: c_int); - - /// Gets the allocator function of the given state. - /// - /// Will update `ud` to be the value passed to [`lua_newstate`]. - pub unsafe fn lua_getallocf(L: *mut lua_State, ud: *mut *mut c_void) -> lua_Alloc; - - /// Stores the given value in the registry and gets a reference to it. - /// - /// This will store a copy of the value at the given index into the registry. The reference, - /// which is an integer that identifies this value, is returned. A reference can then be - /// removed from the registry using [`lua_unref`], allowing it to be garbage collected. - /// References can be reused once freed. - /// - /// If the given value is `nil`, the special reference [`LUA_REFNIL`] is returned. The sentinel - /// reference [`LUA_NOREF`] will never be returned by this function. - pub unsafe fn lua_ref(L: *mut lua_State, idx: c_int) -> c_int; - /// Frees a reference created by [`lua_ref`] from the registry. - pub unsafe fn lua_unref(L: *mut lua_State, ref_: c_int); - - /// Gets the depth of the call stack. - pub unsafe fn lua_stackdepth(L: *mut lua_State) -> c_int; - /// Gets information about a specific function or function invocation. - /// - /// If `level < 0`, it is assumed to be a stack index, and information about the function at - /// that index is retrieved. Otherwise, it is assumed to be a level in the call stack, and - /// information about the invocation at that level is retrieved. - /// - /// The `ar` argument will be filled with the values requested in `what`. Each character in - /// `what` selects some fields in `ar` to be filled. - /// - /// - `s`: fills `source`, `what`, `linedefined`, and `short_src` - /// - `l`: fills `currentline` - /// - `u`: fills `nupvals` - /// - `a`: fills `isvararg` and `nparams` - /// - `n`: fills `name` - /// - `f`: pushes the function onto the stack - /// - /// Returns `1` if `ar` was updated, `0` otherwise. - pub unsafe fn lua_getinfo( - L: *mut lua_State, - level: c_int, - what: *const c_char, - ar: *mut lua_Debug, - ) -> c_int; - /// Pushes a copy of the `n`th argument at the given level onto the stack. - /// - /// Returns `1` if a value was pushed, `0` otherwise. Always returns `0` for invocations to - /// native functions. - pub unsafe fn lua_getargument(L: *mut lua_State, level: c_int, n: c_int) -> c_int; - /// Pushes a copy of the `n`th local variable at the given level onto the stack. - /// - /// Returns a pointer to the name of the variable. If no value was pushed, null is returned. - /// Always returns null for invocations to native functions. - pub unsafe fn lua_getlocal(L: *mut lua_State, level: c_int, n: c_int) -> *const c_char; - /// Pops a value from the stack and sets it as the `n`th local variable at the given level. - /// - /// Returns a pointer to the name of the variable. If no variable was set, null is returned. - /// Always returns null for invocations to native functions. - pub unsafe fn lua_setlocal(L: *mut lua_State, level: c_int, n: c_int) -> *const c_char; - /// Pushes a copy of the `n`th upvalue of the function at the given index onto the stack. - pub unsafe fn lua_getupvalue(L: *mut lua_State, funcindex: c_int, n: c_int) -> *const c_char; - /// Pops a value from the stack and sets it as the `n`th upvalue of the given function. - pub unsafe fn lua_setupvalue(L: *mut lua_State, funcindex: c_int, n: c_int) -> *const c_char; - - /// Enables single stepping for the given thread. - pub unsafe fn lua_singlestep(L: *mut lua_State, enabled: c_int); - /// Sets a breakpoint for the given function at the given line. - pub unsafe fn lua_breakpoint( - L: *mut lua_State, - funcindex: c_int, - line: c_int, - enabled: c_int, - ) -> c_int; - - /// Collects coverage information for the given function. - pub unsafe fn lua_getcoverage( - L: *mut lua_State, - funcindex: c_int, - context: *mut c_void, - callback: lua_Coverage, - ); - - /// Returns a string representation of the stack trace for debugging. - /// - /// This is **NOT thread-safe**, the result is stored in a shared global buffer. - pub unsafe fn lua_debugtrace(L: *mut lua_State) -> *const c_char; - - /// Gets the callbacks used by the given state. - /// - /// These are shared between all coroutines. - pub unsafe fn lua_callbacks(L: *mut lua_State) -> *mut lua_Callbacks; -} diff --git a/lua/chunk.go b/lua/chunk.go index 33bf3f3..7108fd7 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -18,6 +18,7 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { initialStack := ffi.GetTop(state) // Track initial stack size + argsCount := len(args) if c.bytecode != nil { hasLoaded := ffi.LuauLoad(state, c.name, c.bytecode, uint64(len(c.bytecode)), 0) if !hasLoaded { @@ -28,13 +29,15 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { // Push function onto the stack ffi.RawGetI(state, ffi.LUA_REGISTRYINDEX, int32(c.index)) - // Push all arguments onto the stack (deref) + // Push the length and the arguments onto the stack (deref) + ffi.PushNumber(state, ffi.LuaNumber(argsCount)) + argsCount++ for _, arg := range args { arg.deref() } } - status := ffi.Pcall(state, int32(len(args)), -1, 0) + status := ffi.Pcall(state, int32(argsCount), -1, 0) if status != ffi.LUA_OK { return nil, newLuaError(state, int(status)) } diff --git a/lua/registry.go b/lua/registry.go index f4f05c1..9551309 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -24,17 +24,45 @@ var registryTrampolineDtor = C.registryTrampolineDtor //export registryTrampolineImpl func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { - state := (*ffi.LuaState)(lua) + rawState := (*ffi.LuaState)(lua) + state := &Lua{ + inner: &StateWithMemory{ + memState: getMemoryState(rawState), + luaState: rawState, + }, + } + entry := cgo.Handle(handle).Value().(*functionEntry) fn, ok := entry.registry.get(entry.id) if !ok { - ffi.PushString(state, "function not found in registry") - ffi.Error(state) + ffi.PushString(rawState, "function not found in registry") + ffi.Error(rawState) + return 0 + } + + argsCount := int(ffi.ToNumber(rawState, 1)) + args := make([]LuaValue, argsCount) + + for i := range argsCount { + // Lua stack is 1-based, and the first argument is at index 2 (since index 1 is the count) + stackIndex := int32(i + 2) + args[i] = intoLuaValue(state, stackIndex) + } + + returns, err := fn(state, args...) + + if err != nil { + ffi.PushString(rawState, err.Error()) + ffi.Error(rawState) return 0 } - return C.int(fn(state)) + for _, ret := range returns { + ret.deref() + } + + return C.int(len(returns)) } //export registryTrampolineDtorImpl @@ -44,8 +72,10 @@ func registryTrampolineDtorImpl(_ *C.lua_State, handle C.uintptr_t) { cgo.Handle(handle).Delete() } +type GoFunction func(lua *Lua, args ...LuaValue) ([]LuaValue, error) + type functionRegistry struct { - functions map[uintptr]ffi.LuaCFunction + functions map[uintptr]GoFunction nextID uintptr } @@ -56,18 +86,18 @@ type functionEntry struct { func newFunctionRegistry() *functionRegistry { return &functionRegistry{ - functions: make(map[uintptr]ffi.LuaCFunction), + functions: make(map[uintptr]GoFunction), } } -func (fr *functionRegistry) register(fn ffi.LuaCFunction) *functionEntry { +func (fr *functionRegistry) register(fn GoFunction) *functionEntry { fr.nextID++ id := fr.nextID fr.functions[id] = fn return &functionEntry{registry: fr, id: id} } -func (fr *functionRegistry) get(id uintptr) (ffi.LuaCFunction, bool) { +func (fr *functionRegistry) get(id uintptr) (GoFunction, bool) { fn, ok := fr.functions[id] return fn, ok } diff --git a/lua/state.go b/lua/state.go index 41e4cf0..ed899a5 100644 --- a/lua/state.go +++ b/lua/state.go @@ -63,7 +63,7 @@ func (l *Lua) CreateString(str string) *LuaString { return s } -func (l *Lua) CreateFunction(fn ffi.LuaCFunction) *LuaChunk { +func (l *Lua) CreateFunction(fn GoFunction) *LuaChunk { state := l.state() entry := l.fnRegistry.register(fn) diff --git a/main.go b/main.go index c115041..1de13d0 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" - "github.com/CompeyDev/lei/ffi" "github.com/CompeyDev/lei/lua" ) @@ -63,11 +62,12 @@ func main() { fmt.Printf("%s %s\n", k, v) } - cFnChunk := state.CreateFunction(func(L *ffi.LuaState) int32 { - ffi.PushString(L, "Hello") - ffi.PushString(L, "from") - ffi.PushString(L, fmt.Sprintf("Go, %s!", ffi.LCheckString(L, 1))) - return 3 + cFnChunk := state.CreateFunction(func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + return []lua.LuaValue{ + luaState.CreateString("Hello"), + luaState.CreateString("from"), + luaState.CreateString(fmt.Sprintf("Go, %s!", args[0].(*lua.LuaString).ToString())), + }, nil }) returns, callErr := cFnChunk.Call(state.CreateString("Lua")) From c1a9aec2ca545b8bc194d233b35c45847a0f90d0 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 10 Dec 2025 07:59:31 +0000 Subject: [PATCH 21/64] refactor(lua): add type assertions for `LuaValue` impls Also corrected the `LuaValue` implementation for `LuaNil` to return `LUA_REFNIL` instead of 0 for `ref()` call. --- lua/nil.go | 6 +++++- lua/string.go | 2 ++ lua/table.go | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/nil.go b/lua/nil.go index e69b3ff..d3f623a 100644 --- a/lua/nil.go +++ b/lua/nil.go @@ -1,11 +1,15 @@ package lua +import "github.com/CompeyDev/lei/ffi" + type LuaNil struct{ vm *Lua } // // LuaValue Implementation // +var _ LuaValue = (*LuaNil)(nil) + func (n *LuaNil) lua() *Lua { return n.vm } -func (n *LuaNil) ref() int { return 0 } +func (n *LuaNil) ref() int { return ffi.LUA_REFNIL } func (n *LuaNil) deref() int { return 0 } diff --git a/lua/string.go b/lua/string.go index b4a0b20..a17ba8d 100644 --- a/lua/string.go +++ b/lua/string.go @@ -33,6 +33,8 @@ func (s *LuaString) ToPointer() unsafe.Pointer { // LuaValue implementation // +var _ LuaValue = (*LuaString)(nil) + func (s *LuaString) lua() *Lua { return s.vm } func (s *LuaString) ref() int { return s.index } diff --git a/lua/table.go b/lua/table.go index 2a862b3..0a25629 100644 --- a/lua/table.go +++ b/lua/table.go @@ -54,6 +54,8 @@ func (t *LuaTable) Iterable() map[LuaValue]LuaValue { // LuaValue implementation // +var _ LuaValue = (*LuaTable)(nil) + func (t *LuaTable) lua() *Lua { return t.vm } func (t *LuaTable) ref() int { return t.index } From 434855e92e4f7a2ca33f7e52c1292de14ea32429 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 10 Dec 2025 08:01:05 +0000 Subject: [PATCH 22/64] feat(lua): implement `LuaNumber` type Numbers are not an interned type, so we just store the value itself, avoiding any unneeded stack manipulation or registry storage. --- lua/number.go | 25 +++++++++++++++++++++++++ lua/value.go | 9 +++++++++ 2 files changed, 34 insertions(+) create mode 100644 lua/number.go diff --git a/lua/number.go b/lua/number.go new file mode 100644 index 0000000..080ecab --- /dev/null +++ b/lua/number.go @@ -0,0 +1,25 @@ +package lua + +import "github.com/CompeyDev/lei/ffi" + +type LuaNumber struct { + vm *Lua + inner float64 +} + +// +// LuaValue implementation +// + +var _ LuaValue = (*LuaNumber)(nil) + +// Numbers are cheap to copy, so we don't store the reference index + +func (n *LuaNumber) lua() *Lua { return n.vm } +func (n *LuaNumber) ref() int { return ffi.LUA_NOREF } +func (n *LuaNumber) deref() int { + state := n.vm.state() + + ffi.PushNumber(state, ffi.LuaNumber(n.inner)) + return int(ffi.GetTop(state)) +} diff --git a/lua/value.go b/lua/value.go index 854b0ac..0602a90 100644 --- a/lua/value.go +++ b/lua/value.go @@ -36,6 +36,12 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { zero := reflect.Zero(t) switch val := v.(type) { + case *LuaNumber: + if t.Kind() == reflect.Float64 { + num := reflect.ValueOf(val.inner) + return num, nil + } + case *LuaString: if t.Kind() == reflect.String { str := reflect.ValueOf(val.ToString()).Convert(t) @@ -143,6 +149,9 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { state := lua.state() switch ffi.Type(state, index) { + case ffi.LUA_TNUMBER: + num := ffi.ToNumber(state, index) + return &LuaNumber{vm: lua, inner: float64(num)} case ffi.LUA_TSTRING: ref := ffi.Ref(state, index) return &LuaString{vm: lua, index: int(ref)} From 6bbd51be41cf8066aa7f880978e04001d44d3e16 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 10 Dec 2025 08:08:09 +0000 Subject: [PATCH 23/64] feat(ffi, lua): implement `GetRef` and instead of `RawGetI` --- ffi/lua.go | 4 ++++ lua/chunk.go | 2 +- lua/string.go | 2 +- lua/table.go | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index 7a02552..5d0578c 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -737,6 +737,10 @@ func Unref(L *LuaState, ref int32) { C.lua_unref(L, C.int(ref)) } +func GetRef(L *LuaState, ref int32) int32 { + return RawGetI(L, LUA_REGISTRYINDEX, ref) +} + // // ================== // Debug API diff --git a/lua/chunk.go b/lua/chunk.go index 7108fd7..76cf07b 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -27,7 +27,7 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { } } else { // Push function onto the stack - ffi.RawGetI(state, ffi.LUA_REGISTRYINDEX, int32(c.index)) + ffi.GetRef(state, int32(c.index)) // Push the length and the arguments onto the stack (deref) ffi.PushNumber(state, ffi.LuaNumber(argsCount)) diff --git a/lua/string.go b/lua/string.go index a17ba8d..ae78f17 100644 --- a/lua/string.go +++ b/lua/string.go @@ -39,5 +39,5 @@ func (s *LuaString) lua() *Lua { return s.vm } func (s *LuaString) ref() int { return s.index } func (s *LuaString) deref() int { - return int(ffi.RawGetI(s.lua().state(), ffi.LUA_REGISTRYINDEX, int32(s.ref()))) + return int(ffi.GetRef(s.vm.state(), int32(s.ref()))) } diff --git a/lua/table.go b/lua/table.go index 0a25629..ed71bf2 100644 --- a/lua/table.go +++ b/lua/table.go @@ -60,5 +60,5 @@ func (t *LuaTable) lua() *Lua { return t.vm } func (t *LuaTable) ref() int { return t.index } func (t *LuaTable) deref() int { - return int(ffi.RawGetI(t.lua().state(), ffi.LUA_REGISTRYINDEX, int32(t.ref()))) + return int(ffi.GetRef(t.vm.state(), int32(t.ref()))) } From 25cf04df38c120c204092939aae924eacd20273d Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sun, 14 Dec 2025 10:00:39 +0000 Subject: [PATCH 24/64] feat(lua): implement `LuaNumber` and refactor `LuaValue` interface * `LuaValue.lua()` method is now optional, may return a nil pointer * `LuaValue.deref()` is provided with a non-nil `*Lua` state pointer, since `LuaValue.lua()` may be nil if the type is stateless without an associated VM (currently only `LuaNumber` and `LuaNil`) * Added `LuaNumber` type corresponding to Go `float64` and value conversion implementations for it --- lua/chunk.go | 2 +- lua/nil.go | 8 ++++---- lua/number.go | 14 +++++--------- lua/registry.go | 2 +- lua/state.go | 4 ++-- lua/string.go | 8 ++++---- lua/table.go | 18 +++++++++--------- lua/value.go | 17 ++++++++--------- main.go | 10 +++++++++- 9 files changed, 43 insertions(+), 40 deletions(-) diff --git a/lua/chunk.go b/lua/chunk.go index 76cf07b..c15d476 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -33,7 +33,7 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { ffi.PushNumber(state, ffi.LuaNumber(argsCount)) argsCount++ for _, arg := range args { - arg.deref() + arg.deref(c.vm) } } diff --git a/lua/nil.go b/lua/nil.go index d3f623a..c068392 100644 --- a/lua/nil.go +++ b/lua/nil.go @@ -2,7 +2,7 @@ package lua import "github.com/CompeyDev/lei/ffi" -type LuaNil struct{ vm *Lua } +type LuaNil struct{} // // LuaValue Implementation @@ -10,6 +10,6 @@ type LuaNil struct{ vm *Lua } var _ LuaValue = (*LuaNil)(nil) -func (n *LuaNil) lua() *Lua { return n.vm } -func (n *LuaNil) ref() int { return ffi.LUA_REFNIL } -func (n *LuaNil) deref() int { return 0 } +func (n *LuaNil) lua() *Lua { return nil } +func (n *LuaNil) ref() int { return ffi.LUA_REFNIL } +func (n *LuaNil) deref(_ *Lua) int { return 0 } diff --git a/lua/number.go b/lua/number.go index 080ecab..8c2f20b 100644 --- a/lua/number.go +++ b/lua/number.go @@ -2,10 +2,7 @@ package lua import "github.com/CompeyDev/lei/ffi" -type LuaNumber struct { - vm *Lua - inner float64 -} +type LuaNumber float64 // // LuaValue implementation @@ -15,11 +12,10 @@ var _ LuaValue = (*LuaNumber)(nil) // Numbers are cheap to copy, so we don't store the reference index -func (n *LuaNumber) lua() *Lua { return n.vm } +func (n *LuaNumber) lua() *Lua { return nil } func (n *LuaNumber) ref() int { return ffi.LUA_NOREF } -func (n *LuaNumber) deref() int { - state := n.vm.state() - - ffi.PushNumber(state, ffi.LuaNumber(n.inner)) +func (n *LuaNumber) deref(lua *Lua) int { + state := lua.state() + ffi.PushNumber(state, ffi.LuaNumber(*n)) return int(ffi.GetTop(state)) } diff --git a/lua/registry.go b/lua/registry.go index 9551309..5a04867 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -59,7 +59,7 @@ func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { } for _, ret := range returns { - ret.deref() + ret.deref(state) } return C.int(len(returns)) diff --git a/lua/state.go b/lua/state.go index ed899a5..f912c14 100644 --- a/lua/state.go +++ b/lua/state.go @@ -46,7 +46,7 @@ func (l *Lua) CreateTable() *LuaTable { index := ffi.Ref(state, -1) t := &LuaTable{vm: l, index: int(index)} - runtime.SetFinalizer(t, valueUnrefer[*LuaTable](t.lua())) + runtime.SetFinalizer(t, valueUnrefer[*LuaTable](l)) return t } @@ -58,7 +58,7 @@ func (l *Lua) CreateString(str string) *LuaString { index := ffi.Ref(state, -1) s := &LuaString{vm: l, index: int(index)} - runtime.SetFinalizer(s, valueUnrefer[*LuaString](s.lua())) + runtime.SetFinalizer(s, valueUnrefer[*LuaString](l)) return s } diff --git a/lua/string.go b/lua/string.go index ae78f17..0b383b6 100644 --- a/lua/string.go +++ b/lua/string.go @@ -14,7 +14,7 @@ type LuaString struct { func (s *LuaString) ToString() string { state := s.vm.state() - s.deref() + s.deref(s.vm) defer ffi.Pop(state, 1) return ffi.ToString(state, -1) @@ -23,7 +23,7 @@ func (s *LuaString) ToString() string { func (s *LuaString) ToPointer() unsafe.Pointer { state := s.vm.state() - s.deref() + s.deref(s.vm) defer ffi.Pop(state, 1) return ffi.ToPointer(state, -1) @@ -38,6 +38,6 @@ var _ LuaValue = (*LuaString)(nil) func (s *LuaString) lua() *Lua { return s.vm } func (s *LuaString) ref() int { return s.index } -func (s *LuaString) deref() int { - return int(ffi.GetRef(s.vm.state(), int32(s.ref()))) +func (s *LuaString) deref(lua *Lua) int { + return int(ffi.GetRef(lua.state(), int32(s.ref()))) } diff --git a/lua/table.go b/lua/table.go index ed71bf2..b37fd3c 100644 --- a/lua/table.go +++ b/lua/table.go @@ -10,9 +10,9 @@ type LuaTable struct { func (t *LuaTable) Set(key LuaValue, value LuaValue) { state := t.vm.state() - t.deref() // table (-3) - key.deref() // key (-2) - value.deref() // value (-1) + t.deref(t.vm) // table (-3) + key.deref(t.vm) // key (-2) + value.deref(t.vm) // value (-1) ffi.SetTable(state, -3) ffi.Pop(state, 1) @@ -21,11 +21,11 @@ func (t *LuaTable) Set(key LuaValue, value LuaValue) { func (t *LuaTable) Get(key LuaValue) LuaValue { state := t.vm.state() - t.deref() //////////////////// table (-3) - key.deref() //////////////////// key (-2) + t.deref(t.vm) //////////////////// table (-3) + key.deref(t.vm) //////////////////// key (-2) ffi.GetTable(state, -2) - val := intoLuaValue(t.vm, -1) // value (-1) + val := intoLuaValue(t.vm, -1) ////// value (-1) ffi.Pop(state, 2) return val @@ -34,7 +34,7 @@ func (t *LuaTable) Get(key LuaValue) LuaValue { func (t *LuaTable) Iterable() map[LuaValue]LuaValue { state := t.vm.state() - t.deref() + t.deref(t.vm) tableIndex := ffi.GetTop(state) ffi.PushNil(state) @@ -59,6 +59,6 @@ var _ LuaValue = (*LuaTable)(nil) func (t *LuaTable) lua() *Lua { return t.vm } func (t *LuaTable) ref() int { return t.index } -func (t *LuaTable) deref() int { - return int(ffi.GetRef(t.vm.state(), int32(t.ref()))) +func (t *LuaTable) deref(lua *Lua) int { + return int(ffi.GetRef(lua.state(), int32(t.ref()))) } diff --git a/lua/value.go b/lua/value.go index 0602a90..0d3c078 100644 --- a/lua/value.go +++ b/lua/value.go @@ -9,14 +9,12 @@ import ( ) type LuaValue interface { + // Optionally returns the Lua VM this value belongs to lua() *Lua + // Returns the reference index of this value in the Lua registry ref() int - deref() int -} - -func TypeName(val LuaValue) string { - lua := val.lua().state() - return ffi.TypeName(lua, ffi.Type(lua, int32(val.ref()))) + // Dereferences this value onto the Lua stack, returning the stack index + deref(*Lua) int } // @@ -38,7 +36,7 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { switch val := v.(type) { case *LuaNumber: if t.Kind() == reflect.Float64 { - num := reflect.ValueOf(val.inner) + num := reflect.ValueOf(*val).Convert(t) return num, nil } @@ -151,7 +149,8 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { switch ffi.Type(state, index) { case ffi.LUA_TNUMBER: num := ffi.ToNumber(state, index) - return &LuaNumber{vm: lua, inner: float64(num)} + li := LuaNumber(float64(num)) + return &li case ffi.LUA_TSTRING: ref := ffi.Ref(state, index) return &LuaString{vm: lua, index: int(ref)} @@ -159,7 +158,7 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { ref := ffi.Ref(state, index) return &LuaTable{vm: lua, index: int(ref)} case ffi.LUA_TNIL: - return &LuaNil{vm: lua} + return &LuaNil{} default: panic("unsupported Lua type") } diff --git a/main.go b/main.go index 1de13d0..4c17e11 100644 --- a/main.go +++ b/main.go @@ -63,10 +63,12 @@ func main() { } cFnChunk := state.CreateFunction(func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + someNumber := lua.LuaNumber(22713) return []lua.LuaValue{ luaState.CreateString("Hello"), luaState.CreateString("from"), luaState.CreateString(fmt.Sprintf("Go, %s!", args[0].(*lua.LuaString).ToString())), + &someNumber, }, nil }) @@ -77,6 +79,12 @@ func main() { } for i, ret := range returns { - fmt.Printf("Return %d: %s\n", i+1, ret.(*lua.LuaString).ToString()) + str, err := lua.As[string](ret) + if err == nil { + fmt.Printf("Return %d: %s\n", i+1, str) + } else { + num, _ := lua.As[float64](ret) + fmt.Printf("Return %d: %f\n", i+1, num) + } } } From 19f3c4f8a09ee6deb00eeedeb115983e360e0d1e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 23 Dec 2025 07:18:58 +0000 Subject: [PATCH 25/64] feat(lua): support catching panics in Go functions as Lua errors Any panics within Go functions will be handled appropriately and returned as errors on the Lua side. All Lua related error throws are limited only to the C side, and we ensure that Go never calls any errors. By default, `LuaOptions.CatchPanics` is set to true. Also fixes our previous `lua_error` calls on the Go side which would cause a `longjmp` across the boundary causing a Go panic due to a violation of its stack winding rules. --- .gitignore | 3 +++ lua/registry.c | 18 +++++++++++++++++- lua/registry.go | 47 +++++++++++++++++++++++++++++++++++------------ lua/state.go | 7 ++++++- main.go | 2 +- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 1b1dda6..e199e48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,8 @@ # Build script compiled data .lei/ +# Cgo generated files +_obj/ + # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/lua/registry.c b/lua/registry.c index 1a31a25..f325199 100644 --- a/lua/registry.c +++ b/lua/registry.c @@ -3,9 +3,25 @@ #include #include <_cgo_export.h> +typedef struct registryTrampolineImpl_return trampolineResult; + int registryTrampoline(lua_State* L) { uintptr_t* handle_ptr = (uintptr_t*)lua_touserdata(L, lua_upvalueindex(1)); - return registryTrampolineImpl(L, *handle_ptr); + trampolineResult result = registryTrampolineImpl(L, *handle_ptr); + + // Handle errors after crossing the C boundary to prevent a longjmp triggered + // from the Go side, which would violate Go's stack winding rules + + int status = result.r0; + char* err = result.r1; + + // TODO: Figure out what happens if some Lua code calls this without a pcall, longjmp? + if (err != NULL) { + lua_pushstring(L, err); + lua_error(L); + } + + return status; } void registryTrampolineDtor(lua_State* L) { diff --git a/lua/registry.go b/lua/registry.go index 5a04867..512bbf2 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -1,11 +1,14 @@ package lua import ( + "fmt" "runtime/cgo" "github.com/CompeyDev/lei/ffi" ) +//go:generate go tool cgo $GOFILE + /* #cgo CFLAGS: -I../ffi/luau/VM/include #cgo LDFLAGS: -L../ffi/luau/cmake -lLuau.VM -lm -lstdc++ @@ -23,7 +26,7 @@ var registryTrampoline = C.registryTrampoline var registryTrampolineDtor = C.registryTrampolineDtor //export registryTrampolineImpl -func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { +func registryTrampolineImpl(lua *C.lua_State, handle uintptr) (C.int, *C.char) { rawState := (*ffi.LuaState)(lua) state := &Lua{ inner: &StateWithMemory{ @@ -36,9 +39,7 @@ func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { fn, ok := entry.registry.get(entry.id) if !ok { - ffi.PushString(rawState, "function not found in registry") - ffi.Error(rawState) - return 0 + return C.int(-1), C.CString("function not found in registry") } argsCount := int(ffi.ToNumber(rawState, 1)) @@ -50,19 +51,18 @@ func registryTrampolineImpl(lua *C.lua_State, handle C.uintptr_t) C.int { args[i] = intoLuaValue(state, stackIndex) } - returns, err := fn(state, args...) + returns, callErr := fn(state, args...) - if err != nil { - ffi.PushString(rawState, err.Error()) - ffi.Error(rawState) - return 0 + // SAFETY: This must be caught elsewhere to avoid the longjmp + if callErr != nil { + return C.int(-1), C.CString(callErr.Error()) } for _, ret := range returns { ret.deref(state) } - return C.int(len(returns)) + return C.int(len(returns)), nil } //export registryTrampolineDtorImpl @@ -75,8 +75,9 @@ func registryTrampolineDtorImpl(_ *C.lua_State, handle C.uintptr_t) { type GoFunction func(lua *Lua, args ...LuaValue) ([]LuaValue, error) type functionRegistry struct { - functions map[uintptr]GoFunction - nextID uintptr + recoverPanics bool + functions map[uintptr]GoFunction + nextID uintptr } type functionEntry struct { @@ -99,5 +100,27 @@ func (fr *functionRegistry) register(fn GoFunction) *functionEntry { func (fr *functionRegistry) get(id uintptr) (GoFunction, bool) { fn, ok := fr.functions[id] + + if fr.recoverPanics { + rawFn := fn + fn = func(lua *Lua, args ...LuaValue) (result []LuaValue, err error) { + defer func() { + // Deferred panic handler + if recv := recover(); recv != nil { + switch v := recv.(type) { + case error: + err = v + default: + err = fmt.Errorf("go panic: %v", v) + } + } + }() + + result, err = rawFn(lua, args...) + + return result, err + } + } + return fn, ok } diff --git a/lua/state.go b/lua/state.go index f912c14..b97e307 100644 --- a/lua/state.go +++ b/lua/state.go @@ -12,6 +12,7 @@ type LuaOptions struct { InitMemoryState *MemoryState CollectGarbage bool IsSafe bool + CatchPanics bool Compiler *Compiler } @@ -97,6 +98,7 @@ func New() *Lua { return NewWith(StdLibALLSAFE, LuaOptions{ CollectGarbage: true, IsSafe: true, + CatchPanics: true, Compiler: DefaultCompiler(), }) } @@ -148,7 +150,10 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { compiler = DefaultCompiler() } - lua := &Lua{inner: state, compiler: compiler, fnRegistry: newFunctionRegistry()} + fnReg := newFunctionRegistry() + fnReg.recoverPanics = options.CatchPanics + + lua := &Lua{inner: state, compiler: compiler, fnRegistry: fnReg} runtime.SetFinalizer(lua, func(l *Lua) { if options.CollectGarbage { ffi.LuaGc(l.state(), ffi.LUA_GCCOLLECT, 0) diff --git a/main.go b/main.go index 4c17e11..349ce53 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( func main() { mem := lua.NewMemoryState() // mem.SetLimit(250 * 1024) // 250KB max - state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem}) + state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem, CatchPanics: true}) table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("lei") From c6a29adb80eeda08e1400222756db10f34e4ff09 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 23 Dec 2025 14:22:46 +0000 Subject: [PATCH 26/64] fix(ffi): refer to full include path for `lua.h` --- ffi/clua.c | 3 +-- ffi/clua.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ffi/clua.c b/ffi/clua.c index a0a9d27..ad0e06d 100644 --- a/ffi/clua.c +++ b/ffi/clua.c @@ -1,6 +1,5 @@ #include -#include -#include <_cgo_export.h> +#include "luau/VM/include/lua.h" // void* clua_alloc(void* ud, void *ptr, size_t osize, size_t nsize) // { diff --git a/ffi/clua.h b/ffi/clua.h index 004e7ed..b97cc71 100644 --- a/ffi/clua.h +++ b/ffi/clua.h @@ -1,5 +1,5 @@ #include -#include +#include "luau/VM/include/lua.h" lua_State* clua_newstate(void* f, void* ud); l_noret cluaL_errorL(lua_State* L, char* msg); From c40cc22cdaa3f4f1f022d425469a8c992419f269 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 23 Dec 2025 14:24:20 +0000 Subject: [PATCH 27/64] chore(build): replace `go` build wrapper with `go:generate` attrs This is a follow up inspired by some changes in https://github.com/CompeyDev/lei/commit/19f3c4f8a09ee6deb00eeedeb115983e360e0d1e, which was the first commit to start using `go:generate` attributes, specific for cgo header generation. Instead of the `build` package being a CLI wrapper around the `go` command, compiling and injecting required shared libraries to link, we now have a thin command which compiles Luau CMake subprojects using Ninja. We refer to this command in `go:generate` attributes along with the required static libraries that the file expects. This means that users must run the following command to initialize the workspace: ``` go generate ./... ``` FUTURE: Might be worth not having a hard dependency on Ninja, or allowing users to specify their own CMake generator --- build/build.go | 163 +++++++++--------------------------------------- build/cmd.go | 28 ++++++--- ffi/lauxlib.go | 4 +- ffi/lua.go | 4 +- ffi/luacode.go | 4 +- go.mod | 7 --- go.sum | 8 --- lua/registry.go | 2 +- 8 files changed, 58 insertions(+), 162 deletions(-) diff --git a/build/build.go b/build/build.go index c3ce84a..aa20229 100644 --- a/build/build.go +++ b/build/build.go @@ -1,152 +1,49 @@ package main import ( - "fmt" + "log" "os" - "path" "strings" - - "github.com/gookit/color" - "golang.org/x/term" ) -const LUAU_VERSION = "0.634" -const ARTIFACT_NAME = "libLuau.VM.a" - -func bail(err error) { - if err != nil { - panic(err) - } -} - -func cloneSrc() string { - color.Blue.Println("> Cloning luau-lang/luau") - - dir, tempDirErr := os.MkdirTemp("", "lei-build") - bail(tempDirErr) - - // Clone down the Luau repo and checkout the required tag - Exec("git", "", "clone", "https://github.com/luau-lang/luau.git", dir) - Exec("git", dir, "checkout", LUAU_VERSION) - - color.Green.Printf("> Cloned repo to%s\n\n", dir) - return dir -} - -func buildVm(srcPath string, artifactPath string, includesDir string, cmakeFlags ...string) { - color.Blue.Println("> Compile libLuau.VM.a") - - // Build the Luau VM using CMake - buildDir := path.Join(srcPath, "cmake") - buildDirErr := os.Mkdir(buildDir, os.ModePerm) - if !os.IsExist(buildDirErr) { - bail(buildDirErr) - } - - defaultCmakeFlags := []string{"..", "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DLUAU_EXTERN_C=ON", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5"} - Exec("cmake", buildDir, append(defaultCmakeFlags, cmakeFlags...)...) - Exec("cmake", buildDir, "--build", ".", "--target Luau.VM", "--config", "RelWithDebInfo") - - color.Green.Println("> Successfully compiled!\n") - - // Copy the artifact to the artifact directory - artifactFile, artifactErr := os.ReadFile(path.Join(buildDir, ARTIFACT_NAME)) - bail(artifactErr) - bail(os.WriteFile(artifactPath, artifactFile, os.ModePerm)) - - // Copy the header files into the includes directory - headerDir := path.Join(srcPath, "VM", "include") - headerFiles, headerErr := os.ReadDir(headerDir) - bail(headerErr) - for _, file := range headerFiles { - src := path.Join(headerDir, file.Name()) - dest := path.Join(includesDir, file.Name()) - - headerContents, headerReadErr := os.ReadFile(src) - bail(headerReadErr) - - os.WriteFile(dest, headerContents, os.ModePerm) - } -} - func main() { - workDir, workDirErr := os.Getwd() - bail(workDirErr) - - artifactDir := path.Join(workDir, ".lei") - artifactPath := path.Join(artifactDir, ARTIFACT_NAME) - lockfilePath := path.Join(artifactDir, ".lock") - includesDir := path.Join(artifactDir, "includes") - - bail(os.MkdirAll(includesDir, os.ModePerm)) // includesDir is the deepest dir, creates all - - gitignore, gitignoreErr := os.ReadFile(".gitignore") - if gitignoreErr == nil && !strings.Contains(string(gitignore), ".lei") { - color.Yellow.Println("> WARN: The gitignore in the CWD does not include `.lei`, consider adding it") + usage := func() { log.Fatal("Usage: buildProject ") } + if len(os.Args) < 2 { + usage() } - // TODO: Args for clean build - args := os.Args[1:] - - goArgs := []string{} - cmakeFlags := []string{} - features := []string{} + switch os.Args[1] { + case "buildProject": + for _, project := range os.Args[2:] { + if !strings.HasPrefix(project, "Luau.") { + log.Fatalf("Invalid project name: %s", project) + } - // TODO: maybe use env vars for this config instead - for _, arg := range args { - if arg == "--enable-vector4" { - features = append(features, "LUAU_VECTOR4") - // FIXME: This flag apparently isn't recognized by cmake for some reason - cmakeFlags = append(cmakeFlags, "-DLUAU_VECTOR_SIZE=4") - - } else { - goArgs = append(goArgs, arg) + compileLuauProject(project) } - } - lockfileContents, err := os.ReadFile(lockfilePath) - if !os.IsNotExist(err) { - bail(err) + // Display usage menu + case "-h", "--help": + fallthrough + default: + usage() } +} - serFeatures := fmt.Sprintf("%v", features) - toCleanBuild := (string(lockfileContents) != serFeatures) || os.Getenv("LEI_CLEAN_BUILD") == "true" - if _, err := os.Stat(artifactPath); err == nil && !toCleanBuild { - fmt.Printf("[build] Using existing artifact at %s\n", artifactPath) - } else { - srcPath, notUnset := os.LookupEnv("LEI_LUAU_SRC") - if !notUnset { - srcPath = cloneSrc() - defer os.RemoveAll(srcPath) - } - - buildVm(srcPath, artifactPath, includesDir, cmakeFlags...) - bail(os.WriteFile(lockfilePath, []byte(serFeatures), os.ModePerm)) - } +func compileLuauProject(project string) { + if err := os.Mkdir("_obj", os.ModePerm); err == nil || !os.IsExist(err) { + // Directory already exists, i.e., config files generated + Exec( + "cmake", + "-S", "luau", + "-B", "_obj", + "-G", "Ninja", - buildTags := []string{} - if len(features) > 0 { - buildTags = append(buildTags, []string{"-tags", strings.Join(features, ",")}...) + // Flags + "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DLUAU_EXTERN_C=ON", + ) } - w, _, termErr := term.GetSize(int(os.Stdout.Fd())) - bail(termErr) - fmt.Println(strings.Repeat("=", w)) - - subcommand := goArgs[0] - goArgs = goArgs[1:] - combinedArgs := append(buildTags, goArgs...) - cmd, _, _, _ := Command("go"). - WithArgs(append([]string{subcommand}, combinedArgs...)...). - WithVar( - "CGO_LDFLAGS", - fmt.Sprintf("-L %s -lLuau.VM -lm -lstdc++", artifactDir), - ). - WithVar("CGO_CFLAGS", fmt.Sprintf("-I%s", includesDir)). - WithVar("CGO_ENABLED", "1"). - PipeAll(Forward). - ToCommand() - - bail(cmd.Start()) - bail(cmd.Wait()) + Exec("cmake", "--build", "_obj", "-t", project, "--config", "RelWithDebInfo") } diff --git a/build/cmd.go b/build/cmd.go index 072be6e..1aabf3f 100644 --- a/build/cmd.go +++ b/build/cmd.go @@ -3,8 +3,10 @@ package main import ( "bytes" "io" + "log" "os" "os/exec" + "strings" ) type CommandPipeMode int @@ -127,17 +129,23 @@ func pipeModeToWriter(mode CommandPipeMode, def io.Writer) io.Writer { } } -func Exec(name string, dir string, args ...string) { - cmd, _, _, _ := Command(name).WithArgs(args...).Dir(dir).PipeAll(Forward).ToCommand() - startErr := cmd.Start() - if startErr != nil { - panic(startErr) +func Exec(exe string, args ...string) { + cmd, _, _, _ := Command(exe). + WithArgs(args...). + WithVar("CLICOLOR_FORCE", "1"). + PipeAll(Forward). + ToCommand() + + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to start command %s: %v", exe, err) } - cmdErr := cmd.Wait() - if cmdErr != nil { - panic(cmdErr) - // err := cmdErr.(*exec.ExitError) - // os.Exit(err.ExitCode()) + if err := cmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + commandStr := strings.Join(append([]string{exe}, args...), " ") + log.Fatalf("'%s' exited with %d", commandStr, exitErr.ExitCode()) + } + + log.Fatalf("%s command failed: %v", exe, err) } } diff --git a/ffi/lauxlib.go b/ffi/lauxlib.go index 81585ba..782a3ea 100644 --- a/ffi/lauxlib.go +++ b/ffi/lauxlib.go @@ -1,8 +1,10 @@ package ffi +//go:generate go run ../build buildProject Luau.VM + /* #cgo CFLAGS: -Iluau/VM/include -#cgo LDFLAGS: -Lluau/cmake -lLuau.VM -lm -lstdc++ +#cgo LDFLAGS: -L_obj -lLuau.VM -lm -lstdc++ #include #include #include diff --git a/ffi/lua.go b/ffi/lua.go index 5d0578c..36d45b7 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -1,8 +1,10 @@ package ffi +//go:generate go run ../build buildProject Luau.VM + /* #cgo CFLAGS: -Iluau/VM/include -#cgo LDFLAGS: -Lluau/cmake -lLuau.VM -lm -lstdc++ +#cgo LDFLAGS: -L_obj -lLuau.VM -lm -lstdc++ #include #include #include diff --git a/ffi/luacode.go b/ffi/luacode.go index e63d12b..c8500fe 100644 --- a/ffi/luacode.go +++ b/ffi/luacode.go @@ -1,8 +1,10 @@ package ffi +//go:generate go run ../build buildProject Luau.VM Luau.Compiler Luau.Ast + /* #cgo CFLAGS: -Iluau/Compiler/include -#cgo LDFLAGS: -Lluau/cmake -lLuau.Compiler -lLuau.Ast -lm -lstdc++ +#cgo LDFLAGS: -L_obj -lLuau.Compiler -lLuau.Ast -lm -lstdc++ #include #include */ diff --git a/go.mod b/go.mod index 7cd75e9..558c3ab 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,3 @@ module github.com/CompeyDev/lei go 1.23.0 toolchain go1.24.2 - -require ( - github.com/gookit/color v1.5.4 // indirect - github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect -) diff --git a/go.sum b/go.sum index 8e636d2..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +0,0 @@ -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= diff --git a/lua/registry.go b/lua/registry.go index 512bbf2..89e723a 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -11,7 +11,7 @@ import ( /* #cgo CFLAGS: -I../ffi/luau/VM/include -#cgo LDFLAGS: -L../ffi/luau/cmake -lLuau.VM -lm -lstdc++ +#cgo LDFLAGS: -L../ffi/_obj -lLuau.VM -lm -lstdc++ #include #include From b81a5e372a3062f68f05fba993768185f4fa721a Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 23 Dec 2025 16:25:57 +0000 Subject: [PATCH 28/64] fix(lua): `cgo` ignores `CFLAGS` causing include errors --- lua/registry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/registry.go b/lua/registry.go index 89e723a..e37da2a 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -7,7 +7,7 @@ import ( "github.com/CompeyDev/lei/ffi" ) -//go:generate go tool cgo $GOFILE +//go:generate go tool cgo -- -I../ffi/luau/VM/include $GOFILE /* #cgo CFLAGS: -I../ffi/luau/VM/include From 3fe5bcd35389f4dc5b55103d93d0dd68a64ad4b8 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Fri, 23 Jan 2026 17:22:48 +0000 Subject: [PATCH 29/64] fix(lua/registry): free leaked error message on C side --- lua/registry.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/registry.c b/lua/registry.c index f325199..7f1eec4 100644 --- a/lua/registry.c +++ b/lua/registry.c @@ -18,6 +18,7 @@ int registryTrampoline(lua_State* L) { // TODO: Figure out what happens if some Lua code calls this without a pcall, longjmp? if (err != NULL) { lua_pushstring(L, err); + free(err); lua_error(L); } From c65633a9d5ca761fc3048383d43b5a5f7d47200b Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Fri, 23 Jan 2026 19:16:24 +0000 Subject: [PATCH 30/64] refactor(lua/registry): make `GoFunction` a type alias --- lua/registry.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/registry.go b/lua/registry.go index e37da2a..81b6258 100644 --- a/lua/registry.go +++ b/lua/registry.go @@ -29,6 +29,7 @@ var registryTrampolineDtor = C.registryTrampolineDtor func registryTrampolineImpl(lua *C.lua_State, handle uintptr) (C.int, *C.char) { rawState := (*ffi.LuaState)(lua) state := &Lua{ + // FIXME: what about the function registry? inner: &StateWithMemory{ memState: getMemoryState(rawState), luaState: rawState, @@ -72,7 +73,7 @@ func registryTrampolineDtorImpl(_ *C.lua_State, handle C.uintptr_t) { cgo.Handle(handle).Delete() } -type GoFunction func(lua *Lua, args ...LuaValue) ([]LuaValue, error) +type GoFunction = func(lua *Lua, args ...LuaValue) ([]LuaValue, error) type functionRegistry struct { recoverPanics bool From cb545a58e88afc8e664c6c5806c8bbe0268d063e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Fri, 23 Jan 2026 19:17:11 +0000 Subject: [PATCH 31/64] feat(lua): globals API and userdata support * Implement `Lua.GetGlobal` and `Lua.SetGlobal` APIs * Implement `Lua.CreateUserData` and `LuaUserData` type, along with `IntoUserData` interface with a registry pattern to register metamethods, methods, and fields for custom userdata types * Use as shared `pushUpvalue` function to push registry patterned Go pointers to C, which must be passed back to Go using `cgo.Handle`s --- lua/state.go | 75 ++++++++++++++++++++++-- lua/userdata.c | 17 ++++++ lua/userdata.go | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ lua/value.go | 9 +++ main.go | 51 +++++++++++++++++ 5 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 lua/userdata.c create mode 100644 lua/userdata.go diff --git a/lua/state.go b/lua/state.go index b97e307..4186415 100644 --- a/lua/state.go +++ b/lua/state.go @@ -40,6 +40,21 @@ func (l *Lua) Memory() *MemoryState { return l.inner.MemState() } +func (l *Lua) GetGlobal(global string) LuaValue { + state := l.state() + + ffi.GetGlobal(state, global) + value := intoLuaValue(l, -1) + + ffi.Pop(state, 1) + return value +} + +func (l *Lua) SetGlobal(global string, value LuaValue) { + value.deref(l) + ffi.SetGlobal(l.state(), global) +} + func (l *Lua) CreateTable() *LuaTable { state := l.inner.luaState @@ -68,10 +83,7 @@ func (l *Lua) CreateFunction(fn GoFunction) *LuaChunk { state := l.state() entry := l.fnRegistry.register(fn) - handle := cgo.NewHandle(entry) - - ud := (*uintptr)(ffi.NewUserdataDtor(state, uint64(unsafe.Sizeof(uintptr(0))), registryTrampolineDtor)) - *ud = uintptr(handle) + pushUpvalue(state, entry, registryTrampolineDtor) ffi.PushCClosureK(state, registryTrampoline, nil, 1, nil) @@ -82,6 +94,46 @@ func (l *Lua) CreateFunction(fn GoFunction) *LuaChunk { return c } +func (l *Lua) CreateUserData(value IntoUserData) LuaUserData { + state := l.state() + userdata := &LuaUserData{vm: l, inner: value} + + // TOOD: custom destructor support + ud := ffi.NewUserdata(state, uint64(unsafe.Sizeof(uintptr(0)))) + *(*IntoUserData)(unsafe.Pointer(ud)) = value + + if ffi.LNewMetatable(state, "") { + fieldsMap := newFieldMap() + methodsMap := newMethodMap(l.fnRegistry) + metaMethodsMap := newMethodMap(l.fnRegistry) + + value.Fields(fieldsMap) + value.Methods(methodsMap) + value.MetaMethods(metaMethodsMap) + + pushUpvalue(state, fieldsMap, fieldMapDtor) + pushUpvalue(state, methodsMap, methodMapDtor) + + ffi.PushCClosureK(state, indexMt, nil, 2, nil) + ffi.SetField(state, -2, "__index") + + for method, impl := range metaMethodsMap.inner { + if method == "__index" { + panic("Cannot have a manual __index implementation") + } + + pushUpvalue(state, impl, registryTrampolineDtor) + ffi.PushCClosureK(state, registryTrampoline, nil, 1, nil) + ffi.SetField(state, -2, method) + } + } + + ffi.SetMetatable(state, -2) + + userdata.index = int(ffi.Ref(state, -1)) + return *userdata +} + func (l *Lua) SetCompiler(compiler *Compiler) { l.compiler = compiler } @@ -164,3 +216,18 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { return lua } + +func pushUpvalue[T any](state *ffi.LuaState, ptr *T, dtor unsafe.Pointer) *uintptr { + var up *uintptr + + sz := uint64(unsafe.Sizeof(uintptr(0))) + if dtor != nil { + up = (*uintptr)(ffi.NewUserdataDtor(state, sz, dtor)) + } else { + up = (*uintptr)(ffi.NewUserdata(state, sz)) + } + + *up = uintptr(cgo.NewHandle(ptr)) + + return up +} diff --git a/lua/userdata.c b/lua/userdata.c new file mode 100644 index 0000000..2e4c495 --- /dev/null +++ b/lua/userdata.c @@ -0,0 +1,17 @@ +#include +#include +#include <_cgo_export.h> + +int indexMt(lua_State* L) { + const char* key = lua_tostring(L, 2); + if (key == NULL) { + lua_pushnil(L); + return 1; + } + + uintptr_t* fields_handle = (uintptr_t*)lua_touserdata(L, lua_upvalueindex(1)); + uintptr_t* methods_handle = (uintptr_t*)lua_touserdata(L, lua_upvalueindex(2)); + + indexMtImpl(L, *fields_handle, *methods_handle, (char*)key); + return 1; +} diff --git a/lua/userdata.go b/lua/userdata.go new file mode 100644 index 0000000..f421aba --- /dev/null +++ b/lua/userdata.go @@ -0,0 +1,147 @@ +package lua + +//go:generate go tool cgo -- -I../ffi/luau/VM/include $GOFILE + +/* +#cgo CFLAGS: -I../ffi/luau/VM/include +#cgo LDFLAGS: -L../ffi/_obj -lLuau.VM -lm -lstdc++ + +#include +#include +#include + +int indexMt(lua_State* L); +void methodMapDtorImpl(lua_State* L, uintptr_t); +void fieldMapDtorImpl(lua_State* L, uintptr_t); +*/ +import "C" + +import ( + "runtime/cgo" + + "github.com/CompeyDev/lei/ffi" +) + +var indexMt = C.indexMt + +var methodMapDtor = C.methodMapDtorImpl +var fieldMapDtor = C.fieldMapDtorImpl + +type LuaUserData struct { + vm *Lua + index int + inner IntoUserData +} + +func (ud *LuaUserData) Downcast() IntoUserData { + if ud.inner != nil { + return ud.inner + } + + ud.deref(ud.vm) + ptr := ffi.ToUserdata(ud.vm.state(), -1) + + if ptr != nil { + return *(*IntoUserData)(ptr) + } else { + return nil + } +} + +// LuaValue implementation + +var _ LuaValue = (*LuaUserData)(nil) + +func (ud *LuaUserData) lua() *Lua { return ud.vm } +func (ud *LuaUserData) ref() int { return ud.index } + +func (ud *LuaUserData) deref(lua *Lua) int { + return int(ffi.GetRef(lua.state(), int32(ud.ref()))) +} + +type IntoUserData interface { + Methods(*MethodMap) + MetaMethods(*MethodMap) + Fields(*FieldMap) +} + +type ValueRegistry[T any, U any] struct { + inner map[string]T + transformer func(fn U) T +} + +func (vr *ValueRegistry[T, U]) Insert(name string, value any) { + if getter, ok := value.(T); ok { + vr.inner[name] = getter + } else { + vr.inner[name] = vr.transformer(value.(U)) + } +} + +type MethodMap = ValueRegistry[*functionEntry, GoFunction] + +func newMethodMap(fnRegistry *functionRegistry) *MethodMap { + return &MethodMap{ + inner: make(map[string]*functionEntry), + transformer: func(fn GoFunction) *functionEntry { return fnRegistry.register(fn) }, + } +} + +type FieldGetter = func(*Lua) LuaValue +type FieldMap = ValueRegistry[FieldGetter, LuaValue] + +func newFieldMap() *FieldMap { + return &FieldMap{ + inner: make(map[string]FieldGetter), + transformer: func(value LuaValue) FieldGetter { + return func(*Lua) LuaValue { return value } + }, + } +} + +//export indexMtImpl +func indexMtImpl(lua *C.lua_State, fieldHandle, methodHandle uintptr, key *C.char) { + rawState := (*ffi.LuaState)(lua) + state := &Lua{ + // FIXME: what about the function registry? + inner: &StateWithMemory{ + memState: getMemoryState(rawState), + luaState: rawState, + }, + } + keyStr := C.GoString(key) + + // Field lookup + fields := cgo.Handle(fieldHandle).Value().(*FieldMap) + if getter := fields.inner[keyStr]; getter != nil { + value := getter(state) + value.deref(state) + return + } + + // Method lookup + methods := cgo.Handle(methodHandle).Value().(*MethodMap) + if method := methods.inner[keyStr]; method != nil { + pushUpvalue(rawState, method, registryTrampolineDtor) + ffi.PushCClosureK(rawState, registryTrampoline, nil, 1, nil) + return + } + + ffi.PushNil(rawState) +} + +func valueRegistryDtorImpl[T any, U any](handle C.uintptr_t) { + entry := cgo.Handle(handle).Value().(*ValueRegistry[T, U]) + clear(entry.inner) + cgo.Handle(handle).Delete() +} + +//export methodMapDtorImpl +func methodMapDtorImpl(_ *C.lua_State, handle C.uintptr_t) { + valueRegistryDtorImpl[*functionRegistry, GoFunction](handle) +} + +//export fieldMapDtorImpl +func fieldMapDtorImpl(_ *C.lua_State, handle C.uintptr_t) { + valueRegistryDtorImpl[FieldGetter, LuaValue](handle) +} diff --git a/lua/value.go b/lua/value.go index 0d3c078..952992a 100644 --- a/lua/value.go +++ b/lua/value.go @@ -138,6 +138,12 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { case *LuaNil: return zero, nil + case *LuaUserData: + if downcasted := val.Downcast(); downcasted != nil { + return reflect.ValueOf(downcasted).Convert(t), nil + } + + return zero, fmt.Errorf("value isn't userdata") } return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero) @@ -159,6 +165,9 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { return &LuaTable{vm: lua, index: int(ref)} case ffi.LUA_TNIL: return &LuaNil{} + case ffi.LUA_TUSERDATA: + ref := ffi.Ref(state, index) + return &LuaUserData{vm: lua, index: int(ref)} default: panic("unsupported Lua type") } diff --git a/main.go b/main.go index 349ce53..bb90dd6 100644 --- a/main.go +++ b/main.go @@ -87,4 +87,55 @@ func main() { fmt.Printf("Return %d: %f\n", i+1, num) } } + + class := &Class{value: 420.0} + classUd := state.CreateUserData(class) + state.SetGlobal("classUd", &classUd) + + got := state.GetGlobal("classUd").(*lua.LuaUserData).Downcast() + fmt.Println(got.(*Class).value) + + udChunk, udErr := state.Load("udChunk", []byte("print(tostring(classUd), classUd.toggle); classUd.flip(); print(classUd.toggle, classUd.fakeToggle)")) + if udErr != nil { + fmt.Println(udErr) + return + } + + _, udCallErr := udChunk.Call() + if udCallErr != nil { + fmt.Println(udCallErr) + return + } } + +type Class struct{ value float64 } + +var _ lua.IntoUserData = (*Class)(nil) + +func (c *Class) Fields(fields *lua.FieldMap) { + // NOTE: this references takes a copy of the value and mutations hence do + // not persist here. Instead we need a getter which captures the class + // itself + funnyNumber := lua.LuaNumber(c.value) + fields.Insert("fakeToggle", &funnyNumber) + + fields.Insert("toggle", func(*lua.Lua) lua.LuaValue { + value := lua.LuaNumber(c.value) + return &value + }) +} + +func (c *Class) MetaMethods(metaMethods *lua.MethodMap) { + metaMethods.Insert("__tostring", func(vm *lua.Lua, _ ...lua.LuaValue) ([]lua.LuaValue, error) { + return []lua.LuaValue{vm.CreateString("Class")}, nil + }) +} + +func (c *Class) Methods(methods *lua.MethodMap) { + methods.Insert("flip", func(_G *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + c.toggle() + return []lua.LuaValue{}, nil + }) +} + +func (c *Class) toggle() { c.value = 69.0 } From 40b408e4c2f56c11704a4114d919d490a8a9932c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 24 Jan 2026 11:01:23 +0000 Subject: [PATCH 32/64] refactor(lua/compiler): use copy-on-modify pattern for `Compiler` --- lua/compiler.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/lua/compiler.go b/lua/compiler.go index 55b16b8..31d8edb 100644 --- a/lua/compiler.go +++ b/lua/compiler.go @@ -7,43 +7,51 @@ import ( type Compiler struct{ options *ffi.CompileOptions } func (c *Compiler) WithOptimizationLevel(lvl int) *Compiler { - c.options.OptimizationLevel = lvl - return c + opts := *c.options + opts.OptimizationLevel = lvl + return &Compiler{options: &opts} } func (c *Compiler) WithDebugLevel(lvl int) *Compiler { - c.options.DebugLevel = lvl - return c + opts := *c.options + opts.DebugLevel = lvl + return &Compiler{options: &opts} } func (c *Compiler) WithTypeInfoLevel(lvl int) *Compiler { - c.options.TypeInfoLevel = lvl - return c + opts := *c.options + opts.TypeInfoLevel = lvl + return &Compiler{options: &opts} } func (c *Compiler) WithCoverageLevel(lvl int) *Compiler { - c.options.CoverageLevel = lvl - return c + opts := *c.options + opts.CoverageLevel = lvl + return &Compiler{options: &opts} } func (c *Compiler) WithMutableGlobals(globals []string) *Compiler { - c.options.MutableGlobals = append(c.options.MutableGlobals, globals...) - return c + opts := *c.options + opts.MutableGlobals = append(append([]string{}, c.options.MutableGlobals...), globals...) + return &Compiler{options: &opts} } func (c *Compiler) WithUserdataTypes(types []string) *Compiler { - c.options.UserdataTypes = append(c.options.UserdataTypes, types...) - return c + opts := *c.options + opts.UserdataTypes = append(append([]string{}, c.options.UserdataTypes...), types...) + return &Compiler{options: &opts} } func (c *Compiler) WithConstantLibraries(libs []string) *Compiler { - c.options.LibrariesWithKnownMembers = append(c.options.LibrariesWithKnownMembers, libs...) - return c + opts := *c.options + opts.LibrariesWithKnownMembers = append(append([]string{}, c.options.LibrariesWithKnownMembers...), libs...) + return &Compiler{options: &opts} } func (c *Compiler) WithDisabledBuiltins(builtins []string) *Compiler { - c.options.DisabledBuiltins = append(c.options.DisabledBuiltins, builtins...) - return c + opts := *c.options + opts.DisabledBuiltins = append(append([]string{}, c.options.DisabledBuiltins...), builtins...) + return &Compiler{options: &opts} } func (c *Compiler) Compile(source string) ([]byte, error) { From d0a071e21c560aa578b058e9796ba0bfb7c723a2 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 24 Jan 2026 11:02:05 +0000 Subject: [PATCH 33/64] refactor(lua/number): use value receivers for `LuaNumber` --- lua/number.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/number.go b/lua/number.go index 8c2f20b..f784d06 100644 --- a/lua/number.go +++ b/lua/number.go @@ -12,10 +12,10 @@ var _ LuaValue = (*LuaNumber)(nil) // Numbers are cheap to copy, so we don't store the reference index -func (n *LuaNumber) lua() *Lua { return nil } -func (n *LuaNumber) ref() int { return ffi.LUA_NOREF } -func (n *LuaNumber) deref(lua *Lua) int { +func (n LuaNumber) lua() *Lua { return nil } +func (n LuaNumber) ref() int { return ffi.LUA_NOREF } +func (n LuaNumber) deref(lua *Lua) int { state := lua.state() - ffi.PushNumber(state, ffi.LuaNumber(*n)) + ffi.PushNumber(state, ffi.LuaNumber(n)) return int(ffi.GetTop(state)) } From 492b29c06663cc39409e7bac631b5cc2117ab210 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 24 Jan 2026 11:02:48 +0000 Subject: [PATCH 34/64] fix(lua): return pointer in `Lua.CreateUserData` --- lua/state.go | 4 ++-- main.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/state.go b/lua/state.go index 4186415..3e1ed08 100644 --- a/lua/state.go +++ b/lua/state.go @@ -94,7 +94,7 @@ func (l *Lua) CreateFunction(fn GoFunction) *LuaChunk { return c } -func (l *Lua) CreateUserData(value IntoUserData) LuaUserData { +func (l *Lua) CreateUserData(value IntoUserData) *LuaUserData { state := l.state() userdata := &LuaUserData{vm: l, inner: value} @@ -131,7 +131,7 @@ func (l *Lua) CreateUserData(value IntoUserData) LuaUserData { ffi.SetMetatable(state, -2) userdata.index = int(ffi.Ref(state, -1)) - return *userdata + return userdata } func (l *Lua) SetCompiler(compiler *Compiler) { diff --git a/main.go b/main.go index bb90dd6..2dee1c1 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,7 @@ func main() { class := &Class{value: 420.0} classUd := state.CreateUserData(class) - state.SetGlobal("classUd", &classUd) + state.SetGlobal("classUd", classUd) got := state.GetGlobal("classUd").(*lua.LuaUserData).Downcast() fmt.Println(got.(*Class).value) From 77daa134607596683b6741d9d7d6cade9d270502 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sun, 25 Jan 2026 17:16:54 +0000 Subject: [PATCH 35/64] feat(ffi): add `luacodegen` bindings --- ffi/luacodegen.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 ffi/luacodegen.go diff --git a/ffi/luacodegen.go b/ffi/luacodegen.go new file mode 100644 index 0000000..ae1a2c9 --- /dev/null +++ b/ffi/luacodegen.go @@ -0,0 +1,24 @@ +package ffi + +//go:generate go run ../build buildProject Luau.VM Luau.CodeGen + +/* +#cgo CFLAGS: -Iluau/VM/include -Iluau/CodeGen/include +#cgo LDFLAGS: -L_obj -lLuau.VM -lLuau.CodeGen -lm -lstdc++ +#include +#include +#include +*/ +import "C" + +func LuauCodegenSupported() bool { + return C.luau_codegen_supported() == 1 +} + +func LuauCodegenCreate(state *C.lua_State) { + C.luau_codegen_create(state) +} + +func LuauCodegenCompile(state *C.lua_State, idx int) { + C.luau_codegen_compile(state, C.int(idx)) +} From f56d8da43ed9230a7f68de17e863cdb45b049c16 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sun, 25 Jan 2026 17:17:15 +0000 Subject: [PATCH 36/64] feat(lua): support codegen configuration APIs --- lua/chunk.go | 5 +++++ lua/state.go | 29 +++++++++++++++++++++++++---- main.go | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lua/chunk.go b/lua/chunk.go index c15d476..500a45e 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -25,6 +25,11 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { // Miscellaneous error is denoted with a -1 code return nil, &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} } + + // Apply native code generation if requested + if ffi.LuauCodegenSupported() && c.vm.codegenEnabled { + ffi.LuauCodegenCompile(state, -1) + } } else { // Push function onto the stack ffi.GetRef(state, int32(c.index)) diff --git a/lua/state.go b/lua/state.go index 3e1ed08..e7c1ad4 100644 --- a/lua/state.go +++ b/lua/state.go @@ -13,13 +13,15 @@ type LuaOptions struct { CollectGarbage bool IsSafe bool CatchPanics bool + EnableCodegen bool Compiler *Compiler } type Lua struct { - inner *StateWithMemory - compiler *Compiler - fnRegistry *functionRegistry + inner *StateWithMemory + compiler *Compiler + fnRegistry *functionRegistry + codegenEnabled bool } func (l *Lua) Load(name string, input []byte) (*LuaChunk, error) { @@ -40,6 +42,19 @@ func (l *Lua) Memory() *MemoryState { return l.inner.MemState() } +func (l *Lua) SetCodegen(enabled bool) bool { + // NOTE: disabling codegen if it was enabled before still has the codegen + // backend enabled for the state since we already called LuauCodegenCreate + // during state initialization + + supported := ffi.LuauCodegenSupported() + if supported { + l.codegenEnabled = enabled + } + + return supported +} + func (l *Lua) GetGlobal(global string) LuaValue { state := l.state() @@ -151,6 +166,7 @@ func New() *Lua { CollectGarbage: true, IsSafe: true, CatchPanics: true, + EnableCodegen: true, Compiler: DefaultCompiler(), }) } @@ -205,7 +221,12 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { fnReg := newFunctionRegistry() fnReg.recoverPanics = options.CatchPanics - lua := &Lua{inner: state, compiler: compiler, fnRegistry: fnReg} + lua := &Lua{inner: state, compiler: compiler, fnRegistry: fnReg, codegenEnabled: false} + if options.EnableCodegen && ffi.LuauCodegenSupported() { + ffi.LuauCodegenCreate(state.luaState) + lua.codegenEnabled = true + } + runtime.SetFinalizer(lua, func(l *Lua) { if options.CollectGarbage { ffi.LuaGc(l.state(), ffi.LUA_GCCOLLECT, 0) diff --git a/main.go b/main.go index 2dee1c1..6164758 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( func main() { mem := lua.NewMemoryState() // mem.SetLimit(250 * 1024) // 250KB max - state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem, CatchPanics: true}) + state := lua.NewWith(lua.StdLibALLSAFE, lua.LuaOptions{InitMemoryState: mem, CatchPanics: true, EnableCodegen: true}) table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("lei") From 29ee5a95c651b55e1659bf09744876fd71fc336d Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 26 Jan 2026 10:17:55 +0000 Subject: [PATCH 37/64] fix(lua/value): unpredictable override behavior with `As` Map iterations in Go are unordered. We keep track of all matches and order them in terms of their priority depending on the type of the match. This fixes the flaky test case for the tag overrides, as sometimes "name" field would match before the "user" field, which was incorrect behavior. --- lua/value.go | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/lua/value.go b/lua/value.go index 952992a..fef2653 100644 --- a/lua/value.go +++ b/lua/value.go @@ -82,6 +82,8 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { case reflect.Struct: res := reflect.New(t).Elem() + fieldSet := make(map[int]bool) + for key, value := range val.Iterable() { keyStr, ok := key.(*LuaString) if !ok { @@ -91,6 +93,8 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { luaKey := keyStr.ToString() var field reflect.Value var found bool + var priority int // 0 = explicit annotation, 1 = direct match, 2 = lowercase fallback + var fieldIndex int for i := 0; i < t.NumField(); i++ { // Annotation-based field name overrides (eg: `lua:"field_name"`) @@ -98,36 +102,49 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { tagVal, ok := structField.Tag.Lookup("lua") if ok && tagVal == luaKey { field = res.Field(i) - found = true + found, priority = true, 0 + fieldIndex = i break } // Exact matches if structField.Name == luaKey { - field = res.Field(i) - found = true - break + if !found || priority > 1 { + field = res.Field(i) + found, priority = true, 1 + fieldIndex = i + } + continue } // If field is exported, try also using lowercase first character if name := structField.Name; structField.IsExported() { lower := strings.ToLower(name[:1]) + name[1:] if lower == luaKey { - field = res.Field(i) - found = true - break + if !found || priority > 2 { + field = res.Field(i) + found, priority = true, 2 + fieldIndex = i + } } } } if found && field.IsValid() && field.CanSet() { - // Recursively convert value to a reflect value - vVal, err := asReflectValue(value, field.Type()) - if err != nil { - return zero, err - } + // We keep track of whether the field has been found, its priority, and the + // index at which it was found within the struct. If there is an explicit + // annotation, we set the field value directly, otherwise we check that + // the field hasn't already been set in another match, and only set it then + if !fieldSet[fieldIndex] || priority == 0 { + // Recursively convert value to a reflect value + vVal, err := asReflectValue(value, field.Type()) + if err != nil { + return zero, err + } - field.Set(vVal) + field.Set(vVal) + fieldSet[fieldIndex] = true + } } } From cbb9fd30ba85656f80973bd92960954ebb6d582e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 26 Jan 2026 10:26:24 +0000 Subject: [PATCH 38/64] fix(lua/stdlib): luau does not have a `PACKAGE` lib --- lua/state.go | 5 ----- lua/stdlib.go | 5 ----- 2 files changed, 10 deletions(-) diff --git a/lua/state.go b/lua/state.go index e7c1ad4..60b61af 100644 --- a/lua/state.go +++ b/lua/state.go @@ -172,10 +172,6 @@ func New() *Lua { } func NewWith(libs StdLib, options LuaOptions) *Lua { - if libs.Contains(StdLibPACKAGE) { - // TODO: disable c modules for package lib - } - state := newStateWithAllocator(options.InitMemoryState) if state == nil { panic("Failed to create Lua state") @@ -202,7 +198,6 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { // TODO: vector lib {StdLibMATH, ffi.LUA_MATHLIBNAME}: unsafe.Pointer(ffi.MathOpener()), {StdLibBUFFER, ffi.LUA_DBLIBNAME}: unsafe.Pointer(ffi.DebugOpener()), - // TODO: package lib } for library, open := range luaLibs { diff --git a/lua/stdlib.go b/lua/stdlib.go index 0e7789c..12df3e7 100644 --- a/lua/stdlib.go +++ b/lua/stdlib.go @@ -32,10 +32,6 @@ const ( // https://www.lua.org/manual/5.4/manual.html#6.7 StdLibMATH StdLib = 1 << 7 - // PACKAGE library - // https://www.lua.org/manual/5.4/manual.html#6.3 - StdLibPACKAGE StdLib = 1 << 8 - // BUFFER library (Luau) // https://luau.org/library#buffer-library StdLibBUFFER StdLib = 1 << 9 @@ -103,7 +99,6 @@ func (s StdLib) String() string { StdLibUTF8: "UTF8", StdLibBIT: "BIT", StdLibMATH: "MATH", - StdLibPACKAGE: "PACKAGE", StdLibBUFFER: "BUFFER", StdLibVECTOR: "VECTOR", StdLibDEBUG: "DEBUG", From e10e8275da4d19f1c276f0b7eac8c238c47a6bdd Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 26 Jan 2026 11:04:44 +0000 Subject: [PATCH 39/64] feat: refactor lib opener bindings and vector lib support * Remove unused utility functions module * Provide direct bindings to `lua_open*` functions * Instead of hardcoding library names, refer to the exports in the header file instead * Validate the safety of libraries before loading them first --- ffi/lauxlib.go | 46 ++++++++++++++++++++++------------------------ ffi/util.go | 50 -------------------------------------------------- lua/state.go | 41 +++++++++++++++-------------------------- lua/stdlib.go | 22 ++++++++++++---------- main.go | 2 +- 5 files changed, 50 insertions(+), 111 deletions(-) delete mode 100644 ffi/util.go diff --git a/ffi/lauxlib.go b/ffi/lauxlib.go index 782a3ea..5faad1b 100644 --- a/ffi/lauxlib.go +++ b/ffi/lauxlib.go @@ -236,31 +236,29 @@ func LOptString(L *LuaState, n int32, d string) string { } const ( - LUA_COLIBNAME = "coroutine" - LUA_TABLIBNAME = "table" - LUA_OSLIBNAME = "os" - LUA_STRLIBNAME = "string" - LUA_BITLIBNAME = "bit32" - LUA_BUFFERLIBNAME = "buffer" - LUA_UTF8LIBNAME = "utf8" - LUA_MATHLIBNAME = "math" - LUA_DBLIBNAME = "debug" + LUA_COLIBNAME = C.LUA_COLIBNAME + LUA_TABLIBNAME = C.LUA_TABLIBNAME + LUA_OSLIBNAME = C.LUA_OSLIBNAME + LUA_STRLIBNAME = C.LUA_STRLIBNAME + LUA_BITLIBNAME = C.LUA_BITLIBNAME + LUA_BUFFERLIBNAME = C.LUA_BUFFERLIBNAME + LUA_UTF8LIBNAME = C.LUA_UTF8LIBNAME + LUA_MATHLIBNAME = C.LUA_MATHLIBNAME + LUA_DBLIBNAME = C.LUA_DBLIBNAME + LUA_VECLIBNAME = C.LUA_VECLIBNAME ) -// DIVERGENCE: We cannot export wrapper functions around C functions if we want to -// pass them to API functions, we preserve the real C pointer by having 'opener' -// functions - -func CoroutineOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_base) } -func BaseOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_base) } -func TableOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_table) } -func OsOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_os) } -func StringOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_string) } -func Bit32Opener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_bit32) } -func BufferOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_buffer) } -func Utf8Opener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_utf8) } -func MathOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_math) } -func DebugOpener() C.lua_CFunction { return C.lua_CFunction(C.luaopen_debug) } -func LibsOpener() C.lua_CFunction { return C.lua_CFunction(C.luaL_openlibs) } +func OpenBase(L *LuaState) { C.luaopen_base(L) } +func OpenCoroutine(L *LuaState) { C.luaopen_coroutine(L) } +func OpenTable(L *LuaState) { C.luaopen_table(L) } +func OpenOs(L *LuaState) { C.luaopen_os(L) } +func OpenString(L *LuaState) { C.luaopen_string(L) } +func OpenBit32(L *LuaState) { C.luaopen_bit32(L) } +func OpenBuffer(L *LuaState) { C.luaopen_buffer(L) } +func OpenUtf8(L *LuaState) { C.luaopen_utf8(L) } +func OpenMath(L *LuaState) { C.luaopen_math(L) } +func OpenDebug(L *LuaState) { C.luaopen_debug(L) } +func OpenVector(L *LuaState) { C.luaopen_vector(L) } +func LOpenLibs(L *LuaState) { C.luaL_openlibs(L) } // TODO: More utility functions, buffer bindings diff --git a/ffi/util.go b/ffi/util.go deleted file mode 100644 index 9b7982a..0000000 --- a/ffi/util.go +++ /dev/null @@ -1,50 +0,0 @@ -package ffi - -//#include -import "C" -import "unsafe" - -func GetSubtable(L *LuaState, idx int32, fname string) bool { - absIdx := AbsIndex(L, idx) - if !CheckStack(L, 3+20) { - panic("stack overflow") - } - - PushString(L, fname) - if GetTable(L, absIdx) == LUA_TTABLE { - return true - } - - Pop(L, 1) - NewTable(L) - PushString(L, fname) - PushValue(L, -2) - SetTable(L, absIdx) - return false -} - -func RequireLib(L *LuaState, modName string, openF unsafe.Pointer, isGlobal bool) { - if !CheckStack(L, 3+20) { - LErrorL(L, "stack overflow") - } - - GetSubtable(L, LUA_REGISTRYINDEX, "_LOADED") - if GetField(L, -1, modName) == LUA_TNIL { - Pop(L, 1) - PushCFunction(L, openF) - PushString(L, modName) - Call(L, 1, 1) - PushValue(L, -1) - SetField(L, -3, modName) - } - - if isGlobal { - PushNil(L) - SetGlobal(L, modName) - } else { - PushValue(L, -1) - SetGlobal(L, modName) - } - - Replace(L, -2) -} diff --git a/lua/state.go b/lua/state.go index 60b61af..5010447 100644 --- a/lua/state.go +++ b/lua/state.go @@ -177,34 +177,23 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { panic("Failed to create Lua state") } - ffi.RequireLib(state.luaState, "_G", unsafe.Pointer(ffi.BaseOpener()), true) - ffi.Pop(state.luaState, 1) - - // TODO: luau jit stuff - - type Library struct { - lib StdLib - name string + ffi.OpenBase(state.luaState) + luaLibs := map[StdLib]func(*ffi.LuaState){ + StdLibCOROUTINE: ffi.OpenCoroutine, + StdLibTABLE: ffi.OpenTable, + StdLibOS: ffi.OpenOs, + StdLibSTRING: ffi.OpenString, + StdLibUTF8: ffi.OpenUtf8, + StdLibBIT: ffi.OpenBit32, + StdLibBUFFER: ffi.OpenBuffer, + StdLibMATH: ffi.OpenMath, + StdLibDEBUG: ffi.OpenDebug, + StdLibVECTOR: ffi.OpenVector, } - luaLibs := map[Library]unsafe.Pointer{ - {StdLibCOROUTINE, ffi.LUA_COLIBNAME}: unsafe.Pointer(ffi.CoroutineOpener()), - {StdLibTABLE, ffi.LUA_TABLIBNAME}: unsafe.Pointer(ffi.TableOpener()), - {StdLibOS, ffi.LUA_OSLIBNAME}: unsafe.Pointer(ffi.OsOpener()), - {StdLibSTRING, ffi.LUA_STRLIBNAME}: unsafe.Pointer(ffi.StringOpener()), - {StdLibUTF8, ffi.LUA_UTF8LIBNAME}: unsafe.Pointer(ffi.Utf8Opener()), - {StdLibBIT, ffi.LUA_BITLIBNAME}: unsafe.Pointer(ffi.Bit32Opener()), - {StdLibBUFFER, ffi.LUA_BUFFERLIBNAME}: unsafe.Pointer(ffi.BufferOpener()), - // TODO: vector lib - {StdLibMATH, ffi.LUA_MATHLIBNAME}: unsafe.Pointer(ffi.MathOpener()), - {StdLibBUFFER, ffi.LUA_DBLIBNAME}: unsafe.Pointer(ffi.DebugOpener()), - } - - for library, open := range luaLibs { - // FIXME: check safety here maybe? - - if libs.Contains(library.lib) { - ffi.RequireLib(state.luaState, library.name, unsafe.Pointer(open), true) + for library, opener := range luaLibs { + if (!options.IsSafe || StdLibALLSAFE.Contains(library)) && libs.Contains(library) { + opener(state.luaState) } } diff --git a/lua/stdlib.go b/lua/stdlib.go index 12df3e7..ef94a5e 100644 --- a/lua/stdlib.go +++ b/lua/stdlib.go @@ -1,5 +1,7 @@ package lua +import "github.com/CompeyDev/lei/ffi" + // StdLib represents flags describing the set of Lua standard libraries to load. type StdLib uint32 @@ -92,16 +94,16 @@ func (s StdLib) String() string { var libs []string flags := map[StdLib]string{ - StdLibCOROUTINE: "COROUTINE", - StdLibTABLE: "TABLE", - StdLibOS: "OS", - StdLibSTRING: "STRING", - StdLibUTF8: "UTF8", - StdLibBIT: "BIT", - StdLibMATH: "MATH", - StdLibBUFFER: "BUFFER", - StdLibVECTOR: "VECTOR", - StdLibDEBUG: "DEBUG", + StdLibCOROUTINE: ffi.LUA_COLIBNAME, + StdLibTABLE: ffi.LUA_TABLIBNAME, + StdLibOS: ffi.LUA_OSLIBNAME, + StdLibSTRING: ffi.LUA_STRLIBNAME, + StdLibUTF8: ffi.LUA_UTF8LIBNAME, + StdLibBIT: ffi.LUA_BITLIBNAME, + StdLibMATH: ffi.LUA_MATHLIBNAME, + StdLibBUFFER: ffi.LUA_BUFFERLIBNAME, + StdLibVECTOR: ffi.LUA_VECLIBNAME, + StdLibDEBUG: ffi.LUA_VECLIBNAME, } for flag, name := range flags { diff --git a/main.go b/main.go index 6164758..0f1ac3e 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ func main() { fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) fmt.Println(key.ToString(), table.Get(key).(*lua.LuaString).ToString()) - chunk, err := state.Load("main", []byte("print('hello, lei!!!!'); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) + chunk, err := state.Load("main", []byte("print('hello, lei!!!!', math.random()); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) if err != nil { fmt.Println(err) return From c384631f49bd747cd505305607aa8eeef241ff2c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 26 Jan 2026 11:25:45 +0000 Subject: [PATCH 40/64] feat(lua/state): add sandboxing option It might also be worth looking into supporting enabling and disabling the sandbox on the go, which `mlua` supports. It would, however, require a lot more involved approach. `mlua` is able to achieve this by maintaining a separate no-op thread which just holds references to Lua values and the state to prevent GC, along with maintaining a snapshot of the state before it is sandboxed. This way, they can simply `xpush` between the two threads holding their states in order to undo sandboxing, if required. If we consider this approach, it would require a pretty major refactor to also drop our dependence on the Lua registry, which we currently use to hold non-GCable references to data. --- lua/state.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/state.go b/lua/state.go index 5010447..a680ff4 100644 --- a/lua/state.go +++ b/lua/state.go @@ -14,6 +14,7 @@ type LuaOptions struct { IsSafe bool CatchPanics bool EnableCodegen bool + EnableSandbox bool Compiler *Compiler } @@ -197,6 +198,10 @@ func NewWith(libs StdLib, options LuaOptions) *Lua { } } + if options.EnableSandbox { + ffi.LSandbox(state.luaState) + } + compiler := options.Compiler if compiler == nil { compiler = DefaultCompiler() From bb1d6a78761a011120ab746f26849a1dfaa77b46 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 26 Jan 2026 11:39:34 +0000 Subject: [PATCH 41/64] refactor(lua/stdlib): use links to luau docs instead of lua --- lua/stdlib.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lua/stdlib.go b/lua/stdlib.go index ef94a5e..22ea25e 100644 --- a/lua/stdlib.go +++ b/lua/stdlib.go @@ -7,31 +7,31 @@ type StdLib uint32 const ( // COROUTINE library - // https://www.lua.org/manual/5.4/manual.html#6.2 + // https://luau.org/library#coroutine-library StdLibCOROUTINE StdLib = 1 << 0 // TABLE library - // https://www.lua.org/manual/5.4/manual.html#6.6 + // https://luau.org/library#table-library StdLibTABLE StdLib = 1 << 1 // OS library - // https://www.lua.org/manual/5.4/manual.html#6.9 + // https://luau.org/library#os-library StdLibOS StdLib = 1 << 3 // STRING library - // https://www.lua.org/manual/5.4/manual.html#6.4 + // https://luau.org/library#string-library StdLibSTRING StdLib = 1 << 4 // UTF8 library - // https://www.lua.org/manual/5.4/manual.html#6.5 + // https://luau.org/library#utf8-library StdLibUTF8 StdLib = 1 << 5 // BIT library - // https://www.lua.org/manual/5.2/manual.html#6.7 + // https://luau.org/library#bit32-library StdLibBIT StdLib = 1 << 6 // MATH library - // https://www.lua.org/manual/5.4/manual.html#6.7 + // https://luau.org/library#math-library StdLibMATH StdLib = 1 << 7 // BUFFER library (Luau) @@ -43,7 +43,7 @@ const ( StdLibVECTOR StdLib = 1 << 10 // DEBUG library (unsafe) - // https://www.lua.org/manual/5.4/manual.html#6.10 + // https://luau.org/library#debug-library StdLibDEBUG StdLib = 1 << 31 // StdLibNONE represents no libraries From 379dd084c6b03c82f99416c5a0d9455707ec2184 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 08:52:26 +0000 Subject: [PATCH 42/64] fix(ffi): return the actual vector components in `ToVector` --- ffi/lua.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index 36d45b7..cc88b24 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -290,8 +290,21 @@ func ToUnsignedX(L *LuaState, idx int32, isnum *bool) LuaUnsigned { return unsigned } -func ToVector(L *LuaState, idx int32) { - C.lua_tovector(L, C.int(idx)) +// DIVERGENCE: We cannot cast and reinterpret the C owned vector returned as +// a Go value, as it breaks cgo pointer rules. Instead, we allocate new Go +// owned floats on the heap and only read the floats returned by C + +func ToVector(L *C.lua_State, idx int32) (x, y, z *float32) { + vec := C.lua_tovector(L, C.int(idx)) + if vec == nil { + return nil, nil, nil + } + + v := (*[3]C.float)(unsafe.Pointer(vec)) + x, y, z = new(float32), new(float32), new(float32) + *x, *y, *z = float32(v[0]), float32(v[1]), float32(v[2]) + + return } func ToBoolean(L *LuaState, idx int32) bool { From f75e364b93b884ff95d6099f15557c8abd57434c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 08:52:56 +0000 Subject: [PATCH 43/64] feat(lua): implement `LuaVector` type --- lua/value.go | 10 ++++++++++ lua/vector.go | 19 +++++++++++++++++++ main.go | 15 ++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 lua/vector.go diff --git a/lua/value.go b/lua/value.go index fef2653..5ae4f24 100644 --- a/lua/value.go +++ b/lua/value.go @@ -161,6 +161,9 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { } return zero, fmt.Errorf("value isn't userdata") + + case *LuaVector: + return reflect.ValueOf(v), nil } return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero) @@ -185,6 +188,13 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { case ffi.LUA_TUSERDATA: ref := ffi.Ref(state, index) return &LuaUserData{vm: lua, index: int(ref)} + case ffi.LUA_TVECTOR: + x, y, z := ffi.ToVector(state, index) + if x != nil || y != nil || z != nil { + return &LuaVector{*x, *y, *z} + } + + return nil default: panic("unsupported Lua type") } diff --git a/lua/vector.go b/lua/vector.go new file mode 100644 index 0000000..450f605 --- /dev/null +++ b/lua/vector.go @@ -0,0 +1,19 @@ +package lua + +import "github.com/CompeyDev/lei/ffi" + +type LuaVector struct{ X, Y, Z float32 } + +// +// LuaValue implementation +// + +var _ LuaValue = (*LuaVector)(nil) + +func (v LuaVector) lua() *Lua { return nil } +func (v LuaVector) ref() int { return ffi.LUA_NOREF } +func (v LuaVector) deref(lua *Lua) int { + state := lua.state() + ffi.PushVector(state, v.X, v.Y, v.Z) + return int(ffi.GetTop(state)) +} diff --git a/main.go b/main.go index 0f1ac3e..c4d0879 100644 --- a/main.go +++ b/main.go @@ -93,19 +93,28 @@ func main() { state.SetGlobal("classUd", classUd) got := state.GetGlobal("classUd").(*lua.LuaUserData).Downcast() - fmt.Println(got.(*Class).value) + fmt.Println("got:", got.(*Class).value) - udChunk, udErr := state.Load("udChunk", []byte("print(tostring(classUd), classUd.toggle); classUd.flip(); print(classUd.toggle, classUd.fakeToggle)")) + conv, err := lua.As[*Class](classUd) + if err != nil { + fmt.Println(err) + } + + fmt.Println("with as:", *conv) + + udChunk, udErr := state.Load("udChunk", []byte("print(tostring(classUd), classUd.toggle); classUd.flip(); print(classUd.toggle, classUd.fakeToggle); return vector.one")) if udErr != nil { fmt.Println(udErr) return } - _, udCallErr := udChunk.Call() + vectorReturn, udCallErr := udChunk.Call() if udCallErr != nil { fmt.Println(udCallErr) return } + + fmt.Println(vectorReturn[0].(*lua.LuaVector)) } type Class struct{ value float64 } From 2e4a4e35fc939ffde6dabc1862a0289498724f41 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 08:55:56 +0000 Subject: [PATCH 44/64] fix(lua/value): check that none of the coordinates are nil --- lua/value.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/value.go b/lua/value.go index 5ae4f24..8d78148 100644 --- a/lua/value.go +++ b/lua/value.go @@ -190,7 +190,7 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { return &LuaUserData{vm: lua, index: int(ref)} case ffi.LUA_TVECTOR: x, y, z := ffi.ToVector(state, index) - if x != nil || y != nil || z != nil { + if x != nil && y != nil && z != nil { return &LuaVector{*x, *y, *z} } From a31975b364de2ef176820ef143389d7af85c58f0 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 08:57:14 +0000 Subject: [PATCH 45/64] fix(lua/value): check not required, we already know the type --- lua/value.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/value.go b/lua/value.go index 8d78148..5c2405b 100644 --- a/lua/value.go +++ b/lua/value.go @@ -190,11 +190,7 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { return &LuaUserData{vm: lua, index: int(ref)} case ffi.LUA_TVECTOR: x, y, z := ffi.ToVector(state, index) - if x != nil && y != nil && z != nil { - return &LuaVector{*x, *y, *z} - } - - return nil + return &LuaVector{*x, *y, *z} default: panic("unsupported Lua type") } From c45a8a29bfb610d3963fa06be434afe58155e56d Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 09:11:12 +0000 Subject: [PATCH 46/64] fix(lua): memory leak due to `LuaUserData` never being freed --- lua/state.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/state.go b/lua/state.go index a680ff4..bc16fce 100644 --- a/lua/state.go +++ b/lua/state.go @@ -147,6 +147,8 @@ func (l *Lua) CreateUserData(value IntoUserData) *LuaUserData { ffi.SetMetatable(state, -2) userdata.index = int(ffi.Ref(state, -1)) + runtime.SetFinalizer(userdata, valueUnrefer[*LuaUserData](l)) + return userdata } From 2c6958a0d52920569471498666b6dba2996c196d Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 09:53:54 +0000 Subject: [PATCH 47/64] feat(lua): implement `LuaBuffer` type --- lua/buffer.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ lua/state.go | 12 +++++++++ lua/userdata.go | 2 ++ main.go | 26 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 lua/buffer.go diff --git a/lua/buffer.go b/lua/buffer.go new file mode 100644 index 0000000..7050bd4 --- /dev/null +++ b/lua/buffer.go @@ -0,0 +1,66 @@ +package lua + +import ( + "unsafe" + + "github.com/CompeyDev/lei/ffi" +) + +type LuaBuffer struct { + vm *Lua + index int + size uint64 +} + +func (b *LuaBuffer) Read(offset uint64, count uint64) []byte { + b.deref(b.vm) + defer ffi.Pop(b.vm.state(), 1) + + if buf := ffi.ToBuffer(b.vm.state(), -1, &b.size); buf != nil && offset <= b.size { + // Clamp to the size if the count exceeds it + if offset+count > b.size { + count = b.size - offset + } + + // Copy data to Go owned byte array for safety + data := make([]byte, count) + slice := unsafe.Slice((*byte)(buf), b.size) + copy(data, slice[offset:offset+count]) + + return data + } + + return nil +} + +func (b *LuaBuffer) Write(offset uint64, data []byte) { + if len(data) == 0 { + return + } + + b.deref(b.vm) + defer ffi.Pop(b.vm.state(), 1) + + if buf := ffi.ToBuffer(b.vm.state(), -1, &b.size); buf != nil && offset <= b.size { + // Truncate the data to buffer end if exceeding + count := uint64(len(data)) + if offset+count > b.size { + count = b.size - offset + } + + dest := unsafe.Slice((*byte)(buf), b.size) + copy(dest[offset:offset+count], data[:count]) + } +} + +// +// LuaValue implementation +// + +var _ LuaValue = (*LuaBuffer)(nil) + +func (b *LuaBuffer) lua() *Lua { return b.vm } +func (b *LuaBuffer) ref() int { return b.index } +func (b *LuaBuffer) deref(lua *Lua) int { + return int(ffi.GetRef(lua.state(), int32(b.ref()))) +} diff --git a/lua/state.go b/lua/state.go index bc16fce..c318f0d 100644 --- a/lua/state.go +++ b/lua/state.go @@ -152,6 +152,18 @@ func (l *Lua) CreateUserData(value IntoUserData) *LuaUserData { return userdata } +func (l *Lua) CreateBuffer(size uint64) *LuaBuffer { + state := l.state() + + ffi.NewBuffer(state, size) + index := ffi.Ref(state, -1) + + b := &LuaBuffer{vm: l, index: int(index), size: size} + runtime.SetFinalizer(b, valueUnrefer[*LuaBuffer](l)) + + return b +} + func (l *Lua) SetCompiler(compiler *Compiler) { l.compiler = compiler } diff --git a/lua/userdata.go b/lua/userdata.go index f421aba..a40b949 100644 --- a/lua/userdata.go +++ b/lua/userdata.go @@ -48,7 +48,9 @@ func (ud *LuaUserData) Downcast() IntoUserData { } } +// // LuaValue implementation +// var _ LuaValue = (*LuaUserData)(nil) diff --git a/main.go b/main.go index c4d0879..8d06aed 100644 --- a/main.go +++ b/main.go @@ -115,6 +115,32 @@ func main() { } fmt.Println(vectorReturn[0].(*lua.LuaVector)) + + bufChunk, bufChunkErr := state.Load( + "bufChunk", + []byte( + `local str = buffer.readstring(b, 0, 5) + print(str) + buffer.writestring(b, 4, "lei")`, + ), + ) + + if bufChunkErr != nil { + fmt.Println(bufChunkErr) + return + } + + buf := state.CreateBuffer(10) + buf.Write(0, []byte("hello")) + state.SetGlobal("b", buf) + + _, bufErr := bufChunk.Call() + if bufErr != nil { + fmt.Println(bufErr) + return + } + + fmt.Println(string(buf.Read(4, 3))) } type Class struct{ value float64 } From f4078c76087e5747601773df15b5a41567e47cc3 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 10:03:44 +0000 Subject: [PATCH 48/64] chore(ffi): remove unused vector stubs --- ffi/vector3.go | 14 -------------- ffi/vector4.go | 14 -------------- 2 files changed, 28 deletions(-) delete mode 100644 ffi/vector3.go delete mode 100644 ffi/vector4.go diff --git a/ffi/vector3.go b/ffi/vector3.go deleted file mode 100644 index b5b918e..0000000 --- a/ffi/vector3.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !LUAU_VECTOR4 - -package ffi - -/* -#cgo CFLAGS: -Iluau/VM/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/include -// #cgo LDFLAGS: -L${SRCDIR}/luau/cmake -lLuau.VM -lm -lstdc++ -#include -*/ -import "C" - -func PushVector(L *LuaState, x float32, y float32, z float32) { - C.lua_pushvector(L, C.float(x), C.float(y), C.float(z)) -} diff --git a/ffi/vector4.go b/ffi/vector4.go deleted file mode 100644 index e86e2a1..0000000 --- a/ffi/vector4.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build LUAU_VECTOR4 - -package ffi - -/* -#cgo CFLAGS: -Iluau/VM/include -I/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/include -DLUA_VECTOR_SIZE=4 -// #cgo LDFLAGS: -L${SRCDIR}/luau/cmake -lLuau.VM -lm -lstdc++ -#include -*/ -import "C" - -func PushVector(L *LuaState, x float32, y float32, z float32, w float32) { - C.lua_pushvector(L, C.float(x), C.float(y), C.float(z), C.float(w)) -} From ce826a4161b2d3d6a797c40d84c9f970b2be4304 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 27 Jan 2026 10:08:21 +0000 Subject: [PATCH 49/64] fix(ffi/lua): move `PushVector` into `lua.go` --- ffi/lua.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ffi/lua.go b/ffi/lua.go index cc88b24..aac1666 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -440,6 +440,10 @@ func PushLightUserdataTagged(L *LuaState, p unsafe.Pointer, tag int32) { C.lua_pushlightuserdatatagged(L, p, C.int(tag)) } +func PushVector(L *LuaState, x, y, z float32) { + C.lua_pushvector(L, C.float(x), C.float(y), C.float(z)) +} + func NewUserdataTagged(L *LuaState, sz uint64, tag int32) unsafe.Pointer { return C.lua_newuserdatatagged(L, C.size_t(sz), C.int(tag)) } From 97fea6628a3f7d79f2795d770f33cbf486c71101 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 10:33:05 +0000 Subject: [PATCH 50/64] fix(ffi/lua): remove "Lua" prefix from coroutine methods --- ffi/lua.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index aac1666..ed0c224 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -592,23 +592,23 @@ func Pcall(L *LuaState, nargs int32, nresults int32, errfunc int32) int32 { // ======================== // -func LuaYield(L *LuaState, nresults int32) int32 { +func Yield(L *LuaState, nresults int32) int32 { return int32(C.lua_yield(L, C.int(nresults))) } -func LuaBreak(L *LuaState) int32 { +func Break(L *LuaState) int32 { return int32(C.lua_break(L)) } -func LuaResume(L *LuaState, from *LuaState, nargs int32) int32 { +func Resume(L *LuaState, from *LuaState, nargs int32) int32 { return int32(C.lua_resume(L, from, C.int(nargs))) } -func LuaResumeError(L *LuaState, from *LuaState) int32 { +func ResumeError(L *LuaState, from *LuaState) int32 { return int32(C.lua_resumeerror(L, from)) } -func LuaStatus(L *LuaState) int32 { +func Status(L *LuaState) int32 { return int32(C.lua_status(L)) } From 0716720ca794aa11beafc92b8f92df4a6d3e6b1a Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:02:05 +0000 Subject: [PATCH 51/64] feat(lua/value): implement reflection and conversion for `LuaBuffer` --- lua/value.go | 13 +++++++++++++ main.go | 3 +++ 2 files changed, 16 insertions(+) diff --git a/lua/value.go b/lua/value.go index 5c2405b..4594067 100644 --- a/lua/value.go +++ b/lua/value.go @@ -164,6 +164,16 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { case *LuaVector: return reflect.ValueOf(v), nil + + case *LuaBuffer: + kind := t.Kind() + if kind == reflect.Array { + return reflect.ValueOf(val.Read(0, uint64(t.Len()))).Convert(t), nil + } + + if kind == reflect.Slice { + return reflect.ValueOf(val.Read(0, val.size)).Convert(t), nil + } } return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero) @@ -191,6 +201,9 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { case ffi.LUA_TVECTOR: x, y, z := ffi.ToVector(state, index) return &LuaVector{*x, *y, *z} + case ffi.LUA_TBUFFER: + ref := ffi.Ref(state, index) + return &LuaBuffer{vm: lua, index: int(ref), size: ffi.ObjLen(state, ref)} default: panic("unsupported Lua type") } diff --git a/main.go b/main.go index 8d06aed..1861b77 100644 --- a/main.go +++ b/main.go @@ -141,6 +141,9 @@ func main() { } fmt.Println(string(buf.Read(4, 3))) + + bufArr, err := lua.As[[4]uint8](buf) + fmt.Println("rapidly approaching", string(bufArr[:])) } type Class struct{ value float64 } From 261d29efc1e5fdccea967e99626e484f699f5871 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:04:12 +0000 Subject: [PATCH 52/64] feat(lua): implement `LuaThread` type Also abstracts bytecode / closure stack push into a private `pushToStack` method for `LuaChunk` so that it can be reused for thread creation and maintainence (for being able to reuse the same thread multiple times). --- lua/chunk.go | 42 +++++++++++-------- lua/state.go | 18 +++++++++ lua/thread.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 40 ++++++++++++++++++ 4 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 lua/thread.go diff --git a/lua/chunk.go b/lua/chunk.go index 500a45e..8ab98b5 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -17,24 +17,11 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { state := c.vm.state() initialStack := ffi.GetTop(state) // Track initial stack size + c.pushToStack() argsCount := len(args) - if c.bytecode != nil { - hasLoaded := ffi.LuauLoad(state, c.name, c.bytecode, uint64(len(c.bytecode)), 0) - if !hasLoaded { - // Miscellaneous error is denoted with a -1 code - return nil, &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} - } - - // Apply native code generation if requested - if ffi.LuauCodegenSupported() && c.vm.codegenEnabled { - ffi.LuauCodegenCompile(state, -1) - } - } else { - // Push function onto the stack - ffi.GetRef(state, int32(c.index)) - - // Push the length and the arguments onto the stack (deref) + if c.bytecode == nil { + // Chunk is a C function, push length and args ffi.PushNumber(state, ffi.LuaNumber(argsCount)) argsCount++ for _, arg := range args { @@ -66,3 +53,26 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { return results, nil } + +func (c *LuaChunk) pushToStack() error { + state := c.vm.state() + + if c.bytecode == nil { + // Chunk is of a C function, need to deref + ffi.GetRef(state, int32(c.index)) + } else { + // Chunk is bytecode, load it into the VM + hasLoaded := ffi.LuauLoad(state, c.name, c.bytecode, uint64(len(c.bytecode)), 0) + if !hasLoaded { + // Miscellaneous error is denoted with a -1 code + return &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} + } + + // Apply native code generation if requested + if ffi.LuauCodegenSupported() && c.vm.codegenEnabled { + ffi.LuauCodegenCompile(state, -1) + } + } + + return nil +} diff --git a/lua/state.go b/lua/state.go index c318f0d..c7a32de 100644 --- a/lua/state.go +++ b/lua/state.go @@ -164,6 +164,24 @@ func (l *Lua) CreateBuffer(size uint64) *LuaBuffer { return b } +func (l *Lua) CreateThread(chunk *LuaChunk) (*LuaThread, error) { + mainState := l.state() + threadState := ffi.NewThread(mainState) + + chunk.pushToStack() + ffi.XMove(mainState, threadState, 1) + + index := ffi.Ref(mainState, -1) + t := &LuaThread{vm: l, chunk: chunk, index: int(index)} + + runtime.SetFinalizer(t, func(t *LuaThread) { + ffi.LuaClose(t.State()) + ffi.Unref(l.state(), int32(t.ref())) + }) + + return t, nil +} + func (l *Lua) SetCompiler(compiler *Compiler) { l.compiler = compiler } diff --git a/lua/thread.go b/lua/thread.go new file mode 100644 index 0000000..2c46df8 --- /dev/null +++ b/lua/thread.go @@ -0,0 +1,109 @@ +package lua + +import "github.com/CompeyDev/lei/ffi" + +type LuaThread struct { + vm *Lua + chunk *LuaChunk + index int +} + +func (t *LuaThread) State() *ffi.LuaState { + state := t.vm.state() + + t.deref(t.vm) + defer ffi.Pop(state, 1) + + return ffi.ToThread(state, -1) +} + +func (t *LuaThread) Resume() ([]LuaValue, error) { + threadState := t.State() + t.pushMainFunction() + + status := int(ffi.Resume(threadState, nil, 0)) + return t.collectResults(threadState, status) +} + +func (t *LuaThread) ResumeWith(args ...LuaValue) ([]LuaValue, error) { + mainState := t.vm.state() + threadState := t.State() + + // Push the function if required + t.pushMainFunction() + + // Push args length and then the args + argsCount := len(args) + ffi.PushNumber(threadState, ffi.LuaNumber(argsCount)) + + for _, arg := range args { + arg.deref(t.vm) + ffi.XMove(mainState, threadState, 1) + } + + status := int(ffi.Resume(threadState, nil, int32(argsCount+1))) // +1 for count arg + return t.collectResults(threadState, status) +} + +func (t *LuaThread) collectResults(threadState *ffi.LuaState, status int) ([]LuaValue, error) { + if status != ffi.LUA_OK && status != ffi.LUA_YIELD { + // Return error if thread did not run successfully + return nil, newLuaError(threadState, status) + } + + nresults := int(ffi.GetTop(threadState)) + if nresults == 0 { + return nil, nil + } + + mainState := t.vm.state() + results := make([]LuaValue, nresults) + + // Push arguments onto main thread and ref them into LuaValues + for i := range nresults { + ffi.PushValue(threadState, int32(i+1)) + ffi.XMove(threadState, mainState, 1) + + results[i] = intoLuaValue(t.vm, int32(ffi.GetTop(mainState))) + } + + return results, nil +} + +func (t *LuaThread) pushMainFunction() { + if threadState := t.State(); t.Status() == ffi.LUA_OK { + // Reset the thread and push the coroutine function if the thread has + // finished running and returned a non-resumable state + ffi.ResetThread(threadState) + t.chunk.pushToStack() + ffi.XMove(t.vm.state(), threadState, 1) + } +} + +func (t *LuaThread) Status() int { + threadState := t.State() + return int(ffi.Status(threadState)) +} + +func (t *LuaThread) IsYielded() bool { + return t.Status() == ffi.LUA_YIELD +} + +func (t *LuaThread) IsFinished() bool { + status := t.Status() + threadState := t.State() + return status == ffi.LUA_OK && ffi.GetTop(threadState) == 0 +} + +// +// LuaValue implementation +// + +var _ LuaValue = (*LuaThread)(nil) + +func (t *LuaThread) lua() *Lua { return t.vm } +func (t *LuaThread) ref() int { return t.index } + +func (t *LuaThread) deref(lua *Lua) int { + return int(ffi.GetRef(lua.state(), int32(t.ref()))) +} diff --git a/main.go b/main.go index 1861b77..794d5fe 100644 --- a/main.go +++ b/main.go @@ -144,6 +144,46 @@ func main() { bufArr, err := lua.As[[4]uint8](buf) fmt.Println("rapidly approaching", string(bufArr[:])) + + thread, threadErr := state.CreateThread(state.CreateFunction(func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + returns := []lua.LuaValue{ + luaState.CreateString("Hello"), + luaState.CreateString("thread"), + } + + if len(args) != 0 { + returns = append(returns, args[0]) + } else { + fmt.Println("No args for coroutine!") + } + + return returns, nil + })) + + if threadErr != nil { + fmt.Println(threadErr) + return + } + + resultsA, errA := thread.Resume() + resultsB, errB := thread.ResumeWith(state.CreateString("B!")) + + if errA != nil || errB != nil { + fmt.Println("Either thread resume failed") + fmt.Println("A:", errA) + fmt.Println("B:", errB) + return + } + + for _, result := range resultsA { + fmt.Println("Thread A =>", result.(*lua.LuaString).ToString()) + } + + fmt.Println() + + for _, result := range resultsB { + fmt.Println("Thread B =>", result.(*lua.LuaString).ToString()) + } } type Class struct{ value float64 } From b604a16fedcbd63dcef6a74d355f754ae84302b0 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:19:13 +0000 Subject: [PATCH 53/64] feat(lua/value): convert `LuaNumber` into any numeric type Also includes a minor fix for the error message not displaying the real type reflect is trying to convert to, rather displaying only `reflect.Value` for all types. --- lua/value.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lua/value.go b/lua/value.go index 4594067..7f9e3ea 100644 --- a/lua/value.go +++ b/lua/value.go @@ -35,7 +35,16 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { switch val := v.(type) { case *LuaNumber: - if t.Kind() == reflect.Float64 { + // Map of all numeric types for O(1) lookup + var numericKinds = map[reflect.Kind]bool{ + reflect.Int: true, reflect.Int8: true, reflect.Int16: true, reflect.Int32: true, reflect.Int64: true, + reflect.Uint: true, reflect.Uint8: true, reflect.Uint16: true, reflect.Uint32: true, reflect.Uint64: true, + reflect.Uintptr: true, + reflect.Float32: true, reflect.Float64: true, + reflect.Complex64: true, reflect.Complex128: true, + } + + if kind := t.Kind(); numericKinds[kind] { num := reflect.ValueOf(*val).Convert(t) return num, nil } @@ -176,7 +185,7 @@ func asReflectValue(v LuaValue, t reflect.Type) (reflect.Value, error) { } } - return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero) + return zero, fmt.Errorf("cannot convert LuaValue(%T) into %T", v, zero.Type().Name()) } func intoLuaValue(lua *Lua, index int32) LuaValue { From ce4bdd1f13c5eb863182ed7fa9ec835fb8dce1d1 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:20:27 +0000 Subject: [PATCH 54/64] chore(lua/value_test): use numeric types instead of string --- lua/value_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/value_test.go b/lua/value_test.go index 3b9fed0..26cc165 100644 --- a/lua/value_test.go +++ b/lua/value_test.go @@ -31,19 +31,19 @@ func TestAs(t *testing.T) { // 2. Exact field name match t.Run("Exact match", func(t *testing.T) { type Person struct { - Age string // TODO: make this int once we have numbers + Age int } table := state.CreateTable() - table.Set(state.CreateString("Age"), state.CreateString("30")) + table.Set(state.CreateString("Age"), lua.LuaNumber(30)) res, err := lua.As[Person](table) if err != nil { t.Fatal(err) } - if res.Age != "30" { - t.Fatalf("expected '30', got %v", res.Age) + if res.Age != 30 { + t.Fatalf("expected 30, got %v", res.Age) } }) @@ -85,13 +85,13 @@ func TestAs(t *testing.T) { t.Run("Mixed fields", func(t *testing.T) { type Person struct { Name string `lua:"username"` - Age string // TODO: use int once LuaNumber is implemented + Age int Email string } table := state.CreateTable() table.Set(state.CreateString("username"), state.CreateString("Bob")) - table.Set(state.CreateString("Age"), state.CreateString("25")) + table.Set(state.CreateString("Age"), lua.LuaNumber(25)) table.Set(state.CreateString("email"), state.CreateString("bobby@example.com")) res, err := lua.As[Person](table) @@ -99,7 +99,7 @@ func TestAs(t *testing.T) { t.Fatal(err) } - if res.Name != "Bob" || res.Age != "25" || res.Email != "bobby@example.com" { + if res.Name != "Bob" || res.Age != 25 || res.Email != "bobby@example.com" { t.Fatalf("unexpected result: %+v", res) } }) From 02de14b88a969c87aff9b69bb421050c339de15d Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:20:59 +0000 Subject: [PATCH 55/64] chore(lua/chunk): remove resolved TODO comment --- lua/chunk.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/chunk.go b/lua/chunk.go index 8ab98b5..7fa7f76 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -41,7 +41,6 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { return nil, nil } - // TODO: contemplate whether to return LuaValues or go values results := make([]LuaValue, resultsCount) for i := range resultsCount { // The stack has grown by the number of returns of the chunk from the From e026826fccab2bc075f5f1e6f875cd3d7d1f766c Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 4 Feb 2026 11:26:21 +0000 Subject: [PATCH 56/64] fix(lua/value): add missed `LuaThread` value conversion case --- lua/thread.go | 2 +- lua/value.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/thread.go b/lua/thread.go index 2c46df8..f5eee6f 100644 --- a/lua/thread.go +++ b/lua/thread.go @@ -71,7 +71,7 @@ func (t *LuaThread) collectResults(threadState *ffi.LuaState, status int) ([]Lua } func (t *LuaThread) pushMainFunction() { - if threadState := t.State(); t.Status() == ffi.LUA_OK { + if threadState := t.State(); t.Status() == ffi.LUA_OK && t.chunk != nil { // Reset the thread and push the coroutine function if the thread has // finished running and returned a non-resumable state ffi.ResetThread(threadState) diff --git a/lua/value.go b/lua/value.go index 7f9e3ea..cc29273 100644 --- a/lua/value.go +++ b/lua/value.go @@ -213,6 +213,9 @@ func intoLuaValue(lua *Lua, index int32) LuaValue { case ffi.LUA_TBUFFER: ref := ffi.Ref(state, index) return &LuaBuffer{vm: lua, index: int(ref), size: ffi.ObjLen(state, ref)} + case ffi.LUA_TTHREAD: + ref := ffi.Ref(state, index) + return &LuaThread{vm: lua, index: int(ref)} // NOTE: no chunk, can only be executed once default: panic("unsupported Lua type") } From c0294eb6bf4923c64e85ecc515647ef74ce463ea Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 5 Feb 2026 07:27:26 +0000 Subject: [PATCH 57/64] refactor(lua): make `LuaThread.state` private, rearrange --- lua/state.go | 2 +- lua/thread.go | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lua/state.go b/lua/state.go index c7a32de..01d4b27 100644 --- a/lua/state.go +++ b/lua/state.go @@ -175,7 +175,7 @@ func (l *Lua) CreateThread(chunk *LuaChunk) (*LuaThread, error) { t := &LuaThread{vm: l, chunk: chunk, index: int(index)} runtime.SetFinalizer(t, func(t *LuaThread) { - ffi.LuaClose(t.State()) + ffi.LuaClose(t.state()) ffi.Unref(l.state(), int32(t.ref())) }) diff --git a/lua/thread.go b/lua/thread.go index f5eee6f..b7fc3b8 100644 --- a/lua/thread.go +++ b/lua/thread.go @@ -8,17 +8,8 @@ type LuaThread struct { index int } -func (t *LuaThread) State() *ffi.LuaState { - state := t.vm.state() - - t.deref(t.vm) - defer ffi.Pop(state, 1) - - return ffi.ToThread(state, -1) -} - func (t *LuaThread) Resume() ([]LuaValue, error) { - threadState := t.State() + threadState := t.state() t.pushMainFunction() status := int(ffi.Resume(threadState, nil, 0)) @@ -27,7 +18,7 @@ func (t *LuaThread) Resume() ([]LuaValue, error) { func (t *LuaThread) ResumeWith(args ...LuaValue) ([]LuaValue, error) { mainState := t.vm.state() - threadState := t.State() + threadState := t.state() // Push the function if required t.pushMainFunction() @@ -45,6 +36,21 @@ func (t *LuaThread) ResumeWith(args ...LuaValue) ([]LuaValue, error) { return t.collectResults(threadState, status) } +func (t *LuaThread) Status() int { + threadState := t.state() + return int(ffi.Status(threadState)) +} + +func (t *LuaThread) IsYielded() bool { + return t.Status() == ffi.LUA_YIELD +} + +func (t *LuaThread) IsFinished() bool { + status := t.Status() + threadState := t.state() + return status == ffi.LUA_OK && ffi.GetTop(threadState) == 0 +} + func (t *LuaThread) collectResults(threadState *ffi.LuaState, status int) ([]LuaValue, error) { if status != ffi.LUA_OK && status != ffi.LUA_YIELD { // Return error if thread did not run successfully @@ -71,7 +77,7 @@ func (t *LuaThread) collectResults(threadState *ffi.LuaState, status int) ([]Lua } func (t *LuaThread) pushMainFunction() { - if threadState := t.State(); t.Status() == ffi.LUA_OK && t.chunk != nil { + if threadState := t.state(); t.Status() == ffi.LUA_OK && t.chunk != nil { // Reset the thread and push the coroutine function if the thread has // finished running and returned a non-resumable state ffi.ResetThread(threadState) @@ -80,19 +86,13 @@ func (t *LuaThread) pushMainFunction() { } } -func (t *LuaThread) Status() int { - threadState := t.State() - return int(ffi.Status(threadState)) -} +func (t *LuaThread) state() *ffi.LuaState { + state := t.vm.state() -func (t *LuaThread) IsYielded() bool { - return t.Status() == ffi.LUA_YIELD -} + t.deref(t.vm) + defer ffi.Pop(state, 1) -func (t *LuaThread) IsFinished() bool { - status := t.Status() - threadState := t.State() - return status == ffi.LUA_OK && ffi.GetTop(threadState) == 0 + return ffi.ToThread(state, -1) } // From 447dcd792ba05ef91b6ff6f2ad43a201ac048257 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 5 Feb 2026 07:31:56 +0000 Subject: [PATCH 58/64] feat(lua/buffer): implement `IsEmpty` and `Size` methods --- lua/buffer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/buffer.go b/lua/buffer.go index 7050bd4..f47893c 100644 --- a/lua/buffer.go +++ b/lua/buffer.go @@ -12,6 +12,9 @@ type LuaBuffer struct { size uint64 } +func (b *LuaBuffer) Size() uint64 { return b.size } +func (b *LuaBuffer) IsEmpty() bool { return b.size == 0 } + func (b *LuaBuffer) Read(offset uint64, count uint64) []byte { b.deref(b.vm) defer ffi.Pop(b.vm.state(), 1) From bbd713d5d4b68fc72ac3341f05aa30468cc839d4 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 5 Feb 2026 07:43:09 +0000 Subject: [PATCH 59/64] feat(lua): support providing debug name for C functions --- lua/chunk.go | 4 ++-- lua/state.go | 8 ++++---- main.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lua/chunk.go b/lua/chunk.go index 7fa7f76..26405e3 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -7,7 +7,7 @@ import "github.com/CompeyDev/lei/ffi" type LuaChunk struct { vm *Lua - name string + name *string bytecode []byte index int @@ -61,7 +61,7 @@ func (c *LuaChunk) pushToStack() error { ffi.GetRef(state, int32(c.index)) } else { // Chunk is bytecode, load it into the VM - hasLoaded := ffi.LuauLoad(state, c.name, c.bytecode, uint64(len(c.bytecode)), 0) + hasLoaded := ffi.LuauLoad(state, *c.name, c.bytecode, uint64(len(c.bytecode)), 0) if !hasLoaded { // Miscellaneous error is denoted with a -1 code return &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} diff --git a/lua/state.go b/lua/state.go index 01d4b27..0f7a5e6 100644 --- a/lua/state.go +++ b/lua/state.go @@ -26,7 +26,7 @@ type Lua struct { } func (l *Lua) Load(name string, input []byte) (*LuaChunk, error) { - chunk := &LuaChunk{vm: l, bytecode: input} + chunk := &LuaChunk{vm: l, bytecode: input, name: &name} if !isBytecode(input) { bytecode, err := l.compiler.Compile(string(input)) if err != nil { @@ -95,16 +95,16 @@ func (l *Lua) CreateString(str string) *LuaString { return s } -func (l *Lua) CreateFunction(fn GoFunction) *LuaChunk { +func (l *Lua) CreateFunction(name *string, fn GoFunction) *LuaChunk { state := l.state() entry := l.fnRegistry.register(fn) pushUpvalue(state, entry, registryTrampolineDtor) - ffi.PushCClosureK(state, registryTrampoline, nil, 1, nil) + ffi.PushCClosureK(state, registryTrampoline, name, 1, nil) index := ffi.Ref(state, -1) - c := &LuaChunk{vm: l, index: int(index)} + c := &LuaChunk{vm: l, index: int(index), name: name} runtime.SetFinalizer(c, func(c *LuaChunk) { ffi.Unref(state, index) }) return c diff --git a/main.go b/main.go index 794d5fe..d67b73d 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,7 @@ func main() { fmt.Printf("%s %s\n", k, v) } - cFnChunk := state.CreateFunction(func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + cFnChunk := state.CreateFunction(nil, func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { someNumber := lua.LuaNumber(22713) return []lua.LuaValue{ luaState.CreateString("Hello"), @@ -145,7 +145,7 @@ func main() { bufArr, err := lua.As[[4]uint8](buf) fmt.Println("rapidly approaching", string(bufArr[:])) - thread, threadErr := state.CreateThread(state.CreateFunction(func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { + thread, threadErr := state.CreateThread(state.CreateFunction(nil, func(luaState *lua.Lua, args ...lua.LuaValue) ([]lua.LuaValue, error) { returns := []lua.LuaValue{ luaState.CreateString("Hello"), luaState.CreateString("thread"), From 5bb16f4c9f7b003b6f4fa16b77b06eaca1561cd9 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 21 Feb 2026 14:26:45 +0000 Subject: [PATCH 60/64] fix(ffi/lua): `lua_setfenv` returns a boolean-like value --- ffi/lua.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index ed0c224..7e866ff 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -551,8 +551,8 @@ func SetMetatable(L *LuaState, objindex int32) int32 { return int32(C.lua_setmetatable(L, C.int(objindex))) } -func Setfenv(L *LuaState, idx int32) int32 { - return int32(C.lua_setfenv(L, C.int(idx))) +func Setfenv(L *LuaState, idx int32) bool { + return C.lua_setfenv(L, C.int(idx)) == 0 } // From 6ae8a7f0dcaf6441e328174792e591288aec40c3 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 21 Feb 2026 14:27:07 +0000 Subject: [PATCH 61/64] feat(lua/chunk): more convenience methods for `LuaChunk` * Introduced a `ChunkMode` enum to identify the type of the chunk, since `LuaChunk` encompasses practically any executable data (bytecode, source code, and C functions) * Allow getting and setting the function environments * Allow specifying a custom compiler for the chunk which is not the state's own compiler --- lua/chunk.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++------ lua/state.go | 23 +++++++++++---------- main.go | 21 ++++--------------- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/lua/chunk.go b/lua/chunk.go index 26405e3..f3c49a7 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -2,17 +2,42 @@ package lua import "github.com/CompeyDev/lei/ffi" -// NOTE: `bytecode` and `index` are expected to be mutually exclusive +type ChunkMode int + +const ( + // Raw text source code that must be compiled before executing + ChunkModeSOURCE = iota + + // Compiled bytecode that can be directly executed + ChunkModeBYTECODE + + // A C function pointer loaded onto the stack + ChunkModeFUNCTION +) type LuaChunk struct { - vm *Lua + vm *Lua + env *LuaTable + mode ChunkMode + // Values only applicable for source or bytecode types name *string - bytecode []byte + data []byte + compiler *Compiler + // An index is held for chunks of the function type index int } +func (c *LuaChunk) Environment() *LuaTable { return c.env } +func (c *LuaChunk) SetEnvironment(env *LuaTable) { c.env = env } + +func (c *LuaChunk) Mode() ChunkMode { return c.mode } +func (c *LuaChunk) SetMode(mode ChunkMode) { c.mode = mode } + +func (c *LuaChunk) Compiler() *Compiler { return c.compiler } +func (c *LuaChunk) SetCompiler(compiler *Compiler) { c.compiler = compiler } + func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { state := c.vm.state() @@ -20,7 +45,7 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { c.pushToStack() argsCount := len(args) - if c.bytecode == nil { + if c.mode == ChunkModeFUNCTION { // Chunk is a C function, push length and args ffi.PushNumber(state, ffi.LuaNumber(argsCount)) argsCount++ @@ -56,12 +81,25 @@ func (c *LuaChunk) Call(args ...LuaValue) ([]LuaValue, error) { func (c *LuaChunk) pushToStack() error { state := c.vm.state() - if c.bytecode == nil { + if c.data == nil { // Chunk is of a C function, need to deref ffi.GetRef(state, int32(c.index)) } else { // Chunk is bytecode, load it into the VM - hasLoaded := ffi.LuauLoad(state, *c.name, c.bytecode, uint64(len(c.bytecode)), 0) + var bytecode []byte + + if c.mode == ChunkModeSOURCE { + // Need to compile + var err error + if bytecode, err = c.compiler.Compile(string(c.data)); err != nil { + return err + } + } else { + // Already compiled + bytecode = c.data + } + + hasLoaded := ffi.LuauLoad(state, *c.name, bytecode, uint64(len(bytecode)), 0) if !hasLoaded { // Miscellaneous error is denoted with a -1 code return &LuaError{Code: -1, Message: ffi.ToLString(state, -1, nil)} @@ -73,5 +111,13 @@ func (c *LuaChunk) pushToStack() error { } } + if c.env != nil { + // If a custom environment was provided, set it for the loaded value + c.env.deref(c.vm) + if ok := ffi.Setfenv(c.vm.state(), -2); !ok { + return &LuaError{Code: -1, Message: "Failed to set environment for chunk"} + } + } + return nil } diff --git a/lua/state.go b/lua/state.go index 0f7a5e6..1af5849 100644 --- a/lua/state.go +++ b/lua/state.go @@ -25,18 +25,19 @@ type Lua struct { codegenEnabled bool } -func (l *Lua) Load(name string, input []byte) (*LuaChunk, error) { - chunk := &LuaChunk{vm: l, bytecode: input, name: &name} - if !isBytecode(input) { - bytecode, err := l.compiler.Compile(string(input)) - if err != nil { - return nil, err - } - - chunk.bytecode = bytecode +func (l *Lua) Load(name string, input []byte) *LuaChunk { + var mode ChunkMode = ChunkModeSOURCE + if isBytecode(input) { + mode = ChunkModeBYTECODE } - return chunk, nil + return &LuaChunk{ + vm: l, + data: input, + name: &name, + mode: mode, + compiler: l.compiler, + } } func (l *Lua) Memory() *MemoryState { @@ -104,7 +105,7 @@ func (l *Lua) CreateFunction(name *string, fn GoFunction) *LuaChunk { ffi.PushCClosureK(state, registryTrampoline, name, 1, nil) index := ffi.Ref(state, -1) - c := &LuaChunk{vm: l, index: int(index), name: name} + c := &LuaChunk{vm: l, index: int(index), name: name, mode: ChunkModeFUNCTION} runtime.SetFinalizer(c, func(c *LuaChunk) { ffi.Unref(state, index) }) return c diff --git a/main.go b/main.go index d67b73d..7a0644f 100644 --- a/main.go +++ b/main.go @@ -18,13 +18,9 @@ func main() { fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) fmt.Println(key.ToString(), table.Get(key).(*lua.LuaString).ToString()) - chunk, err := state.Load("main", []byte("print('hello, lei!!!!', math.random()); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) - if err != nil { - fmt.Println(err) - return - } - + chunk := state.Load("main", []byte("print('hello, lei!!!!', math.random()); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) values, returnErr := chunk.Call() + if returnErr != nil { fmt.Println(returnErr) return @@ -102,11 +98,7 @@ func main() { fmt.Println("with as:", *conv) - udChunk, udErr := state.Load("udChunk", []byte("print(tostring(classUd), classUd.toggle); classUd.flip(); print(classUd.toggle, classUd.fakeToggle); return vector.one")) - if udErr != nil { - fmt.Println(udErr) - return - } + udChunk := state.Load("udChunk", []byte("print(tostring(classUd), classUd.toggle); classUd.flip(); print(classUd.toggle, classUd.fakeToggle); return vector.one")) vectorReturn, udCallErr := udChunk.Call() if udCallErr != nil { @@ -116,7 +108,7 @@ func main() { fmt.Println(vectorReturn[0].(*lua.LuaVector)) - bufChunk, bufChunkErr := state.Load( + bufChunk := state.Load( "bufChunk", []byte( `local str = buffer.readstring(b, 0, 5) @@ -125,11 +117,6 @@ func main() { ), ) - if bufChunkErr != nil { - fmt.Println(bufChunkErr) - return - } - buf := state.CreateBuffer(10) buf.Write(0, []byte("hello")) state.SetGlobal("b", buf) From 21df3502af3d08bd918b8acc6a65fa84ff22d330 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 25 Feb 2026 11:39:17 +0000 Subject: [PATCH 62/64] fix(lua/chunk): export `ChunkMode` variants as correct type --- lua/chunk.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/chunk.go b/lua/chunk.go index f3c49a7..8469182 100644 --- a/lua/chunk.go +++ b/lua/chunk.go @@ -6,7 +6,7 @@ type ChunkMode int const ( // Raw text source code that must be compiled before executing - ChunkModeSOURCE = iota + ChunkModeSOURCE ChunkMode = iota // Compiled bytecode that can be directly executed ChunkModeBYTECODE From 5260d81535d225415d671b4f5f17a587623be80e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 12 Mar 2026 10:32:55 +0000 Subject: [PATCH 63/64] fix(ffi/lua): `lua_getmetatable` returns bool-like value --- ffi/lua.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffi/lua.go b/ffi/lua.go index 7e866ff..4314c12 100644 --- a/ffi/lua.go +++ b/ffi/lua.go @@ -514,8 +514,8 @@ func SetSafeEnv(L *LuaState, idx int32, enabled bool) { C.lua_setsafeenv(L, C.int(idx), cenabled) } -func GetMetatable(L *LuaState, objindex int32) int32 { - return int32(C.lua_getmetatable(L, C.int(objindex))) +func GetMetatable(L *LuaState, objindex int32) bool { + return int32(C.lua_getmetatable(L, C.int(objindex))) == 1 } func Getfenv(L *LuaState, idx int32) { From 164e75136d98f92c11cdf9f240f36261e646a1c5 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Thu, 12 Mar 2026 10:33:19 +0000 Subject: [PATCH 64/64] feat(lua/table): implement more methods for `LuaTable` Also some miscellaneous makes some bug-fixes. * `Set` - fix memory leak by popping off the table finally * `Get` - fix memory leak by popping off both the table and value finally * `SetMetatable` - pop the metatable left and update the ref since `lua_setmetatable` updates the existing table in place * Implemented `RawSet`, `RawGet`, `Push`, `Pop`, `RawPush`, `RawPop`, `Equals`, `Clear`, `Len`, `IsEmpty` --- lua/table.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++- main.go | 27 +++++++- 2 files changed, 211 insertions(+), 5 deletions(-) diff --git a/lua/table.go b/lua/table.go index b37fd3c..b6139bf 100644 --- a/lua/table.go +++ b/lua/table.go @@ -14,8 +14,10 @@ func (t *LuaTable) Set(key LuaValue, value LuaValue) { key.deref(t.vm) // key (-2) value.deref(t.vm) // value (-1) + // Pop the table off + defer ffi.Pop(state, 1) + ffi.SetTable(state, -3) - ffi.Pop(state, 1) } func (t *LuaTable) Get(key LuaValue) LuaValue { @@ -23,14 +25,168 @@ func (t *LuaTable) Get(key LuaValue) LuaValue { t.deref(t.vm) //////////////////// table (-3) key.deref(t.vm) //////////////////// key (-2) - ffi.GetTable(state, -2) + // Pop the table and value off + defer ffi.Pop(state, 2) + + ffi.GetTable(state, -2) val := intoLuaValue(t.vm, -1) ////// value (-1) - ffi.Pop(state, 2) return val } +func (t *LuaTable) RawSet(key LuaValue, value LuaValue) { + state := t.vm.state() + + t.deref(t.vm) // table (-3) + key.deref(t.vm) // key (-2) + value.deref(t.vm) // value (-1) + + // Pop the table off + defer ffi.Pop(state, 1) + + ffi.RawSet(state, -3) +} + +func (t *LuaTable) RawGet(key LuaValue) LuaValue { + state := t.vm.state() + + t.deref(t.vm) // table (-2) + key.deref(t.vm) // key (-1) + + // Pop the table and value off + defer ffi.Pop(state, 2) + + ffi.RawGet(state, -2) + val := intoLuaValue(t.vm, -1) // value (-1) + + return val +} + +func (t *LuaTable) Push(value LuaValue) { + state := t.vm.state() + + t.deref(t.vm) // table (-2) + value.deref(t.vm) // value (-1) + + // Pop the table and key off + defer ffi.Pop(state, 2) + + // Insert new index and set it to the value + len := ffi.ObjLen(state, -2) + ffi.PushInteger(state, ffi.LuaInteger(len+1)) + ffi.Insert(state, -2) + ffi.SetTable(state, -3) +} + +func (t *LuaTable) Pop() LuaValue { + state := t.vm.state() + + t.deref(t.vm) // table (-1) + + // Pop the table off + defer ffi.Pop(state, 1) + + // Get the last value and nil it out + len := ffi.ObjLen(state, -1) + ffi.PushInteger(state, ffi.LuaInteger(len)) // key (-1), table (-2) + ffi.GetTable(state, -2) // value (-1), table (-2) + val := intoLuaValue(t.vm, -1) + + ffi.PushInteger(state, ffi.LuaInteger(len)) // key (-1), value (-2), table (-3) + ffi.PushNil(state) // nil (-1), key (-2), value (-3), table (-4) + ffi.SetTable(state, -4) + + return val +} + +func (t *LuaTable) RawPush(value LuaValue) { + state := t.vm.state() + + t.deref(t.vm) // table (-2) + value.deref(t.vm) // value (-1) + + // Pop the table off + defer ffi.Pop(state, 1) + + // Insert new index and set it to the value + len := ffi.ObjLen(state, -2) + ffi.PushInteger(state, ffi.LuaInteger(len+1)) // key (-1), value (-2), table (-3) + ffi.Insert(state, -2) // value (-1), key (-2), table (-3) + ffi.RawSet(state, -3) +} + +func (t *LuaTable) RawPop() LuaValue { + state := t.vm.state() + + t.deref(t.vm) // table (-1) + + // Pop the table off + defer ffi.Pop(state, 1) + + // Get the last value and nil it out + len := ffi.ObjLen(state, -1) + ffi.PushInteger(state, ffi.LuaInteger(len)) // key (-1), table (-2) + ffi.RawGet(state, -2) // value (-1), table (-2) + val := intoLuaValue(t.vm, -1) + + ffi.PushInteger(state, ffi.LuaInteger(len)) // key (-1), value (-2), table (-3) + ffi.PushNil(state) // nil (-1), key (-2), value (-3), table (-4) + ffi.RawSet(state, -4) + + return val +} + +func (t *LuaTable) Equals(other LuaValue) bool { + state := t.vm.state() + + // Compare by reference first + otherTable, ok := other.(*LuaTable) + if !ok { + return false + } + if t.index == otherTable.index { + return true + } + + // Compare by value + t.deref(t.vm) // table1 (-2) + otherTable.deref(t.vm) // table2 (-1) + + // Pop off both tables + defer ffi.Pop(state, 2) + + return ffi.Equal(state, -1, -2) +} + +func (t *LuaTable) Clear() { + state := t.vm.state() + + t.deref(t.vm) // table (-1) + + defer ffi.Pop(state, 1) + + // Iterate and nil out all keys + ffi.PushNil(state) + for ffi.Next(state, -2) != 0 { + ffi.Pop(state, 1) + ffi.PushValue(state, -1) + ffi.PushNil(state) + ffi.SetTable(state, -4) + } +} + +func (t *LuaTable) Len() int { + state := t.vm.state() + + t.deref(t.vm) + defer ffi.Pop(state, 1) + + return int(ffi.ObjLen(state, -1)) +} + +func (t *LuaTable) IsEmpty() bool { return t.Len() == 0 } + func (t *LuaTable) Iterable() map[LuaValue]LuaValue { state := t.vm.state() @@ -50,6 +206,33 @@ func (t *LuaTable) Iterable() map[LuaValue]LuaValue { return obj } +func (t *LuaTable) SetMetatable(metatable *LuaTable) { + state := t.vm.state() + + t.deref(t.vm) // table (-2) + metatable.deref(t.vm) // metatable (-1) + + // Set the metatable for the table + ffi.SetMetatable(state, -2) + + // Pop metatable, re-ref the table + ffi.Pop(state, 1) + t.index = int(ffi.Ref(state, -1)) +} + +func (t *LuaTable) GetMetatable() *LuaTable { + state := t.vm.state() + + if ok := ffi.GetMetatable(state, int32(t.index)); ok { + index := ffi.Ref(state, -1) + ffi.Pop(state, 1) + + return &LuaTable{vm: t.vm, index: int(index)} + } + + return nil +} + // // LuaValue implementation // diff --git a/main.go b/main.go index 7a0644f..5e16b66 100644 --- a/main.go +++ b/main.go @@ -13,11 +13,34 @@ func main() { table := state.CreateTable() key, value := state.CreateString("hello"), state.CreateString("lei") - table.Set(key, value) + table.RawSet(key, value) + table.Push(state.CreateString("world")) + + mt, indexMt := state.CreateTable(), state.CreateTable() + indexKey := state.CreateString("hej") + indexMt.Set(indexKey, value) + mt.RawSet(state.CreateString("__index"), indexMt) + + table.SetMetatable(mt) fmt.Printf("Used: %d, Limit: %d\n", mem.Used(), mem.Limit()) - fmt.Println(key.ToString(), table.Get(key).(*lua.LuaString).ToString()) + fmt.Println(key.ToString(), table.RawGet(key).(*lua.LuaString).ToString()) + fmt.Println("key fetched by metatable:", table.Get(indexKey).(*lua.LuaString).ToString()) + fmt.Println("key fetched without metatable:", table.RawGet(indexKey).(*lua.LuaNil)) + fmt.Println("popped value:", table.Pop().(*lua.LuaString).ToString()) + + fmt.Println("len:", table.Len()) + + table.RawPush(state.CreateString("raw")) + fmt.Println("raw popped value:", table.RawPop().(*lua.LuaString).ToString()) + + fmt.Println("equals self:", table.Equals(table)) + fmt.Println("equals other:", table.Equals(indexMt)) + + table.Clear() + fmt.Println("len after clear:", table.Len()) + chunk := state.Load("main", []byte("print('hello, lei!!!!', math.random()); return {['mrrp'] = 'foo', ['meow'] = 'bar'}, 'baz'")) values, returnErr := chunk.Call()