Skip to content

Commit 560fd0a

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent b804811 commit 560fd0a

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

GNUmakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ TEST_PACKAGES_FAST = \
344344
unicode \
345345
unicode/utf16 \
346346
unicode/utf8 \
347+
unique \
347348
$(nil)
348349

349350
# Assume this will go away before Go2, so only check minor version.

loader/goroot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
251251
"runtime/": false,
252252
"sync/": true,
253253
"testing/": true,
254+
"unique/": false,
254255
}
255256

256257
if goMinor >= 19 {

src/unique/handle.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Package unique implements the upstream Go unique package for TinyGo.
2+
//
3+
// It is not a full implementation: while it should behave the same way, it
4+
// doesn't free unreferenced uniqued objects.
5+
package unique
6+
7+
import (
8+
"sync"
9+
"unsafe"
10+
)
11+
12+
var (
13+
// We use a two-level map because that way it's easier to store and retrieve
14+
// values.
15+
globalMap map[unsafe.Pointer]any // map value type is always map[T]Handle[T]
16+
17+
globalMapMutex sync.Mutex
18+
)
19+
20+
// Unique handle for the given value. Comparing two handles is cheap.
21+
type Handle[T comparable] struct {
22+
value *T
23+
}
24+
25+
// Value returns a shallow copy of the T value that produced the Handle.
26+
func (h Handle[T]) Value() T {
27+
return *h.value
28+
}
29+
30+
// Make a new unqique handle for the given value.
31+
func Make[T comparable](value T) Handle[T] {
32+
// Very simple implementation of the unique package. This is much, *much*
33+
// simpler than the upstream implementation. Sadly it's not possible to
34+
// reuse the upstream version because it relies on implementation details of
35+
// the upstream runtime.
36+
// It probably isn't as efficient as the upstream version, but the first
37+
// goal here is compatibility. If the performance is a problem, it can be
38+
// optimized later.
39+
40+
globalMapMutex.Lock()
41+
42+
// The map isn't initialized at program startup (and after a test run), so
43+
// create it.
44+
if globalMap == nil {
45+
globalMap = make(map[unsafe.Pointer]any)
46+
}
47+
48+
// Retrieve the type-specific map, creating it if not yet present.
49+
typeptr, _ := decomposeInterface(value)
50+
var typeSpecificMap map[T]Handle[T]
51+
if typeSpecificMapValue, ok := globalMap[typeptr]; !ok {
52+
typeSpecificMap = make(map[T]Handle[T])
53+
globalMap[typeptr] = typeSpecificMap
54+
} else {
55+
typeSpecificMap = typeSpecificMapValue.(map[T]Handle[T])
56+
}
57+
58+
// Retrieve the handle for the value, creating it if it isn't created yet.
59+
var handle Handle[T]
60+
if h, ok := typeSpecificMap[value]; !ok {
61+
var clone T = value
62+
handle.value = &clone
63+
typeSpecificMap[value] = handle
64+
} else {
65+
handle = h
66+
}
67+
68+
globalMapMutex.Unlock()
69+
70+
return handle
71+
}
72+
73+
//go:linkname decomposeInterface runtime.decomposeInterface
74+
func decomposeInterface(i interface{}) (unsafe.Pointer, unsafe.Pointer)

src/unique/handle_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file is a copy of src/unique/handle_test.go in upstream Go, but with
6+
// some parts removed that rely on Go runtime implementation details.
7+
8+
package unique
9+
10+
import (
11+
"fmt"
12+
"reflect"
13+
"testing"
14+
)
15+
16+
// Set up special types. Because the internal maps are sharded by type,
17+
// this will ensure that we're not overlapping with other tests.
18+
type testString string
19+
type testIntArray [4]int
20+
type testEface any
21+
type testStringArray [3]string
22+
type testStringStruct struct {
23+
a string
24+
}
25+
type testStringStructArrayStruct struct {
26+
s [2]testStringStruct
27+
}
28+
type testStruct struct {
29+
z float64
30+
b string
31+
}
32+
33+
func TestHandle(t *testing.T) {
34+
testHandle[testString](t, "foo")
35+
testHandle[testString](t, "bar")
36+
testHandle[testString](t, "")
37+
testHandle[testIntArray](t, [4]int{7, 77, 777, 7777})
38+
//testHandle[testEface](t, nil) // requires Go 1.20
39+
testHandle[testStringArray](t, [3]string{"a", "b", "c"})
40+
testHandle[testStringStruct](t, testStringStruct{"x"})
41+
testHandle[testStringStructArrayStruct](t, testStringStructArrayStruct{
42+
s: [2]testStringStruct{testStringStruct{"y"}, testStringStruct{"z"}},
43+
})
44+
testHandle[testStruct](t, testStruct{0.5, "184"})
45+
}
46+
47+
func testHandle[T comparable](t *testing.T, value T) {
48+
name := reflect.TypeFor[T]().Name()
49+
t.Run(fmt.Sprintf("%s/%#v", name, value), func(t *testing.T) {
50+
t.Parallel()
51+
52+
v0 := Make(value)
53+
v1 := Make(value)
54+
55+
if v0.Value() != v1.Value() {
56+
t.Error("v0.Value != v1.Value")
57+
}
58+
if v0.Value() != value {
59+
t.Errorf("v0.Value not %#v", value)
60+
}
61+
if v0 != v1 {
62+
t.Error("v0 != v1")
63+
}
64+
65+
drainMaps(t)
66+
})
67+
}
68+
69+
// drainMaps ensures that the internal maps are drained.
70+
func drainMaps(t *testing.T) {
71+
t.Helper()
72+
73+
globalMapMutex.Lock()
74+
globalMap = nil
75+
globalMapMutex.Unlock()
76+
}

0 commit comments

Comments
 (0)