mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-07 04:23:41 +00:00
Add goroutines and function pointers
This commit is contained in:
@@ -7,15 +7,34 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
led := machine.GPIO{17} // LED 1 on the PCA10040
|
||||
go led1()
|
||||
led2()
|
||||
}
|
||||
|
||||
func led1() {
|
||||
led := machine.GPIO{machine.LED}
|
||||
led.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT})
|
||||
for {
|
||||
println("LED on")
|
||||
led.Set(false)
|
||||
runtime.Sleep(runtime.Millisecond * 500)
|
||||
println("+")
|
||||
led.Low()
|
||||
runtime.Sleep(runtime.Millisecond * 1000)
|
||||
|
||||
println("LED off")
|
||||
led.Set(true)
|
||||
runtime.Sleep(runtime.Millisecond * 500)
|
||||
println("-")
|
||||
led.High()
|
||||
runtime.Sleep(runtime.Millisecond * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
func led2() {
|
||||
led := machine.GPIO{machine.LED2}
|
||||
led.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT})
|
||||
for {
|
||||
println(" +")
|
||||
led.Low()
|
||||
runtime.Sleep(runtime.Millisecond * 420)
|
||||
|
||||
println(" -")
|
||||
led.High()
|
||||
runtime.Sleep(runtime.Millisecond * 420)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ func main() {
|
||||
printItf(5)
|
||||
printItf(byte('x'))
|
||||
printItf("foo")
|
||||
|
||||
runFunc(hello) // must be indirect to avoid obvious inlining
|
||||
}
|
||||
|
||||
func runFunc(f func()) {
|
||||
f()
|
||||
}
|
||||
|
||||
func hello() {
|
||||
println("hello from function pointer!")
|
||||
}
|
||||
|
||||
func strlen(s string) int {
|
||||
|
||||
@@ -6,6 +6,13 @@ const Compiler = "tgo"
|
||||
// The bitness of the CPU (e.g. 8, 32, 64). Set by the compiler as a constant.
|
||||
var TargetBits uint8
|
||||
|
||||
func Sleep(d Duration) {
|
||||
// This function is treated specially by the compiler: when goroutines are
|
||||
// used, it is transformed into a llvm.coro.suspend() call.
|
||||
// When goroutines are not used this function behaves as normal.
|
||||
sleep(d)
|
||||
}
|
||||
|
||||
func _panic(message interface{}) {
|
||||
printstring("panic: ")
|
||||
printitf(message)
|
||||
|
||||
@@ -2,9 +2,26 @@ source_filename = "runtime/runtime.ll"
|
||||
|
||||
declare void @runtime.initAll()
|
||||
declare void @main.main()
|
||||
declare i8* @main.main$async(i8*)
|
||||
declare void @runtime.scheduler(i8*)
|
||||
|
||||
; Will be changed to true if there are 'go' statements in the compiled program.
|
||||
@.has_scheduler = private unnamed_addr constant i1 false
|
||||
|
||||
define i32 @main() {
|
||||
call void @runtime.initAll()
|
||||
%has_scheduler = load i1, i1* @.has_scheduler
|
||||
; This branch will be optimized away. Only one of the targets will remain.
|
||||
br i1 %has_scheduler, label %with_scheduler, label %without_scheduler
|
||||
|
||||
with_scheduler:
|
||||
; Initialize main and run the scheduler.
|
||||
%main = call i8* @main.main$async(i8* null)
|
||||
call void @runtime.scheduler(i8* %main)
|
||||
ret i32 0
|
||||
|
||||
without_scheduler:
|
||||
; No scheduler is necessary. Call main directly.
|
||||
call void @main.main()
|
||||
ret i32 0
|
||||
}
|
||||
|
||||
@@ -45,8 +45,32 @@ func putchar(c byte) {
|
||||
nrf.UART0.EVENTS_TXDRDY = 0
|
||||
}
|
||||
|
||||
func Sleep(d Duration) {
|
||||
C.rtc_sleep(C.uint32_t(d / 32)) // TODO: not accurate (must be d / 30.5175...)
|
||||
func sleep(d Duration) {
|
||||
ticks64 := d / 32
|
||||
for ticks64 != 0 {
|
||||
monotime() // update timestamp
|
||||
ticks := uint32(ticks64) & 0x7fffff // 23 bits (to be on the safe side)
|
||||
C.rtc_sleep(C.uint32_t(ticks)) // TODO: not accurate (must be d / 30.5175...)
|
||||
ticks64 -= Duration(ticks)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
timestamp uint64 // microseconds since boottime
|
||||
rtcLastCounter uint32 // 24 bits ticks
|
||||
)
|
||||
|
||||
// Monotonically increasing numer of microseconds since start.
|
||||
//
|
||||
// Note: very long pauses between measurements (more than 8 minutes) may
|
||||
// overflow the counter, leading to incorrect results. This might be fixed by
|
||||
// handling the overflow event.
|
||||
func monotime() uint64 {
|
||||
rtcCounter := uint32(nrf.RTC0.COUNTER)
|
||||
offset := (rtcCounter - rtcLastCounter) % 0xffffff // change since last measurement
|
||||
rtcLastCounter = rtcCounter
|
||||
timestamp += uint64(offset * 32) // TODO: not precise
|
||||
return timestamp
|
||||
}
|
||||
|
||||
func abort() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
// #include <stdio.h>
|
||||
// #include <stdlib.h>
|
||||
// #include <unistd.h>
|
||||
// #include <time.h>
|
||||
import "C"
|
||||
|
||||
const Microsecond = 1
|
||||
@@ -18,10 +19,20 @@ func putchar(c byte) {
|
||||
C.putchar(C.int(c))
|
||||
}
|
||||
|
||||
func Sleep(d Duration) {
|
||||
func sleep(d Duration) {
|
||||
C.usleep(C.useconds_t(d))
|
||||
}
|
||||
|
||||
// Return monotonic time in microseconds.
|
||||
//
|
||||
// TODO: use nanoseconds?
|
||||
// TODO: noescape
|
||||
func monotime() uint64 {
|
||||
var ts C.struct_timespec
|
||||
C.clock_gettime(C.CLOCK_MONOTONIC, &ts)
|
||||
return uint64(ts.tv_sec) * 1000 * 1000 + uint64(ts.tv_nsec) / 1000
|
||||
}
|
||||
|
||||
func abort() {
|
||||
C.abort()
|
||||
}
|
||||
@@ -35,5 +46,5 @@ func alloc(size uintptr) unsafe.Pointer {
|
||||
}
|
||||
|
||||
func free(ptr unsafe.Pointer) {
|
||||
C.free(ptr)
|
||||
//C.free(ptr) // TODO
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
|
||||
package runtime
|
||||
|
||||
// This file implements the Go scheduler using coroutines.
|
||||
// A goroutine contains a whole stack. A coroutine is just a single function.
|
||||
// How do we use coroutines for goroutines, then?
|
||||
// * Every function that contains a blocking call (like sleep) is marked
|
||||
// blocking, and all it's parents (callers) are marked blocking as well
|
||||
// transitively until the root (main.main or a go statement).
|
||||
// * A blocking function that calls a non-blocking function is called as
|
||||
// usual.
|
||||
// * A blocking function that calls a blocking function passes its own
|
||||
// coroutine handle as a parameter to the subroutine and will make sure it's
|
||||
// own coroutine is removed from the scheduler. When the subroutine returns,
|
||||
// it will re-insert the parent into the scheduler.
|
||||
// Note that a goroutine is generally called a 'task' for brevity and because
|
||||
// that's the more common term among RTOSes. But a goroutine and a task are
|
||||
// basically the same thing. Although, the code often uses the word 'task' to
|
||||
// refer to both a coroutine and a goroutine, as most of the scheduler isn't
|
||||
// aware of the difference.
|
||||
//
|
||||
// For more background on coroutines in LLVM:
|
||||
// https://llvm.org/docs/Coroutines.html
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// State/promise of a task. Internally represented as:
|
||||
//
|
||||
// {i8 state, i32 data, i8* next}
|
||||
type taskState struct {
|
||||
state uint8
|
||||
data uint32
|
||||
next taskInstance
|
||||
}
|
||||
|
||||
// Pointer to a task. Wrap unsafe.Pointer to provide some sort of type safety.
|
||||
type taskInstance unsafe.Pointer
|
||||
|
||||
// Various states a task can be in. Not always updated (especially
|
||||
// TASK_STATE_RUNNABLE).
|
||||
const (
|
||||
TASK_STATE_RUNNABLE = iota
|
||||
TASK_STATE_SLEEP
|
||||
TASK_STATE_CALL // waiting for a sub-coroutine
|
||||
)
|
||||
|
||||
// Queues used by the scheduler.
|
||||
//
|
||||
// TODO: runqueueFront can be removed by making the run queue a circular linked
|
||||
// list. The runqueueBack will simply refer to the front in the 'next' pointer.
|
||||
var (
|
||||
runqueueFront taskInstance
|
||||
runqueueBack taskInstance
|
||||
sleepQueue taskInstance
|
||||
sleepQueueBaseTime uint64
|
||||
)
|
||||
|
||||
// Translated to void @llvm.coro.resume(i8*).
|
||||
func _llvm_coro_resume(taskInstance)
|
||||
|
||||
// Translated to void @llvm.coro.destroy(i8*).
|
||||
func _llvm_coro_destroy(taskInstance)
|
||||
|
||||
// Translated to i1 @llvm.coro.done(i8*).
|
||||
func _llvm_coro_done(taskInstance) bool
|
||||
|
||||
// Translated to i8* @llvm.coro.promise(i8*, i32, i1).
|
||||
func _llvm_coro_promise(taskInstance, int32, bool) unsafe.Pointer
|
||||
|
||||
// Get the promise belonging to a task.
|
||||
func taskPromise(t taskInstance) *taskState {
|
||||
return (*taskState)(_llvm_coro_promise(t, 4, false))
|
||||
}
|
||||
|
||||
// Simple logging, for debugging.
|
||||
func scheduleLog(msg string) {
|
||||
//println(msg)
|
||||
}
|
||||
|
||||
// Simple logging with a task pointer, for debugging.
|
||||
func scheduleLogTask(msg string, t taskInstance) {
|
||||
//println(msg, t)
|
||||
}
|
||||
|
||||
// Set the task state to sleep for a given time.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func sleepTask(caller taskInstance, duration Duration) {
|
||||
promise := taskPromise(caller)
|
||||
promise.state = TASK_STATE_SLEEP
|
||||
promise.data = uint32(duration) // TODO: longer durations
|
||||
}
|
||||
|
||||
// Wait for the result of an async call. This means that the parent goroutine
|
||||
// will be removed from the runqueue and be rescheduled by the callee.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func waitForAsyncCall(caller taskInstance) {
|
||||
promise := taskPromise(caller)
|
||||
promise.state = TASK_STATE_CALL
|
||||
}
|
||||
|
||||
// Add a task to the runnable or sleep queue, depending on the state.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func scheduleTask(t taskInstance) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
scheduleLogTask(" schedule task:", t)
|
||||
// See what we should do with this task: try to execute it directly
|
||||
// again or let it sleep for a bit.
|
||||
promise := taskPromise(t)
|
||||
if promise.state == TASK_STATE_CALL {
|
||||
return // calling an async task, the subroutine will re-active the parent
|
||||
} else if promise.state == TASK_STATE_SLEEP && promise.data != 0 {
|
||||
addSleepTask(t)
|
||||
} else {
|
||||
pushTask(t)
|
||||
}
|
||||
}
|
||||
|
||||
// Add this task to the end of the run queue. May also destroy the task if it's
|
||||
// done.
|
||||
func pushTask(t taskInstance) {
|
||||
if _llvm_coro_done(t) {
|
||||
scheduleLogTask(" destroy task:", t)
|
||||
_llvm_coro_destroy(t)
|
||||
return
|
||||
}
|
||||
if runqueueBack == nil { // empty runqueue
|
||||
runqueueBack = t
|
||||
runqueueFront = t
|
||||
} else {
|
||||
lastTaskPromise := taskPromise(runqueueBack)
|
||||
lastTaskPromise.next = t
|
||||
runqueueBack = t
|
||||
}
|
||||
}
|
||||
|
||||
// Get a task from the front of the run queue. May return nil if there is none.
|
||||
func popTask() taskInstance {
|
||||
t := runqueueFront
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
scheduleLogTask(" popTask:", t)
|
||||
promise := taskPromise(t)
|
||||
runqueueFront = promise.next
|
||||
if runqueueFront == nil {
|
||||
runqueueBack = nil
|
||||
}
|
||||
promise.next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// Add this task to the sleep queue, assuming its state is set to sleeping.
|
||||
func addSleepTask(t taskInstance) {
|
||||
now := monotime()
|
||||
if sleepQueue == nil {
|
||||
scheduleLog(" -> sleep new queue")
|
||||
// Create new linked list for the sleep queue.
|
||||
sleepQueue = t
|
||||
sleepQueueBaseTime = now
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure promise.data is relative to the queue time base.
|
||||
promise := taskPromise(t)
|
||||
|
||||
// Insert at front of sleep queue.
|
||||
if promise.data < taskPromise(sleepQueue).data {
|
||||
scheduleLog(" -> sleep at start")
|
||||
taskPromise(sleepQueue).data -= promise.data
|
||||
promise.next = sleepQueue
|
||||
sleepQueue = t
|
||||
return
|
||||
}
|
||||
|
||||
// Add to sleep queue (in the middle or at the end).
|
||||
queueIndex := sleepQueue
|
||||
for {
|
||||
promise.data -= taskPromise(queueIndex).data
|
||||
if taskPromise(queueIndex).next == nil || taskPromise(queueIndex).data > promise.data {
|
||||
if taskPromise(queueIndex).next == nil {
|
||||
scheduleLog(" -> sleep at end")
|
||||
promise.next = nil
|
||||
} else {
|
||||
scheduleLog(" -> sleep in middle")
|
||||
promise.next = taskPromise(queueIndex).next
|
||||
taskPromise(promise.next).data -= promise.data
|
||||
}
|
||||
taskPromise(queueIndex).next = t
|
||||
break
|
||||
}
|
||||
queueIndex = taskPromise(queueIndex).next
|
||||
}
|
||||
}
|
||||
|
||||
// Run the scheduler until all tasks have finished.
|
||||
// It takes an initial task (main.main) to bootstrap.
|
||||
func scheduler(main taskInstance) {
|
||||
// Initial task.
|
||||
scheduleTask(main)
|
||||
|
||||
// Main scheduler loop.
|
||||
for {
|
||||
scheduleLog("\n schedule")
|
||||
now := monotime()
|
||||
|
||||
// Add tasks that are done sleeping to the end of the runqueue so they
|
||||
// will be executed soon.
|
||||
if sleepQueue != nil && now - sleepQueueBaseTime >= uint64(taskPromise(sleepQueue).data) {
|
||||
scheduleLog(" run <- sleep")
|
||||
t := sleepQueue
|
||||
promise := taskPromise(t)
|
||||
sleepQueueBaseTime += uint64(promise.data)
|
||||
sleepQueue = promise.next
|
||||
promise.next = nil
|
||||
pushTask(t)
|
||||
}
|
||||
|
||||
scheduleLog(" <- popTask")
|
||||
t := popTask()
|
||||
if t == nil {
|
||||
if sleepQueue == nil {
|
||||
// No more tasks to execute.
|
||||
// It would be nice if we could detect deadlocks here, because
|
||||
// there might still be functions waiting on each other in a
|
||||
// deadlock.
|
||||
scheduleLog(" no tasks left!")
|
||||
return
|
||||
}
|
||||
scheduleLog(" sleeping...")
|
||||
timeLeft := uint64(taskPromise(sleepQueue).data) - (now - sleepQueueBaseTime)
|
||||
sleep(Duration(timeLeft))
|
||||
continue
|
||||
}
|
||||
|
||||
// Run the given task.
|
||||
scheduleLogTask(" run:", t)
|
||||
_llvm_coro_resume(t)
|
||||
|
||||
// Add the just resumed task to the run queue or the sleep queue.
|
||||
scheduleTask(t)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user