From 560fd0a558772d73ca9b7f26459447e709b515a6 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 23 Jun 2024 14:42:25 +0200 Subject: [PATCH] unique: implement custom version of unique package This version probably isn't as fast as the upstream version, but it is good enough for now. It also doesn't free unreferenced handles like the upstream version. --- GNUmakefile | 1 + loader/goroot.go | 1 + src/unique/handle.go | 74 ++++++++++++++++++++++++++++++++++++++ src/unique/handle_test.go | 76 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/unique/handle.go create mode 100644 src/unique/handle_test.go diff --git a/GNUmakefile b/GNUmakefile index aaaf130d9..1fe2a5a6c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -344,6 +344,7 @@ TEST_PACKAGES_FAST = \ unicode \ unicode/utf16 \ unicode/utf8 \ + unique \ $(nil) # Assume this will go away before Go2, so only check minor version. diff --git a/loader/goroot.go b/loader/goroot.go index 739819bce..7325db5b4 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -251,6 +251,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "runtime/": false, "sync/": true, "testing/": true, + "unique/": false, } if goMinor >= 19 { diff --git a/src/unique/handle.go b/src/unique/handle.go new file mode 100644 index 000000000..67c925d2d --- /dev/null +++ b/src/unique/handle.go @@ -0,0 +1,74 @@ +// Package unique implements the upstream Go unique package for TinyGo. +// +// It is not a full implementation: while it should behave the same way, it +// doesn't free unreferenced uniqued objects. +package unique + +import ( + "sync" + "unsafe" +) + +var ( + // We use a two-level map because that way it's easier to store and retrieve + // values. + globalMap map[unsafe.Pointer]any // map value type is always map[T]Handle[T] + + globalMapMutex sync.Mutex +) + +// Unique handle for the given value. Comparing two handles is cheap. +type Handle[T comparable] struct { + value *T +} + +// Value returns a shallow copy of the T value that produced the Handle. +func (h Handle[T]) Value() T { + return *h.value +} + +// Make a new unqique handle for the given value. +func Make[T comparable](value T) Handle[T] { + // Very simple implementation of the unique package. This is much, *much* + // simpler than the upstream implementation. Sadly it's not possible to + // reuse the upstream version because it relies on implementation details of + // the upstream runtime. + // It probably isn't as efficient as the upstream version, but the first + // goal here is compatibility. If the performance is a problem, it can be + // optimized later. + + globalMapMutex.Lock() + + // The map isn't initialized at program startup (and after a test run), so + // create it. + if globalMap == nil { + globalMap = make(map[unsafe.Pointer]any) + } + + // Retrieve the type-specific map, creating it if not yet present. + typeptr, _ := decomposeInterface(value) + var typeSpecificMap map[T]Handle[T] + if typeSpecificMapValue, ok := globalMap[typeptr]; !ok { + typeSpecificMap = make(map[T]Handle[T]) + globalMap[typeptr] = typeSpecificMap + } else { + typeSpecificMap = typeSpecificMapValue.(map[T]Handle[T]) + } + + // Retrieve the handle for the value, creating it if it isn't created yet. + var handle Handle[T] + if h, ok := typeSpecificMap[value]; !ok { + var clone T = value + handle.value = &clone + typeSpecificMap[value] = handle + } else { + handle = h + } + + globalMapMutex.Unlock() + + return handle +} + +//go:linkname decomposeInterface runtime.decomposeInterface +func decomposeInterface(i interface{}) (unsafe.Pointer, unsafe.Pointer) diff --git a/src/unique/handle_test.go b/src/unique/handle_test.go new file mode 100644 index 000000000..864b11f23 --- /dev/null +++ b/src/unique/handle_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file is a copy of src/unique/handle_test.go in upstream Go, but with +// some parts removed that rely on Go runtime implementation details. + +package unique + +import ( + "fmt" + "reflect" + "testing" +) + +// Set up special types. Because the internal maps are sharded by type, +// this will ensure that we're not overlapping with other tests. +type testString string +type testIntArray [4]int +type testEface any +type testStringArray [3]string +type testStringStruct struct { + a string +} +type testStringStructArrayStruct struct { + s [2]testStringStruct +} +type testStruct struct { + z float64 + b string +} + +func TestHandle(t *testing.T) { + testHandle[testString](t, "foo") + testHandle[testString](t, "bar") + testHandle[testString](t, "") + testHandle[testIntArray](t, [4]int{7, 77, 777, 7777}) + //testHandle[testEface](t, nil) // requires Go 1.20 + testHandle[testStringArray](t, [3]string{"a", "b", "c"}) + testHandle[testStringStruct](t, testStringStruct{"x"}) + testHandle[testStringStructArrayStruct](t, testStringStructArrayStruct{ + s: [2]testStringStruct{testStringStruct{"y"}, testStringStruct{"z"}}, + }) + testHandle[testStruct](t, testStruct{0.5, "184"}) +} + +func testHandle[T comparable](t *testing.T, value T) { + name := reflect.TypeFor[T]().Name() + t.Run(fmt.Sprintf("%s/%#v", name, value), func(t *testing.T) { + t.Parallel() + + v0 := Make(value) + v1 := Make(value) + + if v0.Value() != v1.Value() { + t.Error("v0.Value != v1.Value") + } + if v0.Value() != value { + t.Errorf("v0.Value not %#v", value) + } + if v0 != v1 { + t.Error("v0 != v1") + } + + drainMaps(t) + }) +} + +// drainMaps ensures that the internal maps are drained. +func drainMaps(t *testing.T) { + t.Helper() + + globalMapMutex.Lock() + globalMap = nil + globalMapMutex.Unlock() +}