Compare commits

...

16 Commits

Author SHA1 Message Date
ALI Hamza be4b312d49
refactor(name): change repository references to golang-wasm 2021-03-22 23:58:55 +07:00
Chan Wen Xu 421c331333
fix: Call FromJSValue using the correct parameter 2021-03-23 00:30:51 +07:00
Chan Wen Xu 8eadf27091
feat: Implement FromJSValue 2021-03-23 00:30:42 +07:00
Chan Wen Xu 9faa609930
fix: Use function wrapper provided by JS 2021-03-21 21:55:07 +07:00
Chan Wen Xu 37088bf382
docs: Document unexported functions
Some unexported functions are non-trivial and may be confusing. This
commit clarifies their use.
2021-03-21 21:55:07 +07:00
Chan Wen Xu ad5b341b1e
feat: Add reflection to create JS value from Go value 2021-03-21 21:55:07 +07:00
Chan Wen Xu f3864a59ca
feat: Implement Promise 2021-03-21 21:55:07 +07:00
Chan Wen Xu 392b175abf
feat: Implement a type-safe Object struct 2021-03-21 21:55:07 +07:00
ALI Hamza da1769920a
feat(js): implement wrapper for interfacing with Go functions in JS 2021-03-21 20:52:49 +07:00
ALI Hamza 3b119bd81a
fix(js): do callback on correct type when go compilation fails 2021-03-21 20:48:05 +07:00
ALI Hamza 92296f6a56
refactor(js): apply pr suggestions 2021-03-21 16:25:22 +07:00
ALI Hamza caaad47c74
refactor(js): move webpack loader to src 2021-03-21 12:21:34 +07:00
ALI Hamza f6150013d5
feat: add LICENSE 2021-03-21 12:12:43 +07:00
ALI Hamza f6d264ea40
refactor(js): remove debug calls in bridge.js 2021-03-21 12:12:38 +07:00
ALI Hamza ef20dff682
feat(js): error when GOROOT not found, add dev server to example 2021-03-21 12:11:36 +07:00
ALI Hamza f19165c53d
feat(js): add basic webpack loader with example 2021-03-21 12:11:36 +07:00
23 changed files with 12000 additions and 9 deletions

4
.gitignore vendored

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

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

@ -0,0 +1,21 @@
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.

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

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Golang-WASM</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
{
"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"
}
}

@ -0,0 +1,20 @@
package main
import (
"fmt"
"syscall/js"
)
func main() {
fmt.Println("Hello from go-mod-wasm!")
setup()
c := make(chan bool, 0) // To use anything from Go WASM, the program may not exit.
<-c
}
func setup() {
fmt.Println("golang-wasm initialized")
js.Global()
}

@ -0,0 +1,8 @@
import wasm from './api/main.go';
const { hello, helloName } = wasm;
(async () => {
console.log(await hello());
console.log(await helloName("world"));
})()

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

@ -0,0 +1,79 @@
const g = global || window || self;
if (!g.__go_wasm__) {
g.__go_wasm__ = {};
}
const maxTime = 3 * 1000;
const bridge = g.__go_wasm__;
/**
* Wrapper is used by Go to run all Go functions in JS.
* Go functions always return an object of the following spec:
* {
* result: undefined | any // undefined when error is returned, or function returns undefined
* error: Error | undefined // undefined when no error is present
* }
*/
function wrapper(goFunc) {
return (...args) => {
const result = goFunc.apply(undefined, args);
if (result.error instanceof Error) {
throw result.error;
}
return result.result;
}
}
bridge.__wrapper__ = wrapper
function sleep() {
return new Promise(requestAnimationFrame);
}
export default function (getBytes) {
let proxy;
async function init() {
const go = new g.Go();
let bytes = await getBytes;
let result = await WebAssembly.instantiate(bytes, go.importObject);
go.run(result.instance);
bridge.__proxy__ = proxy
setTimeout(() => {
if (bridge.__ready__ !== true) {
console.warn("Golang Wasm Bridge (__go_wasm__.__ready__) still not true after max time");
}
}, maxTime);
}
init();
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]);
return;
}
try {
res(bridge[key].apply(undefined, args));
} catch (e) {
rej(e)
}
})
};
}
}
);
return proxy;
}

@ -0,0 +1,85 @@
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));
}
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);
this.addContextDependency(modDir);
cb(null,
`require('!${wasmSavePath}');
import goWasm from '${path.join(__dirname, 'bridge.js')}';
const wasm = fetch('${emitPath}').then(response => response.arrayBuffer());
export default goWasm(wasm);`);
})();
}

@ -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)
ptrX := reflect.New(paramType).Interface()
err := FromJSValue(v, ptrX)
if err != nil {
return nil, err
}
in = append(in, reflect.ValueOf(ptrX).Elem())
}
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,411 @@
package wasm
import (
"errors"
"fmt"
"reflect"
"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 {
v := reflect.ValueOf(out)
if v.Kind() != reflect.Ptr || v.IsNil() {
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())
}

@ -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)
}