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
21 changed files with 11470 additions and 50 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);`);
})();
}

@ -9,8 +9,17 @@ import (
// 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))
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
@ -18,30 +27,38 @@ func toJSFunc(x reflect.Value) js.Value {
hasError = funcType.Out(funcType.NumOut()-1) == errorType
}
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return funcWrapper.Invoke(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
return ToJSValue(goThrowable{
Error: NewError(err),
})
}
out := x.Call(in)
if !hasError {
return ToJSValue(returnValue(out))
return ToJSValue(goThrowable{
Result: returnValue(out),
})
}
lastParam := out[len(out)-1]
if !lastParam.IsNil() {
throwWrapper.Invoke(NewError(err))
return nil
return ToJSValue(goThrowable{
Error: NewError(lastParam.Interface().(error)),
})
}
return ToJSValue(returnValue(out[:len(out)-1]))
}).JSValue()
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 {
@ -51,9 +68,14 @@ func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Valu
}
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
}
@ -61,21 +83,28 @@ func conformJSValueToType(funcType reflect.Type, this js.Value, values []js.Valu
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)
ptrX := reflect.New(paramType).Interface()
err := FromJSValue(v, ptrX)
if err != nil {
return nil, err
}
in = append(in, reflect.ValueOf(x))
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 {
if len(x) == 1 {
return ToJSValue(x[0])
switch len(x) {
case 0:
return js.Undefined()
case 1:
return ToJSValue(x[0].Interface())
}
xInterface := make([]interface{}, 0, len(x))
@ -83,5 +112,5 @@ func returnValue(x []reflect.Value) js.Value {
xInterface = append(xInterface, v.Interface())
}
return ToJSValue(x)
return ToJSValue(xInterface)
}

@ -62,7 +62,7 @@ func (o Object) Get(path ...string) (js.Value, error) {
return current, nil
}
// Expect is a helper function that calls Get and checks the type of the result.
// 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) {
@ -81,12 +81,13 @@ func (o Object) Expect(expectedType js.Type, path ...string) (js.Value, error) {
return value, nil
}
// Delete deletes the property from the object.
// 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)
}
@ -97,7 +98,11 @@ func (o Object) Index(i int) js.Value {
}
// 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)
}
@ -111,14 +116,14 @@ func (o Object) Length() int {
return o.value.Length()
}
// Set sets the property p to the value of js.ValueOf(x).
// Set sets the property p to the value of ToJSValue(x).
func (o Object) Set(p string, x interface{}) {
o.value.Set(p, x)
o.value.Set(p, ToJSValue(x))
}
// SetIndex sets the index i to the value of js.ValueOf(x).
// SetIndex sets the index i to the value of ToJSValue(x).
func (o Object) SetIndex(i int, x interface{}) {
o.value.SetIndex(i, x)
o.value.SetIndex(i, ToJSValue(x))
}
// String returns the object marshalled as a JSON string for debugging purposes.

@ -15,11 +15,6 @@ func (p *Promise) FromJSValue(value js.Value) error {
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.
// The handler is spawned in its own goroutine.
func NewPromise(handler func() (interface{}, error)) Promise {
@ -53,7 +48,7 @@ func NewPromise(handler func() (interface{}, error)) Promise {
go func() {
select {
case r := <-resultChan:
resolve.Invoke(r)
resolve.Invoke(ToJSValue(r))
case err := <-errChan:
reject.Invoke(NewError(err))
}

@ -1,11 +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 {
// TODO
return fmt.Errorf("unimplemented")
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())
}

@ -24,14 +24,16 @@ import (
// 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()
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, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr,
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:
@ -69,6 +71,7 @@ func ToJSValue(x interface{}) js.Value {
}
}
// 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 {
@ -83,6 +86,7 @@ func toJSArray(x reflect.Value) js.Value {
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 {
@ -92,6 +96,10 @@ func mapToJSObject(x reflect.Value) js.Value {
obj := objectConstructor.New()
iter := x.MapRange()
for {
if !iter.Next() {
break
}
key := iter.Key()
value := iter.Value().Interface()
switch key := key.Interface().(type) {
@ -123,15 +131,12 @@ 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",
x.Interface()))
}
if !iter.Next() {
break
}
}
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 {
@ -143,12 +148,16 @@ func structToJSObject(x reflect.Value) js.Value {
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)))
obj.Set(name, ToJSValue(x.Field(i).Interface()))
}
return obj

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