From ad5b341b1e043b90752693615447a02d8472037a Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Sat, 20 Mar 2021 23:45:44 +0800 Subject: [PATCH] feat: Add reflection to create JS value from Go value --- wasm/function.go | 105 ++++++++++++++++++++++++++++ wasm/object.go | 8 +-- wasm/reflect_from.go | 11 +++ wasm/reflect_to.go | 161 +++++++++++++++++++++++++++++++++++++++++++ wasm/wasm.go | 32 +++++++++ 5 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 wasm/function.go create mode 100644 wasm/reflect_from.go create mode 100644 wasm/reflect_to.go create mode 100644 wasm/wasm.go diff --git a/wasm/function.go b/wasm/function.go new file mode 100644 index 0000000..5256c3b --- /dev/null +++ b/wasm/function.go @@ -0,0 +1,105 @@ +package wasm + +import ( + "errors" + "reflect" + "syscall/js" +) + +// ErrInvalidArgumentType is returned when a generated Go function wrapper receives invalid argument types from JS. +var ErrInvalidArgumentType = errors.New("invalid argument passed into Go function") + +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +type goThrowable struct { + Result js.Value `wasm:"result"` + Error js.Value `wasm:"error"` +} + +func toJSFunc(x reflect.Value) js.Value { + funcType := x.Type() + var hasError bool + if funcType.NumOut() != 0 { + hasError = funcType.Out(funcType.NumOut()-1) == errorType + } + + return js.FuncOf(func(this js.Value, args []js.Value) interface{} { + in, err := conformJSValueToType(funcType, this, args) + if err != nil { + return ToJSValue(goThrowable{ + Error: NewError(err), + }) + } + + out := x.Call(in) + + if !hasError { + return ToJSValue(goThrowable{ + Result: returnValue(out), + }) + } + + lastParam := out[len(out)-1] + if !lastParam.IsNil() { + return ToJSValue(goThrowable{ + Error: NewError(lastParam.Interface().(error)), + }) + } + return ToJSValue(goThrowable{ + Result: returnValue(out[:len(out)-1]), + }) + }).JSValue() +} + +var jsValueType = reflect.TypeOf(js.Value{}) + +func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Value) ([]reflect.Value, error) { + if funcType.NumIn() == 0 { + if len(values) != 0 { + return nil, ErrInvalidArgumentType + } + return []reflect.Value{}, nil + } + + if funcType.In(0) == jsValueType { + values = append([]js.Value{this}, values...) + } + + if funcType.IsVariadic() && funcType.NumIn()-1 > len(values) { + return nil, ErrInvalidArgumentType + } + + if !funcType.IsVariadic() && funcType.NumIn() != len(values) { + return nil, ErrInvalidArgumentType + } + + in := make([]reflect.Value, 0, len(values)) + for i, v := range values { + paramType := funcType.In(i) + x := reflect.Zero(paramType).Interface() + err := FromJSValue(v, &x) + if err != nil { + return nil, err + } + + in = append(in, reflect.ValueOf(x)) + } + + return in, nil +} + +func returnValue(x []reflect.Value) js.Value { + switch len(x) { + case 0: + return js.Undefined() + case 1: + return ToJSValue(x[0].Interface()) + } + + xInterface := make([]interface{}, 0, len(x)) + for _, v := range x { + xInterface = append(xInterface, v.Interface()) + } + + return ToJSValue(xInterface) +} diff --git a/wasm/object.go b/wasm/object.go index f1c3fe5..b26b45d 100644 --- a/wasm/object.go +++ b/wasm/object.go @@ -116,14 +116,14 @@ func (o Object) Length() int { return o.value.Length() } -// Set sets the property p to the value of js.ValueOf(x). +// Set sets the property p to the value of ToJSValue(x). func (o Object) Set(p string, x interface{}) { - o.value.Set(p, x) + o.value.Set(p, ToJSValue(x)) } -// SetIndex sets the index i to the value of js.ValueOf(x). +// SetIndex sets the index i to the value of ToJSValue(x). func (o Object) SetIndex(i int, x interface{}) { - o.value.SetIndex(i, x) + o.value.SetIndex(i, ToJSValue(x)) } // String returns the object marshalled as a JSON string for debugging purposes. diff --git a/wasm/reflect_from.go b/wasm/reflect_from.go new file mode 100644 index 0000000..6fe4039 --- /dev/null +++ b/wasm/reflect_from.go @@ -0,0 +1,11 @@ +package wasm + +import ( + "fmt" + "syscall/js" +) + +func FromJSValue(x js.Value, out interface{}) error { + // TODO + return fmt.Errorf("unimplemented") +} diff --git a/wasm/reflect_to.go b/wasm/reflect_to.go new file mode 100644 index 0000000..fa94c4f --- /dev/null +++ b/wasm/reflect_to.go @@ -0,0 +1,161 @@ +package wasm + +import ( + "fmt" + "reflect" + "syscall/js" + "unsafe" +) + +// ToJSValue converts a given Go value into its equivalent JS form. +// +// One special case is that complex numbers (complex64 and complex128) are converted into objects with a real and imag +// property holding a number each. +// +// A function is converted into a JS function where the function returns an error if the provided arguments do not conform +// to the Go equivalent but otherwise calls the Go function. +// +// The "this" argument of a function is always passed to the Go function if its first parameter is of type js.Value. +// Otherwise, it is simply ignored. +// +// If the last return value of a function is an error, it will be thrown in JS if it's non-nil. +// If the function returns multiple non-error values, it is converted to an array when returning to JS. +// +// It panics when a channel or a map with keys other than string and integers are passed in. +func ToJSValue(x interface{}) js.Value { + if x == nil { + return js.Null() + } + + // Fast path for basic types that do not require reflection. + switch x := x.(type) { + case js.Value: + return x + case js.Wrapper: + return x.JSValue() + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, + unsafe.Pointer, float32, float64, string: + return js.ValueOf(x) + case complex64: + return js.ValueOf(map[string]interface{}{ + "real": real(x), + "imag": imag(x), + }) + case complex128: + return js.ValueOf(map[string]interface{}{ + "real": real(x), + "imag": imag(x), + }) + } + + value := reflect.ValueOf(x) + + if value.Kind() == reflect.Ptr { + value = reflect.Indirect(value) + if !value.IsValid() { + return js.Undefined() + } + } + + switch value.Kind() { + case reflect.Array, reflect.Slice: + return toJSArray(value) + case reflect.Func: + return toJSFunc(value) + case reflect.Map: + return mapToJSObject(value) + case reflect.Struct: + return structToJSObject(value) + default: + panic(fmt.Sprintf("cannot convert %T to a JS value", x)) + } +} + +func toJSArray(x reflect.Value) js.Value { + arrayConstructor, err := Global().Get("Array") + if err != nil { + panic("Array constructor not found") + } + + array := arrayConstructor.New() + for i := 0; i < x.Len(); i++ { + array.SetIndex(i, ToJSValue(x.Index(i).Interface())) + } + + return array +} + +func mapToJSObject(x reflect.Value) js.Value { + objectConstructor, err := Global().Get("Object") + if err != nil { + panic("Object constructor not found") + } + + obj := objectConstructor.New() + iter := x.MapRange() + for { + if !iter.Next() { + break + } + + key := iter.Key() + value := iter.Value().Interface() + switch key := key.Interface().(type) { + case int: + obj.SetIndex(key, ToJSValue(value)) + case int8: + obj.SetIndex(int(key), ToJSValue(value)) + case int16: + obj.SetIndex(int(key), ToJSValue(value)) + case int32: + obj.SetIndex(int(key), ToJSValue(value)) + case int64: + obj.SetIndex(int(key), ToJSValue(value)) + case uint: + obj.SetIndex(int(key), ToJSValue(value)) + case uint8: + obj.SetIndex(int(key), ToJSValue(value)) + case uint16: + obj.SetIndex(int(key), ToJSValue(value)) + case uint32: + obj.SetIndex(int(key), ToJSValue(value)) + case uint64: + obj.SetIndex(int(key), ToJSValue(value)) + case uintptr: + obj.SetIndex(int(key), ToJSValue(value)) + case string: + obj.Set(key, ToJSValue(value)) + default: + panic(fmt.Sprintf("cannot convert %T into a JS value as its key is not a string or an integer", + x.Interface())) + } + } + + return obj +} + +func structToJSObject(x reflect.Value) js.Value { + objectConstructor, err := Global().Get("Object") + if err != nil { + panic("Object constructor not found") + } + + obj := objectConstructor.New() + + structType := x.Type() + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.PkgPath != "" { + continue + } + + name := field.Name + if tagName, ok := field.Tag.Lookup("wasm"); ok { + name = tagName + } + + obj.Set(name, ToJSValue(x.Field(i).Interface())) + } + + return obj +} diff --git a/wasm/wasm.go b/wasm/wasm.go new file mode 100644 index 0000000..56d94c2 --- /dev/null +++ b/wasm/wasm.go @@ -0,0 +1,32 @@ +package wasm + +// Magic values to communicate with the JS library. +const ( + globalIdent = "__go_wasm__" + readyHint = "__ready__" +) + +var bridge Object + +func init() { + bridgeJS, err := Global().Get(globalIdent) + if err != nil { + panic("JS wrapper " + globalIdent + " not found") + } + + bridge, err = NewObject(bridgeJS) + if err != nil { + panic("JS wrapper " + globalIdent + " is not an object") + } +} + +// Ready notifies the JS bridge that the WASM is ready. +// It should be called when every value and function is exposed. +func Ready() { + Expose(readyHint, true) +} + +// Expose exposes a copy of the provided value in JS. +func Expose(property string, x interface{}) { + bridge.Set(property, x) +}