mirror of https://github.com/tinygo-org/tinygo.git
Browse Source
This change implements a new "scheduler" for WebAssembly using binaryen's asyncify transform. This is more reliable than the current "coroutines" transform, and works with non-Go code in the call stack. runtime (js/wasm): handle scheduler nesting If WASM calls into JS which calls back into WASM, it is possible for the scheduler to nest. The event from the callback must be handled immediately, so the task cannot simply be deferred to the outer scheduler. This creates a minimal scheduler loop which is used to handle such nesting.pull/2260/head
Nia Waldvogel
3 years ago
committed by
Ron Evans
31 changed files with 785 additions and 62 deletions
@ -0,0 +1,2 @@ |
|||
* |
|||
!.gitignore |
@ -0,0 +1,210 @@ |
|||
; ModuleID = 'goroutine.go' |
|||
source_filename = "goroutine.go" |
|||
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" |
|||
target triple = "wasm32-unknown-wasi" |
|||
|
|||
%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } |
|||
%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } |
|||
%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" } |
|||
%"internal/task.state" = type { i32, i8*, %"internal/task.stackState", i1 } |
|||
%"internal/task.stackState" = type { i32, i32 } |
|||
%runtime.chanSelectState = type { %runtime.channel*, i8* } |
|||
|
|||
@"main$string" = internal unnamed_addr constant [4 x i8] c"test", align 1 |
|||
|
|||
declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
declare void @main.regularFunction(i32, i8*, i8*) |
|||
|
|||
declare void @runtime.deadlock(i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define linkonce_odr void @"main.regularFunction$gowrapper"(i8* %0) unnamed_addr #1 { |
|||
entry: |
|||
%unpack.int = ptrtoint i8* %0 to i32 |
|||
call void @main.regularFunction(i32 %unpack.int, i8* undef, i8* undef) #0 |
|||
call void @runtime.deadlock(i8* undef, i8* undef) #0 |
|||
unreachable |
|||
} |
|||
|
|||
declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #2 { |
|||
entry: |
|||
%unpack.int = ptrtoint i8* %0 to i32 |
|||
call void @"main.inlineFunctionGoroutine$1"(i32 %unpack.int, i8* undef, i8* undef) |
|||
call void @runtime.deadlock(i8* undef, i8* undef) #0 |
|||
unreachable |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
%n = call i8* @runtime.alloc(i32 4, i8* nonnull inttoptr (i32 3 to i8*), i8* undef, i8* null) #0 |
|||
%0 = bitcast i8* %n to i32* |
|||
store i32 3, i32* %0, align 4 |
|||
%1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0 |
|||
%2 = bitcast i8* %1 to i32* |
|||
store i32 5, i32* %2, align 4 |
|||
%3 = getelementptr inbounds i8, i8* %1, i32 4 |
|||
%4 = bitcast i8* %3 to i8** |
|||
store i8* %n, i8** %4, align 4 |
|||
call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.closureFunctionGoroutine$1$gowrapper" to i32), i8* nonnull %1, i32 8192, i8* undef, i8* null) #0 |
|||
%5 = load i32, i32* %0, align 4 |
|||
call void @runtime.printint32(i32 %5, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
%unpack.ptr = bitcast i8* %context to i32* |
|||
store i32 7, i32* %unpack.ptr, align 4 |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define linkonce_odr void @"main.closureFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #3 { |
|||
entry: |
|||
%1 = bitcast i8* %0 to i32* |
|||
%2 = load i32, i32* %1, align 4 |
|||
%3 = getelementptr inbounds i8, i8* %0, i32 4 |
|||
%4 = bitcast i8* %3 to i8** |
|||
%5 = load i8*, i8** %4, align 4 |
|||
call void @"main.closureFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef) |
|||
call void @runtime.deadlock(i8* undef, i8* undef) #0 |
|||
unreachable |
|||
} |
|||
|
|||
declare void @runtime.printint32(i32, i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.funcGoroutine(i8* %fn.context, void ()* %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
%0 = call i8* @runtime.alloc(i32 12, i8* null, i8* undef, i8* null) #0 |
|||
%1 = bitcast i8* %0 to i32* |
|||
store i32 5, i32* %1, align 4 |
|||
%2 = getelementptr inbounds i8, i8* %0, i32 4 |
|||
%3 = bitcast i8* %2 to i8** |
|||
store i8* %fn.context, i8** %3, align 4 |
|||
%4 = getelementptr inbounds i8, i8* %0, i32 8 |
|||
%5 = bitcast i8* %4 to void ()** |
|||
store void ()* %fn.funcptr, void ()** %5, align 4 |
|||
call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @main.funcGoroutine.gowrapper to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define linkonce_odr void @main.funcGoroutine.gowrapper(i8* %0) unnamed_addr #4 { |
|||
entry: |
|||
%1 = bitcast i8* %0 to i32* |
|||
%2 = load i32, i32* %1, align 4 |
|||
%3 = getelementptr inbounds i8, i8* %0, i32 4 |
|||
%4 = bitcast i8* %3 to i8** |
|||
%5 = load i8*, i8** %4, align 4 |
|||
%6 = getelementptr inbounds i8, i8* %0, i32 8 |
|||
%7 = bitcast i8* %6 to void (i32, i8*, i8*)** |
|||
%8 = load void (i32, i8*, i8*)*, void (i32, i8*, i8*)** %7, align 4 |
|||
call void %8(i32 %2, i8* %5, i8* undef) #0 |
|||
call void @runtime.deadlock(i8* undef, i8* undef) #0 |
|||
unreachable |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
ret void |
|||
} |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
%copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*) |
|||
|
|||
; Function Attrs: nounwind |
|||
define hidden void @main.startInterfaceMethod(i32 %itf.typecode, i8* %itf.value, i8* %context, i8* %parentHandle) unnamed_addr #0 { |
|||
entry: |
|||
%0 = call i8* @runtime.alloc(i32 16, i8* null, i8* undef, i8* null) #0 |
|||
%1 = bitcast i8* %0 to i8** |
|||
store i8* %itf.value, i8** %1, align 4 |
|||
%2 = getelementptr inbounds i8, i8* %0, i32 4 |
|||
%.repack = bitcast i8* %2 to i8** |
|||
store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"main$string", i32 0, i32 0), i8** %.repack, align 4 |
|||
%.repack1 = getelementptr inbounds i8, i8* %0, i32 8 |
|||
%3 = bitcast i8* %.repack1 to i32* |
|||
store i32 4, i32* %3, align 4 |
|||
%4 = getelementptr inbounds i8, i8* %0, i32 12 |
|||
%5 = bitcast i8* %4 to i32* |
|||
store i32 %itf.typecode, i32* %5, align 4 |
|||
call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper" to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0 |
|||
ret void |
|||
} |
|||
|
|||
declare void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8*, i8*, i32, i32, i8*, i8*) #5 |
|||
|
|||
; Function Attrs: nounwind |
|||
define linkonce_odr void @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper"(i8* %0) unnamed_addr #6 { |
|||
entry: |
|||
%1 = bitcast i8* %0 to i8** |
|||
%2 = load i8*, i8** %1, align 4 |
|||
%3 = getelementptr inbounds i8, i8* %0, i32 4 |
|||
%4 = bitcast i8* %3 to i8** |
|||
%5 = load i8*, i8** %4, align 4 |
|||
%6 = getelementptr inbounds i8, i8* %0, i32 8 |
|||
%7 = bitcast i8* %6 to i32* |
|||
%8 = load i32, i32* %7, align 4 |
|||
%9 = getelementptr inbounds i8, i8* %0, i32 12 |
|||
%10 = bitcast i8* %9 to i32* |
|||
%11 = load i32, i32* %10, align 4 |
|||
call void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8* %2, i8* %5, i32 %8, i32 %11, i8* undef, i8* undef) #0 |
|||
call void @runtime.deadlock(i8* undef, i8* undef) #0 |
|||
unreachable |
|||
} |
|||
|
|||
attributes #0 = { nounwind } |
|||
attributes #1 = { nounwind "tinygo-gowrapper"="main.regularFunction" } |
|||
attributes #2 = { nounwind "tinygo-gowrapper"="main.inlineFunctionGoroutine$1" } |
|||
attributes #3 = { nounwind "tinygo-gowrapper"="main.closureFunctionGoroutine$1" } |
|||
attributes #4 = { nounwind "tinygo-gowrapper" } |
|||
attributes #5 = { "tinygo-invoke"="reflect/methods.Print(string)" "tinygo-methods"="reflect/methods.Print(string)" } |
|||
attributes #6 = { nounwind "tinygo-gowrapper"="interface:{Print:func:{basic:string}{}}.Print$invoke" } |
@ -0,0 +1,127 @@ |
|||
//go:build scheduler.asyncify
|
|||
// +build scheduler.asyncify
|
|||
|
|||
package task |
|||
|
|||
import ( |
|||
"unsafe" |
|||
) |
|||
|
|||
// 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))) |
|||
|
|||
//go:linkname runtimePanic runtime.runtimePanic
|
|||
func runtimePanic(str string) |
|||
|
|||
// state is a structure which holds a reference to the state of the task.
|
|||
// When the task is suspended, the stack pointers are saved here.
|
|||
type state struct { |
|||
// entry is the entry function of the task.
|
|||
// This is needed every time the function is invoked so that asyncify knows what to rewind.
|
|||
entry uintptr |
|||
|
|||
// args are a pointer to a struct holding the arguments of the function.
|
|||
args unsafe.Pointer |
|||
|
|||
// stackState is the state of the stack while unwound.
|
|||
stackState |
|||
|
|||
launched bool |
|||
} |
|||
|
|||
// stackState is the saved state of a stack while unwound.
|
|||
// The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two.
|
|||
type stackState struct { |
|||
// asyncify is the stack pointer of the asyncify stack.
|
|||
// This starts from the bottom and grows upwards.
|
|||
asyncifysp uintptr |
|||
|
|||
// asyncify is stack pointer of the C stack.
|
|||
// This starts from the top and grows downwards.
|
|||
csp uintptr |
|||
} |
|||
|
|||
// start creates and starts a new goroutine with the given function and arguments.
|
|||
// The new goroutine is immediately started.
|
|||
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) { |
|||
t := &Task{} |
|||
t.state.initialize(fn, args, stackSize) |
|||
runqueuePushBack(t) |
|||
} |
|||
|
|||
//export tinygo_launch
|
|||
func (*state) launch() |
|||
|
|||
//go:linkname align runtime.align
|
|||
func align(p uintptr) uintptr |
|||
|
|||
// initialize the state and prepare to call the specified function with the specified argument bundle.
|
|||
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) { |
|||
// Save the entry call.
|
|||
s.entry = fn |
|||
s.args = args |
|||
|
|||
// Create a stack.
|
|||
stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0))) |
|||
|
|||
// Calculate stack base addresses.
|
|||
s.asyncifysp = uintptr(unsafe.Pointer(&stack[0])) |
|||
s.csp = uintptr(unsafe.Pointer(&stack[0])) + uintptr(len(stack))*unsafe.Sizeof(uintptr(0)) |
|||
stack[0] = stackCanary |
|||
} |
|||
|
|||
//go:linkname runqueuePushBack runtime.runqueuePushBack
|
|||
func runqueuePushBack(*Task) |
|||
|
|||
// 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.
|
|||
func Pause() { |
|||
// This is mildly unsafe but this is also the only place we can do this.
|
|||
if *(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) != stackCanary { |
|||
runtimePanic("stack overflow") |
|||
} |
|||
|
|||
currentTask.state.unwind() |
|||
|
|||
*(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) = stackCanary |
|||
} |
|||
|
|||
//export tinygo_unwind
|
|||
func (*stackState) unwind() |
|||
|
|||
// Resume the task until it pauses or completes.
|
|||
// This may only be called from the scheduler.
|
|||
func (t *Task) Resume() { |
|||
// The current task must be saved and restored because this can nest on WASM with JS.
|
|||
prevTask := currentTask |
|||
currentTask = t |
|||
if !t.state.launched { |
|||
t.state.launch() |
|||
t.state.launched = true |
|||
} else { |
|||
t.state.rewind() |
|||
} |
|||
currentTask = prevTask |
|||
if t.state.asyncifysp > t.state.csp { |
|||
runtimePanic("stack overflow") |
|||
} |
|||
} |
|||
|
|||
//export tinygo_rewind
|
|||
func (*state) rewind() |
|||
|
|||
// OnSystemStack returns whether the caller is running on the system stack.
|
|||
func OnSystemStack() bool { |
|||
// If there is not an active goroutine, then this must be running on the system stack.
|
|||
return Current() == nil |
|||
} |
@ -0,0 +1,99 @@ |
|||
.globaltype __stack_pointer, i32 |
|||
|
|||
.global tinygo_unwind |
|||
.type tinygo_unwind,@function |
|||
tinygo_unwind: // func (state *stackState) unwind() |
|||
.functype tinygo_unwind (i32) -> () |
|||
// Check if we are rewinding. |
|||
i32.const 0 |
|||
i32.load8_u tinygo_rewinding |
|||
if // if tinygo_rewinding { |
|||
// Stop rewinding. |
|||
call stop_rewind |
|||
i32.const 0 |
|||
i32.const 0 |
|||
i32.store8 tinygo_rewinding // tinygo_rewinding = false; |
|||
else |
|||
// Save the C stack pointer (destination structure pointer is in local 0). |
|||
local.get 0 |
|||
global.get __stack_pointer |
|||
i32.store 4 // state.csp = getCurrentStackPointer() |
|||
// Ask asyncify to unwind. |
|||
// When resuming, asyncify will return this function with tinygo_rewinding set to true. |
|||
local.get 0 |
|||
call start_unwind // asyncify.start_unwind(state) |
|||
end_if |
|||
return |
|||
end_function |
|||
|
|||
.global tinygo_launch |
|||
.type tinygo_launch,@function |
|||
tinygo_launch: // func (state *state) launch() |
|||
.functype tinygo_launch (i32) -> () |
|||
// Switch to the goroutine's C stack. |
|||
global.get __stack_pointer // prev := getCurrentStackPointer() |
|||
local.get 0 |
|||
i32.load 12 |
|||
global.set __stack_pointer // setStackPointer(state.csp) |
|||
// Get the argument pack and entry pointer. |
|||
local.get 0 |
|||
i32.load 4 // args := state.args |
|||
local.get 0 |
|||
i32.load 0 // fn := state.entry |
|||
// Launch the entry function. |
|||
call_indirect (i32) -> () // fn(args) |
|||
// Stop unwinding. |
|||
call stop_unwind |
|||
// Restore the C stack. |
|||
global.set __stack_pointer // setStackPointer(prev) |
|||
return |
|||
end_function |
|||
|
|||
.global tinygo_rewind |
|||
.type tinygo_rewind,@function |
|||
tinygo_rewind: // func (state *state) rewind() |
|||
.functype tinygo_rewind (i32) -> () |
|||
// Switch to the goroutine's C stack. |
|||
global.get __stack_pointer // prev := getCurrentStackPointer() |
|||
local.get 0 |
|||
i32.load 12 |
|||
global.set __stack_pointer // setStackPointer(state.csp) |
|||
// Get the argument pack and entry pointer. |
|||
local.get 0 |
|||
i32.load 4 // args := state.args |
|||
local.get 0 |
|||
i32.load 0 // fn := state.entry |
|||
// Prepare to rewind. |
|||
i32.const 0 |
|||
i32.const 1 |
|||
i32.store8 tinygo_rewinding // tinygo_rewinding = true; |
|||
local.get 0 |
|||
i32.const 8 |
|||
i32.add |
|||
call start_rewind // asyncify.start_rewind(&state.stackState) |
|||
// Launch the entry function. |
|||
// This will actually rewind the call stack. |
|||
call_indirect (i32) -> () // fn(args) |
|||
// Stop unwinding. |
|||
call stop_unwind |
|||
// Restore the C stack. |
|||
global.set __stack_pointer // setStackPointer(prev) |
|||
return |
|||
end_function |
|||
|
|||
.functype start_unwind (i32) -> () |
|||
.import_module start_unwind, asyncify |
|||
.functype stop_unwind () -> () |
|||
.import_module stop_unwind, asyncify |
|||
.functype start_rewind (i32) -> () |
|||
.import_module start_rewind, asyncify |
|||
.functype stop_rewind () -> () |
|||
.import_module stop_rewind, asyncify |
|||
|
|||
.hidden tinygo_rewinding # @tinygo_rewinding |
|||
.type tinygo_rewinding,@object |
|||
.section .bss.tinygo_rewinding,"",@ |
|||
.globl tinygo_rewinding |
|||
tinygo_rewinding: |
|||
.int8 0 # 0x0 |
|||
.size tinygo_rewinding, 1 |
Loading…
Reference in new issue