lua: add recursive read-only bindings for borrowed slices

This commit is contained in:
Meo597
2026-10-15 06:36:12 +08:00
parent ef4f4888df
commit b408f7f0c7
5 changed files with 187 additions and 3 deletions
+49
View File
@@ -1,6 +1,9 @@
package lua
import (
"net"
"reflect"
glua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
)
@@ -22,6 +25,52 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
}
}
// NewReadOnlySlicePusher borrows slices without copying and recursively makes
// nested slice types read-only through their shared metatables. IP byte views
// and masks are also read-only. Other referenced elements retain their mutability.
func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
sliceMetatable := luar.MT(L, []T{})
if sliceMetatable.RawGetString("__readonly") != glua.LTrue {
readOnly := L.NewFunction(func(L *glua.LState) int {
L.RaiseError("slice is read-only")
return 0
})
setSliceReadOnly[[]T](L, readOnly)
}
return NewSlicePusher[T](L)
}
func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) {
var visit func(reflect.Type)
visit = func(typ reflect.Type) {
if typ.Kind() != reflect.Slice {
return
}
metatable := luar.MT(L, reflect.Zero(typ).Interface())
if metatable.RawGetString("__readonly") == glua.LTrue {
return
}
// Mark before descending because named slice types can be recursive.
metatable.RawSetString("__readonly", glua.LTrue)
metatable.RawSetString("__newindex", readOnly)
// Reject explicitly so Lua cannot fall back to the other operand.
metatable.RawSetString("__add", readOnly)
if typ == reflect.TypeFor[net.IP]() {
// A nil receiver lets AppendText return its argument as a writable []byte.
methods := metatable.RawGetString("methods").(*glua.LTable)
appendText := methods.RawGetString("AppendText")
methods.ForEach(func(key, method glua.LValue) {
if method == appendText {
methods.RawSet(key, glua.LNil)
}
})
visit(reflect.TypeFor[net.IPMask]())
}
visit(typ.Elem())
}
visit(reflect.TypeFor[S]())
}
// DirectMethod handles a Lua call without luar's reflected method invocation.
// It returns the result count and whether it handled the arguments. On false,
// it must leave the stack unchanged for the original luar wrapper.