aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2024-08-02 17:10:52 +0200
committerRon Evans <[email protected]>2024-10-23 12:25:27 +0100
commitb8fe75a9dd4949a08b78b2f3dd06fa3c697573dd (patch)
tree79350981ea8af9440f86818cd17c49741a948c89
parent0f95b4102d83b6977277ea7d5e87ad538eddc5ef (diff)
downloadtinygo-b8fe75a9dd4949a08b78b2f3dd06fa3c697573dd.tar.gz
tinygo-b8fe75a9dd4949a08b78b2f3dd06fa3c697573dd.zip
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and MacOS.
-rw-r--r--builder/musl.go1
-rw-r--r--compileopts/target.go6
-rw-r--r--main_test.go9
-rw-r--r--src/os/signal/signal.go14
-rw-r--r--src/runtime/runtime_unix.go228
-rw-r--r--src/runtime/signal.c89
-rw-r--r--src/runtime/wait_other.go2
-rw-r--r--testdata/signal.go42
-rw-r--r--testdata/signal.txt2
9 files changed, 374 insertions, 19 deletions
diff --git a/builder/musl.go b/builder/musl.go
index 8130981e6..ecae118e4 100644
--- a/builder/musl.go
+++ b/builder/musl.go
@@ -128,6 +128,7 @@ var libMusl = Library{
"mman/*.c",
"math/*.c",
"multibyte/*.c",
+ "signal/" + arch + "/*.s",
"signal/*.c",
"stdio/*.c",
"string/*.c",
diff --git a/compileopts/target.go b/compileopts/target.go
index ab9f871f3..b5df5b911 100644
--- a/compileopts/target.go
+++ b/compileopts/target.go
@@ -391,7 +391,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
)
spec.ExtraFiles = append(spec.ExtraFiles,
"src/runtime/os_darwin.c",
- "src/runtime/runtime_unix.c")
+ "src/runtime/runtime_unix.c",
+ "src/runtime/signal.c")
case "linux":
spec.Linker = "ld.lld"
spec.RTLib = "compiler-rt"
@@ -412,7 +413,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
}
spec.ExtraFiles = append(spec.ExtraFiles,
- "src/runtime/runtime_unix.c")
+ "src/runtime/runtime_unix.c",
+ "src/runtime/signal.c")
case "windows":
spec.Linker = "ld.lld"
spec.Libc = "mingw-w64"
diff --git a/main_test.go b/main_test.go
index 93da10736..136128d51 100644
--- a/main_test.go
+++ b/main_test.go
@@ -79,6 +79,7 @@ func TestBuild(t *testing.T) {
"oldgo/",
"print.go",
"reflect.go",
+ "signal.go",
"slice.go",
"sort.go",
"stdlib.go",
@@ -217,6 +218,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
isWASI := strings.HasPrefix(options.Target, "wasi")
isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm"))
+ isBaremetal := options.Target == "simavr" || options.Target == "cortex-m-qemu" || options.Target == "riscv-qemu"
for _, name := range tests {
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
@@ -281,6 +283,13 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
continue
}
}
+ if isWebAssembly || isBaremetal || options.GOOS == "windows" {
+ switch name {
+ case "signal.go":
+ // Signals only work on POSIX-like systems.
+ continue
+ }
+ }
name := name // redefine to avoid race condition
t.Run(name, func(t *testing.T) {
diff --git a/src/os/signal/signal.go b/src/os/signal/signal.go
deleted file mode 100644
index 41ceaf485..000000000
--- a/src/os/signal/signal.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2012 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.
-
-package signal
-
-import (
- "os"
-)
-
-// Just stubbing the functions for now since signal handling is not yet implemented in tinygo
-func Reset(sig ...os.Signal) {}
-func Ignore(sig ...os.Signal) {}
-func Notify(c chan<- os.Signal, sig ...os.Signal) {}
diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go
index ba5d5a593..c4fd3285b 100644
--- a/src/runtime/runtime_unix.go
+++ b/src/runtime/runtime_unix.go
@@ -3,6 +3,8 @@
package runtime
import (
+ "math/bits"
+ "sync/atomic"
"unsafe"
)
@@ -12,6 +14,9 @@ func libc_write(fd int32, buf unsafe.Pointer, count uint) int
//export usleep
func usleep(usec uint) int
+//export pause
+func pause() int32
+
// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// Note: off_t is defined as int64 because:
// - musl (used on Linux) always defines it as int64
@@ -217,8 +222,47 @@ func nanosecondsToTicks(ns int64) timeUnit {
}
func sleepTicks(d timeUnit) {
- // timeUnit is in nanoseconds, so need to convert to microseconds here.
- usleep(uint(d) / 1000)
+ // When there are no signal handlers present, we can simply go to sleep.
+ if !hasSignals {
+ // timeUnit is in nanoseconds, so need to convert to microseconds here.
+ usleep(uint(d) / 1000)
+ return
+ }
+
+ if GOOS == "darwin" {
+ // Check for incoming signals.
+ if checkSignals() {
+ // Received a signal, so there's probably at least one goroutine
+ // that's runnable again.
+ return
+ }
+
+ // WARNING: there is a race condition here. If a signal arrives between
+ // checkSignals() and usleep(), the usleep() call will not exit early so
+ // the signal is delayed until usleep finishes or another signal
+ // arrives.
+ // There doesn't appear to be a simple way to fix this on MacOS.
+
+ // timeUnit is in nanoseconds, so need to convert to microseconds here.
+ result := usleep(uint(d) / 1000)
+ if result != 0 {
+ checkSignals()
+ }
+ } else {
+ // Linux (and various other POSIX systems) implement sigtimedwait so we
+ // can do this in a non-racy way.
+ tinygo_wfi_mask(activeSignals)
+ if checkSignals() {
+ tinygo_wfi_unmask()
+ return
+ }
+ signal := tinygo_wfi_sleep(activeSignals, uint64(d))
+ if signal >= 0 {
+ tinygo_signal_handler(signal)
+ checkSignals()
+ }
+ tinygo_wfi_unmask()
+ }
}
func getTime(clock int32) uint64 {
@@ -307,3 +351,183 @@ func growHeap() bool {
setHeapEnd(heapStart + heapSize)
return true
}
+
+func init() {
+ // Set up a channel to receive signals into.
+ signalChan = make(chan uint32, 1)
+}
+
+var signalChan chan uint32
+
+// Indicate whether signals have been registered.
+var hasSignals bool
+
+// Mask of signals that have been received. The signal handler atomically ORs
+// signals into this value.
+var receivedSignals uint32
+
+var activeSignals uint32
+
+//go:linkname signal_enable os/signal.signal_enable
+func signal_enable(s uint32) {
+ if s >= 32 {
+ // TODO: to support higher signal numbers, we need to turn
+ // receivedSignals into a uint32 array.
+ runtimePanicAt(returnAddress(0), "unsupported signal number")
+ }
+ hasSignals = true
+ activeSignals |= 1 << s
+ // It's easier to implement this function in C.
+ tinygo_signal_enable(s)
+}
+
+//go:linkname signal_ignore os/signal.signal_ignore
+func signal_ignore(s uint32) {
+ if s >= 32 {
+ // TODO: to support higher signal numbers, we need to turn
+ // receivedSignals into a uint32 array.
+ runtimePanicAt(returnAddress(0), "unsupported signal number")
+ }
+ activeSignals &^= 1 << s
+ tinygo_signal_ignore(s)
+}
+
+//go:linkname signal_disable os/signal.signal_disable
+func signal_disable(s uint32) {
+ if s >= 32 {
+ // TODO: to support higher signal numbers, we need to turn
+ // receivedSignals into a uint32 array.
+ runtimePanicAt(returnAddress(0), "unsupported signal number")
+ }
+ activeSignals &^= 1 << s
+ tinygo_signal_disable(s)
+}
+
+//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle
+func signal_waitUntilIdle() {
+ // Make sure all signals are sent on the channel.
+ for atomic.LoadUint32(&receivedSignals) != 0 {
+ checkSignals()
+ Gosched()
+ }
+
+ // Make sure all signals are processed.
+ for len(signalChan) != 0 {
+ Gosched()
+ }
+}
+
+//export tinygo_signal_enable
+func tinygo_signal_enable(s uint32)
+
+//export tinygo_signal_ignore
+func tinygo_signal_ignore(s uint32)
+
+//export tinygo_signal_disable
+func tinygo_signal_disable(s uint32)
+
+// void tinygo_signal_handler(int sig);
+//
+//export tinygo_signal_handler
+func tinygo_signal_handler(s int32) {
+ // This loop is essentially the atomic equivalent of the following:
+ //
+ // receivedSignals |= 1 << s
+ //
+ // TODO: use atomic.Uint32.And once we drop support for Go 1.22 instead of
+ // this loop.
+ for {
+ mask := uint32(1) << uint32(s)
+ val := atomic.LoadUint32(&receivedSignals)
+ swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, val|mask)
+ if swapped {
+ break
+ }
+ }
+}
+
+//go:linkname signal_recv os/signal.signal_recv
+func signal_recv() uint32 {
+ // Function called from os/signal to get the next received signal.
+ val := <-signalChan
+ checkSignals()
+ return val
+}
+
+// Atomically find a signal that previously occured and send it into the
+// signalChan channel. Return true if at least one signal was delivered this
+// way, false otherwise.
+func checkSignals() bool {
+ gotSignals := false
+ for {
+ // Extract the lowest numbered signal number from receivedSignals.
+ val := atomic.LoadUint32(&receivedSignals)
+ if val == 0 {
+ // There is no signal ready to be received by the program (common
+ // case).
+ return gotSignals
+ }
+ num := uint32(bits.TrailingZeros32(val))
+
+ // Do a non-blocking send on signalChan.
+ select {
+ case signalChan <- num:
+ // There was room free in the channel, so remove the signal number
+ // from the receivedSignals mask.
+ gotSignals = true
+ default:
+ // Could not send the signal number on the channel. This means
+ // there's still a signal pending. In that case, let it be received
+ // at which point checkSignals is called again to put the next one
+ // in the channel buffer.
+ return gotSignals
+ }
+
+ // Atomically clear the signal number from receivedSignals.
+ // TODO: use atomic.Uint32.Or once we drop support for Go 1.22 instead
+ // of this loop.
+ for {
+ newVal := val &^ (1 << num)
+ swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, newVal)
+ if swapped {
+ break
+ }
+ val = atomic.LoadUint32(&receivedSignals)
+ }
+ }
+}
+
+//export tinygo_wfi_mask
+func tinygo_wfi_mask(active uint32)
+
+//export tinygo_wfi_sleep
+func tinygo_wfi_sleep(active uint32, timeout uint64) int32
+
+//export tinygo_wfi_wait
+func tinygo_wfi_wait(active uint32) int32
+
+//export tinygo_wfi_unmask
+func tinygo_wfi_unmask()
+
+func waitForEvents() {
+ if hasSignals {
+ // We could have used pause() here, but that function is impossible to
+ // use in a race-free way:
+ // https://www.cipht.net/2023/11/30/perils-of-pause.html
+ // Therefore we need something better.
+ // Note: this is unsafe with multithreading, because sigprocmask is only
+ // defined for single-threaded applictions.
+ tinygo_wfi_mask(activeSignals)
+ if checkSignals() {
+ tinygo_wfi_unmask()
+ return
+ }
+ signal := tinygo_wfi_wait(activeSignals)
+ tinygo_signal_handler(signal)
+ checkSignals()
+ tinygo_wfi_unmask()
+ } else {
+ // The program doesn't use signals, so this is a deadlock.
+ runtimePanic("deadlocked: no event source")
+ }
+}
diff --git a/src/runtime/signal.c b/src/runtime/signal.c
new file mode 100644
index 000000000..a462518c1
--- /dev/null
+++ b/src/runtime/signal.c
@@ -0,0 +1,89 @@
+//go:build none
+
+// Ignore the //go:build above. This file is manually included on Linux and
+// MacOS to provide os/signal support.
+
+#include <stdint.h>
+#include <signal.h>
+#include <time.h>
+#include <unistd.h>
+
+// Signal handler in the runtime.
+void tinygo_signal_handler(int sig);
+
+// Enable a signal from the runtime.
+void tinygo_signal_enable(uint32_t sig) {
+ struct sigaction act = { 0 };
+ act.sa_handler = &tinygo_signal_handler;
+ sigaction(sig, &act, NULL);
+}
+
+void tinygo_signal_ignore(uint32_t sig) {
+ struct sigaction act = { 0 };
+ act.sa_handler = SIG_IGN;
+ sigaction(sig, &act, NULL);
+}
+
+void tinygo_signal_disable(uint32_t sig) {
+ struct sigaction act = { 0 };
+ act.sa_handler = SIG_DFL;
+ sigaction(sig, &act, NULL);
+}
+
+// Implement waitForEvents and sleep with signals.
+// Warning: sigprocmask is not defined in a multithreaded program so will need
+// to be replaced with something else once we implement threading on POSIX.
+
+// Signals active before a call to tinygo_wfi_mask.
+static sigset_t active_signals;
+
+static void tinygo_set_signals(sigset_t *mask, uint32_t signals) {
+ sigemptyset(mask);
+ for (int i=0; i<32; i++) {
+ if ((signals & (1<<i)) != 0) {
+ sigaddset(mask, i);
+ }
+ }
+}
+
+// Mask the given signals.
+// This function must always restore the previous signals using
+// tinygo_wfi_unmask, to create a critical section.
+void tinygo_wfi_mask(uint32_t active) {
+ sigset_t mask;
+ tinygo_set_signals(&mask, active);
+
+ sigprocmask(SIG_BLOCK, &mask, &active_signals);
+}
+
+// Wait until a signal becomes pending (or is already pending), and return the
+// signal.
+#if !defined(__APPLE__)
+int tinygo_wfi_sleep(uint32_t active, uint64_t timeout) {
+ sigset_t active_set;
+ tinygo_set_signals(&active_set, active);
+
+ struct timespec ts = {0};
+ ts.tv_sec = timeout / 1000000000;
+ ts.tv_nsec = timeout % 1000000000;
+
+ int result = sigtimedwait(&active_set, NULL, &ts);
+ return result;
+}
+#endif
+
+// Wait until any of the active signals becomes pending (or returns immediately
+// if one is already pending).
+int tinygo_wfi_wait(uint32_t active) {
+ sigset_t active_set;
+ tinygo_set_signals(&active_set, active);
+
+ int sig = 0;
+ sigwait(&active_set, &sig);
+ return sig;
+}
+
+// Restore previous signal mask.
+void tinygo_wfi_unmask(void) {
+ sigprocmask(SIG_SETMASK, &active_signals, NULL);
+}
diff --git a/src/runtime/wait_other.go b/src/runtime/wait_other.go
index b51d4b64b..f1487e396 100644
--- a/src/runtime/wait_other.go
+++ b/src/runtime/wait_other.go
@@ -1,4 +1,4 @@
-//go:build !tinygo.riscv && !cortexm
+//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin
package runtime
diff --git a/testdata/signal.go b/testdata/signal.go
new file mode 100644
index 000000000..a82991f08
--- /dev/null
+++ b/testdata/signal.go
@@ -0,0 +1,42 @@
+package main
+
+// Test POSIX signals.
+// TODO: run `tinygo test os/signal` instead, once CGo errno return values are
+// supported.
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+func main() {
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, syscall.SIGUSR1)
+
+ // Wait for signals to arrive.
+ go func() {
+ for sig := range c {
+ if sig == syscall.SIGUSR1 {
+ println("got expected signal")
+ } else {
+ println("got signal:", sig.String())
+ }
+ }
+ }()
+
+ // Send the signal.
+ syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
+
+ time.Sleep(time.Millisecond * 100)
+
+ // Stop notifying.
+ // (This is just a smoke test, it's difficult to test the default behavior
+ // in a unit test).
+ signal.Ignore(syscall.SIGUSR1)
+
+ signal.Stop(c)
+
+ println("exiting signal program")
+}
diff --git a/testdata/signal.txt b/testdata/signal.txt
new file mode 100644
index 000000000..c4726d717
--- /dev/null
+++ b/testdata/signal.txt
@@ -0,0 +1,2 @@
+got expected signal
+exiting signal program