Adding a WebAssembly GLFW project to a React application

May 10, 2024 (2mo ago)

When I wrote the previous article on how to render data visualizations from scratch it took me a few tries to get a working WebAssembly implementation for that kind of project working so I wanted to detail the steps that actually worked in the end.

Compiling a Raylib project to web assembly with Emscripten

If you follow the directions in the Raylib github it is very easy to compile a project to emscripten. You basically need to do a small modification of the code to pull out the render loop (which you can put behind a flag) and then compile the project with emscripten. Something like:

emcc -o www/"$html_file".js \ src/main_web.c "src/$gui_file" ./include/web/raylib/src/libraylib.a \ -I. -I./include/web -L. -L/include/web/raylib/src/libraylib.a \ -s USE_GLFW=3 -s ENVIRONMENT='web' -s EXPORT_NAME="'Module_${html_file}'" -s ASYNCIFY -s MODULARIZE=1 -s EXPORT_ES6=1 -DPLATFORM_WEB \ -Os -Wall

A few notes on some of the flags here:

This will result in a .wasm file and a .js file that your site will use for loading the project. `` If you have a very simple vanilla html / js site and want to have this be the only canvas on the page then at this point you are basically done. That is what happens in the examples on Raylib. But if you want to integrate many canvases into a single page and do it in React there are a few more issues that pop up.

Then the only other basic set up is to ensure that those static wasm files can be picked up by web assembly. In Next.js you edit the configuration to be something like:

const nextConfig = { reactStrictMode: true, swcMinify: true, pageExtensions: ['mdx', 'tsx'], webpack(config, { isServer, dev }) { config.output.webassemblyModuleFilename = isServer && !dev ? '../static/wasm/[modulehash].wasm' : 'static/wasm/[modulehash].wasm'; config.resolve.alias['@'] = path.join(__dirname, '@'); config.experiments = { ...config.experiments, asyncWebAssembly: true, layers: true }; return config; } };

Issue with multiple canvas elements

My default the javascript that is output from emscripten will add some global variables to the window object which are not scoped to a particular project. In my case that was a no-go because I have multiple canvases / web assembly modules on a single page.

To remedy this you can add a flag to the compiler to change the prefix of the global variables. Eg the EXPORT_NAME flag in the command above. This will change the global variables to be Module_${html_file} instead of just Module.

While this fixes the issue with conflicting global variables we still will face another issue when it comes to using these javsacript files that emscripten exports in a React / Next.js project.

The remaining problem is that we have absolute file paths of javasript modules that need to be included.

It would be really nice if we could just write a component which was like

<WasmCanvas path="path/to/out.js" />

and then we dynamically pick up that module. I've found that at least with the default Next.js React setup this does not work -- I think the reason is that those javascript modules are not imported anywhere in the project and so don't get picked up as possible targets for dynamic importing.

I could be wrong about this... but I couldn't seem to find a way to get it to load the module dynamically.

What I settled on: One component for Each Wasm

Because of the issue with dynamically loading the correct module at runtime (which would be ideal) I settled on just hardcoding the module imports for each wasm in a separate React component and then using those in the project.

The final result is something like this:

'use client'; import React from 'react'; export const BarsRender: React.FC<{ script: string }> = () => { const canvasRef = React.useRef<null | HTMLCanvasElement>(null); React.useEffect(() => { const loadWasm = async canvas => { // idealy this import would have been dynamic at runtime // (eg I could pass the file path as a prop) // but webpack doesn't like that const moduleScript = await import('../../lib/bar-chart-c/bars.js'); const wasmModule = await moduleScript.default({ canvas }); return wasmModule; }; if (canvasRef.current) { loadWasm(canvasRef.current) } }, [canvasRef.current]); return ( <> <canvas ref={canvasRef} className="emscripten w-[560px] h-[380px]" id="canvas" onContextMenu={event => event.preventDefault()} tabIndex={-1} ></canvas> </> ); }; export default BarsRender;

This works pretty well -- I just then have a component for every wasm module that I want to render on the page instead of having a nice single component, but it otherwise works well.

Hopefully this was helpful if you get stuck in a similar place while trying to load multiple web assembly modules exported by emscripten. If you happen to know a better way to do this then please drop me a line on X.