Bringing in a Web Devs Tool Chain
Up until now we’ve been writing our Go code and then using the go build
command to generate our WebAssembly bundle, and sure, this works fine, does what we need it to do, but it doesn’t really fit with how we web developers would be approaching it.
Us web developers are not shy of using a compiler step, or at least a build task, whether you’re converting from one language to another using TypeScript/Fable/Flow/etc., down-leveling ESNext to ESNow or just doing bundling and minifying of multiple scripts into one, it’s rare to find a JavaScript application these days that it’s using a tool like gulp, rollup, parcel or webpack.
I prefer webpack so I decided to look at incorporating it into my process by writing a custom Loader.
A Quick Intro to webpack
If you’re unfamiliar with webpack you really should check out their docs as I won’t do it justice here. Instead I want to focus on the core part of webpack that we need to leverage and how it works.
Because webpack is designed to be a generic module bundler it doesn’t understand how to deal with different languages, whether that’s JSX in React, TypeScript or in our case Go. For that we need to bring in a Loader. A Loader is essentially a JavaScript function that takes the contents of the file you’re “loading” and expects you to return some JavaScript that can be run in the generated bundle.
This means that in our JavaScript file we can write the following:
|
|
And tell webpack to use the right loader when it finds a *.go
file to hopefully generate what we need it to generate.
Ultimately, our goal is to be able to write something like this:
|
|
Now let’s look at how we achieve this.
Creating a Loader
TL;DR: If you don’t really want to see the process you can just check out the source code for the loader and install it into your own project.
As I mentioned above, the loader that we create is just a JavaScript function that receives the contents of the file we’re loading passed into it, meaning we’ll get our raw Go code, which is not particularly helpful because we need to pass the file path to go build
, not the file contents.
But never fear, the loader has a Loader API that we can leverage, and the first thing is that we want to get resourcePath
which gives us the full path to the file. Fantastic, now we are able to send that over to go build
!
Generating WASM in our Loader
We’re going to need to execute go build
in our loader, and to do that we can use child_process
to spawn it.
But before that we’ll need to find the path to the go
binary and for that we’ll use the GOROOT
environment variable (that we learnt about in the first post).
Finally we’re going to use execFile
which is asynchronous, and we’ll have to tell webpack that this loader is async
.
Our loader is starting to look like this (note: I’ve chosen to write this with TypeScript rather than plain ol’ JavaScript):
|
|
I’m also creating the environment variables (in opts
) that sets the appropriate GOOS
and GOARCH
for WASM.
For the file that we generate, I’ll just append .wasm
to the end of the resource that we’re processing. This means that we should be fine writing to disk, but some error handling on the writability of the disk could be useful…
Generating JavaScript for webpack
We’re successfully generating our WASM file but it’s a) dropped in what’s likely our src
folder, not where the rest of the webpack bundles will go and b) we still have to write a bunch of code to use it.
For our objective of it being just like any other piece of JavaScript we’ll want to generate something to give back to webpack. But what will we need to generate?
If we think about it there are two things we need in JavaScript to use a Go WASM binary:
wasm_exec.js
- The WebAssembly loader
Well I think this is something that webpack should do for us, we don’t want to have to write that code ourselves!
We’re going to build up a large string template to send back to webpack, starting with the bootstrapper for WebAssembly:
|
|
This code will be inserted into our bundle and used when we import wasm from './main.go'
, but that only starts up the WASM runtime, what about accessing the stuff we registered?
I decided that I want to enforce the callback pattern from the last post, and that means we’ll need to return something, but what the heck should we return? We’ve got no idea what the names of the functions from Go will be, so how do we know what to return to the import
statement?!
JavaScript Proxies to the Rescue
If you’ve ever done programming with Ruby you may have come across the method_missing
method on BasicObject
which you can use to do metaprogramming. In C# you can do a similar thing with the DLR.
But if you haven’t come across this, basically it’s a special function that gets executed on an object when there are no members of it that match, a last ditch attempt to handle an error before it is thrown.
Unfortunately, JavaScript doesn’t have such a method, but we do have Proxy
.
A Proxy is a wrapper around an object that allows you to do interception of standard JavaScript operations, get, set, etc. and with this we can simulate the method_missing
from Ruby.
Here’s a basic example:
|
|
And we’ll see:
> "captured call to foo"
> "foo"
> "captured call to bar"
> "method_missing"
So we can capture all calls and do something with them before, after or completely replace them.
And we’re going to use that to wrap WASM with our callback pattern:
|
|
Because we register stuff on the global object our proxy is actually of a blank object, since we don’t really want to proxy window
, and anyway we can just ignore the target
that the proxy receives anyway.
Putting it all together
It’s time to put together our template that we’ll give back to webpack, and that will be executed when you import a Go file:
|
|
Ok, it’s a little more advanced that the few snippets above, but let me explain some of the additions:
- Since we are asynchronously loading the WASM file using
fetch
there is the possibility that we’d try and use an exported function before it’s been made available. This would most likely happen if you have a large bundle and/or a slow network connection, so I’ve introduced asleep
function which usesrequestAnimationFrame
as a sleeper (so chucking stuff in the event loop) and waiting until the WASM initialization function completes and setsready
totrue
- I’ve aliased the global that we’re working with so you can use the generated code in Node.js or a browser
- I’m not exposing it as a callback pattern, instead I’m exposing it as a
Promise
, meaning you canasync
/await
with it - I added some error handling, if you call a function that can’t be found the
Promise
is rejected - It also supports setting values not just functions from Go
Finishing our Loader
Template? ✔
Generating WASM file? ✔
Time to combine all of this together so that we can actually run the Loader.
|
|
Remember how we generated the WASM file into the same location on disk as the original .go
file? Well that’s fine to output as go build
requires, but we actually want it to go with the rest of the webpack output. To do this we use the emitFile
method on the loader context, providing it the contents of the file as a Buffer
. That’s why I use readFileSync
to get the file into memory, then I unlinkSync
to delete it from disk, since the original output isn’t needed anymore.
Finally I generate a require
statement to the wasm_exec.js
file that is bundled with the loader (I had to make a minor change to it so it worked with webpack). You’ll see this message in the debugging console:
../lib/wasm_exec.js 9:19-26
Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
This is because the wasm_exec.js
file is being added as a require
statement to webpack but we’re not explicitly exporting anything from it (since it just augments the global
scope), meaning webpack is unsure what we’re actually using in there and it can’t undertake tree shaking to remove unneeded code (and thus optimise the application).
Conclusion
All the code for the loader is on GitHub and I’ve published the loader on npm as golang-wasm-async-loader
. GitHub contains a (works on my machine) example of it in action if you’d like to try it out.
Bonus Round: Ditching Globals and Improving the Go Experience
The astute observer among you will have looked at the source code published to GitHub and noticed it’s not quite what I posted above.
One thing that’s constantly irked me with the stuff I’d read from Go on how to work with WASM is that everything seems to use js.Global
as a dumping place for their functions/values/etc. and that is rather unpleasant because you shouldn’t pollute window
/global
/self
.
I decided that I wanted my loader to address this and to also make it a little easier in Go to work with this, removing the need to understand the JavaScript callback pattern.
So the loader’s GitHub repository also contains a Go package called gobridge
which gives you helpers to register functions and values in Go to JavaScript.
This means I can write some code like so:
|
|
And use gobridge.RegisterCallback
and not worry about working with js.FuncOf
or where to register it in the JavaScript object graph.
And that latter part is important because I don’t want to dump everything on global, I want to namespace it.
Let’s update the JavaScript we’re generating in the Loader to include this:
|
|
Now our Go code can use that, via the gobridge
and we don’t have to worry about trashing anything on window
in the browser!