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:
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.)
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.
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
.