From cea21c535ecdc4d34f27711f1751a6f9e5f66b9e Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Mon, 21 Sep 2026 11:11:29 +0800 Subject: [PATCH] Export reflectx types from reachable state roots --- internal/reflectxtype/method.go | 6 +- internal/reflectxtype/method_cache_test.go | 4 +- internal/reflectxtype/method_missing_test.go | 8 +- internal/reflectxtype/method_shared_test.go | 4 +- internal/reflectxtype/reflectxtype.go | 85 ++++------------- internal/reflectxtype/reflectxtype_test.go | 16 ++-- internal/reflectxtype/roots_test.go | 65 +++++++++++++ internal/reflectxtype/roundtrip_test.go | 21 +++-- internal/reflectxtype/runtime.go | 52 ----------- internal/reflectxtype/struct_test.go | 2 +- internal/state/encode.go | 15 +-- internal/state/reflectx_method_linux_test.go | 3 + internal/state/reflectx_roots_linux_test.go | 98 ++++++++++++++++++++ internal/state/reflectx_test.go | 2 +- 14 files changed, 228 insertions(+), 153 deletions(-) create mode 100644 internal/reflectxtype/roots_test.go create mode 100644 internal/state/reflectx_roots_linux_test.go diff --git a/internal/reflectxtype/method.go b/internal/reflectxtype/method.go index 6690af0..8ecbfb6 100644 --- a/internal/reflectxtype/method.go +++ b/internal/reflectxtype/method.go @@ -79,8 +79,10 @@ func concreteMethodSet(typ reflect.Type) ([]reflectx.Method, []reflect.Value, [] pkg = methodPackage(struct{ bytes *byte }{(*byte)(name)}) } key := identity{method.Name, pkg} - if pointer { - interfaces[key] = methodText(rt, raw[i].ifn) != zeroMethod + // StructOf can promote methods onto T without adding them to *T, + // e.g. struct{ *bytes.Buffer }. Keep an entry from either side. + if methodText(rt, raw[i].ifn) != zeroMethod { + interfaces[key] = true } if pointer { if index, ok := values[key]; ok { diff --git a/internal/reflectxtype/method_cache_test.go b/internal/reflectxtype/method_cache_test.go index 4f8e247..0416614 100644 --- a/internal/reflectxtype/method_cache_test.go +++ b/internal/reflectxtype/method_cache_test.go @@ -74,7 +74,7 @@ func TestMethodCacheIdentity(t *testing.T) { } // Export keeps both types. Only an already-shared method gets one ID. - sent, err := Export() + sent, err := Export(originals[:]) if err != nil { t.Fatal(err) } @@ -151,7 +151,7 @@ func TestMethodCacheIdentity(t *testing.T) { t.Logf("import: %d icall slots; First{10}.Read()=%d, Second{20}.Read()=%d", after-before, results[0], results[1]) // Returning preserves IDs and reuses the original host type identities. - returned, err := guest.Export() + returned, err := guest.Export(nil) if err != nil { t.Fatal(err) } diff --git a/internal/reflectxtype/method_missing_test.go b/internal/reflectxtype/method_missing_test.go index 3dcd13f..c817219 100644 --- a/internal/reflectxtype/method_missing_test.go +++ b/internal/reflectxtype/method_missing_test.go @@ -38,6 +38,9 @@ func TestMissingMethodSignatures(t *testing.T) { } embedded := reflect.StructOf([]reflect.StructField{{Name: "Buffer", Type: pointer, Anonymous: true}}) + if got := reflect.PointerTo(embedded).NumMethod(); got != 0 { + t.Fatalf("value-only method fixture has %d pointer methods", got) + } raw = runtimeMethods((*[2]unsafe.Pointer)(unsafe.Pointer(&embedded))[1]) var missing int for _, method := range raw { @@ -54,7 +57,7 @@ func TestMissingMethodSignatures(t *testing.T) { if missing != stripped { t.Fatalf("StructOf nil signatures: got %d, want %d", missing, stripped) } - methods, functions, _, entries := concreteMethodSet(embedded) + methods, functions, hasInterface, entries := concreteMethodSet(embedded) if len(methods) != len(raw)-missing || len(functions) != len(methods) || len(entries) != len(methods) { t.Fatalf("dynamic method tables: methods=%d functions=%d entries=%d, want %d", len(methods), len(functions), len(entries), len(raw)-missing) } @@ -65,6 +68,9 @@ func TestMissingMethodSignatures(t *testing.T) { if method.Name == "String" { receiver := reflect.New(embedded).Elem() receiver.Field(0).Set(reflect.ValueOf(bytes.NewBufferString("retained method"))) + if _, ok := receiver.Interface().(interface{ String() string }); !ok || !hasInterface[i] { + t.Fatal("value method lost its interface entry when the pointer type has no methods") + } if got := functions[i].Call([]reflect.Value{receiver})[0].String(); got != "retained method" { t.Fatalf("valid method returned %q", got) } diff --git a/internal/reflectxtype/method_shared_test.go b/internal/reflectxtype/method_shared_test.go index 0ee3725..b4923c4 100644 --- a/internal/reflectxtype/method_shared_test.go +++ b/internal/reflectxtype/method_shared_test.go @@ -51,7 +51,7 @@ func TestSharedMethodEntries(t *testing.T) { reflect.SliceOf(typ) originals = append(originals, typ) } - snapshot, err := Export() + snapshot, err := Export(originals) if err != nil { t.Fatal(err) } @@ -134,7 +134,7 @@ func TestSharedMethodEntries(t *testing.T) { entries[method.Name] = current[j] } } - returned, err := table.Export() + returned, err := table.Export(nil) if err != nil { t.Fatal(err) } diff --git a/internal/reflectxtype/reflectxtype.go b/internal/reflectxtype/reflectxtype.go index 3d48e5d..e5b177b 100644 --- a/internal/reflectxtype/reflectxtype.go +++ b/internal/reflectxtype/reflectxtype.go @@ -15,7 +15,6 @@ import ( "reflect" "runtime" "sync" - "unsafe" "github.com/goplus/reflectx" ) @@ -52,40 +51,29 @@ func (t *ReflectType) Resolve(id uint32) (reflect.Type, error) { // construction and Context.Reset calls before starting a transfer. var transferMu sync.Mutex -// Export reads reflectx.Default's type caches and standard reflect caches. -// A type created outside Default can be exposed with reflect.SliceOf(typ), as -// state already does for reflecttype. Cache enumeration is not a whole-process -// registry or an atomic snapshot of external constructor calls. -func Export() (*Snapshot, error) { - return export(nil) +// Export saves roots and their type dependencies, including complete method +// sets. Unrelated cached types and their method environments are not exported. +// An empty root list produces an empty snapshot. +func Export(roots []reflect.Type) (*Snapshot, error) { + return export(nil, roots) } -// Export preserves this import's type and method IDs and appends newly created -// types. For example, a returned Node still has its original snapshot ID even -// when the guest's reflect caches enumerate entries in a different order. -func (t *ReflectType) Export() (*Snapshot, error) { - return export(t) +// Export preserves this import's type and method IDs and appends roots and +// their dependencies. For example, an Added type can refer to an imported +// Node without renumbering Node or exporting unrelated guest-created types. +func (t *ReflectType) Export(roots []reflect.Type) (*Snapshot, error) { + return export(t, roots) } -func export(previous *ReflectType) (*Snapshot, error) { +func export(previous *ReflectType, roots []reflect.Type) (*Snapshot, error) { if runtime.Version() != "go1.26.6" { return nil, fmt.Errorf("reflectxtype requires go1.26.6, got %s", runtime.Version()) } transferMu.Lock() defer transferMu.Unlock() - roots := cachedTypes() - // The leading three fields match reflectx v1.7.8 Context. Retain typed - // references, including both sides of the embedded-method lookup cache. - ctx := (*struct { - embed map[reflect.Type]reflect.Type - structs map[string][]reflect.Type - interfaces map[string]reflect.Type - })(unsafe.Pointer(reflectx.Default)) e := exporter{ ids: make(map[reflect.Type]uint32), - needed: make(map[reflect.Type]bool), - visiting: make(map[reflect.Type]bool), sharedMethods: make(map[methodEntries]int), } if previous != nil { @@ -122,25 +110,12 @@ func export(previous *ReflectType) (*Snapshot, error) { e.entries[i] = entry } } - for from, to := range ctx.embed { - roots = append(roots, from, to) - e.needed[from], e.needed[to] = true, true - } - for _, bucket := range ctx.structs { - for _, typ := range bucket { - roots = append(roots, typ) - e.needed[typ] = true + for i, typ := range roots { + if typ == nil { + return nil, fmt.Errorf("nil reflectx root type at index %d", i) } - } - for _, typ := range ctx.interfaces { - roots = append(roots, typ) - e.needed[typ] = true - } - for _, typ := range roots { - if e.requiresReflectx(typ) { - if _, err := e.intern(typ); err != nil { - return nil, err - } + if _, err := e.intern(typ); err != nil { + return nil, err } } data := binary.AppendUvarint(nil, uint64(len(e.entries))) @@ -159,39 +134,11 @@ const concreteMethods = 1 << 9 type exporter struct { ids map[reflect.Type]uint32 entries [][]byte - needed map[reflect.Type]bool - visiting map[reflect.Type]bool methods []reflect.Value retainedMethods map[reflect.Type][]method sharedMethods map[methodEntries]int } -func (e *exporter) requiresReflectx(typ reflect.Type) bool { - if builtinTypes[typ.Kind()] == typ { - return false - } - if _, ok := staticTypes().byType[typ]; ok { - return false - } - if need, ok := e.needed[typ]; ok { - return need - } - if typ.Name() != "" || typ.Kind() == reflect.Interface || typ.Kind() == reflect.Struct || reflectx.NumMethodX(typ) != 0 || e.visiting[typ] { - e.needed[typ] = true - return true - } - e.visiting[typ] = true - need := false - for _, child := range appendDependencies(nil, typ) { - if e.requiresReflectx(child) { - need = true - } - } - delete(e.visiting, typ) - e.needed[typ] = need - return need -} - func (e *exporter) intern(typ reflect.Type) (uint32, error) { if id, ok := e.ids[typ]; ok { return id, nil diff --git a/internal/reflectxtype/reflectxtype_test.go b/internal/reflectxtype/reflectxtype_test.go index 203ab05..24765d3 100644 --- a/internal/reflectxtype/reflectxtype_test.go +++ b/internal/reflectxtype/reflectxtype_test.go @@ -143,7 +143,7 @@ func assertType(t *testing.T, want, got reflect.Type, seen map[reflect.Type]refl func TestRoundTrip(t *testing.T) { want := sampleTypes() - snapshot, err := Export() + snapshot, err := Export(want) if err != nil { t.Fatal(err) } @@ -155,7 +155,7 @@ func TestRoundTrip(t *testing.T) { for _, typ := range want { id := snapshot.IDs[typ] if id == 0 { - t.Fatalf("missing cached type %v", typ) + t.Fatalf("missing root type %v", typ) } value, err := got.Resolve(id) if err != nil { @@ -174,7 +174,7 @@ func TestRoundTrip(t *testing.T) { if _, err := got.Resolve(uint32(len(got.types)) + 1); err == nil { t.Fatal("accepted unknown ID") } - second, err := Export() + second, err := Export([]reflect.Type{seen[want[0]]}) if err != nil { t.Fatal(err) } @@ -227,7 +227,7 @@ func TestFreshProcess(t *testing.T) { return } node := sampleTypes()[0] - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{node}) if err != nil { t.Fatal(err) } @@ -243,7 +243,7 @@ func TestFreshProcess(t *testing.T) { func TestDefaultCache(t *testing.T) { typ := reflectx.InterfaceOf(nil, []reflect.Method{{Name: "DefaultCache", Type: reflect.TypeFor[func()]()}}) - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } @@ -274,7 +274,7 @@ func TestRecursiveStorage(t *testing.T) { for _, typ := range want { reflect.SliceOf(typ) } - snapshot, err := Export() + snapshot, err := Export(want) if err != nil { t.Fatal(err) } @@ -313,7 +313,7 @@ func TestRecursiveStorage(t *testing.T) { func TestConcurrentOpen(t *testing.T) { typ := sampleTypes()[0] - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } @@ -348,7 +348,7 @@ func TestConcreteMethods(t *testing.T) { t.Fatal(err) } reflect.SliceOf(typ) - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } diff --git a/internal/reflectxtype/roots_test.go b/internal/reflectxtype/roots_test.go new file mode 100644 index 0000000..35fe94b --- /dev/null +++ b/internal/reflectxtype/roots_test.go @@ -0,0 +1,65 @@ +package reflectxtype + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "github.com/goplus/reflectx" +) + +func TestExportRoots(t *testing.T) { + dependency := reflectx.NamedTypeOf("example/roots", "Value", reflect.TypeFor[int]()) + unrelated := reflectx.NamedTypeOf("example/roots", "Value", reflect.TypeFor[int]()) + reflect.SliceOf(unrelated) + signature := reflect.FuncOf([]reflect.Type{dependency}, []reflect.Type{dependency}, false) + root := reflectx.InterfaceOf(nil, []reflect.Method{{Name: "Use", Type: signature}}) + snapshot, err := Export([]reflect.Type{root, root}) + if err != nil { + t.Fatal(err) + } + if snapshot.IDs[root] == 0 || snapshot.IDs[signature] == 0 || snapshot.IDs[dependency] == 0 { + t.Fatal("root lost its method signature or type dependency") + } + if snapshot.IDs[unrelated] != 0 { + t.Fatal("export included an unrelated cached type with the same name") + } + single, err := Export([]reflect.Type{root}) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(single.Data, snapshot.Data) { + t.Fatal("repeated roots duplicated type records") + } + table, err := Open(snapshot.Data) + if err != nil { + t.Fatal(err) + } + got, err := table.Resolve(snapshot.IDs[root]) + if err != nil { + t.Fatal(err) + } + dep, err := table.Resolve(snapshot.IDs[dependency]) + if err != nil { + t.Fatal(err) + } + method := got.Method(0) + if method.Name != "Use" || method.Type.In(0) != dep || method.Type.Out(0) != dep { + t.Fatal("restored interface lost its signature dependency") + } + + empty, err := Export(nil) + if err != nil { + t.Fatal(err) + } + if len(empty.IDs) != 0 || len(empty.Methods) != 0 { + t.Fatal("empty roots exported cached types or methods") + } + if _, err := Open(empty.Data); err != nil { + t.Fatal(err) + } + if _, err := Export([]reflect.Type{nil}); err == nil || !strings.Contains(err.Error(), "nil reflectx root type") { + t.Fatalf("nil root: %v", err) + } +} diff --git a/internal/reflectxtype/roundtrip_test.go b/internal/reflectxtype/roundtrip_test.go index 2be4892..0bbd564 100644 --- a/internal/reflectxtype/roundtrip_test.go +++ b/internal/reflectxtype/roundtrip_test.go @@ -14,7 +14,7 @@ import ( func TestRetainedTypeIDs(t *testing.T) { original := sampleTypes() - sent, err := Export() + sent, err := Export(original) if err != nil { t.Fatal(err) } @@ -28,7 +28,9 @@ func TestRetainedTypeIDs(t *testing.T) { } added := reflectx.NamedTypeOf("example/return", "Added", reflect.SliceOf(node)) reflect.SliceOf(added) - returned, err := guest.Export() + unrelated := reflectx.NamedTypeOf("example/return", "Unrelated", reflect.TypeFor[int]()) + reflect.SliceOf(unrelated) + returned, err := guest.Export([]reflect.Type{added}) if err != nil { t.Fatal(err) } @@ -41,6 +43,9 @@ func TestRetainedTypeIDs(t *testing.T) { if returned.IDs[added] <= uint32(len(sent.IDs)) { t.Fatal("new type reused a retained ID") } + if returned.IDs[unrelated] != 0 { + t.Fatal("return exported an unrelated cached type") + } host, err := sent.Open(returned.Data) if err != nil { t.Fatal(err) @@ -55,7 +60,7 @@ func TestRetainedTypeIDs(t *testing.T) { if err != nil || got == added || got.Elem() != original[0] { t.Fatalf("new type did not resolve its retained dependency: %v, %v", got, err) } - again, err := host.Export() + again, err := host.Export(nil) if err != nil { t.Fatal(err) } @@ -71,7 +76,7 @@ func TestRetainedTypeIDs(t *testing.T) { func TestRetainedTypeDefinitionChanged(t *testing.T) { typ := reflectx.NamedTypeOf("example/retained", "OriginalName", reflect.TypeFor[int]()) reflect.SliceOf(typ) - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } @@ -118,7 +123,7 @@ func TestRetainedMethodIDs(t *testing.T) { t.Fatal(err) } reflect.SliceOf(typ) - sent, err := Export() + sent, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } @@ -150,7 +155,7 @@ func TestRetainedMethodIDs(t *testing.T) { if current[1].Name != "execWith" || current[1].PkgPath != "example/b" { t.Fatalf("fixture did not reorder methods: %v", current) } - returned, err := guest.Export() + returned, err := guest.Export(nil) if err != nil { t.Fatal(err) } @@ -192,7 +197,7 @@ func TestMethodInterfacePolicy(t *testing.T) { t.Fatal(err) } reflect.SliceOf(typ) - snapshot, err := Export() + snapshot, err := Export([]reflect.Type{typ}) if err != nil { t.Fatal(err) } @@ -227,7 +232,7 @@ func TestMethodInterfacePolicy(t *testing.T) { if got := method.Func.Call([]reflect.Value{value})[0].Int(); got != 42 { t.Fatalf("reflection method returned %d, want 42", got) } - if _, err := table.Export(); err != nil { + if _, err := table.Export(nil); err != nil { t.Fatalf("interface policy changed during return: %v", err) } } diff --git a/internal/reflectxtype/runtime.go b/internal/reflectxtype/runtime.go index 0f56168..7e5278e 100644 --- a/internal/reflectxtype/runtime.go +++ b/internal/reflectxtype/runtime.go @@ -115,55 +115,3 @@ var builtinTypes = [...]reflect.Type{ reflect.Complex64: reflect.TypeFor[complex64](), reflect.Complex128: reflect.TypeFor[complex128](), reflect.String: reflect.TypeFor[string](), reflect.UnsafePointer: reflect.TypeFor[unsafe.Pointer](), } - -// These layouts match Go 1.26.6. The two bucket caches are append-only; readers -// use sync.Map.Range without taking the writers' mutexes. -type cacheLayout struct { - mu sync.Mutex - m sync.Map -} - -//go:linkname pointerCache reflect.ptrMap -var pointerCache sync.Map - -//go:linkname compositeCache reflect.lookupCache -var compositeCache sync.Map - -//go:linkname functionCache reflect.funcLookupCache -var functionCache cacheLayout - -//go:linkname structCache reflect.structLookupCache -var structCache cacheLayout - -func cachedTypes() []reflect.Type { - seen := make(map[reflect.Type]bool) - var result []reflect.Type - add := func(typ reflect.Type) { - if !seen[typ] { - seen[typ] = true - result = append(result, typ) - } - } - pointerCache.Range(func(_, value any) bool { - add(reflectToType(reflect.ValueOf(value).UnsafePointer())) - return true - }) - compositeCache.Range(func(_, value any) bool { - add(value.(reflect.Type)) - return true - }) - functionCache.m.Range(func(_, value any) bool { - bucket := reflect.ValueOf(value) - for i := 0; i < bucket.Len(); i++ { - add(reflectToType(bucket.Index(i).UnsafePointer())) - } - return true - }) - structCache.m.Range(func(_, value any) bool { - for _, typ := range value.([]reflect.Type) { - add(typ) - } - return true - }) - return result -} diff --git a/internal/reflectxtype/struct_test.go b/internal/reflectxtype/struct_test.go index b272e0b..f03ea6b 100644 --- a/internal/reflectxtype/struct_test.go +++ b/internal/reflectxtype/struct_test.go @@ -71,7 +71,7 @@ func TestStructTypeIsolation(t *testing.T) { t.Fatal("pointer lost its element identity") } } - returned, err := guest.Export() + returned, err := guest.Export(nil) if err != nil { t.Fatal(err) } diff --git a/internal/state/encode.go b/internal/state/encode.go index fc28680..f42463b 100644 --- a/internal/state/encode.go +++ b/internal/state/encode.go @@ -637,7 +637,7 @@ func (es *encodeState) findType(typ reflect.Type) typeSpec { return ref } // Static types need not occur in reflect's caches. SliceOf always caches - // its result, making typ a dependency discoverable by parameterless Export. + // its result, making typ a dependency discoverable by reflecttype.Export. // For example, this includes a named struct passed only as a reflect.Type. reflect.SliceOf(typ) if es.reflected == nil { @@ -926,20 +926,21 @@ func (es *encodeState) Save(obj reflect.Value) { if err != nil { Failf("export reflect types: %w", err) } - needExtended := extended != nil + var roots []reflect.Type for typ := range es.reflected { if snapshot.IDs[typ] == 0 { - needExtended = true - break + roots = append(roots, typ) } } - if !needExtended { + if len(roots) == 0 && extended == nil { break } + // Only this graph's types may add method environments. A global + // cache can also contain types owned by unrelated interpreters. if es.reflectx != nil { - extended, err = es.reflectx.Export() + extended, err = es.reflectx.Export(roots) } else { - extended, err = reflectxtype.Export() + extended, err = reflectxtype.Export(roots) } if err != nil { Failf("export reflectx types: %w", err) diff --git a/internal/state/reflectx_method_linux_test.go b/internal/state/reflectx_method_linux_test.go index 12ae8e4..4423f3d 100644 --- a/internal/state/reflectx_method_linux_test.go +++ b/internal/state/reflectx_method_linux_test.go @@ -176,6 +176,9 @@ func TestReflectxNativeMethodProcess(t *testing.T) { embed := func(receiver any, tag reflect.StructTag) reflect.Value { value := reflect.ValueOf(receiver) typ := reflect.StructOf([]reflect.StructField{{Name: value.Type().Elem().Name(), Type: value.Type(), Anonymous: true, Tag: tag}}) + // A dynamic name makes the promoted native methods part of the + // reflectxtype snapshot exercised by this fixture. + reflectx.SetTypeName(typ, "example/native", "Embedded"+value.Type().Elem().Name()) result := reflect.New(typ).Elem() result.Field(0).Set(value) return result diff --git a/internal/state/reflectx_roots_linux_test.go b/internal/state/reflectx_roots_linux_test.go new file mode 100644 index 0000000..a18d2cf --- /dev/null +++ b/internal/state/reflectx_roots_linux_test.go @@ -0,0 +1,98 @@ +//go:build linux && (amd64 || arm64) + +package state + +import ( + "context" + "reflect" + "testing" + + "github.com/goplus/reflectx" +) + +func TestReflectxMethodRoots(t *testing.T) { + for _, referenced := range []bool{false, true} { + name := "unrelated" + if referenced { + name = "referenced_by_method" + } + t.Run(name, func(t *testing.T) { + methodContext := reflectx.NewContext() + t.Cleanup(methodContext.Reset) + foreignCounter := 20 + foreignType := methodContext.NewMethodSet(reflectx.NamedTypeOf("example/roots", "Peer", reflect.TypeFor[int]()), 0, 1) + foreignMethod := reflectx.MakeMethod("Number", "", true, reflect.TypeFor[func() int](), func([]reflect.Value) []reflect.Value { + foreignCounter++ + return []reflect.Value{reflect.ValueOf(foreignCounter)} + }) + if err := methodContext.SetMethodSet(foreignType, []reflectx.Method{foreignMethod}, false); err != nil { + t.Fatal(err) + } + // Populate the global cache in both cases. Only the second case + // gives the root method an actual reference to this other type. + reflect.SliceOf(foreignType) + var other any + if referenced { + other = reflect.New(foreignType).Interface() + } + counter := 10 + typ := methodContext.NewMethodSet(reflectx.NamedTypeOf("example/roots", "Root", reflect.TypeFor[int]()), 0, 1) + method := reflectx.MakeMethod("Number", "", true, reflect.TypeFor[func() int](), func([]reflect.Value) []reflect.Value { + counter++ + n := counter + if other != nil { + n += other.(interface{ Number() int }).Number() + } + return []reflect.Value{reflect.ValueOf(n)} + }) + if err := methodContext.SetMethodSet(typ, []reflectx.Method{method}, false); err != nil { + t.Fatal(err) + } + type root struct { + Type reflect.Type + Value any + Counter *int + } + src := root{typ, reflect.New(typ).Interface(), &counter} + original := src.Value + var host, guest State + mem := make([]byte, 1<<20) + ctx := context.Background() + n, _, err := host.Save(ctx, mem, &src) + if err != nil { + t.Fatal(err) + } + snapshot := host.saved.reflectxSnapshot + if snapshot == nil || (snapshot.IDs[foreignType] != 0) != referenced { + t.Fatal("method roots did not distinguish a reference from a cached type") + } + wantMethods, wantNumber, wantForeign := 1, 11, 20 + if referenced { + wantMethods, wantNumber, wantForeign = 2, 32, 21 + } + if len(snapshot.Methods) != wantMethods { + t.Fatalf("method count: got %d, want %d", len(snapshot.Methods), wantMethods) + } + var dst root + if _, err := guest.Load(ctx, mem[:n], &dst); err != nil { + t.Fatal(err) + } + if got := dst.Value.(interface{ Number() int }).Number(); got != wantNumber { + t.Fatalf("restored method: got %d, want %d", got, wantNumber) + } + if *dst.Counter != 11 || counter != 10 || foreignCounter != 20 { + t.Fatal("method captures lost their aliases or modified host storage") + } + n, _, err = guest.Save(ctx, mem, &dst) + if err != nil { + t.Fatal(err) + } + if _, err := host.Load(ctx, mem[:n], &src); err != nil { + t.Fatal(err) + } + if src.Type != typ || src.Value != original || src.Counter != &counter || counter != 11 || foreignCounter != wantForeign { + t.Fatal("return lost type identity, object identity or method capture writes") + } + }) + } +} diff --git a/internal/state/reflectx_test.go b/internal/state/reflectx_test.go index 58acedf..9bfe1b8 100644 --- a/internal/state/reflectx_test.go +++ b/internal/state/reflectx_test.go @@ -210,7 +210,7 @@ func TestReflectxStateInterfaceAndFields(t *testing.T) { func TestReflectxStateInvalidReference(t *testing.T) { src := reflectxFixture().Type reflect.SliceOf(src) - snapshot, err := reflectxtype.Export() + snapshot, err := reflectxtype.Export([]reflect.Type{src}) if err != nil { t.Fatal(err) }