It's become pretty easy to call functions in WASM from the web. Let's take a simple example, assuming we have a function called add
written in Rust that adds two numbers together.
Here's the source code in Rust:
// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.// Import the wasm-bindgen crate.use wasm_bindgen::prelude::*;// Our Add function// wasm-pack requires "exported" functions// to include #[wasm_bindgen]#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 {return a + b;}
Then we can compile it into WASM using wasm-pack and use it in our web application:
wasm-pack build --target web
Starting with Next.js 11, you can import WASM files either by importing them with next/dynamic
components or using WebAssembly
APIs.
For example, let's import the WASM file using next/dynamic
:
import dynamic from 'next/dynamic'export interface AddModuleExports {add(a: Number, b: Number): Number}interface RustComponentProps {number: Number}const RustComponent = dynamic({loader: async () => {// Import the wasm module// @ts-ignoreconst exports = (await import('../add.wasm')) as AddModuleExportsconst { add } = exports// Return a React component that calls the add_one method on the wasm modulereturn ({ a, b }: RustComponentProps) => (<div><>{add(a, b)}</></div>)},})
Or you can use WebAssembly
APIs to fetch and import the WASM file from a static path:
// Put `add.wasm` in `public/wasm/` folder// (or any other static folder)WebAssembly.instantiateStreaming(// Fetch the file and stream into the WebAssembly runtimefetch('/wasm/add.wasm')).then((result) => result.instance.exports.add(1, 1)) // = 2
Ok great, it works well for simple functions, but what if we want to call more complex functions? What about other languages?
At Indent, we primarily write TypeScript on the frontend and Go on the backend. Sometimes there's logic we have to replicate on both sides of the stack that can either be redundant or, worse, drift in logic. Let's see what it's like to call a function written in Go from a Next.js application.
We have some functionality called TestPolicy
that tests customer-defined policies in a client-side simulator to make sure they're working as expected. We want to call this function from inside our Next.js application to save users an extra network request while debugging.
In our Next.js project, we'll create a file cmd/simulator/main.go
with the following source code:
//go:build js && wasmpackage mainimport ("errors""fmt""syscall/js")func main() {testFunc := js.FuncOf(testWrapper)js.Global().Set("testPolicy", testFunc)defer testFunc.Release()<-make(chan bool)}// testWrapper wraps the validate function with `syscall/js` parametersfunc testWrapper(_ js.Value, args []js.Value) any {if len(args) < 1 {return response("", errors.New("missing `policy` argument"))} else if len(args) < 2 {return response("", errors.New("missing `test` argument"))}policy := args[0].String()test := args[1].String()output, err := TestPolicy(policy, test)if err != nil {return response(false, err)}return response(output, nil)}// TestPolicy is out-of-scope for this example, we'll just assume it returns true with no errorfunc TestPolicy(policy string, test string) (bool, error) {return true, nil}func response(out any, err error) any {if err != nil {out = err.Error()}return map[string]any{"output": out, "isError": err != nil}}
We can use the standard Go toolchain to compile this into WASM:
# Put in package.json scripts as "build:wasm"GOOS=js GOARCH=wasm \go build -ldflags=\"-s -w\" \-o public/wasm/simulator.wasm \cmd/simulator/main.go
Once we've built the WASM, we can try importing it into our Next.js application like we did earlier... But we get this error:
Module not found: Can't resolve 'gojs'
Screenshot of Google search with zero results
It's never a great sign when the error message is a phrase that has 0 results on Google. Let's see what's going on.
The only "gojs" I'm familiar with is gojs.net the diagramming tool. And the gojs
package on NPM is also for the diagramming tool. What is going on here?
Well, it turns out that Go wasm modules require wasm_exec.js
to be loaded in the browser. This file is included in the standard Go toolchain, but it's not referring to the package on NPM.
With wasm_exec.js
loaded, we can instantiate the module:
// Go is defined in wasm_exec.jsconst go = new Go()WebAssembly.instantiateStreaming(// Fetch the file and stream into the WebAssembly runtimefetch('/wasm/simulator.wasm'),// importObject is where `gojs` is definedgo.importObject).then((result) => {// Since we used `js.Global().Set` in Go, we can access the function globallyconst result = window.testPolicy('<policy>', '<test>')console.log(result)})
And it works!
{"output":true,"isError":false}
But when we looked at the WASM file, it was pretty big: 43MB. Can we make it smaller since we're loading it client-side?
Let's gzip
the WASM file and see what happens:
gzip --best -f < public/static/wasm/simulator.wasm \> public/static/wasm/simulator.wasm.gz
The problem is that when we try to stream the gzipped file, we get an error because the streaming WebAssembly API doesn't support gzipped files. We can use pako
to gunzip the file before instantiating:
import pako from 'pako'const go = new Go()const imports = go.importObjectconst res = await fetch('/static/wasm/cel.wasm.gz')const gzBuf = await res.arrayBuffer()let buffer = pako.ungzip(gzBuf)// (Firefox) Sometimes buffer is double-gzippedif (buffer[0] === 0x1f && buffer[1] === 0x8b) {buffer = pako.ungzip(buffer)}const result = await WebAssembly.instantiate(buffer, imports)// Running the module will define `window.testPolicy`go.run(result.instance)
Great! Now, we've got a small WASM file that we can use to call functions written in Go from inside our Next.js application. In future blog posts, we'll cover other languages like Python, using WASM in API routes, and Next.js 13 Server Actions.
Have any questions? Feel free to reach out to me on Twitter/𝕏 @fouadmatin or send us an email: [email protected]
Are you on a team that's building a product that store customer data or need to perform sensitive operations on your systems?
Solve the root problem of over permissioned users and grant time-bound access to production. Use configurable peer or auto-approvals to remove bottlenecks for granting on-call or low-risk access instantly.
Want to learn more about Indent? Feel free to schedule time that works for you and we're happy to answer any questions you have!