diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index d21ef7ca0..5d03e8539 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -292,6 +292,14 @@ func makeTypeHasher(t reflect.Type) typeHasherFunc { return hashAddr } + // Types that implement their own hashing. + if t.Kind() != reflect.Pointer && t.Kind() != reflect.Interface { + // A method can be implemented on either the value receiver or pointer receiver. + if t.Implements(selfHasherType) || reflect.PointerTo(t).Implements(selfHasherType) { + return makeSelfHasherImpl(t) + } + } + // Types that can have their memory representation directly hashed. if typeIsMemHashable(t) { return makeMemHasher(t.Size()) @@ -350,6 +358,13 @@ func hashAddr(h *hasher, p pointer) { } } +func makeSelfHasherImpl(t reflect.Type) typeHasherFunc { + return func(h *hasher, p pointer) { + e := p.asValue(t) + e.Interface().(SelfHasher).Hash(&h.Block512) + } +} + func hashString(h *hasher, p pointer) { s := *p.asString() h.HashUint64(uint64(len(s))) diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 5868c051a..05efa451f 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -29,6 +29,7 @@ import ( "tailscale.com/types/ptr" "tailscale.com/util/deephash/testtype" "tailscale.com/util/dnsname" + "tailscale.com/util/hashx" "tailscale.com/version" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" @@ -41,6 +42,14 @@ func (p appendBytes) AppendTo(b []byte) []byte { return append(b, p...) } +type implsSelfHasherValueRecv struct { + emit uint64 +} + +func (s implsSelfHasherValueRecv) Hash(h *hashx.Block512) { + h.HashUint64(s.emit) +} + func TestHash(t *testing.T) { type tuple [2]any type iface struct{ X any } @@ -169,6 +178,12 @@ func TestHash(t *testing.T) { b[0] = 1 return b }()))}, wantEq: false}, + {in: tuple{&implsSelfHasher{}, &implsSelfHasher{}}, wantEq: true}, + {in: tuple{(*implsSelfHasher)(nil), (*implsSelfHasher)(nil)}, wantEq: true}, + {in: tuple{(*implsSelfHasher)(nil), &implsSelfHasher{}}, wantEq: false}, + {in: tuple{&implsSelfHasher{emit: 1}, &implsSelfHasher{emit: 2}}, wantEq: false}, + {in: tuple{implsSelfHasherValueRecv{emit: 1}, implsSelfHasherValueRecv{emit: 2}}, wantEq: false}, + {in: tuple{implsSelfHasherValueRecv{emit: 2}, implsSelfHasherValueRecv{emit: 2}}, wantEq: true}, } for _, tt := range tests { diff --git a/util/deephash/types.go b/util/deephash/types.go index 76fe43c76..5a2045dcc 100644 --- a/util/deephash/types.go +++ b/util/deephash/types.go @@ -7,11 +7,25 @@ import ( "net/netip" "reflect" "time" + + "tailscale.com/util/hashx" ) +// SelfHasher is the interface implemented by types that can compute their own hash +// by writing values through the given parameter. +// +// Implementations of Hash MUST NOT call `Reset` or `Sum` on the provided argument. +// +// This interface should not be considered stable and is likely to change in the +// future. +type SelfHasher interface { + Hash(*hashx.Block512) +} + var ( - timeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem() - netipAddrType = reflect.TypeOf((*netip.Addr)(nil)).Elem() + timeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem() + netipAddrType = reflect.TypeOf((*netip.Addr)(nil)).Elem() + selfHasherType = reflect.TypeOf((*SelfHasher)(nil)).Elem() ) // typeIsSpecialized reports whether this type has specialized hashing. @@ -21,6 +35,11 @@ func typeIsSpecialized(t reflect.Type) bool { case timeTimeType, netipAddrType: return true default: + if t.Kind() != reflect.Pointer && t.Kind() != reflect.Interface { + if t.Implements(selfHasherType) || reflect.PointerTo(t).Implements(selfHasherType) { + return true + } + } return false } } diff --git a/util/deephash/types_test.go b/util/deephash/types_test.go index 88d9cc373..cce5b52a5 100644 --- a/util/deephash/types_test.go +++ b/util/deephash/types_test.go @@ -12,8 +12,17 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/structs" + "tailscale.com/util/hashx" ) +type implsSelfHasher struct { + emit uint64 +} + +func (s *implsSelfHasher) Hash(h *hashx.Block512) { + h.HashUint64(s.emit) +} + func TestTypeIsMemHashable(t *testing.T) { tests := []struct { val any @@ -67,6 +76,7 @@ func TestTypeIsMemHashable(t *testing.T) { false}, {[0]chan bool{}, true}, {struct{ f [0]func() }{}, true}, + {&implsSelfHasher{}, false}, } for _, tt := range tests { got := typeIsMemHashable(reflect.TypeOf(tt.val)) @@ -102,6 +112,7 @@ func TestTypeIsRecursive(t *testing.T) { {val: unsafe.Pointer(nil), want: false}, {val: make(RecursiveChan), want: true}, {val: make(chan int), want: false}, + {val: (*implsSelfHasher)(nil), want: false}, } for _, tt := range tests { got := typeIsRecursive(reflect.TypeOf(tt.val))