diff --git a/example/basic/dist/index.html b/example/basic/dist/index.html index b2ca710..4958b51 100644 --- a/example/basic/dist/index.html +++ b/example/basic/dist/index.html @@ -3,10 +3,23 @@ - Example + Golang-WASM Example +

This is an example Golang-WASM Project.

+

+ + Sample Go code + +

+ +

Value from Go:

+

 

+ +

Call function from Go:

+ +

 

diff --git a/example/basic/src/api/bridge.go b/example/basic/src/api/bridge.go new file mode 100644 index 0000000..bca3fb5 --- /dev/null +++ b/example/basic/src/api/bridge.go @@ -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__") +} diff --git a/example/basic/src/api/main.go b/example/basic/src/api/main.go index 5e6c8bf..75090c5 100644 --- a/example/basic/src/api/main.go +++ b/example/basic/src/api/main.go @@ -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() } diff --git a/example/basic/src/index.js b/example/basic/src/index.js index b491452..020dbc6 100644 --- a/example/basic/src/index.js +++ b/example/basic/src/index.js @@ -2,7 +2,17 @@ import wasm from './api/main.go'; const { hello, helloName } = wasm; -(async () => { - console.log(await hello()); - console.log(await helloName("world")); -})() \ No newline at end of file +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() \ No newline at end of file diff --git a/package.json b/package.json index b5d9165..4d2f981 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/bridge.js b/src/bridge.js index 6073124..4c85da6 100644 --- a/src/bridge.js +++ b/src/bridge.js @@ -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 - setTimeout(() => { - if (bridge.__ready__ !== true) { - console.warn("Golang Wasm Bridge (__go_wasm__.__ready__) still not true after max time"); - } - }, maxTime); } init(); + setTimeout(() => { + if (bridge.__ready__ !== true) { + console.warn("Golang WASM Bridge (__go_wasm__.__ready__) still not true after max time"); + } + }, maxTime); 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; } diff --git a/src/index.js b/src/index.js index d12ff1c..a68600d 100644 --- a/src/index.js +++ b/src/index.js @@ -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}');