From c446e9a89f7af48c0716507cfb1f8c0eb897d06c Mon Sep 17 00:00:00 2001 From: Chris Shepherd Date: Sat, 22 Apr 2023 18:07:15 +0100 Subject: [PATCH 1/3] Add Return method Also adds Status type. --- webview.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/webview.go b/webview.go index cb70515a7..02d7586da 100644 --- a/webview.go +++ b/webview.go @@ -50,6 +50,16 @@ const ( HintMax = C.WEBVIEW_HINT_MAX ) +type Status int + +const ( + // Binding call succeeded + StatusSuccess = 0 + + // Binding call failed + StatusError = 1 +) + type WebView interface { // Run runs the main loop until it's terminated. After this function exits - @@ -110,6 +120,10 @@ type WebView interface { // f must be a function // f must return either value and error or just error Bind(name string, f interface{}) error + + // Return sends a response to the JavaScript callback. + // id is the callback id passed to the f function in Bind. + Return(id string, status Status, result interface{}) } type webview struct { @@ -313,3 +327,18 @@ func (w *webview) Bind(name string, f interface{}) error { C.CgoWebViewBind(w.w, cname, C.uintptr_t(index)) return nil } + +func (w *webview) Return(id string, status Status, result interface{}) { + jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) } + resultString := "" + + if status == StatusError { + resultString = jsString(result.(error).Error()) + } else { + resultString = jsString(result) + } + + s := C.CString(resultString) + defer C.free(unsafe.Pointer(s)) + C.webview_return(w.w, C.CString(id), C.int(status), s) +} From 8918907256e6832b851ac7d4138f540e6c1f5ac3 Mon Sep 17 00:00:00 2001 From: Chris Shepherd Date: Sat, 22 Apr 2023 18:09:33 +0100 Subject: [PATCH 2/3] Refactor Bind to enable consumer to call Return --- examples/bind.go | 72 +++++++++++++++++++++++++++++++++------- webview.go | 86 +++++++++++++++++------------------------------- webview_test.go | 8 ++--- 3 files changed, 95 insertions(+), 71 deletions(-) diff --git a/examples/bind.go b/examples/bind.go index ece8a92f9..3b1650391 100644 --- a/examples/bind.go +++ b/examples/bind.go @@ -1,25 +1,49 @@ package main -import "github.com/webview/webview" +import ( + "fmt" + "time" + + "github.com/webview/webview" +) const html = `
You tapped 0 time(s).
+
+ + + +
` type IncrementResult struct { Count uint `json:"count"` } +type SendNameResult struct { + Name string `json:"name"` +} + func main() { var count uint = 0 w := webview.New(false) @@ -28,9 +52,33 @@ func main() { w.SetSize(480, 320, webview.HintNone) // A binding that increments a value and immediately returns the new value. - w.Bind("increment", func() IncrementResult { + w.Bind("increment", func(id string) { count++ - return IncrementResult{Count: count} + + fmt.Println("incrementing count to", count) + + w.Return( + id, + webview.StatusSuccess, + IncrementResult{Count: count}, + ) + }) + + // A binding that returns a string after a delay. + w.Bind("sendName", func(id string, name string) { + go func() { + time.Sleep(2 * time.Second) + + var nameString string + + if len(name) > 0 { + nameString = name + } else { + nameString = "World" + } + + w.Return(id, webview.StatusSuccess, SendNameResult{Name: nameString}) + }() }) w.SetHtml(html) diff --git a/webview.go b/webview.go index 02d7586da..e6fe5875f 100644 --- a/webview.go +++ b/webview.go @@ -117,8 +117,8 @@ type WebView interface { // Request string is a JSON array of all the arguments passed to the // JavaScript function. // - // f must be a function - // f must return either value and error or just error + // f must be a function that accepts an id string as the first argument, and + // then any other arguments that you want to pass from the JavaScript function. Bind(name string, f interface{}) error // Return sends a response to the JavaScript callback. @@ -134,7 +134,7 @@ var ( m sync.Mutex index uintptr dispatch = map[uintptr]func(){} - bindings = map[uintptr]func(id, req string) (interface{}, error){} + bindings = map[uintptr]func(id, req string) error{} ) func boolToInt(b bool) C.int { @@ -236,21 +236,16 @@ func _webviewBindingGoCallback(w C.webview_t, id *C.char, req *C.char, index uin m.Lock() f := bindings[uintptr(index)] m.Unlock() - jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) } - status, result := 0, "" - if res, err := f(C.GoString(id), C.GoString(req)); err != nil { - status = -1 - result = jsString(err.Error()) - } else if b, err := json.Marshal(res); err != nil { - status = -1 - result = jsString(err.Error()) - } else { - status = 0 - result = string(b) + + // handle any error trying to call the binding function + if err := f(C.GoString(id), C.GoString(req)); err != nil { + jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) } + result := jsString(err.Error()) + + s := C.CString(result) + defer C.free(unsafe.Pointer(s)) + C.webview_return(w, id, C.int(StatusError), s) } - s := C.CString(result) - defer C.free(unsafe.Pointer(s)) - C.webview_return(w, id, C.int(status), s) } func (w *webview) Bind(name string, f interface{}) error { @@ -259,62 +254,43 @@ func (w *webview) Bind(name string, f interface{}) error { if v.Kind() != reflect.Func { return errors.New("only functions can be bound") } - // f must return either value and error or just error - if n := v.Type().NumOut(); n > 2 { - return errors.New("function may only return a value or a value+error") + + if v.Type().NumIn() > 0 && v.Type().In(0).Kind() != reflect.String { + return errors.New("first argument must be a string for the request id") } - binding := func(id, req string) (interface{}, error) { + binding := func(id, req string) error { raw := []json.RawMessage{} if err := json.Unmarshal([]byte(req), &raw); err != nil { - return nil, err + return err } isVariadic := v.Type().IsVariadic() numIn := v.Type().NumIn() - if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn) { - return nil, errors.New("function arguments mismatch") + + if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn-1) { + return errors.New("function arguments mismatch") + } + args := []reflect.Value{ + reflect.ValueOf(id), } - args := []reflect.Value{} for i := range raw { var arg reflect.Value - if isVariadic && i >= numIn-1 { + // skip `id` when looking at `f` parameters + argNum := i + 1 + if isVariadic && argNum >= numIn-1 { arg = reflect.New(v.Type().In(numIn - 1).Elem()) } else { - arg = reflect.New(v.Type().In(i)) + arg = reflect.New(v.Type().In(argNum)) } if err := json.Unmarshal(raw[i], arg.Interface()); err != nil { - return nil, err + return err } args = append(args, arg.Elem()) } - errorType := reflect.TypeOf((*error)(nil)).Elem() - res := v.Call(args) - switch len(res) { - case 0: - // No results from the function, just return nil - return nil, nil - case 1: - // One result may be a value, or an error - if res[0].Type().Implements(errorType) { - if res[0].Interface() != nil { - return nil, res[0].Interface().(error) - } - return nil, nil - } - return res[0].Interface(), nil - case 2: - // Two results: first one is value, second is error - if !res[1].Type().Implements(errorType) { - return nil, errors.New("second return value must be an error") - } - if res[1].Interface() == nil { - return res[0].Interface(), nil - } - return res[0].Interface(), res[1].Interface().(error) - default: - return nil, errors.New("unexpected number of return values") - } + + v.Call(args) + return nil } m.Lock() diff --git a/webview_test.go b/webview_test.go index 49f24d0c1..8871d6775 100644 --- a/webview_test.go +++ b/webview_test.go @@ -11,12 +11,12 @@ func Example() { w := New(true) defer w.Destroy() w.SetTitle("Hello") - w.Bind("noop", func() string { + w.Bind("noop", func(id string) { log.Println("hello") - return "hello" + w.Return(id, StatusSuccess, "hello") }) - w.Bind("add", func(a, b int) int { - return a + b + w.Bind("add", func(id string, a, b int) { + w.Return(id, StatusSuccess, a+b) }) w.Bind("quit", func() { w.Terminate() From 909c7cb58364ee2c887c42b1e77824b5664f75f0 Mon Sep 17 00:00:00 2001 From: Chris Shepherd Date: Tue, 25 Apr 2023 22:30:13 +0100 Subject: [PATCH 3/3] Switch to using a channel instead --- examples/bind.go | 79 +++++++++---------------- webview.go | 147 +++++++++++++++++++++++++++++------------------ webview_test.go | 13 ++++- 3 files changed, 132 insertions(+), 107 deletions(-) diff --git a/examples/bind.go b/examples/bind.go index 3b1650391..e8f4395c8 100644 --- a/examples/bind.go +++ b/examples/bind.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "time" "github.com/webview/webview" @@ -9,41 +8,32 @@ import ( const html = `
You tapped 0 time(s).
-
- - - -
+ +
Result of computation: 0
` type IncrementResult struct { Count uint `json:"count"` } -type SendNameResult struct { - Name string `json:"name"` -} - func main() { var count uint = 0 w := webview.New(false) @@ -52,33 +42,20 @@ func main() { w.SetSize(480, 320, webview.HintNone) // A binding that increments a value and immediately returns the new value. - w.Bind("increment", func(id string) { + w.Bind("increment", func() IncrementResult { count++ - - fmt.Println("incrementing count to", count) - - w.Return( - id, - webview.StatusSuccess, - IncrementResult{Count: count}, - ) + return IncrementResult{Count: count} }) - // A binding that returns a string after a delay. - w.Bind("sendName", func(id string, name string) { + // A binding that performs a long-running computation and returns the result + w.Bind("compute", func(a, b int) chan webview.BindCallbackResult { + ch := make(chan webview.BindCallbackResult) go func() { - time.Sleep(2 * time.Second) - - var nameString string - - if len(name) > 0 { - nameString = name - } else { - nameString = "World" - } - - w.Return(id, webview.StatusSuccess, SendNameResult{Name: nameString}) + defer close(ch) + time.Sleep(1 * time.Second) + ch <- webview.BindCallbackResult{Value: a * b, Error: nil} }() + return ch }) w.SetHtml(html) diff --git a/webview.go b/webview.go index e6fe5875f..b6fd2a6d5 100644 --- a/webview.go +++ b/webview.go @@ -50,15 +50,15 @@ const ( HintMax = C.WEBVIEW_HINT_MAX ) -type Status int - -const ( - // Binding call succeeded - StatusSuccess = 0 - - // Binding call failed - StatusError = 1 -) +type BindCallbackResult struct { + // Error is an error returned by the callback. If error is not nil - the + // result of the callback is ignored. + Error error + + // Value is a value returned by the callback. If error is not nil - the + // result of the callback is ignored. + Value interface{} +} type WebView interface { @@ -117,13 +117,10 @@ type WebView interface { // Request string is a JSON array of all the arguments passed to the // JavaScript function. // - // f must be a function that accepts an id string as the first argument, and - // then any other arguments that you want to pass from the JavaScript function. + // f must be a function + // f can return either value and error or just error + // f can return a channel of BindCallbackResult as the value Bind(name string, f interface{}) error - - // Return sends a response to the JavaScript callback. - // id is the callback id passed to the f function in Bind. - Return(id string, status Status, result interface{}) } type webview struct { @@ -134,7 +131,7 @@ var ( m sync.Mutex index uintptr dispatch = map[uintptr]func(){} - bindings = map[uintptr]func(id, req string) error{} + bindings = map[uintptr]func(id, req string) (interface{}, error){} ) func boolToInt(b bool) C.int { @@ -237,15 +234,32 @@ func _webviewBindingGoCallback(w C.webview_t, id *C.char, req *C.char, index uin f := bindings[uintptr(index)] m.Unlock() - // handle any error trying to call the binding function - if err := f(C.GoString(id), C.GoString(req)); err != nil { + // retain a reference to the id and params strings + reqId := C.GoString(id) + reqParams := C.GoString(req) + + // run the callback in a separate goroutine + go func() { jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) } - result := jsString(err.Error()) + status, result := 0, "" + if res, err := f(reqId, reqParams); err != nil { + status = -1 + result = jsString(err.Error()) + } else if b, err := json.Marshal(res); err != nil { + status = -1 + result = jsString(err.Error()) + } else { + status = 0 + result = string(b) + } + + cId := C.CString(reqId) + defer C.free(unsafe.Pointer(cId)) s := C.CString(result) defer C.free(unsafe.Pointer(s)) - C.webview_return(w, id, C.int(StatusError), s) - } + C.webview_return(w, cId, C.int(status), s) + }() } func (w *webview) Bind(name string, f interface{}) error { @@ -254,43 +268,81 @@ func (w *webview) Bind(name string, f interface{}) error { if v.Kind() != reflect.Func { return errors.New("only functions can be bound") } - - if v.Type().NumIn() > 0 && v.Type().In(0).Kind() != reflect.String { - return errors.New("first argument must be a string for the request id") + // f must return either value and error or just error + if n := v.Type().NumOut(); n > 2 { + return errors.New("function may only return a value or a value+error") } - binding := func(id, req string) error { + binding := func(id, req string) (interface{}, error) { raw := []json.RawMessage{} if err := json.Unmarshal([]byte(req), &raw); err != nil { - return err + return nil, err } isVariadic := v.Type().IsVariadic() numIn := v.Type().NumIn() - - if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn-1) { - return errors.New("function arguments mismatch") - } - args := []reflect.Value{ - reflect.ValueOf(id), + if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn) { + return nil, errors.New("function arguments mismatch") } + args := []reflect.Value{} for i := range raw { var arg reflect.Value - // skip `id` when looking at `f` parameters - argNum := i + 1 - if isVariadic && argNum >= numIn-1 { + if isVariadic && i >= numIn-1 { arg = reflect.New(v.Type().In(numIn - 1).Elem()) } else { - arg = reflect.New(v.Type().In(argNum)) + arg = reflect.New(v.Type().In(i)) } if err := json.Unmarshal(raw[i], arg.Interface()); err != nil { - return err + return nil, err } args = append(args, arg.Elem()) } - - v.Call(args) - return nil + errorType := reflect.TypeOf((*error)(nil)).Elem() + res := v.Call(args) + switch len(res) { + case 0: + // No results from the function, just return nil + return nil, nil + case 1: + // One result may be a value, or an error + if res[0].Type().Implements(errorType) { + if res[0].Interface() != nil { + return nil, res[0].Interface().(error) + } + return nil, nil + } else if res[0].Type().Kind() == reflect.Chan { + if res[0].Type().Elem() != reflect.TypeOf(BindCallbackResult{}) { + return nil, errors.New("channel must be of type CallbackResult") + } + + // Wait for the channel to receive a value + val, ok := res[0].Recv() + + if !ok { + return nil, errors.New("channel closed") + } + + callbackResult := val.Interface().(BindCallbackResult) + + if callbackResult.Error != nil { + return nil, callbackResult.Error + } + + return callbackResult.Value, nil + } + return res[0].Interface(), nil + case 2: + // Two results: first one is value, second is error + if !res[1].Type().Implements(errorType) { + return nil, errors.New("second return value must be an error") + } + if res[1].Interface() == nil { + return res[0].Interface(), nil + } + return res[0].Interface(), res[1].Interface().(error) + default: + return nil, errors.New("unexpected number of return values") + } } m.Lock() @@ -303,18 +355,3 @@ func (w *webview) Bind(name string, f interface{}) error { C.CgoWebViewBind(w.w, cname, C.uintptr_t(index)) return nil } - -func (w *webview) Return(id string, status Status, result interface{}) { - jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) } - resultString := "" - - if status == StatusError { - resultString = jsString(result.(error).Error()) - } else { - resultString = jsString(result) - } - - s := C.CString(resultString) - defer C.free(unsafe.Pointer(s)) - C.webview_return(w.w, C.CString(id), C.int(status), s) -} diff --git a/webview_test.go b/webview_test.go index 8871d6775..97206f938 100644 --- a/webview_test.go +++ b/webview_test.go @@ -18,6 +18,14 @@ func Example() { w.Bind("add", func(id string, a, b int) { w.Return(id, StatusSuccess, a+b) }) + w.Bind("subtract", func(a, b int) chan BindCallbackResult { + ch := make(chan BindCallbackResult) + go func() { + defer close(ch) + ch <- BindCallbackResult{Value: a - b, Error: nil} + }() + return ch + }) w.Bind("quit", func() { w.Terminate() }) @@ -31,7 +39,10 @@ func Example() { console.log('noop res', res); add(1, 2).then(function(res) { console.log('add res', res); - quit(); + subtract(5, 3).then(function(res) { + console.log('subtract res', res); + quit(); + }); }); }); };