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
main.go
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);
go.run(inst.instance);
Here's what's happening line by line.
- 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. - We then create
go
as an instance of theGo
"class". - 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. - We then use another built in async operation,
readAll
to read the whole buffer from the wasm file. This will give us aUint8Array
that represents the bytes of the of wasm file. - 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 theimportObject
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. - 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.
Top comments (0)