Deno Webassembly Running a Go program in Deno via WASM

taterbase profile image George ・4 min read

Deno v1.0 landed this week and I just wanted to take a moment to talk about how you can run a Go program in Deno via WASM bytecode. If you don't know what Deno is be sure to click that link and read up on the release as it's incredibly interesting. Long story short it's a Rust runtime that comes bundled with V8 and can interpret JavaScript/TypeScript (and WASM) natively within a secure environment.

To start, we'll need to write a Go program. Let's do something trivial just to prove it works. We'll write this in a file called main.go.

package main

import "fmt"

func main() {
        fmt.Println("hello deno")

Great, we can run go build -o hello-deno and we'll get a binary that we can run called hello-deno. Executing that binary is as easy as ./hello-deno.

taterbase:~$ ls
taterbase:~$ go build -o hello-deno
taterbase:~$ ls
hello-deno main.go 
taterbase:~$ ./hello-deno
hello deno

Here we've confirmed the program will build and run natively. Now, let's generate the WASM bytecode. Go has great docs on how to generate WASM binaries. I'll cut to the chase and tell you that in order to cross-compile our code to WASM we'll need to set two environment variables. GOOS=js and GOARCH=wasm. Typically when cross-compiling Go code you need to specify the target operating system/runtime environment (GOOS) in this case js for JavaScript, and the target architecture (GOARCH) which is wasm. Let's do that now.

taterbase:~$ GOOS=js GOARCH=wasm go build -o deno.wasm
taterbase:~$ ls
deno.wasm  hello-deno  main.go

Now that we have our WASM bytecode we can start setting up the scaffolding needed to execute it within Deno. One important note about running WASM generated from Go code is you must import a support js file that Go provides in its installation directory. You can copy it like so cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . (this is detailed in the Go WebAssembly docs linked above).

taterbase:~$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
taterbase:~$ ls
deno.wasm  hello-deno  main.go  wasm_exec.js

Let's write the bootstrap js code now. I'm going to call it deno.js

import * as _ from "./wasm_exec.js";
const go = new window.Go();
const f = await Deno.open("./deno.wasm")
const buf = await Deno.readAll(f);
const inst = await WebAssembly.instantiate(buf, go.importObject);

Here's what's happening line by line.

  1. The import at the top is to just bring the go support js code into the runtime. It attaches a constructor, Go, to the window object for us to use later.
  2. We then create go as an instance of the Go "class".
  3. Using a core Deno api, we open the wasm bytecode file. Opening a file is an asynchronous action and we use the await keyword to the tell the program to let the operation finish before proceeding.
  4. We then use another built in async operation, readAll to read the whole buffer from the wasm file. This will give us a Uint8Array that represents the bytes of the of wasm file.
  5. We then create a WebAssembly instance, passing in our byte array and the importObject provided by our Go support code. I'm not completely clear on the value of the importObject but from what I gather it maps important values/functions that the modules inside the WASM bytecode expect to be available to execute. All I know at this moment is it's required for execution, so pass it in.
  6. We then use the support go instance to run instance itself. This executes the wasm code!

Let's run it and see what happens.

taterbase:~$ deno run deno.js
error: Uncaught PermissionDenied: read access to "/home/taterbase/wasm-go/deno.wasm", run again with the --allow-read flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async Object.open ($deno$/files.ts:37:15)
    at async file:///home/taterbase/wasm-go/deno.js:3:11

We've run up against one of Deno's highly touted features, out of the box security. By default Deno won't let us read/write from the filesystem (or even make network calls for that matter). We need to explicitly allow it access to the filesystem.

taterbase:~$ deno run --allow-read deno.js
hello deno

There you have it. We took Go code, compiled it to wasm bytecode, and ran it within Deno! I hope you find this helpful. Most logic can be cross-compiled and run successfully however things get tricky when you start doing i/o. I've been doing some reading and while I can't get tcp socket listening in a Go program working out of the box I hope to do another writeup in the future showing a solution for that.

Until then, happy hacking.


Editor guide