diff --git a/README.md b/README.md index a73aa6b..24a81e1 100644 --- a/README.md +++ b/README.md @@ -24,28 +24,70 @@ import "golang.design/x/mainthread" func main() { mainthread.Init(fn) } -// fn is the actual main function +// fn is the actual main function func fn() { - // ... do whatever you want to do ... + // ... do stuff ... // mainthread.Call returns when f1 returns. Note that if f1 blocks // it will also block the execution of any subsequent calls on the // main thread. mainthread.Call(f1) - // ... do whatever you want to do ... + // ... do stuff ... + // mainthread.Go returns immediately and f2 is scheduled to be // executed in the future. mainthread.Go(f2) - // ... do whatever you want to do ... + // ... do stuff ... } func f1() { ... } func f2() { ... } ``` +If the given function triggers a panic, and called via `mainthread.Call`, +then the panic will be propagated to the same goroutine. One can capture +that panic, when possible: + +```go +defer func() { + if r := recover(); r != nil { + println(r) + } +}() + +mainthread.Call(func() { ... }) // if panic +``` + +If the given function triggers a panic, and called via `mainthread.Go`, +then the panic will be cached internally, until a call to the `Error()` method: + +```go +mainthread.Go(func() { ... }) // if panics + +// ... do stuff ... + +if err := mainthread.Error(); err != nil { // can be captured here. + println(err) +} +``` + +Note that a panic happens before `mainthread.Error()` returning the +panicked error. If one needs to guarantee `mainthread.Error()` indeed +captured the panic, a dummy function can be used as synchornization: + +```go +mainthread.Go(func() { panic("die") }) // if panics +mainthread.Call(func() {}) // for execution synchronization +err := mainthread.Error() // err must be non-nil +``` + + +It is possible to cache up to a maximum of 42 panicked errors. +More errors are ignored. + ## When do you need this package? Read this to learn more about the design purpose of this package: diff --git a/mainthread.go b/mainthread.go index c0e58b7..b42c203 100644 --- a/mainthread.go +++ b/mainthread.go @@ -16,27 +16,63 @@ // // // fn is the actual main function // func fn() { -// // ... do whatever you want to do ... +// // ... do stuff ... // -// // mainthread.Call returns when f1 returns. Note that if f1 -// // blocks it will also block the execution of any subsequent -// // calls on the main thread. +// // mainthread.Call returns when f1 returns. Note that if f1 blocks +// // it will also block the execution of any subsequent calls on the +// // main thread. // mainthread.Call(f1) // -// // ... do whatever you want to do ... +// // ... do stuff ... +// // // // mainthread.Go returns immediately and f2 is scheduled to be // // executed in the future. // mainthread.Go(f2) // -// // ... do whatever you want to do ... +// // ... do stuff ... // } // // func f1() { ... } // func f2() { ... } +// +// If the given function triggers a panic, and called via `mainthread.Call`, +// then the panic will be propagated to the same goroutine. One can capture +// that panic, when possible: +// +// defer func() { +// if r := recover(); r != nil { +// println(r) +// } +// }() +// +// mainthread.Call(func() { ... }) // if panic +// +// If the given function triggers a panic, and called via `mainthread.Go`, +// then the panic will be cached internally, until a call to the `Error()` method: +// +// mainthread.Go(func() { ... }) // if panics +// +// // ... do stuff ... +// +// if err := mainthread.Error(); err != nil { // can be captured here. +// println(err) +// } +// +// Note that a panic happens before `mainthread.Error()` returning the +// panicked error. If one needs to guarantee `mainthread.Error()` indeed +// captured the panic, a dummy function can be used as synchornization: +// +// mainthread.Go(func() { panic("die") }) // if panics +// mainthread.Call(func() {}) // for execution synchronization +// err := mainthread.Error() // err must be non-nil +// +// It is possible to cache up to a maximum of 42 panicked errors. +// More errors are ignored. package mainthread // import "golang.design/x/mainthread" import ( + "fmt" "runtime" "sync" ) @@ -50,12 +86,12 @@ func init() { // // Init must be called in the main.main function. func Init(main func()) { - done := donePool.Get().(chan struct{}) + done := donePool.Get().(chan error) defer donePool.Put(done) go func() { defer func() { - done <- struct{}{} + done <- nil }() main() }() @@ -63,10 +99,26 @@ func Init(main func()) { for { select { case f := <-funcQ: - f.fn() - if f.done != nil { - f.done <- struct{}{} - } + func() { + defer func() { + r := recover() + if f.done != nil { + if r != nil { + f.done <- fmt.Errorf("%v", r) + } else { + f.done <- nil + } + } else { + if r != nil { + select { + case erroQ <- fmt.Errorf("%v", r): + default: + } + } + } + }() + f.fn() + }() case <-done: return } @@ -75,11 +127,14 @@ func Init(main func()) { // Call calls f on the main thread and blocks until f finishes. func Call(f func()) { - done := donePool.Get().(chan struct{}) + done := donePool.Get().(chan error) defer donePool.Put(done) - funcQ <- funcData{fn: f, done: done} - <-done + data := funcData{fn: f, done: done} + funcQ <- data + if err := <-done; err != nil { + panic(err) + } } // Go schedules f to be called on the main thread. @@ -87,14 +142,29 @@ func Go(f func()) { funcQ <- funcData{fn: f} } +// Error returns an error that is captured if there are any panics +// happened on the mainthread. +// +// It is possible to cache up to a maximum of 42 panicked errors. +// More errors are ignored. +func Error() error { + select { + case err := <-erroQ: + return err + default: + return nil + } +} + var ( funcQ = make(chan funcData, runtime.GOMAXPROCS(0)) + erroQ = make(chan error, 42) donePool = sync.Pool{New: func() interface{} { - return make(chan struct{}) + return make(chan error) }} ) type funcData struct { fn func() - done chan struct{} + done chan error } diff --git a/mainthread_test.go b/mainthread_test.go index d606ce3..f25bf7b 100644 --- a/mainthread_test.go +++ b/mainthread_test.go @@ -92,3 +92,28 @@ func TestGo(t *testing.T) { case <-done: } } + +func TestPanickedFuncCall(t *testing.T) { + defer func() { + if r := recover(); r != nil { + return + } + t.Fatalf("expected to panic, but actually not") + }() + + mainthread.Call(func() { + panic("die") + }) +} + +func TestPanickedFuncGo(t *testing.T) { + defer func() { + if err := mainthread.Error(); err != nil { + return + } + t.Fatalf("expected to panic, but actually not") + }() + + mainthread.Go(func() { panic("die") }) + mainthread.Call(func() {}) // for sync +}