From 38d86b9e818c93d7616963bb29aba2e3457d3d61 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 | 87 ++++++++++++++++++++++++ wasm/reflect_from.go | 11 +++ wasm/reflect_to.go | 155 +++++++++++++++++++++++++++++++++++++++++++ wasm/wasm.go | 15 +++++ 4 files changed, 268 insertions(+) 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..c5f30b0 --- /dev/null +++ b/wasm/function.go @@ -0,0 +1,87 @@ +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)) + +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 { + throwWrapper.Invoke(NewError(err)) + return nil + } + + out := x.Call(in) + + if !hasError { + return ToJSValue(returnValue(out)) + } + + lastParam := out[len(out)-1] + if !lastParam.IsNil() { + throwWrapper.Invoke(NewError(err)) + return nil + } + return ToJSValue(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() != 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 { + if len(x) == 1 { + return ToJSValue(x[0]) + } + + xInterface := make([]interface{}, 0, len(x)) + for _, v := range x { + xInterface = append(xInterface, v.Interface()) + } + + return ToJSValue(x) +} 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..d694277 --- /dev/null +++ b/wasm/reflect_to.go @@ -0,0 +1,155 @@ +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.Undefined() + } + + // Fast path for basic types that do not require reflection. + switch x := x.(type) { + case js.Value: + return x + case js.Wrapper, 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 { + 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())) + } + + if !iter.Next() { + break + } + } + + 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) + name := field.Name + if tagName, ok := field.Tag.Lookup("wasm"); ok { + name = tagName + } + + obj.Set(name, ToJSValue(x.Field(i))) + } + + return obj +} diff --git a/wasm/wasm.go b/wasm/wasm.go new file mode 100644 index 0000000..002d899 --- /dev/null +++ b/wasm/wasm.go @@ -0,0 +1,15 @@ +package wasm + +import "syscall/js" + +const globalIdent = "__go_wasm__" + +var throwWrapper js.Value + +func init() { + var err error + throwWrapper, err = Global().Get(globalIdent, "__throw__") + if err != nil { + panic("JS wrapper __go_wasm__.__throw__ not found") + } +}