Compare commits

..

3 Commits

22 changed files with 50 additions and 11622 deletions

4
.gitignore vendored

@ -1,4 +0,0 @@
node_modules
.gocache
wasm_exec.js
*.wasm

@ -1,3 +0,0 @@
example
wasm
.gocache

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Hamza Ali and Chan Wen Xu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1 +0,0 @@
dist/main.js

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Golang-WASM Example</title>
</head>
<body>
<h1>This is an example Golang-WASM Project.</h1>
<h3>
<a href="https://gitea.teamortix.com/Team-Ortix/go-mod-wasm/src/branch/master/example/basic/src/api/main.go">
Sample Go code
</a>
</h3>
<h2>Value from Go:</h2>
<h3 id="value">&nbsp;</h3>
<h2>Call function from Go:</h2>
<input type="text" id="input" value="Team Ortix" />
<h3 id="funcValue">&nbsp;</h3>
<script src="main.js"></script>
</body>
</html>

@ -1,3 +0,0 @@
module example
go 1.16

File diff suppressed because it is too large Load Diff

@ -1,15 +0,0 @@
{
"name": "example",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"start": "GOROOT=`go env GOROOT` webpack serve"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.27.0",
"webpack-dev-server": "^3.11.2"
}
}

@ -1,79 +0,0 @@
package main
import "syscall/js"
var (
// bridgeName is the namesace for all functions and values set.
//
// The returning JavaScript proxy via the webpack loader will look for functions and values under this namespace.
bridgeName = "__go_wasm__"
// The JS object of the __go_wasm__ value.
bridge js.Value
// Wrapper is a simple JS function that when called with a Go Function, will return a new function that will throw
// if the property `error` is an instance of JavaScript's `error`.
//
// All Go functions in the bridgeName proxy are expected to be the result of calling wrapper with the Go function.
wrapper js.Value
)
// newReturnValue creates an object with the value as the result.
// See wrapGoFunc for the reasoning behind style style of returning values from Go functions.
func newReturnValue(value interface{}) js.Value {
jsObject := js.Global().Get("Object").New()
jsObject.Set("result", value)
return jsObject
}
// newReturnError creates an object with the goError's message and creates a Javascript Error object with the message.
//
// See wrapGoFunc for the reasoning behind style style of returning values from Go functions.
func newReturnError(goErr error) js.Value {
jsObject := js.Global().Get("Object").New()
jsError := js.Global().Get("Error")
jsObject.Set("error", jsError.New(goErr.Error()))
return jsObject
}
// Using this wrapper makes it possible to throw errors in go-fashion.
// This means that all wrapped functions must return value and an error (respectively).
//
// The __wrapper__ function from JS will automatically throw if the returned object has an 'error' property.
// Inversely, it will automatically give the result value if that property exists.
// All Go functions directly returned via wasm should keep this in mind.
func wrapGoFunc(f func(js.Value, []js.Value) (interface{}, error)) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
res, err := f(this, args)
if err != nil {
return newReturnError(err)
}
return newReturnValue(res)
})
}
func setFunc(name string, f func(js.Value, []js.Value) (interface{}, error)) {
bridge.Set(name, wrapper.Invoke(wrapGoFunc(f)))
}
func setValue(name string, value interface{}) {
bridge.Set(name, value)
}
// Toggling the __ready__ value in the bridge lets JS know that everything is setup.
// Setting __ready__ to true can help prevent possible race conditions of Wasm being called before everything is
// registered, and potentially crashing applications.
func ready() {
bridge.Set("__ready__", true)
<-make(chan bool, 0) // To use anything from Go WASM, the program may not exit.
}
// We want to make sure that this is always ran first. This means that we can make sure that whenever functions are
// initialized, they are able to be set to the bridge and wrapper.
func init() {
bridge = js.Global().Get(bridgeName)
wrapper = bridge.Get("__wrapper__")
}

@ -1,28 +0,0 @@
package main
import (
"fmt"
"syscall/js"
)
const hello = "Hello!"
// helloName's first value is JavaScript's `this`.
// However, the way that the JS bridge is written, it will always be JavaScript's undefined.
//
// If returning a non-nil error value, the resulting promise will be rejected by API consumers.
// The rejected value will JavaScript's Error, with the message being the go error's message.
//
// See other examples which use the Go WASM bridge api, which show more flexibility and type safety when interacting
// with JavaScript.
func helloName(_ js.Value, args []js.Value) (interface{}, error) {
return fmt.Sprintf("Hello, %s!", args[0].String()), nil
}
func main() {
fmt.Println("golang-wasm initialized")
setFunc("helloName", helloName)
setValue("hello", hello)
ready()
}

@ -1,18 +0,0 @@
import wasm from './api/main.go';
const { hello, helloName } = wasm;
const value = document.getElementById("value");
const input = document.getElementById("input");
const funcValue = document.getElementById("funcValue");
const run = async () => {
value.innerText = await hello();
funcValue.innerText = await helloName(input.value);
input.addEventListener("keyup", async (e) => {
funcValue.innerText = await helloName(e.target.value);
})
}
run()

@ -1,53 +0,0 @@
const path = require('path');
module.exports = {
entry: './src/index.js',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
compress: true,
port: 3000,
},
mode: "development",
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: [".js", ".go"],
fallback: {
"fs": false,
"os": false,
"util": false,
"tls": false,
"net": false,
"path": false,
"zlib": false,
"http": false,
"https": false,
"stream": false,
"crypto": false,
}
},
module: {
rules: [
{
test: /\.go$/,
use: [
{
loader: path.resolve(__dirname, '../../src/index.js')
}
]
}
]
},
performance: {
assetFilter: (file) => {
return !/(\.wasm|.map)$/.test(file)
}
},
ignoreWarnings: [
{
module: /wasm_exec.js$/
}
]
};

1264
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,11 +1,14 @@
{ {
"name": "golang-wasm", "name": "go-mod-wasm",
"version": "0.0.1", "version": "0.0.1",
"description": "A webpack-based configuration to work with wasm using Go.", "description": "whatever",
"main": "src/index.js", "main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitea.teamortix.com/Team-Ortix/golang-wasm" "url": "https://gitea.teamortix.com/Team-Ortix/go-mod-wasm"
}, },
"keywords": [ "keywords": [
"golang", "golang",
@ -15,9 +18,5 @@
"webpack" "webpack"
], ],
"author": "hhhapz, chanbakjsd", "author": "hhhapz, chanbakjsd",
"license": "MIT", "license": "MIT"
"dependencies": { }
"lookpath": "^1.2.0",
"webpack": "^5.27.0"
}
}

@ -1,119 +0,0 @@
// Initially, the __go_wasm__ object will be an empty object.
const g = global || window || self;
if (!g.__go_wasm__) {
g.__go_wasm__ = {};
}
/**
* The maximum amount of time that we would expect Wasm to take to initialize.
* If it doesn't initialize after this time, we send a warning to console.
* Most likely something has gone wrong if it takes more than 3 seconds to initialize.
*/
const maxTime = 3 * 1000;
/**
* bridge is an easier way to refer to the Go WASM object.
*/
const bridge = g.__go_wasm__;
/**
* Wrapper is used by Go to run all Go functions in JS.
*
* @param {Function} goFunc a function that is expected to return an object of the following specification:
* {
* result: undefined | any // undefined when error is returned, or function returns undefined
* error: Error | undefined // undefined when no error is present
* }
*
* @returns {Function} returns a function that take arguments which are used to call the Go function.
*/
function wrapper(goFunc) {
return (...args) => {
const result = goFunc.apply(undefined, args);
if (result.error instanceof Error) {
throw result.error;
}
return result.result;
}
}
/**
* Sleep is used when awaiting for Go Wasm to initialize.
* It uses the lowest possible sane delay time (via requestAnimationFrame).
* However, if the window is not focused, requestAnimationFrame never returns.
* A timeout will ensure to be called after 50 ms, regardless of whether or not the tab is in focus.
*
* @returns {Promise} an always-resolving promise when a tick has been completed
*/
function sleep() {
return new Promise((res) => {
requestAnimationFrame(() => res());
setTimeout(() => {
res();
}, 50);
});
}
/**
* @param {ArrayBuffer} getBytes a promise that is bytes of the Go Wasm object.
*
* @returns {Proxy} an object that can be used to call Wasm's objects and properly parse their results.
*
* All values that want to be retrieved from the proxy, regardless of if they are a function or not, must be retrieved
* as if they are from a function call.
*
* If a non-function value is returned however arguments are provided, a warning will be printed.
*/
export default function (getBytes) {
let proxy;
async function init() {
bridge.__wrapper__ = wrapper;
const go = new g.Go();
let bytes = await getBytes;
let result = await WebAssembly.instantiate(bytes, go.importObject);
go.run(result.instance);
}
init();
setTimeout(() => {
if (bridge.__ready__ !== true) {
console.warn("Golang WASM Bridge (__go_wasm__.__ready__) still not true after max time");
}
}, maxTime);
proxy = new Proxy(
{},
{
get: (_, key) => {
return (...args) => {
return new Promise(async (res, rej) => {
while (bridge.__ready__ !== true) {
await sleep();
}
if (typeof bridge[key] !== 'function') {
res(bridge[key]);
if (args.length !== 0) {
console.warn("Retrieved value from WASM returned non-error type, however called with arguments.")
}
return;
}
try {
res(bridge[key].apply(undefined, args));
} catch (e) {
rej(e);
}
})
};
}
}
);
bridge.__proxy__ = proxy;
return proxy;
}

@ -1,87 +0,0 @@
const fs = require("fs/promises");
const util = require("util");
const execFile = util.promisify(require("child_process").execFile);
const path = require("path");
const { lookpath } = require("lookpath");
module.exports = function (source) {
const cb = this.async();
const goBin = lookpath("go");
if (!goBin) {
return cb(new Error("go bin not found in path."));
}
if (!process.env.GOROOT) {
return cb(new Error("Could not find GOROOT in environment.\n" +
"Please try adding this to your script:\n" +
"GOROOT=`go env GOROOT` npm run ..."));
}
const parent = path.dirname(this.resourcePath);
const outFile = this.resourcePath.slice(0, -2) + "wasm";
let modDir = parent;
const opts = {
cwd: parent,
env: {
GOPATH: process.env.GOPATH || path.join(process.env.HOME, "go"),
GOROOT: process.env.GOROOT,
GOCACHE: path.join(__dirname, ".gocache"),
GOOS: "js",
GOARCH: "wasm",
},
};
(async () => {
let found = false;
const root = path.resolve(path.sep);
while (path.resolve(modDir) != root) {
found = await fs.access(path.join(modDir, 'go.mod')).then(() => true).catch(() => false);
if (found) {
break;
}
modDir = path.join(modDir, "..");
}
if (!found) {
return cb(new Error("Could not find go.mod in any parent directory of " + this.resourcePath));
}
// Having context dependency before compilation means if any file apart from the imported one fails in
// compilation, the updates are still watched.
this.addContextDependency(modDir);
const wasmOrigPath = path.join(process.env.GOROOT, "misc", "wasm", "wasm_exec.js");
const wasmSavePath = path.join(__dirname, 'wasm_exec.js');
const errorPaths = ["\t" + wasmOrigPath, "\t" + wasmSavePath];
if (!(await fs.access(wasmOrigPath).then(() => true).catch(() => false)) &&
!(await fs.access(wasmSavePath).then(() => true).catch(() => false))) {
return cb(new Error("Could not find wasm_exec.js file. Invalid GOROOT? Searched paths:\n" +
errorPaths.join(",\n") + "\n"));
}
const result = await execFile("go", ["build", "-o", outFile, parent], opts)
.then(() => true)
.catch(e => e);
if (result instanceof Error) {
return cb(result);
}
found = await fs.access(wasmSavePath).then(() => true).catch(() => false);
if (!found) fs.copyFile(wasmOrigPath, wasmSavePath);
const contents = await fs.readFile(outFile);
fs.unlink(outFile);
const emitPath = path.basename(outFile);
this.emitFile(emitPath, contents);
cb(null,
`require('!${wasmSavePath}');
import goWasm from '${path.join(__dirname, 'bridge.js')}';
const wasm = fetch('${emitPath}').then(response => response.arrayBuffer());
export default goWasm(wasm);`);
})();
}

@ -9,17 +9,8 @@ import (
// ErrInvalidArgumentType is returned when a generated Go function wrapper receives invalid argument types from 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 ErrInvalidArgumentType = errors.New("invalid argument passed into Go function")
var errorType = reflect.TypeOf((*error)(nil)).Elem() var errorType = reflect.TypeOf(error(nil))
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 { func toJSFunc(x reflect.Value) js.Value {
funcType := x.Type() funcType := x.Type()
var hasError bool var hasError bool
@ -27,38 +18,30 @@ func toJSFunc(x reflect.Value) js.Value {
hasError = funcType.Out(funcType.NumOut()-1) == errorType hasError = funcType.Out(funcType.NumOut()-1) == errorType
} }
return funcWrapper.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} { return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
in, err := conformJSValueToType(funcType, this, args) in, err := conformJSValueToType(funcType, this, args)
if err != nil { if err != nil {
return ToJSValue(goThrowable{ throwWrapper.Invoke(NewError(err))
Error: NewError(err), return nil
})
} }
out := x.Call(in) out := x.Call(in)
if !hasError { if !hasError {
return ToJSValue(goThrowable{ return ToJSValue(returnValue(out))
Result: returnValue(out),
})
} }
lastParam := out[len(out)-1] lastParam := out[len(out)-1]
if !lastParam.IsNil() { if !lastParam.IsNil() {
return ToJSValue(goThrowable{ throwWrapper.Invoke(NewError(err))
Error: NewError(lastParam.Interface().(error)), return nil
})
} }
return ToJSValue(goThrowable{ return ToJSValue(returnValue(out[:len(out)-1]))
Result: returnValue(out[:len(out)-1]), }).JSValue()
})
}))
} }
var jsValueType = reflect.TypeOf(js.Value{}) 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) { func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Value) ([]reflect.Value, error) {
if funcType.NumIn() == 0 { if funcType.NumIn() == 0 {
if len(values) != 0 { if len(values) != 0 {
@ -68,14 +51,9 @@ func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Valu
} }
if funcType.In(0) == jsValueType { 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...) values = append([]js.Value{this}, values...)
} }
if funcType.IsVariadic() && funcType.NumIn()-1 > len(values) {
return nil, ErrInvalidArgumentType
}
if !funcType.IsVariadic() && funcType.NumIn() != len(values) { if !funcType.IsVariadic() && funcType.NumIn() != len(values) {
return nil, ErrInvalidArgumentType return nil, ErrInvalidArgumentType
} }
@ -83,28 +61,21 @@ func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Valu
in := make([]reflect.Value, 0, len(values)) in := make([]reflect.Value, 0, len(values))
for i, v := range values { for i, v := range values {
paramType := funcType.In(i) paramType := funcType.In(i)
ptrX := reflect.New(paramType).Interface() x := reflect.Zero(paramType).Interface()
err := FromJSValue(v, ptrX) err := FromJSValue(v, &x)
if err != nil { if err != nil {
return nil, err return nil, err
} }
in = append(in, reflect.ValueOf(ptrX).Elem()) in = append(in, reflect.ValueOf(x))
} }
return in, nil 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 { func returnValue(x []reflect.Value) js.Value {
switch len(x) { if len(x) == 1 {
case 0: return ToJSValue(x[0])
return js.Undefined()
case 1:
return ToJSValue(x[0].Interface())
} }
xInterface := make([]interface{}, 0, len(x)) xInterface := make([]interface{}, 0, len(x))
@ -112,5 +83,5 @@ func returnValue(x []reflect.Value) js.Value {
xInterface = append(xInterface, v.Interface()) xInterface = append(xInterface, v.Interface())
} }
return ToJSValue(xInterface) return ToJSValue(x)
} }

@ -62,7 +62,7 @@ func (o Object) Get(path ...string) (js.Value, error) {
return current, nil return current, nil
} }
// Expect is a helper function that calls Get and checks the type of the final result. // Expect is a helper function that calls Get and checks the type of the result.
// It returns a TypeMismatchError if a non-object is encountered while descending the path or the final type does not // 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. // match with the provided expected type.
func (o Object) Expect(expectedType js.Type, path ...string) (js.Value, error) { func (o Object) Expect(expectedType js.Type, path ...string) (js.Value, error) {
@ -81,13 +81,12 @@ func (o Object) Expect(expectedType js.Type, path ...string) (js.Value, error) {
return value, nil return value, nil
} }
// Delete removes property p from the object. // Delete deletes the property from the object.
func (o Object) Delete(p string) { func (o Object) Delete(p string) {
o.value.Delete(p) o.value.Delete(p)
} }
// Equal checks if the object is equal to another value. // 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 { func (o Object) Equal(v js.Value) bool {
return o.value.Equal(v) return o.value.Equal(v)
} }
@ -98,11 +97,7 @@ func (o Object) Index(i int) js.Value {
} }
// InstanceOf implements the instanceof operator in JavaScript. // 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 { func (o Object) InstanceOf(t js.Value) bool {
if t.Type() != js.TypeFunction {
return false
}
return o.value.InstanceOf(t) return o.value.InstanceOf(t)
} }
@ -116,14 +111,14 @@ func (o Object) Length() int {
return o.value.Length() return o.value.Length()
} }
// Set sets the property p to the value of ToJSValue(x). // Set sets the property p to the value of js.ValueOf(x).
func (o Object) Set(p string, x interface{}) { func (o Object) Set(p string, x interface{}) {
o.value.Set(p, ToJSValue(x)) o.value.Set(p, x)
} }
// SetIndex sets the index i to the value of ToJSValue(x). // SetIndex sets the index i to the value of js.ValueOf(x).
func (o Object) SetIndex(i int, x interface{}) { func (o Object) SetIndex(i int, x interface{}) {
o.value.SetIndex(i, ToJSValue(x)) o.value.SetIndex(i, x)
} }
// String returns the object marshalled as a JSON string for debugging purposes. // String returns the object marshalled as a JSON string for debugging purposes.

@ -15,6 +15,11 @@ func (p *Promise) FromJSValue(value js.Value) error {
return err return err
} }
// JSValue turns a Promise to a JS value.
func (p Promise) JSValue() js.Value {
return p.Object.JSValue()
}
// NewPromise returns a promise that is fulfilled or rejected when the provided handler returns. // NewPromise returns a promise that is fulfilled or rejected when the provided handler returns.
// The handler is spawned in its own goroutine. // The handler is spawned in its own goroutine.
func NewPromise(handler func() (interface{}, error)) Promise { func NewPromise(handler func() (interface{}, error)) Promise {
@ -48,7 +53,7 @@ func NewPromise(handler func() (interface{}, error)) Promise {
go func() { go func() {
select { select {
case r := <-resultChan: case r := <-resultChan:
resolve.Invoke(ToJSValue(r)) resolve.Invoke(r)
case err := <-errChan: case err := <-errChan:
reject.Invoke(NewError(err)) reject.Invoke(NewError(err))
} }

@ -1,411 +1,11 @@
package wasm package wasm
import ( import (
"errors"
"fmt" "fmt"
"reflect"
"syscall/js" "syscall/js"
) )
// ErrMultipleReturnValue is an error where a JS function is attempted to be unmarshalled into a Go function with
// multiple return values.
var ErrMultipleReturnValue = errors.New("a JS function can only return one value")
// InvalidFromJSValueError is an error where an invalid argument is passed to FromJSValue.
// The argument to Unmarshal must be a non-nil pointer.
type InvalidFromJSValueError struct {
Type reflect.Type
}
// Error implements error.
func (e InvalidFromJSValueError) Error() string {
return "invalid argument passed to FromJSValue. Got type " + e.Type.String()
}
// InvalidTypeError is an error where the JS value cannot be unmarshalled into the provided Go type.
type InvalidTypeError struct {
JSType js.Type
GoType reflect.Type
}
// Error implements error.
func (e InvalidTypeError) Error() string {
return "invalid unmarshalling: cannot unmarshal " + e.JSType.String() + " into " + e.GoType.String()
}
// InvalidArrayError is an error where the JS's array length do not match Go's array length.
type InvalidArrayError struct {
Expected int
Actual int
}
// Error implements error.
func (e InvalidArrayError) Error() string {
return fmt.Sprintf(
"invalid unmarshalling: expected array of length %d to match Go array but got JS array of length %d",
e.Expected, e.Actual,
)
}
// Decoder is an interface which manually decodes js.Value on its own.
// It overrides in FromJSValue.
type Decoder interface {
FromJSValue(js.Value) error
}
// FromJSValue converts a given js.Value to the Go equivalent.
// The new value of 'out' is undefined if FromJSValue returns an error.
//
// When a JS function is unmarshalled into a Go function with only one return value, the returned JS value is casted
// into the type of the return value. If the conversion fails, the function call panics.
//
// When a JS function is unmarshalled into a Go function with two return values, the second one being error, the
// conversion error is returned instead.
func FromJSValue(x js.Value, out interface{}) error { func FromJSValue(x js.Value, out interface{}) error {
v := reflect.ValueOf(out) // TODO
if v.Kind() != reflect.Ptr || v.IsNil() { return fmt.Errorf("unimplemented")
return &InvalidFromJSValueError{reflect.TypeOf(v)}
}
return decodeValue(x, v.Elem())
}
// decodeValue decodes the provided js.Value into the provided reflect.Value.
func decodeValue(x js.Value, v reflect.Value) error {
// If we have undefined or null, we need to be able to set to the pointer itself.
// All code beyond this point are pointer-unaware so we handle undefined or null first.
if x.Type() == js.TypeUndefined || x.Type() == js.TypeNull {
return decodeNothing(v)
}
// Implementations of Decoder are probably on pointer so do it before pointer code.
if d, ok := v.Addr().Interface().(Decoder); ok {
return d.FromJSValue(x)
}
// Make sure everything is initialized and indirect it.
// This prevents other decode functions from having to handle pointers.
if v.Kind() == reflect.Ptr {
initializePointerIfNil(v)
v = reflect.Indirect(v)
}
if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
// It's a interface{} so we just create the easiest Go representation we can in createInterface.
res := createInterface(x)
if res != nil {
v.Set(reflect.ValueOf(res))
}
return nil
}
// Directly set v if it's a js.Value.
if _, ok := v.Interface().(js.Value); ok {
v.Set(reflect.ValueOf(x))
return nil
}
// Go the reflection route.
switch x.Type() {
case js.TypeBoolean:
return decodeBoolean(x, v)
case js.TypeNumber:
return decodeNumber(x, v)
case js.TypeString:
return decodeString(x, v)
case js.TypeSymbol:
return decodeSymbol(x, v)
case js.TypeObject:
if isArray(x) {
return decodeArray(x, v)
}
return decodeObject(x, v)
case js.TypeFunction:
return decodeFunction(x, v)
default:
panic("unknown JS type: " + x.Type().String())
}
}
// decodeNothing decodes an undefined or a null into the provided reflect.Value.
func decodeNothing(v reflect.Value) error {
if v.Kind() != reflect.Ptr {
return InvalidTypeError{js.TypeNull, v.Type()}
}
v.Set(reflect.ValueOf(nil))
return nil
}
// decodeBoolean decodes a bool into the provided reflect.Value.
func decodeBoolean(x js.Value, v reflect.Value) error {
if v.Kind() != reflect.Bool {
return InvalidTypeError{js.TypeBoolean, v.Type()}
}
v.SetBool(x.Bool())
return nil
}
// decodeNumber decodes a JS number into the provided reflect.Value, truncating as necessary.
func decodeNumber(x js.Value, v reflect.Value) error {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v.SetInt(int64(x.Float()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v.SetUint(uint64(x.Float()))
case reflect.Float32, reflect.Float64:
v.SetFloat(x.Float())
default:
return InvalidTypeError{js.TypeNumber, v.Type()}
}
return nil
}
// decodeString decodes a JS string into the provided reflect.Value.
func decodeString(x js.Value, v reflect.Value) error {
if v.Kind() != reflect.String {
return InvalidTypeError{js.TypeString, v.Type()}
}
v.SetString(x.String())
return nil
}
// decodeSymbol decodes a JS symbol into the provided reflect.Value.
func decodeSymbol(x js.Value, v reflect.Value) error {
// TODO Decode it into a symbol type.
return InvalidTypeError{js.TypeSymbol, v.Type()}
}
// decodeArray decodes a JS array into the provided reflect.Value.
func decodeArray(x js.Value, v reflect.Value) error {
jsLen := x.Length()
switch v.Kind() {
case reflect.Array:
if jsLen != v.Len() {
return InvalidArrayError{v.Len(), jsLen}
}
case reflect.Slice:
newSlice := reflect.MakeSlice(v.Type(), jsLen, jsLen)
v.Set(newSlice)
default:
return InvalidTypeError{js.TypeObject, v.Type()}
}
for i := 0; i < jsLen; i++ {
err := FromJSValue(x.Index(i), v.Index(i).Addr().Interface())
if err != nil {
return err
}
}
return nil
}
// decodeObject decodes a JS object into the provided reflect.Value.
func decodeObject(x js.Value, v reflect.Value) error {
switch v.Kind() {
case reflect.Struct:
return decodeObjectIntoStruct(x, v)
case reflect.Map:
return decodeObjectIntoMap(x, v)
default:
return InvalidTypeError{js.TypeObject, v.Type()}
}
}
// decodeObject decodes a JS object into the provided reflect.Value struct.
func decodeObjectIntoStruct(x js.Value, v reflect.Value) error {
for i := 0; i < v.Type().NumField(); i++ {
fieldType := v.Type().Field(i)
if fieldType.PkgPath != "" {
continue
}
name := fieldType.Name
tagName, tagOK := fieldType.Tag.Lookup("wasm")
if tagOK {
name = tagName
}
err := decodeValue(x.Get(name), v.Field(i))
if err != nil {
if tagOK {
return fmt.Errorf("in field %s (JS %s): %w", fieldType.Name, tagName, err)
}
return fmt.Errorf("in field %s: %w", fieldType.Name, err)
}
}
return nil
}
func decodeObjectIntoMap(x js.Value, v reflect.Value) error {
mapType := v.Type()
keyType := mapType.Key()
valType := mapType.Elem()
switch keyType.Kind() {
case reflect.String:
case reflect.Interface:
if keyType.NumMethod() != 0 {
return InvalidTypeError{js.TypeObject, mapType}
}
default:
return InvalidTypeError{js.TypeObject, mapType}
}
// TODO: Use Object API
obj, err := Global().Get("Object")
if err != nil {
panic("Object not found")
}
var keys []string
err = FromJSValue(obj.Call("keys", x), &keys)
if err != nil {
panic("Object.keys returned non-string-array.")
}
for _, k := range keys {
valuePtr := reflect.New(valType).Interface()
err := FromJSValue(x.Get(k), valuePtr)
if err != nil {
return err
}
v.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(valuePtr).Elem())
}
return nil
}
// decodeFunction decodes a JS function into the provided reflect.Value.
func decodeFunction(x js.Value, v reflect.Value) error {
funcType := v.Type()
outCount := funcType.NumOut()
switch outCount {
case 0, 1:
case 2:
if funcType.Out(1) != errorType {
return ErrMultipleReturnValue
}
default:
return ErrMultipleReturnValue
}
v.Set(reflect.MakeFunc(funcType, func(args []reflect.Value) []reflect.Value {
argsJS := make([]interface{}, 0, len(args))
for _, v := range args {
argsJS = append(argsJS, ToJSValue(v.Interface()))
}
jsReturn := x.Invoke(argsJS...)
if outCount == 0 {
return []reflect.Value{}
}
returnPtr := reflect.New(funcType.Out(0)).Interface()
err := FromJSValue(jsReturn, returnPtr)
returnVal := reflect.ValueOf(returnPtr).Elem()
if err != nil {
if outCount == 1 {
panic("error decoding JS return value: " + err.Error())
}
return []reflect.Value{returnVal, reflect.ValueOf(err)}
}
switch outCount {
case 1:
return []reflect.Value{returnVal}
case 2:
return []reflect.Value{returnVal, reflect.ValueOf(nil)}
default:
panic("unexpected amount of return values")
}
}))
return nil
}
// createInterface creates a representation of the provided js.Value.
func createInterface(x js.Value) interface{} {
switch x.Type() {
case js.TypeUndefined, js.TypeNull:
return nil
case js.TypeBoolean:
return x.Bool()
case js.TypeNumber:
return x.Float()
case js.TypeString:
return x.String()
case js.TypeSymbol:
// We can't convert it to a Go value in a meaningful way.
return x
case js.TypeObject:
if isArray(x) {
return createArray(x)
}
return createObject(x)
case js.TypeFunction:
var a func(...interface{}) (interface{}, error)
err := FromJSValue(x, &a)
if err != nil {
panic("error creating function: " + err.Error())
}
return a
default:
panic("unknown JS type: " + x.Type().String())
}
}
// createArray creates a slice of interface representing the js.Value.
func createArray(x js.Value) interface{} {
result := make([]interface{}, x.Length())
for i := range result {
result[i] = createInterface(x.Index(i))
}
return result
}
// createObject creates a representation of the provided JS object.
func createObject(x js.Value) interface{} {
// TODO: Use Object API
obj, err := Global().Get("Object")
if err != nil {
panic("Object not found")
}
var keys []string
err = FromJSValue(obj.Call("keys", x), &keys)
if err != nil {
panic("Object.keys returned non-string-array.")
}
result := make(map[string]interface{}, len(keys))
for _, v := range keys {
result[v] = createInterface(x.Get(v))
}
return result
}
// isArray calls the JS function Array.isArray to check if the provided js.Value is an array.
func isArray(x js.Value) bool {
arr, err := Global().Get("Array")
if err != nil {
panic("Array not found")
}
return arr.Call("isArray", x).Bool()
}
// initializePointerIfNil checks if the pointer is nil and initializes it as necessary.
func initializePointerIfNil(v reflect.Value) {
if v.Kind() != reflect.Ptr {
return
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
initializePointerIfNil(v.Elem())
} }

@ -24,16 +24,14 @@ import (
// It panics when a channel or a map with keys other than string and integers are passed in. // It panics when a channel or a map with keys other than string and integers are passed in.
func ToJSValue(x interface{}) js.Value { func ToJSValue(x interface{}) js.Value {
if x == nil { if x == nil {
return js.Null() return js.Undefined()
} }
// Fast path for basic types that do not require reflection. // Fast path for basic types that do not require reflection.
switch x := x.(type) { switch x := x.(type) {
case js.Value: case js.Value:
return x return x
case js.Wrapper: case js.Wrapper, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr,
return x.JSValue()
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr,
unsafe.Pointer, float32, float64, string: unsafe.Pointer, float32, float64, string:
return js.ValueOf(x) return js.ValueOf(x)
case complex64: case complex64:
@ -71,7 +69,6 @@ func ToJSValue(x interface{}) js.Value {
} }
} }
// toJSArray converts the provided array or slice to a JS array.
func toJSArray(x reflect.Value) js.Value { func toJSArray(x reflect.Value) js.Value {
arrayConstructor, err := Global().Get("Array") arrayConstructor, err := Global().Get("Array")
if err != nil { if err != nil {
@ -86,7 +83,6 @@ func toJSArray(x reflect.Value) js.Value {
return array return array
} }
// mapToJSObject converts the provided map to a JS object.
func mapToJSObject(x reflect.Value) js.Value { func mapToJSObject(x reflect.Value) js.Value {
objectConstructor, err := Global().Get("Object") objectConstructor, err := Global().Get("Object")
if err != nil { if err != nil {
@ -96,10 +92,6 @@ func mapToJSObject(x reflect.Value) js.Value {
obj := objectConstructor.New() obj := objectConstructor.New()
iter := x.MapRange() iter := x.MapRange()
for { for {
if !iter.Next() {
break
}
key := iter.Key() key := iter.Key()
value := iter.Value().Interface() value := iter.Value().Interface()
switch key := key.Interface().(type) { switch key := key.Interface().(type) {
@ -131,12 +123,15 @@ func mapToJSObject(x reflect.Value) js.Value {
panic(fmt.Sprintf("cannot convert %T into a JS value as its key is not a string or an integer", panic(fmt.Sprintf("cannot convert %T into a JS value as its key is not a string or an integer",
x.Interface())) x.Interface()))
} }
if !iter.Next() {
break
}
} }
return obj return obj
} }
// structToJSObject converts a struct to a JS object.
func structToJSObject(x reflect.Value) js.Value { func structToJSObject(x reflect.Value) js.Value {
objectConstructor, err := Global().Get("Object") objectConstructor, err := Global().Get("Object")
if err != nil { if err != nil {
@ -148,16 +143,12 @@ func structToJSObject(x reflect.Value) js.Value {
structType := x.Type() structType := x.Type()
for i := 0; i < structType.NumField(); i++ { for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i) field := structType.Field(i)
if field.PkgPath != "" {
continue
}
name := field.Name name := field.Name
if tagName, ok := field.Tag.Lookup("wasm"); ok { if tagName, ok := field.Tag.Lookup("wasm"); ok {
name = tagName name = tagName
} }
obj.Set(name, ToJSValue(x.Field(i).Interface())) obj.Set(name, ToJSValue(x.Field(i)))
} }
return obj return obj

@ -2,42 +2,14 @@ package wasm
import "syscall/js" import "syscall/js"
// Magic values to communicate with the JS library. const globalIdent = "__go_wasm__"
const (
globalIdent = "__go_wasm__"
readyHint = "__ready__"
funcWrapperName = "__wrapper__"
)
var ( var throwWrapper js.Value
bridge Object
funcWrapper js.Value
)
func init() { func init() {
bridgeJS, err := Global().Get(globalIdent) var err error
throwWrapper, err = Global().Get(globalIdent, "__throw__")
if err != nil { if err != nil {
panic("JS wrapper " + globalIdent + " not found") panic("JS wrapper __go_wasm__.__throw__ 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)
} }