From afa28af4e001841f9dc70893d1254c5569862103 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:06:48 +0800 Subject: [PATCH] lua: defer read-only slice element bindings until first access --- common/lua/luar.go | 19 +++++++++++++++- common/lua/luar_test.go | 50 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/common/lua/luar.go b/common/lua/luar.go index 1bfbce7c3..53a8f10b9 100644 --- a/common/lua/luar.go +++ b/common/lua/luar.go @@ -28,6 +28,7 @@ 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. +// Element bindings are initialized on first indexing or iteration. func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) { sliceMetatable := luar.MT(L, []T{}) if sliceMetatable.RawGetString("__readonly") != glua.LTrue { @@ -66,7 +67,23 @@ func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) { }) visit(reflect.TypeFor[net.IPMask]()) } - visit(typ.Elem()) + if element := typ.Elem(); element.Kind() == reflect.Slice { + index := metatable.RawGetString("__index").(*glua.LFunction) + call := metatable.RawGetString("__call").(*glua.LFunction) + initialize := func() { + visit(element) + metatable.RawSetString("__index", index) + metatable.RawSetString("__call", call) + } + metatable.RawSetString("__index", L.NewFunction(func(L *glua.LState) int { + initialize() + return index.GFunction(L) + })) + metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int { + initialize() + return call.GFunction(L) + })) + } } visit(reflect.TypeFor[S]()) } diff --git a/common/lua/luar_test.go b/common/lua/luar_test.go index 64ba13624..15ba4c4dd 100644 --- a/common/lua/luar_test.go +++ b/common/lua/luar_test.go @@ -2,6 +2,7 @@ package lua import ( "net" + "reflect" "testing" glua "github.com/yuin/gopher-lua" @@ -117,6 +118,53 @@ end } } +func TestReadOnlyIPSlicePusherLazy(t *testing.T) { + for _, tc := range []struct{ name, access string }{ + {"index", `local ip = ips[1]`}, + {"iterate", `local _, ip = ips()()`}, + {"coroutine", `local co = coroutine.create(function() return ips[1] end); local ok, ip = coroutine.resume(co); assert(ok)`}, + } { + t.Run(tc.name, func(t *testing.T) { + L := glua.NewState() + defer L.Close() + methods := 0 + luar.GetConfig(L).MethodNames = func(_ reflect.Type, method reflect.Method) []string { + methods++ + return []string{method.Name} + } + NewReadOnlySlicePusher[net.IP](L)(L, []net.IP{{192, 0, 2, 1}}) + L.SetGlobal("ips", L.Get(-1)) + L.Pop(1) + if err := L.DoString(`assert(#ips == 1); assert(not pcall(function() ips[1] = nil end))`); err != nil { + t.Fatal(err) + } + if methods != 0 { + t.Fatal("IP methods initialized without reading an element") + } + source := tc.access + ` +assert(ip:String() == "192.0.2.1" and ip.AppendText == nil) +assert(not pcall(function() ip[1] = 0 end)) +assert(not pcall(function() ip:To4()[1] = 0 end)) +assert(not pcall(function() ip:To16()[1] = 0 end)) +assert(not pcall(function() ip:DefaultMask()[1] = 0 end)) +` + if err := L.DoString(source); err != nil { + t.Fatal(err) + } + initialized := methods + if initialized == 0 { + t.Fatal("IP access did not initialize its methods") + } + if err := L.DoString(source); err != nil { + t.Fatal(err) + } + if methods != initialized { + t.Fatal("IP methods initialized more than once") + } + }) + } +} + func TestReadOnlyNestedSlicePusher(t *testing.T) { L := glua.NewState() defer L.Close() @@ -128,7 +176,6 @@ func TestReadOnlyNestedSlicePusher(t *testing.T) { NewReadOnlySlicePusher[[]net.IP](L)(L, ips) L.SetGlobal("ips", L.Get(-1)) L.Pop(1) - readOnly := luar.MT(L, ips[0]).RawGetString("__newindex") if err := L.DoString(` local function readOnly(value) assert(not pcall(function() value[1] = value[1] end)) @@ -152,6 +199,7 @@ assert(ip.AppendText == nil and ip.appendText == nil) `); err != nil { t.Fatal(err) } + readOnly := luar.MT(L, ips[0]).RawGetString("__newindex") NewReadOnlySlicePusher[net.IP](L) if luar.MT(L, ips[0]).RawGetString("__newindex") != readOnly { t.Fatal("nested slice metatable was initialized again")