Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions internal/reflectxtype/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions internal/reflectxtype/method_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 7 additions & 1 deletion internal/reflectxtype/method_missing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/reflectxtype/method_shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
85 changes: 16 additions & 69 deletions internal/reflectxtype/reflectxtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"reflect"
"runtime"
"sync"
"unsafe"

"github.com/goplus/reflectx"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)))
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions internal/reflectxtype/reflectxtype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
65 changes: 65 additions & 0 deletions internal/reflectxtype/roots_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading