cmd/viewer, types/views, util/codegen: add viewer support for custom container types

This adds support for container-like types such as Container[T] that
don't explicitly specify a view type for T. Instead, a package implementing
a container type should also implement and export a ContainerView[T, V] type
and a ContainerViewOf(*Container[T]) ContainerView[T, V] function, which
returns a view for the specified container, inferring the element view type V
from the element type T.

Updates #12736

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2024-07-14 11:45:55 -05:00
committed by Nick Khyl
parent e7bf6e716b
commit 20562a4fb9
7 changed files with 544 additions and 5 deletions

View File

@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"maps"
"reflect"
"slices"
"go4.org/mem"
@@ -111,6 +112,13 @@ type StructView[T any] interface {
AsStruct() T
}
// Cloner is any type that has a Clone function returning a deep-clone of the receiver.
type Cloner[T any] interface {
// Clone returns a deep-clone of the receiver.
// It returns nil, when the receiver is nil.
Clone() T
}
// ViewCloner is any type that has had View and Clone funcs generated using
// tailscale.com/cmd/viewer.
type ViewCloner[T any, V StructView[T]] interface {
@@ -555,3 +563,46 @@ func (m MapFn[K, T, V]) Range(f MapRangeFn[K, V]) {
}
}
}
// ContainsPointers reports whether T contains any pointers,
// either explicitly or implicitly.
// It has special handling for some types that contain pointers
// that we know are free from memory aliasing/mutation concerns.
func ContainsPointers[T any]() bool {
return containsPointers(reflect.TypeFor[T]())
}
func containsPointers(typ reflect.Type) bool {
switch typ.Kind() {
case reflect.Pointer, reflect.UnsafePointer:
return true
case reflect.Chan, reflect.Map, reflect.Slice:
return true
case reflect.Array:
return containsPointers(typ.Elem())
case reflect.Interface, reflect.Func:
return true // err on the safe side.
case reflect.Struct:
if isWellKnownImmutableStruct(typ) {
return false
}
for i := range typ.NumField() {
if containsPointers(typ.Field(i).Type) {
return true
}
}
}
return false
}
func isWellKnownImmutableStruct(typ reflect.Type) bool {
switch typ.String() {
case "time.Time":
// time.Time contains a pointer that does not need copying
return true
case "netip.Addr", "netip.Prefix", "netip.AddrPort":
return true
default:
return false
}
}

View File

@@ -10,6 +10,7 @@ import (
"reflect"
"strings"
"testing"
"unsafe"
qt "github.com/frankban/quicktest"
)
@@ -22,6 +23,16 @@ type viewStruct struct {
StringsPtr *Slice[string] `json:",omitempty"`
}
type noPtrStruct struct {
Int int
Str string
}
type withPtrStruct struct {
Int int
StrPtr *string
}
func BenchmarkSliceIteration(b *testing.B) {
var data []viewStruct
for i := range 10000 {
@@ -189,3 +200,215 @@ func TestSliceMapKey(t *testing.T) {
}
}
}
func TestContainsPointers(t *testing.T) {
tests := []struct {
name string
typ reflect.Type
wantPtrs bool
}{
{
name: "bool",
typ: reflect.TypeFor[bool](),
wantPtrs: false,
},
{
name: "int",
typ: reflect.TypeFor[int](),
wantPtrs: false,
},
{
name: "int8",
typ: reflect.TypeFor[int8](),
wantPtrs: false,
},
{
name: "int16",
typ: reflect.TypeFor[int16](),
wantPtrs: false,
},
{
name: "int32",
typ: reflect.TypeFor[int32](),
wantPtrs: false,
},
{
name: "int64",
typ: reflect.TypeFor[int64](),
wantPtrs: false,
},
{
name: "uint",
typ: reflect.TypeFor[uint](),
wantPtrs: false,
},
{
name: "uint8",
typ: reflect.TypeFor[uint8](),
wantPtrs: false,
},
{
name: "uint16",
typ: reflect.TypeFor[uint16](),
wantPtrs: false,
},
{
name: "uint32",
typ: reflect.TypeFor[uint32](),
wantPtrs: false,
},
{
name: "uint64",
typ: reflect.TypeFor[uint64](),
wantPtrs: false,
},
{
name: "uintptr",
typ: reflect.TypeFor[uintptr](),
wantPtrs: false,
},
{
name: "string",
typ: reflect.TypeFor[string](),
wantPtrs: false,
},
{
name: "float32",
typ: reflect.TypeFor[float32](),
wantPtrs: false,
},
{
name: "float64",
typ: reflect.TypeFor[float64](),
wantPtrs: false,
},
{
name: "complex64",
typ: reflect.TypeFor[complex64](),
wantPtrs: false,
},
{
name: "complex128",
typ: reflect.TypeFor[complex128](),
wantPtrs: false,
},
{
name: "netip-Addr",
typ: reflect.TypeFor[netip.Addr](),
wantPtrs: false,
},
{
name: "netip-Prefix",
typ: reflect.TypeFor[netip.Prefix](),
wantPtrs: false,
},
{
name: "netip-AddrPort",
typ: reflect.TypeFor[netip.AddrPort](),
wantPtrs: false,
},
{
name: "bool-ptr",
typ: reflect.TypeFor[*bool](),
wantPtrs: true,
},
{
name: "string-ptr",
typ: reflect.TypeFor[*string](),
wantPtrs: true,
},
{
name: "netip-Addr-ptr",
typ: reflect.TypeFor[*netip.Addr](),
wantPtrs: true,
},
{
name: "unsafe-ptr",
typ: reflect.TypeFor[unsafe.Pointer](),
wantPtrs: true,
},
{
name: "no-ptr-struct",
typ: reflect.TypeFor[noPtrStruct](),
wantPtrs: false,
},
{
name: "ptr-struct",
typ: reflect.TypeFor[withPtrStruct](),
wantPtrs: true,
},
{
name: "string-array",
typ: reflect.TypeFor[[5]string](),
wantPtrs: false,
},
{
name: "int-ptr-array",
typ: reflect.TypeFor[[5]*int](),
wantPtrs: true,
},
{
name: "no-ptr-struct-array",
typ: reflect.TypeFor[[5]noPtrStruct](),
wantPtrs: false,
},
{
name: "with-ptr-struct-array",
typ: reflect.TypeFor[[5]withPtrStruct](),
wantPtrs: true,
},
{
name: "string-slice",
typ: reflect.TypeFor[[]string](),
wantPtrs: true,
},
{
name: "int-ptr-slice",
typ: reflect.TypeFor[[]int](),
wantPtrs: true,
},
{
name: "no-ptr-struct-slice",
typ: reflect.TypeFor[[]noPtrStruct](),
wantPtrs: true,
},
{
name: "string-map",
typ: reflect.TypeFor[map[string]string](),
wantPtrs: true,
},
{
name: "int-map",
typ: reflect.TypeFor[map[int]int](),
wantPtrs: true,
},
{
name: "no-ptr-struct-map",
typ: reflect.TypeFor[map[string]noPtrStruct](),
wantPtrs: true,
},
{
name: "chan",
typ: reflect.TypeFor[chan int](),
wantPtrs: true,
},
{
name: "func",
typ: reflect.TypeFor[func()](),
wantPtrs: true,
},
{
name: "interface",
typ: reflect.TypeFor[any](),
wantPtrs: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotPtrs := containsPointers(tt.typ); gotPtrs != tt.wantPtrs {
t.Errorf("got %v; want %v", gotPtrs, tt.wantPtrs)
}
})
}
}