aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/internal/task/task_asyncify.go
blob: 637a6b22373fb5a0865a6a22127bdf1070fd9de7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//go: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 unsafe.Pointer

	// asyncify is stack pointer of the C stack.
	// This starts from the top and grows downwards.
	csp unsafe.Pointer

	// Pointer to the first (lowest address) of the stack. It must never be
	// overwritten. It can be checked from time to time to see whether a stack
	// overflow happened in the past.
	canaryPtr *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)
	scheduleTask(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 := runtime_alloc(stackSize, nil)

	// 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 = (*uintptr)(stack)
	*s.canaryPtr = stackCanary

	// Calculate stack base addresses.
	s.asyncifysp = unsafe.Add(stack, unsafe.Sizeof(uintptr(0)))
	s.csp = unsafe.Add(stack, stackSize)
}

// 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() {
	if *currentTask.state.canaryPtr != stackCanary {
		runtimePanic("stack overflow")
	}

	currentTask.state.unwind()
}

//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
	t.gcData.swap()
	currentTask = t
	if !t.state.launched {
		t.state.launch()
		t.state.launched = true
	} else {
		t.state.rewind()
	}
	currentTask = prevTask
	t.gcData.swap()
	if uintptr(t.state.asyncifysp) > uintptr(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
}