WIP: documentation + bug fixes for JS / examples #5

Draft
hamza wants to merge 5 commits from hamza/golang-wasm:master into master
7 changed files with 182 additions and 41 deletions

@ -3,10 +3,23 @@
<head>
<meta charset="utf-8" />
<title>Example</title>
<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>

@ -0,0 +1,79 @@
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__")
}

@ -5,27 +5,24 @@ import (
"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
}
const hello = "Sample value"
func helloName(_ js.Value, args []js.Value) interface{} {
return fmt.Sprintf("Hello, %s!", args[0].String())
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 setup() {
bridge := js.Global().Get("__go_wasm__")
bridge.Set("__ready__", true)
bridge.Set("hello", hello)
bridge.Set("helloName", js.FuncOf(helloName))
func main() {
fmt.Println("golang-wasm initialized")
js.Global()
setFunc("helloName", helloName)
setValue("hello", hello)
ready()
}

@ -2,7 +2,17 @@ import wasm from './api/main.go';
const { hello, helloName } = wasm;
(async () => {
console.log(await hello());
console.log(await helloName("world"));
})()
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,11 +1,11 @@
{
"name": "go-mod-wasm",
"version": "0.1.0",
"name": "golang-wasm",
"version": "0.0.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",

@ -1,20 +1,31 @@
// 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.
* Go functions always return an object of the following spec:
*
* @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) => {
@ -25,28 +36,52 @@ function wrapper(goFunc) {
return result.result;
}
}
bridge.__wrapper__ = wrapper
/**
* 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(requestAnimationFrame);
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);
bridge.__proxy__ = proxy
}
init();
setTimeout(() => {
if (bridge.__ready__ !== true) {
console.warn("Golang Wasm Bridge (__go_wasm__.__ready__) still not true after max time");
console.warn("Golang WASM Bridge (__go_wasm__.__ready__) still not true after max time");
}
}, maxTime);
}
init();
proxy = new Proxy(
@ -61,13 +96,17 @@ export default function (getBytes) {
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)
rej(e);
}
})
};
@ -75,5 +114,6 @@ export default function (getBytes) {
}
);
bridge.__proxy__ = proxy;
return proxy;
}

@ -43,11 +43,14 @@ module.exports = function (source) {
}
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];
@ -73,7 +76,6 @@ module.exports = function (source) {
const emitPath = path.basename(outFile);
this.emitFile(emitPath, contents);
this.addContextDependency(modDir);
cb(null,
`require('!${wasmSavePath}');