This commit changes signal handling in a few ways:
* It stubs signals for all wasm targets (not just wasi) and baremetal,
since none of those have traditional POSIX signals. And moves the
code for that into a single file, instead of duplicating it.
* It removes the stub for signal_ignored since the value `false` might
be wrong in some cases and it doesn't usually seem to be called (it
is not called in tsgo). Should be trivial to re-add if it is shown
to be needed.
* It adds a stub for `os/signal.signalWaitUntilIdle` which _is_ called
by tsgo.
With `-opt=2`, WriteByte gets inlined everywhere a println statement
exists. This blows up binary size for very little gain. In my case, the
binary size roughly doubled. Instead, don't inline it so that the binary
size remains somewhat reasonable. This might slow down WriteByte a tiny
bit, but likely not by any significant amount.
Errors are part of API, and the exported rp2 errors seemed arbitrary.
For example, the very particular ErrRP2040I2CDisable was exported, but
errI2CWriteTimeout (which is defined on all platforms) is not.
While here, remove "RP2040" from an error name and make the messages
consistent and idiomatic.
The `gosched` call introduce arbitrary long delays in general, and in
TinyGo particular because the goroutine scheduler is cooperative and
doesn't preempt busy (e.g. compute-heavy) goroutines.
Before this change, the timeout logic would read, simplified:
deadline := now() + timeout
startTX()
for !txDone() {
if now() > deadline { return timeoutError }
gosched() // (1)
}
startRx() // (2)
for !rxDone() {
// (3)
if now() > deadline { return timeoutError }
gosched()
}
What could happen in a busy system is:
- The gosched marked (1) would push now() to be > than deadline.
- startRx is called (2), but the call to rxDone immediately after would
report it not yet done.
- The check marked (3) would fail, even though only a miniscule amount
of time has passed between startRx and the check.
This change ensures that the timeout clock discounts time spent in
`gosched`. The logic now reads, around every call to `gosched`:
deadline := now() + timeout
startTX()
for !txDone() {
if now() > deadline { return timeoutError }
before := now()
gosched()
deadline += now() - before
}
I tested this change by simulating a busy goroutine:
go func() {
for {
// Busy.
before := time.Now()
for time.Since(before) < 100*time.Millisecond {
}
// Sleep.
time.Sleep(100 * time.Millisecond)
}
}()
and testing that I2C transfers would no longer time out.
I only discovered this issue after a while. All the baremetal PWM
implementations use pointers to a PWM instance, instead of the PWM
instance itself. For consistency (and because it's a better idea in
general), the simulated PWMs need to work the same.
Without this change, a pending interrupt would spuriously trigger
immediately after enabling. This happens if an interrupt is triggered
during flashing (e.g. by DMA), which survives the subsequent reset.
This behaviour matches e.g. `machine.irqSet` in machine_rp2_rp2350.go.
See 7f970a45, whose symptoms were likely caused by spurious interrupts.
For the threads scheduler, it makes sense to have NumCPU available.
For all other schedulers, the number of available CPUs is practically
limited to one by the scheduler (even though the system might have more
CPUs).
This variable is only necessary on the cooperative and none scheduler.
It is not used on the threads scheduler.
The reason for moving is that the upcoming multicore baremetal scheduler
also needs mainExited but of a different type: an atomic variable
instead of a plain boolean.
This is more descriptive: the call is to exit a task, not to pause it.
This also makes it more obvious that there's an optimization
opportunity: to free the stack explicitly after the goroutine returns
(or to keep it as a cache for the next stack allocation).
This is not a scheduler in the runtime, instead every goroutine is
mapped to a single OS thread - meaning 1:1 scheduling.
While this may not perform well (or at all) for large numbers of
threads, it greatly simplifies many things in the runtime. For example,
blocking syscalls can be called directly instead of having to use epoll
or similar. Also, we don't need to do anything special to call C code -
the default stack is all we need.
Somewhat surprisingly, this results in smaller code than the old code
with the cooperative (tasks) scheduler. Probably because the new RWMutex
is also simpler.
Using a global lock may be slow, but it is certainly simple and safe.
If this global lock becomes a bottleneck, we can of course look into
making the GC truly support multithreading.
Instead of just incrementing the timestamp, this causes the system to
actually sleep when calling time.Sleep. The direct effect is that this
works as expected:
$ tinygo run -target=riscv-qemu examples/serial
hello world!
hello world!
hello world!
[..etc]
This commit also adds a bare bones handler for exceptions (such as
invalid memory writes), since we're adding an interrupt handler anyway.
While this patch doesn't add that much functionality, having interrupt
support is going to be needed for multicore support on riscv-qemu. My
plan is to first add this support to riscv-qemu (based on the earlier
work I did for the RP2040 and demoed at FOSDEM 2025) and once the basics
are in place and fully tested we can extend this support to the RP2040.
Writing for QEMU first makes it much easier to debug any issues that
will come up.
This directive caused the code to be put in a non-executable area on
Windows which caused a segmentation fault. This patch fixes the issue by
removing `.section` directives, fixing windows/arm64 support.
I'm surprised this worked as long as it did, since it looks like the
goroutine stack did not get scanned. Or maybe the RCX register contained
the stack pointer by accident. In any case, it now uses the correct
register (RCX instead of RDI on Windows) for passing the stack pointer
as the first parameter.
This adds support for the well-known Boehm GC. It's significantly faster
than our own naive GC and could be used as an alternative on bigger
systems.
In the future, this GC might also be supported on WebAssembly with some
extra work. Right now it's Linux only (though Windows/MacOS shouldn't be
too difficult to add).
For example, with -gc=none and -gc=leaking, no heap needs to be
allocated when initializing the runtime. And some GCs (like -gc=custom)
are responsible for allocating the heap themselves.