Skip to content

Commit dd1ebbd

Browse files
aykevldeadprogram
authored andcommitted
runtime: implement race-free signals using futexes
This requires an API introduced in MacOS 11. I think that's fine, since the version before that (MacOS 10.15) is EOL since 2022. Though if needed, we could certainly work around it by using an older and slightly less nice API.
1 parent 9567146 commit dd1ebbd

File tree

10 files changed

+248
-179
lines changed

10 files changed

+248
-179
lines changed

GNUmakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,7 @@ endif
935935
@cp -rp lib/musl/src/malloc build/release/tinygo/lib/musl/src
936936
@cp -rp lib/musl/src/mman build/release/tinygo/lib/musl/src
937937
@cp -rp lib/musl/src/math build/release/tinygo/lib/musl/src
938+
@cp -rp lib/musl/src/misc build/release/tinygo/lib/musl/src
938939
@cp -rp lib/musl/src/multibyte build/release/tinygo/lib/musl/src
939940
@cp -rp lib/musl/src/signal build/release/tinygo/lib/musl/src
940941
@cp -rp lib/musl/src/stdio build/release/tinygo/lib/musl/src

builder/musl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ var libMusl = Library{
128128
"malloc/mallocng/*.c",
129129
"mman/*.c",
130130
"math/*.c",
131+
"misc/*.c",
131132
"multibyte/*.c",
132133
"signal/" + arch + "/*.s",
133134
"signal/*.c",

compileopts/target.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
390390
"-platform_version", "macos", platformVersion, platformVersion,
391391
)
392392
spec.ExtraFiles = append(spec.ExtraFiles,
393+
"src/internal/futex/futex_darwin.c",
393394
"src/runtime/os_darwin.c",
394395
"src/runtime/runtime_unix.c",
395396
"src/runtime/signal.c")
@@ -413,6 +414,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
413414
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
414415
}
415416
spec.ExtraFiles = append(spec.ExtraFiles,
417+
"src/internal/futex/futex_linux.c",
416418
"src/runtime/runtime_unix.c",
417419
"src/runtime/signal.c")
418420
case "windows":

lib/macos-minimal-sdk

loader/goroot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
243243
"internal/binary/": false,
244244
"internal/bytealg/": false,
245245
"internal/cm/": false,
246+
"internal/futex/": false,
246247
"internal/fuzz/": false,
247248
"internal/reflectlite/": false,
248249
"internal/gclayout": false,

src/internal/futex/futex.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package futex
2+
3+
// Cross platform futex implementation.
4+
// Futexes are supported on all major operating systems and on WebAssembly.
5+
//
6+
// For more information, see: https://outerproduct.net/futex-dictionary.html
7+
8+
import (
9+
"sync/atomic"
10+
"unsafe"
11+
)
12+
13+
// A futex is a way for userspace to wait with the pointer as the key, and for
14+
// another thread to wake one or all waiting threads keyed on the same pointer.
15+
//
16+
// A futex does not change the underlying value, it only reads it before going
17+
// to sleep (atomically) to prevent lost wake-ups.
18+
type Futex struct {
19+
atomic.Uint32
20+
}
21+
22+
// Atomically check for cmp to still be equal to the futex value and if so, go
23+
// to sleep. Return true if we were definitely awoken by a call to Wake or
24+
// WakeAll, and false if we can't be sure of that.
25+
func (f *Futex) Wait(cmp uint32) bool {
26+
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)
27+
28+
// We *could* detect a zero return value from the futex system call which
29+
// would indicate we got awoken by a Wake or WakeAll call. However, this is
30+
// what the manual page has to say:
31+
//
32+
// > Note that a wake-up can also be caused by common futex usage patterns
33+
// > in unrelated code that happened to have previously used the futex
34+
// > word's memory location (e.g., typical futex-based implementations of
35+
// > Pthreads mutexes can cause this under some conditions). Therefore,
36+
// > callers should always conservatively assume that a return value of 0
37+
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
38+
// > user-space synchronization scheme) to decide whether to continue to
39+
// > block or not.
40+
//
41+
// I'm not sure whether we do anything like pthread does, so to be on the
42+
// safe side we say we don't know whether the wakeup was spurious or not and
43+
// return false.
44+
return false
45+
}
46+
47+
// Like Wait, but times out after the number of nanoseconds in timeout.
48+
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
49+
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
50+
}
51+
52+
// Wake a single waiter.
53+
func (f *Futex) Wake() {
54+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)))
55+
}
56+
57+
// Wake all waiters.
58+
func (f *Futex) WakeAll() {
59+
tinygo_futex_wake_all((*uint32)(unsafe.Pointer(&f.Uint32)))
60+
}
61+
62+
//export tinygo_futex_wait
63+
func tinygo_futex_wait(addr *uint32, cmp uint32)
64+
65+
//export tinygo_futex_wait_timeout
66+
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)
67+
68+
//export tinygo_futex_wake
69+
func tinygo_futex_wake(addr *uint32)
70+
71+
//export tinygo_futex_wake_all
72+
func tinygo_futex_wake_all(addr *uint32)

src/internal/futex/futex_darwin.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//go:build none
2+
3+
// This file is manually included, to avoid CGo which would cause a circular
4+
// import.
5+
6+
#include <stdint.h>
7+
8+
// This API isn't documented by Apple, but it is used by LLVM libc++ (so should
9+
// be stable) and has been documented extensively here:
10+
// https://outerproduct.net/futex-dictionary.html
11+
12+
int __ulock_wait(uint32_t operation, void *addr, uint64_t value, uint32_t timeout_us);
13+
int __ulock_wait2(uint32_t operation, void *addr, uint64_t value, uint64_t timeout_ns, uint64_t value2);
14+
int __ulock_wake(uint32_t operation, void *addr, uint64_t wake_value);
15+
16+
// Operation code.
17+
#define UL_COMPARE_AND_WAIT 1
18+
19+
// Flags to the operation value.
20+
#define ULF_WAKE_ALL 0x00000100
21+
#define ULF_NO_ERRNO 0x01000000
22+
23+
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
24+
__ulock_wait(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, 0);
25+
}
26+
27+
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
28+
// Make sure that an accidental use of a zero timeout is not treated as an
29+
// infinite timeout. Return if it's zero since it wouldn't be waiting for
30+
// any significant time anyway.
31+
// Probably unnecessary, but guards against potential bugs.
32+
if (timeout == 0) {
33+
return;
34+
}
35+
36+
// Note: __ulock_wait2 is available since MacOS 11.
37+
// I think that's fine, since the version before that (MacOS 10.15) is EOL
38+
// since 2022. Though if needed, we could certainly use __ulock_wait instead
39+
// and deal with the smaller timeout value.
40+
__ulock_wait2(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, timeout, 0);
41+
}
42+
43+
void tinygo_futex_wake(uint32_t *addr) {
44+
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, 0);
45+
}
46+
47+
void tinygo_futex_wake_all(uint32_t *addr) {
48+
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO|ULF_WAKE_ALL, addr, 0);
49+
}

src/internal/futex/futex_linux.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//go:build none
2+
3+
// This file is manually included, to avoid CGo which would cause a circular
4+
// import.
5+
6+
#include <limits.h>
7+
#include <stdint.h>
8+
#include <sys/syscall.h>
9+
#include <time.h>
10+
#include <unistd.h>
11+
12+
#define FUTEX_WAIT 0
13+
#define FUTEX_WAKE 1
14+
#define FUTEX_PRIVATE_FLAG 128
15+
16+
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
17+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, NULL, NULL, 0);
18+
}
19+
20+
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
21+
struct timespec ts = {0};
22+
ts.tv_sec = timeout / 1000000000;
23+
ts.tv_nsec = timeout % 1000000000;
24+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, &ts, NULL, 0);
25+
}
26+
27+
void tinygo_futex_wake(uint32_t *addr) {
28+
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, 1, NULL, NULL, 0);
29+
}
30+
31+
void tinygo_futex_wake_all(uint32_t *addr) {
32+
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, INT_MAX, NULL, NULL, 0);
33+
}

0 commit comments

Comments
 (0)