Writing a Goboy: building for Webassembly

December 20, 2019

One of my aims with Goboy was always to deploy a web-based version using Webassembly. It actually turned out be remarkably straightforward (see here).

The first step is to build for the Webassembly architecture. Following the instructions in the Go repo, the build incantation was simple:

GO111MODULE=on GOOS=js GOARCH=wasm go build -o builds/wasm/goboy.wasm cmd/goboy/main.go

That ran in the browser using the helper JavaScript specified in the Go repo. Ebiten, the game runner library I’m using, supports Webassembly and so things were painless.

Goboy crashed immediately, however, because the command file relies on os.Executable, which isn’t implemented for WebAssembly and didn’t make sense in the browser anyway. What I wanted to do was allow the user to select a ROM using the file upload dialog and then start up Goboy with that ROM. I had to create a new goboy-wasm command, which exposed a loadROM interface to JS:

js.Global().Set("loadROM", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
	var rom []byte
	for _, el := range args[1].String() {
		rom = append(rom, byte(el))
	}
	romChannel <- jsRom{
		name: args[0].String(),
		data: rom,
	}
	return nil
}))

select {
case rom := <-romChannel:
	runGame(rom.data)
}

js.FuncOf is used to define a function that’s exposed as loadROM. The syntax is a little clunky but it’s really cool to be able to define Go functions that appear in JavaScript:

<script>
  const go = new Go();
  fetch("/js/goboy.wasm")
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.instantiate(bytes, go.importObject))
    .then(result => go.run(result.instance));

  window.addEventListener("message", msg => {
      window.loadROM(msg.data.name, msg.data.data);
  });
</script>

I also had to use WebAssembly.instantiate rather than the recommended WebAssembly.instantiateStreaming because it doesn’t check the MIME type and I was lazy and didn’t want to have to go and add a wasm MIME type to my server. It’s highly recommended to run Ebiten in an iframe so I put the script above in an iframe and had an outer HTML page that contained the user instructions and handled the ROM uploading. The ROM data is passed to the inner wasm iframe using postMessage, which is why I listen for message events.

The main problem I encountered was making sure that the ROM data was encoded and decoded correctly. I ended up using the FileReader API’s readAsBinaryString method. Possibly there is a more efficient solution out there but it worked for me. This is the outer HTML file:

<html>
  <head>
    <title>Goboy</title>
  </head>
  <body>
    <iframe src="wasm.html" id="goboy" height="288" width="320"></iframe>
    <div>
      <label for="id">Choose ROM: </label>
      <input type="file" id="rom" onchange="handleROM(this.files[0])">
    </div>
    <script>
      goboy = document.getElementById("goboy").contentWindow;
      function handleROM(file) {
          const reader = new FileReader();
          reader.onload = () => {
              goboy.postMessage({name: file.name, data: reader.result}, "*");
          };
          reader.readAsBinaryString(file);
      };
    </script>
  </body>
</html>

Creating a working WebAssembly deployment took a few hours, the vast majority of which were spent fiddling around with different ways to encode and decode the ROM data across the JS/Go boundary. Ebiten really made things easy. I was expecting to have to create a canvas and maybe write some rendering code but everything Just Works. Downsides are that Goboy isn’t super speedy in the browser and the .wasm file clocks in at 4MB, albeit without compression.