/wasm-next

Demo nextjs repo with rust WebAssembly

Primary LanguageTypeScript

nextjs, WebAssembly, and wasm-bindgen

This repository demonstrates how to access WebAssembly compiled from rust in a nextjs frontend, both with and without wasm-bindgen. The result is currently in action at https://wasm-next-xi.vercel.app, which shows three output panels from three difference WebAssembly interfaces:

1. Simple WebAssembly processing of single numeric inputs

The first interface is a slightly modified version of the nextjs example at https://github.com/vercel/next.js/tree/canary/examples/with-webassembly, including a WebAssembly module generated from a rust crate, instead of the simple .rs file used in the Vercel example.

The crate is defined in the /wasm directory, and built with the npm script, npm run build:wasm. This command compiles the WebAssembly binary module in the ./pkg directory, where this ./pkg location must also be specified in next.config.js. All of the files, including the compiled binaries, are then committed with this repository, and the whole site built with npm run build. (Compiling binaries on a server requires the community-supported rust runtime.)

2. WebAssembly processing of vectors

The second example uses standard WebAssembly interfaces to accept two input vectors, and returns the result of adding each pair of input elements. The main rust function for this is mult_two in wasm/src/lib.rs. This function demonstrates the standard procedure to pass vectors between TypeScript and Rust: as a pointer to the start of the vector in memory, and an integer defining the length of the vector. The vectors may then be assembled in rust as on lines 16-17 of wasm/src/lib.rs. The length of the return vector must be stored in rust as a global variable, which can then be accessed using the function get_result_len().

The interface to these two WebAssembly functions from TypeScript is demonstrated in src/components/WasmVectorMult.tsx, which demonstrates how the compiled WebAssembly binary module must be explicitly imported in order to access its functions.

3. nextjs, WebAssembly, and wasm-bindgen

The previous example demonstrates some of the intricacies of passing complex, variable-length objects between TypeScript and WebAssembly. The wasm-bindgen crate provides a cleaner interface for passing complex objects between TypeScript and WebAssembly. The final component here uses wasm-bindgen to read two local JSON files bundled with this repository and containing columns of numeric values, to extract a specified column from each of those files, and to compute pairwise average values.

The TypeScript interface using wasm-bindgen is in src/components/WasmBindGen.tsx, where Line 41 demonstrates that the compiled WebAssembly module is accessed in this case by an asynchronous fetch call (equivalent to await import calls in the previous two examples). These calls in nextjs can only access public URLs, which means that the WebAssembly binary must be accessible from the ./public directory of this repository. The package.json file includes a final command to copy the compiled binary from the ./pkg directory across to ./public/pkg. Note that the binary must be copied, not moved, so that copies of the compiled binary must be held both in the internal ./pkg directory, and mirrored in the ./public/pkg directory. (Alternative approaches that avoid this duplication require manually editing the testcrate.js file each time it is automatically re-generated by wasm-pack.)

The WasmBindGen.tsx file uses two main react effects, one to load the JSON files into the module, and the second to pass the associated data to WebAssembly and wait for the response. The JSON data are converted to strings in TypeScript before passing to rust, allowing wasm-bindgen to use generic &str objects, rather than explicit pointers to memory addresses and object lengths. And that, finally, is the whole point of using wasm-bindgen: to avoid the kind of explicit interaction with underlying memory that was necessary in the previous vector example.

One final and important point is that WebAssembly interfaces, with or without wasm-bindgen, are generally defined in a .js file in the WebAssembly build directory (in this case, testcrate.js). These interfaces are automatically generated by wasm-pack, and must be imported into any JavaScript file in which they are used. The previous two examples imported functions directory from the WebAssembly binary. JavaScript interfaces to the wasm-bindgen functions defined in wasm/src/lib.rs are automatically generated in /pkg/testcrate.js, and may be imported and used as in the first line of WasmBindGen.tsx:

import * as wasm_js from "@/../pkg/testcrate.js";

The binary module itself must then also be initalised, and its memory usage synchronised with the JavaScript code, with Line 47 of WasmBindGen.tsx:

const wasm_binary = wasm_js.initSync(bytes);

The two environments have been named here to explicitly demonstrate that initSync is defined in the JavaScript interface, testcrate.js, which may then be called to load and synchronise the WebAssembly module as wasm_binary. Although wasm_binary is not used any further in this example, this initialisation is necessary to enable the module to access memory, and if necessary this memory can be subsequently accessed as wasm_binary.memory.