mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-09 05:23:40 +00:00
refactor coroutine lowering and tasks
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package task
|
||||
|
||||
const asserts = false
|
||||
|
||||
// Queue is a FIFO container of tasks.
|
||||
// The zero value is an empty queue.
|
||||
type Queue struct {
|
||||
head, tail *Task
|
||||
}
|
||||
|
||||
// Push a task onto the queue.
|
||||
func (q *Queue) Push(t *Task) {
|
||||
if asserts && t.Next != nil {
|
||||
panic("runtime: pushing a task to a queue with a non-nil Next pointer")
|
||||
}
|
||||
if q.tail != nil {
|
||||
q.tail.Next = t
|
||||
}
|
||||
q.tail = t
|
||||
t.Next = nil
|
||||
if q.head == nil {
|
||||
q.head = t
|
||||
}
|
||||
}
|
||||
|
||||
// Pop a task off of the queue.
|
||||
func (q *Queue) Pop() *Task {
|
||||
t := q.head
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
q.head = t.Next
|
||||
if q.tail == t {
|
||||
q.tail = nil
|
||||
}
|
||||
t.Next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// Append pops the contents of another queue and pushes them onto the end of this queue.
|
||||
func (q *Queue) Append(other *Queue) {
|
||||
if q.head == nil {
|
||||
q.head = other.head
|
||||
} else {
|
||||
q.tail.Next = other.head
|
||||
}
|
||||
q.tail = other.tail
|
||||
other.head, other.tail = nil, nil
|
||||
}
|
||||
|
||||
// Stack is a LIFO container of tasks.
|
||||
// The zero value is an empty stack.
|
||||
// This is slightly cheaper than a queue, so it can be preferable when strict ordering is not necessary.
|
||||
type Stack struct {
|
||||
top *Task
|
||||
}
|
||||
|
||||
// Push a task onto the stack.
|
||||
func (s *Stack) Push(t *Task) {
|
||||
if asserts && t.Next != nil {
|
||||
panic("runtime: pushing a task to a stack with a non-nil Next pointer")
|
||||
}
|
||||
s.top, t.Next = t, s.top
|
||||
}
|
||||
|
||||
// Pop a task off of the stack.
|
||||
func (s *Stack) Pop() *Task {
|
||||
t := s.top
|
||||
if t != nil {
|
||||
s.top = t.Next
|
||||
}
|
||||
t.Next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// tail follows the chain of tasks.
|
||||
// If t is nil, returns nil.
|
||||
// Otherwise, returns the task in the chain where the Next field is nil.
|
||||
func (t *Task) tail() *Task {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
for t.Next != nil {
|
||||
t = t.Next
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Queue moves the contents of the stack into a queue.
|
||||
// Elements can be popped from the queue in the same order that they would be popped from the stack.
|
||||
func (s *Stack) Queue() Queue {
|
||||
head := s.top
|
||||
s.top = nil
|
||||
return Queue{
|
||||
head: head,
|
||||
tail: head.tail(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Task is a state of goroutine for scheduling purposes.
|
||||
type Task struct {
|
||||
// Next is a field which can be used to make a linked list of tasks.
|
||||
Next *Task
|
||||
|
||||
// Ptr is a field which can be used for storing a pointer.
|
||||
Ptr unsafe.Pointer
|
||||
|
||||
// Data is a field which can be used for storing state information.
|
||||
Data uint
|
||||
|
||||
// state is the underlying running state of the task.
|
||||
state state
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// +build scheduler.coroutines
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// rawState is an underlying coroutine state exposed by llvm.coro.
|
||||
// This matches *i8 in LLVM.
|
||||
type rawState uint8
|
||||
|
||||
//go:export llvm.coro.resume
|
||||
func (s *rawState) resume()
|
||||
|
||||
type state struct{ *rawState }
|
||||
|
||||
//go:export llvm.coro.noop
|
||||
func noopState() *rawState
|
||||
|
||||
// Resume the task until it pauses or completes.
|
||||
func (t *Task) Resume() {
|
||||
t.state.resume()
|
||||
}
|
||||
|
||||
// setState is used by the compiler to set the state of the function at the beginning of a function call.
|
||||
// Returns the state of the caller.
|
||||
func (t *Task) setState(s *rawState) *rawState {
|
||||
caller := t.state
|
||||
t.state = state{s}
|
||||
return caller.rawState
|
||||
}
|
||||
|
||||
// returnTo is used by the compiler to return to the state of the caller.
|
||||
func (t *Task) returnTo(parent *rawState) {
|
||||
t.state = state{parent}
|
||||
t.returnCurrent()
|
||||
}
|
||||
|
||||
// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced.
|
||||
func (t *Task) returnCurrent() {
|
||||
scheduleTask(t)
|
||||
}
|
||||
|
||||
//go:linkname scheduleTask runtime.runqueuePushBack
|
||||
func scheduleTask(*Task)
|
||||
|
||||
// setReturnPtr is used by the compiler to store the return buffer into the task.
|
||||
// This buffer is where the return value of a function that is about to be called will be stored.
|
||||
func (t *Task) setReturnPtr(buf unsafe.Pointer) {
|
||||
t.Ptr = buf
|
||||
}
|
||||
|
||||
// getReturnPtr is used by the compiler to get the return buffer stored into the task.
|
||||
// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller.
|
||||
func (t *Task) getReturnPtr() unsafe.Pointer {
|
||||
return t.Ptr
|
||||
}
|
||||
|
||||
// createTask returns a new task struct initialized with a no-op state.
|
||||
func createTask() *Task {
|
||||
return &Task{
|
||||
state: state{noopState()},
|
||||
}
|
||||
}
|
||||
|
||||
// start invokes a function in a new goroutine. Calls to this are inserted by the compiler.
|
||||
// The created goroutine starts running immediately.
|
||||
// This is implemented inside the compiler.
|
||||
func start(fn uintptr, args unsafe.Pointer)
|
||||
|
||||
// Current returns the current active task.
|
||||
// This is implemented inside the compiler.
|
||||
func Current() *Task
|
||||
|
||||
// Pause suspends the current running task.
|
||||
// This is implemented inside the compiler.
|
||||
func Pause()
|
||||
|
||||
type taskHolder interface {
|
||||
setState(*rawState) *rawState
|
||||
returnTo(*rawState)
|
||||
returnCurrent()
|
||||
setReturnPtr(unsafe.Pointer)
|
||||
getReturnPtr() unsafe.Pointer
|
||||
}
|
||||
|
||||
// If there are no direct references to the task methods, they will not be discovered by the compiler, and this will trigger a compiler error.
|
||||
// Instantiating this interface forces discovery of these methods.
|
||||
var _ = taskHolder((*Task)(nil))
|
||||
|
||||
func fake() {
|
||||
// Hack to ensure intrinsics are discovered.
|
||||
Current()
|
||||
go func() {}()
|
||||
Pause()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// +build scheduler.none
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtimePanic runtime.runtimePanic
|
||||
func runtimePanic(str string)
|
||||
|
||||
func Pause() {
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
|
||||
func Current() *Task {
|
||||
runtimePanic("scheduler is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func start(fn uintptr, args unsafe.Pointer) {
|
||||
// The compiler will error if this is reachable.
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
|
||||
type state struct{}
|
||||
|
||||
func (t *Task) Resume() {
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// +build scheduler.tasks
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtimePanic runtime.runtimePanic
|
||||
func runtimePanic(str string)
|
||||
|
||||
// Stack canary, to detect a stack overflow. The number is a random number
|
||||
// generated by random.org. The bit fiddling dance is necessary because
|
||||
// otherwise Go wouldn't allow the cast to a smaller integer size.
|
||||
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
|
||||
|
||||
// state is a structure which holds a reference to the state of the task.
|
||||
// When the task is suspended, the registers are stored onto the stack and the stack pointer is stored into sp.
|
||||
type state struct {
|
||||
// sp is the stack pointer of the saved state.
|
||||
// When the task is inactive, the saved registers are stored at the top of the stack.
|
||||
sp uintptr
|
||||
|
||||
// canaryPtr points to the top word of the stack (the lowest address).
|
||||
// This is used to detect stack overflows.
|
||||
// When initializing the goroutine, the stackCanary constant is stored there.
|
||||
// If the stack overflowed, the word will likely no longer equal stackCanary.
|
||||
canaryPtr *uintptr
|
||||
}
|
||||
|
||||
// currentTask is the current running task, or nil if currently in the scheduler.
|
||||
var currentTask *Task
|
||||
|
||||
// Current returns the current active task.
|
||||
func Current() *Task {
|
||||
return currentTask
|
||||
}
|
||||
|
||||
// Pause suspends the current task and returns to the scheduler.
|
||||
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
|
||||
func Pause() {
|
||||
// Check whether the canary (the lowest address of the stack) is still
|
||||
// valid. If it is not, a stack overflow has occured.
|
||||
if *currentTask.state.canaryPtr != stackCanary {
|
||||
runtimePanic("goroutine stack overflow")
|
||||
}
|
||||
currentTask.state.pause()
|
||||
}
|
||||
|
||||
// Resume the task until it pauses or completes.
|
||||
// This may only be called from the scheduler.
|
||||
func (t *Task) Resume() {
|
||||
currentTask = t
|
||||
t.state.resume()
|
||||
currentTask = nil
|
||||
}
|
||||
|
||||
// initialize the state and prepare to call the specified function with the specified argument bundle.
|
||||
func (s *state) initialize(fn uintptr, args unsafe.Pointer) {
|
||||
// Create a stack.
|
||||
stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
// Invoke architecture-specific initialization.
|
||||
s.archInit(stack, fn, args)
|
||||
}
|
||||
|
||||
//go:linkname runqueuePushBack runtime.runqueuePushBack
|
||||
func runqueuePushBack(*Task)
|
||||
|
||||
// start creates and starts a new goroutine with the given function and arguments.
|
||||
// The new goroutine is scheduled to run later.
|
||||
func start(fn uintptr, args unsafe.Pointer) {
|
||||
t := &Task{}
|
||||
t.state.initialize(fn, args)
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// +build scheduler.tasks, cortexm
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const stackSize = 1024
|
||||
|
||||
// calleeSavedRegs is the list of registers that must be saved and restored when
|
||||
// switching between tasks. Also see scheduler_cortexm.S that relies on the
|
||||
// exact layout of this struct.
|
||||
type calleeSavedRegs struct {
|
||||
r4 uintptr
|
||||
r5 uintptr
|
||||
r6 uintptr
|
||||
r7 uintptr
|
||||
r8 uintptr
|
||||
r9 uintptr
|
||||
r10 uintptr
|
||||
r11 uintptr
|
||||
|
||||
pc uintptr
|
||||
}
|
||||
|
||||
// registers gets a pointer to the registers stored at the top of the stack.
|
||||
func (s *state) registers() *calleeSavedRegs {
|
||||
return (*calleeSavedRegs)(unsafe.Pointer(s.sp))
|
||||
}
|
||||
|
||||
// startTask is a small wrapper function that sets up the first (and only)
|
||||
// argument to the new goroutine and makes sure it is exited when the goroutine
|
||||
// finishes.
|
||||
//go:extern tinygo_startTask
|
||||
var startTask [0]uint8
|
||||
|
||||
// archInit runs architecture-specific setup for the goroutine startup.
|
||||
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
|
||||
// Set up the stack canary, a random number that should be checked when
|
||||
// switching from the task back to the scheduler. The stack canary pointer
|
||||
// points to the first word of the stack. If it has changed between now and
|
||||
// the next stack switch, there was a stack overflow.
|
||||
s.canaryPtr = &stack[0]
|
||||
*s.canaryPtr = stackCanary
|
||||
|
||||
// Store the initial sp for the startTask function (implemented in assembly).
|
||||
s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))]))
|
||||
|
||||
// Initialize the registers.
|
||||
// These will be popped off of the stack on the first resume of the goroutine.
|
||||
r := s.registers()
|
||||
|
||||
// Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S).
|
||||
// This assembly code calls a function (passed in r4) with a single argument (passed in r5).
|
||||
// After the function returns, it calls Pause().
|
||||
r.pc = uintptr(unsafe.Pointer(&startTask))
|
||||
|
||||
// Pass the function to call in r4.
|
||||
// This function is a compiler-generated wrapper which loads arguments out of a struct pointer.
|
||||
// See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information.
|
||||
r.r4 = fn
|
||||
|
||||
// Pass the pointer to the arguments struct in r5.
|
||||
r.r5 = uintptr(args)
|
||||
}
|
||||
|
||||
func (s *state) resume() {
|
||||
switchToTask(s.sp)
|
||||
}
|
||||
|
||||
//export tinygo_switchToTask
|
||||
func switchToTask(uintptr)
|
||||
|
||||
//export tinygo_switchToScheduler
|
||||
func switchToScheduler(*uintptr)
|
||||
|
||||
func (s *state) pause() {
|
||||
switchToScheduler(&s.sp)
|
||||
}
|
||||
|
||||
//export tinygo_pause
|
||||
func pause() {
|
||||
Pause()
|
||||
}
|
||||
Reference in New Issue
Block a user