Compare commits

..

5 Commits

6 changed files with 177 additions and 25 deletions

@ -3,10 +3,23 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Golang-WASM</title> <title>Golang-WASM Example</title>
</head> </head>
<body> <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> <script src="main.js"></script>
</body> </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,16 +5,24 @@ import (
"syscall/js" "syscall/js"
) )
func main() { const hello = "Hello!"
fmt.Println("Hello from go-mod-wasm!")
setup()
c := make(chan bool, 0) // To use anything from Go WASM, the program may not exit. // helloName's first value is JavaScript's `this`.
<-c // 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() { func main() {
fmt.Println("golang-wasm initialized") 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; const { hello, helloName } = wasm;
(async () => { const value = document.getElementById("value");
console.log(await hello()); const input = document.getElementById("input");
console.log(await helloName("world")); 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,20 +1,31 @@
// Initially, the __go_wasm__ object will be an empty object.
const g = global || window || self; const g = global || window || self;
if (!g.__go_wasm__) { if (!g.__go_wasm__) {
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; const maxTime = 3 * 1000;
/**
* bridge is an easier way to refer to the Go WASM object.
*/
const bridge = g.__go_wasm__; const bridge = g.__go_wasm__;
/** /**
* Wrapper is used by Go to run all Go functions in JS. * 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 * result: undefined | any // undefined when error is returned, or function returns undefined
* error: Error | undefined // undefined when no error is present * 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) { function wrapper(goFunc) {
return (...args) => { return (...args) => {
@ -25,28 +36,52 @@ function wrapper(goFunc) {
return result.result; 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() { 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) { export default function (getBytes) {
let proxy; let proxy;
async function init() { async function init() {
bridge.__wrapper__ = wrapper;
const go = new g.Go(); const go = new g.Go();
let bytes = await getBytes; let bytes = await getBytes;
let result = await WebAssembly.instantiate(bytes, go.importObject); let result = await WebAssembly.instantiate(bytes, go.importObject);
go.run(result.instance); go.run(result.instance);
bridge.__proxy__ = proxy }
init();
setTimeout(() => { setTimeout(() => {
if (bridge.__ready__ !== true) { 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); }, maxTime);
}
init();
proxy = new Proxy( proxy = new Proxy(
@ -61,13 +96,17 @@ export default function (getBytes) {
if (typeof bridge[key] !== 'function') { if (typeof bridge[key] !== 'function') {
res(bridge[key]); res(bridge[key]);
if (args.length !== 0) {
console.warn("Retrieved value from WASM returned non-error type, however called with arguments.")
}
return; return;
} }
try { try {
res(bridge[key].apply(undefined, args)); res(bridge[key].apply(undefined, args));
} catch (e) { } catch (e) {
rej(e) rej(e);
} }
}) })
}; };
@ -75,5 +114,6 @@ export default function (getBytes) {
} }
); );
bridge.__proxy__ = proxy;
return proxy; return proxy;
} }

@ -43,11 +43,14 @@ module.exports = function (source) {
} }
modDir = path.join(modDir, ".."); modDir = path.join(modDir, "..");
} }
if (!found) { if (!found) {
return cb(new Error("Could not find go.mod in any parent directory of " + this.resourcePath)); 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 wasmOrigPath = path.join(process.env.GOROOT, "misc", "wasm", "wasm_exec.js");
const wasmSavePath = path.join(__dirname, 'wasm_exec.js'); const wasmSavePath = path.join(__dirname, 'wasm_exec.js');
const errorPaths = ["\t" + wasmOrigPath, "\t" + wasmSavePath]; const errorPaths = ["\t" + wasmOrigPath, "\t" + wasmSavePath];
@ -73,7 +76,6 @@ module.exports = function (source) {
const emitPath = path.basename(outFile); const emitPath = path.basename(outFile);
this.emitFile(emitPath, contents); this.emitFile(emitPath, contents);
this.addContextDependency(modDir);
cb(null, cb(null,
`require('!${wasmSavePath}'); `require('!${wasmSavePath}');