feat: Implement Object, Promise and reflection #3

Merged
hamza merged 5 commits from chanbakjsd/go-mod-wasm:reflect into master 2021-03-21 13:56:15 +07:00
8 changed files with 639 additions and 0 deletions

@ -0,0 +1,13 @@
package wasm
import "syscall/js"
// NewError returns a JS Error with the provided Go error's error message.
func NewError(goErr error) js.Value {
errConstructor, err := Global().Expect(js.TypeFunction, "Error")
if err != nil {
panic("Error constructor not found")
}
return errConstructor.New(goErr.Error())
}

@ -0,0 +1,116 @@
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"`
}
// toJSFunc takes a reflect.Value of a Go function and converts it to a JS function that:
// Errors if the parameter types do not conform to the Go function signature,
// Throws an error if the last returned value is an error and is non-nil,
// Return an array if there's multiple non-error return values.
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 funcWrapper.Invoke(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]),
})
}))
}
var jsValueType = reflect.TypeOf(js.Value{})
// conformJSValueToType attempts to convert the provided JS values to reflect.Values that match the
// types expected for the parameters of funcType.
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 {
// If the first parameter is a js.Value, it is assumed to be the value of `this`.
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
}
// returnValue wraps returned values by Go in a JS-friendly way.
// If there are no returned values, it returns undefined.
// If there is exactly one, it returns the JS equivalent.
// If there is more than one, it returns an array containing the JS equivalent of every returned value.
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)
}

@ -0,0 +1,3 @@
module gitea.teamortix.com/Team-Ortix/go-mod-wasm/wasm
go 1.16

@ -0,0 +1,142 @@
package wasm
import (
"fmt"
"syscall/js"
)
// TypeMismatchError is returned when a function is called with a js.Value that has the incorrect type.
type TypeMismatchError struct {
Expected js.Type
Actual js.Type
}
func (e TypeMismatchError) Error() string {
return fmt.Sprintf("expected %v type, got %v type instead", e.Expected, e.Actual)
}
// Global returns the global object as a Object.
// If the global object is not an object, it panics.
func Global() Object {
global, err := NewObject(js.Global())
if err != nil {
panic(err)
}
return global
}
// Object is a statically typed Object instance of js.Value.
// It should be instantiated with NewObject where it is checked for type instead of directly.
// Calling methods on a zero Object is undefined behaviour.
type Object struct {
value js.Value
}
// NewObject instantiates a new Object with the provided js.Value.
// If the js.Value is not an Object, it returns a TypeMismatchError.
func NewObject(raw js.Value) (Object, error) {
if raw.Type() != js.TypeObject {
return Object{}, TypeMismatchError{
Expected: js.TypeObject,
Actual: raw.Type(),
}
}
return Object{raw}, nil
}
// Get recursively gets the Object's properties, returning a TypeMismatchError if it encounters a non-object while
// descending through the object.
func (o Object) Get(path ...string) (js.Value, error) {
current := o.value
for _, v := range path {
if current.Type() != js.TypeObject {
return js.Value{}, TypeMismatchError{
Expected: js.TypeObject,
Actual: current.Type(),
}
}
current = current.Get(v)
}
return current, nil
}
// Expect is a helper function that calls Get and checks the type of the final result.
// It returns a TypeMismatchError if a non-object is encountered while descending the path or the final type does not
// match with the provided expected type.
func (o Object) Expect(expectedType js.Type, path ...string) (js.Value, error) {
value, err := o.Get(path...)
if err != nil {
return js.Value{}, err
}
if value.Type() != expectedType {
return js.Value{}, TypeMismatchError{
Expected: expectedType,
Actual: value.Type(),
}
}
return value, nil
}
// Delete removes property p from the object.
func (o Object) Delete(p string) {
o.value.Delete(p)
}
// Equal checks if the object is equal to another value.
// It is equivalent to JS's === operator.
func (o Object) Equal(v js.Value) bool {
return o.value.Equal(v)
}
// Index indexes into the object.
func (o Object) Index(i int) js.Value {
return o.value.Index(i)
}
// InstanceOf implements the instanceof operator in JavaScript.
// If t is not a constructor, this function returns false.
func (o Object) InstanceOf(t js.Value) bool {
if t.Type() != js.TypeFunction {
return false
}
return o.value.InstanceOf(t)
}
// JSValue implements the js.Wrapper interface.
func (o Object) JSValue() js.Value {
return o.value
}
// Length returns the "length" property of the object.
func (o Object) Length() int {
return o.value.Length()
}
// Set sets the property p to the value of ToJSValue(x).
func (o Object) Set(p string, x interface{}) {
o.value.Set(p, ToJSValue(x))
}
// SetIndex sets the index i to the value of ToJSValue(x).
func (o Object) SetIndex(i int, x interface{}) {
o.value.SetIndex(i, ToJSValue(x))
}
// String returns the object marshalled as a JSON string for debugging purposes.
func (o Object) String() string {
stringify, err := Global().Expect(js.TypeFunction, "JSON", "stringify")
if err != nil {
panic(err)
}
jsonStr := stringify.Invoke(o)
if jsonStr.Type() != js.TypeString {
panic("JSON.stringify returned a " + jsonStr.Type().String())
}
return jsonStr.String()
}

@ -0,0 +1,145 @@
package wasm
import "syscall/js"
// Promise is an instance of a JS promise.
// The zero value of this struct is not a valid Promise.
type Promise struct {
Object
}
// FromJSValue turns a JS value to a Promise.
func (p *Promise) FromJSValue(value js.Value) error {
var err error
p.Object, err = NewObject(value)
return err
}
// NewPromise returns a promise that is fulfilled or rejected when the provided handler returns.
// The handler is spawned in its own goroutine.
func NewPromise(handler func() (interface{}, error)) Promise {
resultChan := make(chan interface{})
errChan := make(chan error)
// Invoke the handler in a new goroutine.
go func() {
result, err := handler()
if err != nil {
errChan <- err
return
}
resultChan <- result
}()
// Create a JS promise handler.
var jsHandler js.Func
jsHandler = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
panic("not enough arguments are passed to the Promise constructor handler")
}
resolve := args[0]
reject := args[1]
if resolve.Type() != js.TypeFunction || reject.Type() != js.TypeFunction {
panic("invalid type passed to Promise constructor handler")
}
go func() {
select {
case r := <-resultChan:
resolve.Invoke(ToJSValue(r))
case err := <-errChan:
reject.Invoke(NewError(err))
}
// Free up resources now that we are done.
jsHandler.Release()
}()
return nil
})
promise, err := Global().Expect(js.TypeFunction, "Promise")
if err != nil {
panic("Promise constructor not found")
}
return mustJSValueToPromise(promise.New(jsHandler))
}
// PromiseAll creates a promise that is fulfilled when all the provided promises have been fulfilled.
// The promise is rejected when any of the promises provided rejects.
// It is implemented by calling Promise.all on JS.
func PromiseAll(promise ...Promise) Promise {
promiseAll, err := Global().Expect(js.TypeFunction, "Promise", "all")
if err != nil {
panic("Promise.all not found")
}
pInterface := make([]interface{}, 0, len(promise))
for _, v := range promise {
pInterface = append(pInterface, v)
}
return mustJSValueToPromise(promiseAll.Invoke(pInterface))
}
// PromiseAllSettled creates a promise that is fulfilled when all the provided promises have been fulfilled or rejected.
// It is implemented by calling Promise.allSettled on JS.
func PromiseAllSettled(promise ...Promise) Promise {
promiseAllSettled, err := Global().Expect(js.TypeFunction, "Promise", "allSettled")
if err != nil {
panic("Promise.allSettled not found")
}
pInterface := make([]interface{}, 0, len(promise))
for _, v := range promise {
pInterface = append(pInterface, v)
}
return mustJSValueToPromise(promiseAllSettled.Invoke(pInterface))
}
// PromiseAny creates a promise that is fulfilled when any of the provided promises have been fulfilled.
// The promise is rejected when all of the provided promises gets rejected.
// It is implemented by calling Promise.any on JS.
func PromiseAny(promise ...Promise) Promise {
promiseAny, err := Global().Expect(js.TypeFunction, "Promise", "any")
if err != nil {
panic("Promise.any not found")
}
pInterface := make([]interface{}, 0, len(promise))
for _, v := range promise {
pInterface = append(pInterface, v)
}
return mustJSValueToPromise(promiseAny.Invoke(pInterface))
}
// PromiseRace creates a promise that is fulfilled or rejected when one of the provided promises fulfill or reject.
// It is implemented by calling Promise.race on JS.
func PromiseRace(promise ...Promise) Promise {
promiseRace, err := Global().Expect(js.TypeFunction, "Promise", "race")
if err != nil {
panic("Promise.race not found")
}
pInterface := make([]interface{}, 0, len(promise))
for _, v := range promise {
pInterface = append(pInterface, v)
}
return mustJSValueToPromise(promiseRace.Invoke(pInterface))
}
func mustJSValueToPromise(v js.Value) Promise {
var p Promise
err := p.FromJSValue(v)
if err != nil {
panic("Expected a Promise from JS standard library")
}
return p
}

@ -0,0 +1,13 @@
package wasm
import (
"fmt"
"syscall/js"
)
// FromJSValue converts a given js.Value to the Go equivalent.
// The new value of 'out' is undefined if FromJSValue returns an error.
func FromJSValue(x js.Value, out interface{}) error {
// TODO
return fmt.Errorf("unimplemented")
}

@ -0,0 +1,164 @@
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))
}
}
// toJSArray converts the provided array or slice to a JS array.
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
}
// mapToJSObject converts the provided map to a JS object.
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
}
// structToJSObject converts a struct to a JS object.
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
}

@ -0,0 +1,43 @@
package wasm
import "syscall/js"
// Magic values to communicate with the JS library.
const (
globalIdent = "__go_wasm__"
readyHint = "__ready__"
funcWrapperName = "__wrapper__"
)
var (
bridge Object
funcWrapper js.Value
)
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")
}
funcWrapper, err = bridge.Get(funcWrapperName)
if err != nil {
panic("JS wrapper " + globalIdent + "." + funcWrapperName + " not found")
}
}
// 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)
}