├── .gitignore ├── LICENSE ├── Makefile ├── README-inlined-version.md ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.js ├── index.css ├── index.js ├── locateFile.js └── matrixMultiply.c └── stressit.png /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # webasm output 4 | src/matrixMultiply.mjs 5 | public/matrixMultiply.wasm 6 | 7 | 8 | # dependencies 9 | /node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /coverage 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bobbie Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | public/matrixMultiply.wasm: src/matrixMultiply.mjs 2 | src/matrixMultiply.mjs: src/matrixMultiply.c 3 | emcc --no-entry src/matrixMultiply.c -o src/matrixMultiply.mjs \ 4 | --pre-js src/locateFile.js \ 5 | -s ENVIRONMENT='web' \ 6 | -s EXPORT_NAME='createModule' \ 7 | -s USE_ES6_IMPORT_META=0 \ 8 | -s EXPORTED_FUNCTIONS='["_add", "_matrixMultiply", "_malloc", "_free"]' \ 9 | -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ 10 | -O3 11 | mv src/matrixMultiply.wasm public/matrixMultiply.wasm 12 | 13 | .PHONY: clean 14 | clean: 15 | rm public/matrixMultiply.wasm src/matrixMultiply.mjs -------------------------------------------------------------------------------- /README-inlined-version.md: -------------------------------------------------------------------------------- 1 | This is the original README, which used `-s SINGLE_FILE=1` to in-line the generated WASM into the `.mjs` file. 2 | This results in an overhead in file size. 3 | 4 | The main README describes a better solution that I found later. 5 | 6 | --- 7 | 8 | # React C/C++ WASM demo 9 | 10 | This project is a minimal [create-react-app](https://create-react-app.dev/) project that demonstrates how to compile C/C++ code into an ES6 WebAssembly module and use it in a create-react-app React app (without having to eject). 11 | 12 | This is useful for getting native performance out of a computation-heavy part of a React app - for example, scientific/engineering simulations, video processing, or any other [WebAssembly Use Case](https://webassembly.org/docs/use-cases/). 13 | 14 | ## Table of contents 15 | 16 | - [Table of contents](#table-of-contents) 17 | - [How to run](#how-to-run) 18 | - [How to get here from a fresh create-react-app](#how-to-get-here-from-a-fresh-create-react-app) 19 | - [Extending](#extending) 20 | - [Caveats](#caveats) 21 | - [Notes](#notes) 22 | - [Motivation](#motivation) 23 | - [How I got here](#how-i-got-here) 24 | - [Explaining the emcc compiler invocation](#explaining-the-emcc-compiler-invocation) 25 | - [Other helpful resources](#other-helpful-resources) 26 | 27 | ## How to run 28 | 29 | Prerequisites: 30 | 31 | - [npm](https://www.npmjs.com/get-npm) 32 | - [Emscripten toolchain](https://emscripten.org/docs/getting_started/index.html) 33 | - GNU `make` 34 | 35 | Run `make`. 36 | 37 | The default Makefile target will compile `matrixMultiply.c` into `matrixMultiply.mjs`, which is imported in App.js. 38 | 39 | After this, you can `npm install` and `npm start` to run the local dev server. 40 | 41 | At localhost:3000 (which should be automatically opened by `npm start`), you will briefly see `Loading webassembly...`, then some output which shows some math (which is done using WebAssembly). 42 | 43 | ## How to get here from a fresh create-react-app 44 | 45 | 1. Add `src/matrixMultiply.c` 46 | 2. Add Makefile with command to compile `src/matrixMultiply.mjs` 47 | 3. Add `"ignorePatterns": ["src/matrixMultiply.mjs"]` to `eslintConfig` in package.json 48 | - This is required because the ES6 module (`.mjs` file) fails linting 49 | 4. Import `createModule` from the .mjs file in App.js, instantiate it (which returns a Promise), and resolve the Promise to do things with the resulting module (`Module` in App.js). 50 | 51 | All the interesting code is in `src/matrixMultiply.c` and `App.js`. 52 | The Makefile shows how to compile the .c file into the .mjs file. 53 | The ESLint config change is just required to build the app. 54 | 55 | ## Extending 56 | 57 | To make changes to the React code, edit `App.js`. 58 | 59 | To make changes to the C code, edit `matrixMultiply.c` and run `make` again. 60 | 61 | You can play with the `emcc` command if you need something else from the compiler (`make -B` is useful to force re-run the command during development). 62 | 63 | ## Caveats 64 | 65 | 1. If you use this, your app will probably be bigger and might load slower. 66 | Compiling to single-file .mjs in-lines the WASM content as base64 in the variable `wasmBinaryFile`. 67 | Base64 encoding adds an overhead of 33% in file size; this will also be loaded with your app instead of being fetched asynchronously. 68 | Maybe this is fine for you, if you need the WebAssembly to do anything. 69 | 70 | ## Notes 71 | 72 | ### Motivation 73 | 74 | My friend Louis was writing an educational mechanical engineering game, where you are given the image of a stress distribution and need to draw in the forces that would produce it. 75 | 76 | ![An image of two rainbow stress distribution diagrams. The top diagram is labeled Target. The bottom diagram shows a user-provided force vector and the resulting stress diagram.](./stressit.png) 77 | 78 | But, it was slow, causing my browser to hang on the larger levels - 79 | in profiling we found almost all the time was spent in a large matrix multiply in a finite element method calculation. 80 | 81 | Matrix multiply felt like an ideal use case for WebAssembly: a highly numerical, all-computational task where native performance would help. But when I tried to use WebAssembly with React, it seemed to be very hard without doing one of the following: 82 | 83 | - Ejecting from create-react-app to mess with the `webpack` config 84 | - Using `react-app-rewired` or `craco` to mess with webpack without ejecting 85 | - Hosting the .wasm file somewhere else entirely and fetching it 86 | 87 | Eventually I ended up with the solution shown here. 88 | Compared against the original implementation with [math.js](https://mathjs.org/), our WASM naive matrix multiply at -O0 (no optimization) was ~50% (1.5x) faster in Chrome and ~5,000% faster (51x) faster in Safari (\*). 89 | It got 10x faster again at -O3, which gave us a new performance bottleneck in a pure JS matrix inversion! 90 | There is a lot more performance to squeeze out: this matrix multiply implementation can get a lot faster (as any [213/CS:APP](http://csapp.cs.cmu.edu/) student would know from Cache Lab), and we can continue to move more work into the WASM module. 91 | That work is still in progress, but when it's done I'll link it here. 92 | 93 | \* I didn't look into why this was such a big difference (or if it was some mistake in recording times). 94 | The pure JS code was faster in Chrome than in Safari, maybe because of V8 engine performance over JavaScriptCore. 95 | But, Safari's WASM code also ran twice as fast as Chrome's WASM code. 96 | 97 | ### How I got here 98 | 99 | Most of the intro-to-WebAssembly-type articles I found while my search involved using compiling to a .wasm file, and then fetching and instantiating it with `instantiateStreaming`. 100 | I instantly ran into problems when I tried this with create-react-app, because the default webpack configuration wouldn't let me serve a .wasm file. 101 | The rest of the intro articles used `.html` scaffolding targets - I also had issues getting this to work with create-react-app. 102 | 103 | So, initially I would generate an .wasm file, and use the approach from [sipavlovic/wasm2js](https://github.com/sipavlovic/wasm2js) to include it as base64 (you can see this in older commits on this repo). 104 | This worked well for my simple "add two integers" function. 105 | But I ran into issues when I needed methods on the `Module` object to work with memory to pass around arrays for matrixMultiply. 106 | 107 | Eventually I figured out `emcc` can directly generate ES6 Javascript modules, with base64-inlined code! 108 | 109 | ``` 110 | -o 111 | 112 | [link] When linking an executable, the target file name extension defines the output type to be generated: 113 | 114 | .mjs : ES6 JavaScript module (+ separate .wasm file if emitting WebAssembly). 115 | ``` 116 | 117 | Starting from [this cryptic note in the emcc docs](https://emscripten.org/docs/tools_reference/emcc.html), I tried and failed a bunch of times. 118 | With the help of Github issues and source code, I eventually ended up with the command in the Makefile (which is explained line-by-line in the next section). 119 | 120 | Honestly I might be missing something, but I'll try writing this up and contributing back to docs and hopefully someone can check it. 121 | 122 | ### Explaining the emcc compiler invocation 123 | 124 | The Makefile target has this command to generate the target `src/matrixMultiply.js`: 125 | 126 | ``` 127 | src/matrixMultiply.mjs: src/matrixMultiply.c 128 | emcc --no-entry src/matrixMultiply.c -o src/matrixMultiply.mjs \ 129 | -s ENVIRONMENT='web' \ 130 | -s SINGLE_FILE=1 \ 131 | -s EXPORT_NAME='createModule' \ 132 | -s USE_ES6_IMPORT_META=0 \ 133 | -s EXPORTED_FUNCTIONS='["_add", "_matrixMultiply", "_malloc", "_free"]' \ 134 | -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ 135 | -O3 136 | ``` 137 | 138 | Let's go line-by-line. 139 | 140 | --- 141 | 142 | ``` 143 | emcc --no-entry src/matrixMultiply.c -o src/matrixMultiply.mjs \ 144 | ``` 145 | 146 | `emcc src/matrixMultiply.c -o src/matrixMultiply.mjs` says, compile the source .c file into a .mjs file (ES6 Javascript module). 147 | 148 | `--no-entry` is an argument for the linker `wasm-ld` that says we do not have an entrypoint (by default, the main() function). 149 | This is because we basically have a library that we are just picking functions out of. 150 | 151 | --- 152 | 153 | ``` 154 | -s ENVIRONMENT='web' \ 155 | ``` 156 | 157 | All of the `-s` options are documented only in the [settings.js source code](https://github.com/emscripten-core/emscripten/blob/main/src/settings.js), not anywhere on the docs site. 158 | 159 | Here we want to run in the normal web environment for our React app. 160 | So, we disable the environments for webview, web worker, Node.js, and JS shell because we will never run there. 161 | 162 | --- 163 | 164 | ``` 165 | -s SINGLE_FILE=1 \ 166 | ``` 167 | 168 | This option inlines the .wasm file into the .mjs file, as the base64 string `wasmBinaryFile`. 169 | This is the main change that allows us to run our code without changing the webpack configuration. 170 | 171 | --- 172 | 173 | ``` 174 | -s EXPORT_NAME='createModule' \ 175 | ``` 176 | 177 | Since we set the output type as `.mjs` above, emcc will [automatically set MODULARIZE=1 and EXPORT_ES6=1](https://github.com/emscripten-core/emscripten/blob/5f45300c9997d5f13f6f8c008e91c8cf5ba74399/emcc.py#L1215-L1217). 178 | This will create an ES6 Javascript module, with a function that returns a Promise that resolves to the Module object (that is constantly referred to in the docs). 179 | 180 | By default, that factory function is called `Module`, which is confusing because to use it you would need to write something like this: 181 | 182 | ```javascript 183 | import Module from "./matrixMultiply.mjs"; 184 | const myModule = await Module(); 185 | myModule.ccall(/* or whatever */); 186 | ``` 187 | 188 | ...even though the emscripten docs constantly refer to `Module.ccall`, `Module._malloc`, and so on. 189 | 190 | So instead, we follow [the advice in the FAQ](https://emscripten.org/docs/getting_started/FAQ.html?highlight=modularize#how-can-i-tell-when-the-page-is-fully-loaded-and-it-is-safe-to-call-compiled-functions) to rename it to `createModule`. 191 | 192 | --- 193 | 194 | ``` 195 | -s USE_ES6_IMPORT_META=0 \ 196 | ``` 197 | 198 | By default, the generated module uses `import.meta.url`. 199 | This caused my webpack to error out with `Module parse failed: Unexpected token`; setting USE_ES6_IMPORT_META=0 falls back to a polyfill which does run without error: 200 | 201 | ```diff 202 | - var _scriptDir = import.meta.url; 203 | + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; 204 | ``` 205 | 206 | ^ This diff shows the change when setting that flag to 0. 207 | 208 | --- 209 | 210 | ``` 211 | -s EXPORTED_FUNCTIONS='["_add", "_matrixMultiply", "_malloc", "_free"]' \ 212 | ``` 213 | 214 | Exporting these C function names [ensures that they will not be optimized out](https://emscripten.org/docs/getting_started/FAQ.html?highlight=exported_functions#why-do-functions-in-my-c-c-source-code-vanish-when-i-compile-to-javascript-and-or-i-get-no-functions-to-process). 215 | Actually, since we have `EMSCRIPTEN_KEEPALIVE` on `add` and `matrixMultiply`, we technically don't need these here. 216 | But I think it's nice to have explicit reminders of what these functions are, plus it adds a little snippet that aborts with error if you mistakenly call `._add()` or `._matrixMultiply()` on the Promise (as opposed to the Module that the Promise resolves to). 217 | 218 | --- 219 | 220 | ``` 221 | -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ 222 | ``` 223 | 224 | These are the standard ways to call compiled C functions from Javascript. 225 | In the example App.js, we use `cwrap` to get functions that we can call again later. 226 | We could also use `ccall` to make a single call to the function. 227 | See [Emscripten docs](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html?highlight=ccall#interacting-with-code-ccall-cwrap) for more info. 228 | 229 | --- 230 | 231 | ``` 232 | -O3 233 | ``` 234 | 235 | This flag optimizes the compiled code to make it load and run faster. 236 | See Emscripten docs on [Optimizing Code](https://emscripten.org/docs/optimizing/Optimizing-Code.html) for details. 237 | 238 | --- 239 | 240 | ### Other helpful resources 241 | 242 | - The memory management code in `wrapMatrixMultiply` is pretty tedious - Dan Ruta's post on [Passing and returning WebAssembly array parameters](https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97) was helpful to me, and their package [wasm-arrays](https://github.com/DanRuta/wasm-arrays) looks useful for 1-D arrays. 243 | - It looks like [Parcel has a great story around WebAssembly integration](https://parceljs.org/webAssembly.html). I haven't personally tried it yet, but I think it's definitely worth considering (especially if you're not already using create-react-app). 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React C/C++ WASM demo 2 | 3 | This project is a minimal [create-react-app](https://create-react-app.dev/) project that demonstrates how to compile C/C++ code into an ES6 WebAssembly module and use it in a create-react-app React app (without having to eject). 4 | 5 | This is useful for getting native performance out of a computation-heavy part of a React app - for example, scientific/engineering simulations, video processing, or any other [WebAssembly Use Case](https://webassembly.org/docs/use-cases/). 6 | 7 | ## Table of contents 8 | 9 | - [Table of contents](#table-of-contents) 10 | - [How to run](#how-to-run) 11 | - [How to get here from a fresh create-react-app](#how-to-get-here-from-a-fresh-create-react-app) 12 | - [Extending](#extending) 13 | - [Notes](#notes) 14 | - [Motivation](#motivation) 15 | - [How I got here](#how-i-got-here) 16 | - [Explaining the emcc compiler invocation](#explaining-the-emcc-compiler-invocation) 17 | - [Other helpful resources](#other-helpful-resources) 18 | 19 | ## How to run 20 | 21 | Prerequisites: 22 | 23 | - [npm](https://www.npmjs.com/get-npm) 24 | - [Emscripten toolchain](https://emscripten.org/docs/getting_started/index.html) 25 | - GNU `make` 26 | 27 | Run `make`. 28 | 29 | The default Makefile target will compile `matrixMultiply.c` into `matrixMultiply.mjs`, which is imported in App.js. 30 | 31 | After this, you can `npm install` and `npm start` to run the local dev server. 32 | 33 | At localhost:3000 (which should be automatically opened by `npm start`), you will briefly see `Loading webassembly...`, then some output which shows some math (which is done using WebAssembly). 34 | 35 | ## How to get here from a fresh create-react-app 36 | 37 | 1. Add `src/matrixMultiply.c` 38 | 2. Add Makefile with command to compile `src/matrixMultiply.mjs` (and move `matrixMultiply.wasm` into the `public` folder) 39 | 3. Add `"ignorePatterns": ["src/matrixMultiply.mjs"]` to `eslintConfig` in package.json 40 | - This is required because the ES6 module (`.mjs` file) fails linting 41 | 4. Import `createModule` from the .mjs file in App.js, instantiate it (which returns a Promise), and resolve the Promise to do things with the resulting module (`Module` in App.js). 42 | 43 | All the interesting code is in `src/matrixMultiply.c` and `App.js`. 44 | The Makefile shows how to compile the .c file into the .mjs file. 45 | The ESLint config change is just required to build the app. 46 | 47 | ## Extending 48 | 49 | To make changes to the React code, edit `App.js`. 50 | 51 | To make changes to the C code, edit `matrixMultiply.c` and run `make` again. 52 | 53 | You can play with the `emcc` command if you need something else from the compiler (`make -B` is useful to force re-run the command during development). 54 | 55 | ## Notes 56 | 57 | ### Motivation 58 | 59 | My friend Louis was writing an educational mechanical engineering game, where you are given the image of a stress distribution and need to draw in the forces that would produce it. 60 | 61 | ![An image of two rainbow stress distribution diagrams. The top diagram is labeled Target. The bottom diagram shows a user-provided force vector and the resulting stress diagram.](./stressit.png) 62 | 63 | But, it was slow, causing my browser to hang on the larger levels - 64 | in profiling we found almost all the time was spent in a large matrix multiply in a finite element method calculation. 65 | 66 | Matrix multiply felt like an ideal use case for WebAssembly: a highly numerical, all-computational task where native performance would help. But when I tried to use WebAssembly with React, it seemed to be very hard without doing one of the following: 67 | 68 | - Ejecting from create-react-app to mess with the `webpack` config 69 | - Using `react-app-rewired` or `craco` to mess with webpack without ejecting 70 | - Hosting the .wasm file somewhere else entirely and fetching it 71 | 72 | Eventually I ended up with the solution shown in [README-inlined-version.md](README-inlined-version.md), which inlines the WASM into the `.mjs` file. 73 | That's not ideal because the WASM binary is Base64-encoded, which makes files larger. 74 | 75 | (Then over a year later, I realized there's a better way to do it entirely by using the `--pre-js` option to read the WASM file out of the `public` folder, which serves the file directly. 76 | Thanks Evangelos for helping me figure this out.) 77 | 78 | Compared against the original implementation with [math.js](https://mathjs.org/), our WASM naive matrix multiply at -O0 (no optimization) was ~50% (1.5x) faster in Chrome and ~5,000% faster (51x) faster in Safari (\*). 79 | It got 10x faster again at -O3, which gave us a new performance bottleneck in a pure JS matrix inversion! 80 | There is a lot more performance to squeeze out: this matrix multiply implementation can get a lot faster (as any [213/CS:APP](http://csapp.cs.cmu.edu/) student would know from Cache Lab), and we can continue to move more work into the WASM module. 81 | That work is still in progress, but when it's done I'll link it here. 82 | 83 | \* I didn't look into why this was such a big difference (or if it was some mistake in recording times). 84 | The pure JS code was faster in Chrome than in Safari, maybe because of V8 engine performance over JavaScriptCore. 85 | But, Safari's WASM code also ran twice as fast as Chrome's WASM code. 86 | 87 | ### How I got here 88 | 89 | Most of the intro-to-WebAssembly-type articles I found while my search involved using compiling to a .wasm file, and then fetching and instantiating it with `instantiateStreaming`. 90 | I instantly ran into problems when I tried this with create-react-app, because the default webpack configuration wouldn't let me serve a .wasm file. 91 | The rest of the intro articles used `.html` scaffolding targets - I also had issues getting this to work with create-react-app. 92 | 93 | So, initially I would generate an .wasm file, and use the approach from [sipavlovic/wasm2js](https://github.com/sipavlovic/wasm2js) to include it as base64 (you can see this in older commits on this repo). 94 | This worked well for my simple "add two integers" function. 95 | But I ran into issues when I needed methods on the `Module` object to work with memory to pass around arrays for matrixMultiply. 96 | 97 | Eventually I figured out `emcc` can directly generate ES6 Javascript modules, with base64-inlined code. 98 | 99 | ``` 100 | -o 101 | 102 | [link] When linking an executable, the target file name extension defines the output type to be generated: 103 | 104 | .mjs : ES6 JavaScript module (+ separate .wasm file if emitting WebAssembly). 105 | ``` 106 | 107 | Starting from [this cryptic note in the emcc docs](https://emscripten.org/docs/tools_reference/emcc.html), I tried and failed a bunch of times. 108 | With the help of Github issues and source code, I eventually ended up with the command in the [README-inlined-version.md](README-inlined-version.md). 109 | 110 | Then I ran into a similar issue related to loading `.data` files for the WASM virtual filesystem. 111 | For some reason this made me remember the `public` file exists, and I spent some time reading through the `prettier`-formatted `.mjs` file to see where I could tweak the path that the WASM file is loaded from. 112 | It wasn't very hard to search for a literal `.wasm`, which led me to `locateFile` and the solution described here. 113 | 114 | ### Explaining the emcc compiler invocation 115 | 116 | The Makefile target has this command to generate the target `src/matrixMultiply.js`: 117 | 118 | ``` 119 | src/matrixMultiply.mjs: src/matrixMultiply.c 120 | emcc --no-entry src/matrixMultiply.c -o src/matrixMultiply.mjs \ 121 | --pre-js src/locateFile.js \ 122 | -s ENVIRONMENT='web' \ 123 | -s EXPORT_NAME='createModule' \ 124 | -s USE_ES6_IMPORT_META=0 \ 125 | -s EXPORTED_FUNCTIONS='["_add", "_matrixMultiply", "_malloc", "_free"]' \ 126 | -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ 127 | -O3 128 | mv src/matrixMultiply.wasm public/matrixMultiply.wasm 129 | ``` 130 | 131 | Let's go line-by-line. 132 | 133 | --- 134 | 135 | ``` 136 | emcc --no-entry src/matrixMultiply.c -o src/matrixMultiply.mjs \ 137 | ``` 138 | 139 | `emcc src/matrixMultiply.c -o src/matrixMultiply.mjs` says, compile the source .c file into a .mjs file (ES6 Javascript module). 140 | 141 | `--no-entry` is an argument for the linker `wasm-ld` that says we do not have an entrypoint (by default, the main() function). 142 | This is because we basically have a library that we are just picking functions out of. 143 | 144 | --- 145 | 146 | ``` 147 | --pre-js src/locateFile.js \ 148 | ``` 149 | 150 | This uses `emcc`'s [`--pre-js`](https://emscripten.org/docs/tools_reference/emcc.html#emcc-pre-js) function to override the behavior of the `locateFile` function, as described here: 151 | 152 | ```javascript 153 | oldLocateFile = (path, scriptDirectory) => scriptDirectory + path; 154 | newLocateFile = (path, scriptDirectory_unused) => path; 155 | ``` 156 | 157 | This function determines the location of the `.wasm` file that is fetched. 158 | With the old implementation, it expects `./static/js`, where `create-react-app` places the `bundle.js` created from the Javascript in `src`. 159 | But because of the `webpack` pain mentioned above, we'd rather look in the root directory, where unmodified `public` files go. 160 | 161 | --- 162 | 163 | ``` 164 | -s ENVIRONMENT='web' \ 165 | ``` 166 | 167 | All of the `-s` options are documented only in the [settings.js source code](https://github.com/emscripten-core/emscripten/blob/main/src/settings.js), not anywhere on the docs site. 168 | 169 | Here we want to run in the normal web environment for our React app. 170 | So, we disable the environments for webview, web worker, Node.js, and JS shell because we will never run there. 171 | 172 | --- 173 | 174 | ``` 175 | -s SINGLE_FILE=1 \ 176 | ``` 177 | 178 | This option inlines the .wasm file into the .mjs file, as the base64 string `wasmBinaryFile`. 179 | This is the main change that allows us to run our code without changing the webpack configuration. 180 | 181 | --- 182 | 183 | ``` 184 | -s EXPORT_NAME='createModule' \ 185 | ``` 186 | 187 | Since we set the output type as `.mjs` above, emcc will [automatically set MODULARIZE=1 and EXPORT_ES6=1](https://github.com/emscripten-core/emscripten/blob/5f45300c9997d5f13f6f8c008e91c8cf5ba74399/emcc.py#L1215-L1217). 188 | This will create an ES6 Javascript module, with a function that returns a Promise that resolves to the Module object (that is constantly referred to in the docs). 189 | 190 | By default, that factory function is called `Module`, which is confusing because to use it you would need to write something like this: 191 | 192 | ```javascript 193 | import Module from "./matrixMultiply.mjs"; 194 | const myModule = await Module(); 195 | myModule.ccall(/* or whatever */); 196 | ``` 197 | 198 | ...even though the emscripten docs constantly refer to `Module.ccall`, `Module._malloc`, and so on. 199 | 200 | So instead, we follow [the advice in the FAQ](https://emscripten.org/docs/getting_started/FAQ.html?highlight=modularize#how-can-i-tell-when-the-page-is-fully-loaded-and-it-is-safe-to-call-compiled-functions) to rename it to `createModule`. 201 | 202 | --- 203 | 204 | ``` 205 | -s USE_ES6_IMPORT_META=0 \ 206 | ``` 207 | 208 | By default, the generated module uses `import.meta.url`. 209 | This caused my webpack to error out with `Module parse failed: Unexpected token`; setting USE_ES6_IMPORT_META=0 falls back to a polyfill which does run without error: 210 | 211 | ```diff 212 | - var _scriptDir = import.meta.url; 213 | + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; 214 | ``` 215 | 216 | ^ This diff shows the change when setting that flag to 0. 217 | 218 | --- 219 | 220 | ``` 221 | -s EXPORTED_FUNCTIONS='["_add", "_matrixMultiply", "_malloc", "_free"]' \ 222 | ``` 223 | 224 | Exporting these C function names [ensures that they will not be optimized out](https://emscripten.org/docs/getting_started/FAQ.html?highlight=exported_functions#why-do-functions-in-my-c-c-source-code-vanish-when-i-compile-to-javascript-and-or-i-get-no-functions-to-process). 225 | Actually, since we have `EMSCRIPTEN_KEEPALIVE` on `add` and `matrixMultiply`, we technically don't need these here. 226 | But I think it's nice to have explicit reminders of what these functions are, plus it adds a little snippet that aborts with error if you mistakenly call `._add()` or `._matrixMultiply()` on the Promise (as opposed to the Module that the Promise resolves to). 227 | 228 | --- 229 | 230 | ``` 231 | -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ 232 | ``` 233 | 234 | These are the standard ways to call compiled C functions from Javascript. 235 | In the example App.js, we use `cwrap` to get functions that we can call again later. 236 | We could also use `ccall` to make a single call to the function. 237 | See [Emscripten docs](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html?highlight=ccall#interacting-with-code-ccall-cwrap) for more info. 238 | 239 | --- 240 | 241 | ``` 242 | -O3 243 | ``` 244 | 245 | This flag optimizes the compiled code to make it load and run faster. 246 | See Emscripten docs on [Optimizing Code](https://emscripten.org/docs/optimizing/Optimizing-Code.html) for details. 247 | 248 | --- 249 | 250 | ```shell 251 | mv src/matrixMultiply.wasm public/matrixMultiply.wasm 252 | ``` 253 | 254 | Finally, we move the `.wasm` file into the public folder. 255 | I don't think there's an easy way to do this from the `emcc` command, but it's not like calling `mv` is very hard. 256 | 257 | This approach can also be used for the `.data` files generated by `--preload-file` for the WebAssembly virtual filesystem. 258 | You'll want to check the generated `.mjs` for the logic around `REMOTE_PACKAGE_NAME` to see if the right path is being fetched. 259 | 260 | --- 261 | 262 | ### Other helpful resources 263 | 264 | - The memory management code in `wrapMatrixMultiply` is pretty tedious - Dan Ruta's post on [Passing and returning WebAssembly array parameters](https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97) was helpful to me, and their package [wasm-arrays](https://github.com/DanRuta/wasm-arrays) looks useful for 1-D arrays. 265 | - It looks like [Parcel has a great story around WebAssembly integration](https://parceljs.org/webAssembly.html). I haven't personally tried it yet, but I think it's definitely worth considering (especially if you're not already using create-react-app). 266 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-wasm-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.10", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.8.3", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-scripts": "4.0.3", 12 | "web-vitals": "^1.1.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ], 25 | "ignorePatterns": ["src/matrixMultiply.mjs"] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import createModule from "./matrixMultiply.mjs"; 3 | 4 | function wrapMatrixMultiply(Module) { 5 | // JS-friendly wrapper around the WASM call 6 | return function (firstMatrix, secondMatrix) { 7 | // multiplies two square matrices (as 2-D arrays) of the same size and returns the result 8 | const length = firstMatrix.length; 9 | 10 | // set up input arrays with the input data 11 | const flatFirst = new Float32Array(firstMatrix.flat()); 12 | const flatSecond = new Float32Array(secondMatrix.flat()); 13 | const buffer1 = Module._malloc( 14 | flatFirst.length * flatFirst.BYTES_PER_ELEMENT 15 | ); 16 | const buffer2 = Module._malloc( 17 | flatSecond.length * flatSecond.BYTES_PER_ELEMENT 18 | ); 19 | Module.HEAPF32.set(flatFirst, buffer1 >> 2); 20 | Module.HEAPF32.set(flatSecond, buffer2 >> 2); 21 | 22 | // allocate memory for the result array 23 | const resultBuffer = Module._malloc( 24 | flatFirst.length * flatFirst.BYTES_PER_ELEMENT 25 | ); 26 | 27 | // make the call 28 | const resultPointer = Module.ccall( 29 | "matrixMultiply", 30 | "number", 31 | ["number", "number", "number", "number"], 32 | [buffer1, buffer2, resultBuffer, length] 33 | ); 34 | 35 | // get the data from the returned pointer into an flat array 36 | const resultFlatArray = []; 37 | for (let i = 0; i < length ** 2; i++) { 38 | resultFlatArray.push( 39 | Module.HEAPF32[resultPointer / Float32Array.BYTES_PER_ELEMENT + i] 40 | ); 41 | } 42 | 43 | // convert the flat array back into an array of arrays 44 | const result = []; 45 | while (resultFlatArray.length) { 46 | result.push(resultFlatArray.splice(0, length)); 47 | } 48 | Module._free(buffer1); 49 | Module._free(buffer2); 50 | Module._free(resultBuffer); 51 | return result; 52 | }; 53 | } 54 | 55 | function App() { 56 | const [add, setAdd] = useState(); 57 | const [matrixMultiply, setMatrixMultiply] = useState(); 58 | useEffect( 59 | // useEffect here is roughly equivalent to putting this in componentDidMount for a class component 60 | () => { 61 | createModule().then((Module) => { 62 | // need to use callback form (() => function) to ensure that `add` is set to the function 63 | // if you use setX(myModule.cwrap(...)) then React will try to set newX = myModule.cwrap(currentX), which is wrong 64 | setAdd(() => Module.cwrap("add", "number", ["number", "number"])); 65 | setMatrixMultiply(() => wrapMatrixMultiply(Module)); 66 | }); 67 | }, 68 | [] 69 | ); 70 | 71 | if (!add || !matrixMultiply) { 72 | return "Loading webassembly..."; 73 | } 74 | 75 | const result = matrixMultiply( 76 | [ 77 | [1, 2], 78 | [3, 4], 79 | ], 80 | [ 81 | [5, 6], 82 | [7, 8], 83 | ] 84 | ); 85 | 86 | return ( 87 |
88 |

Pls work

89 |
1 + 2 = {add(1, 2)}
90 |
[[1, 2], [3, 4]] @ [[5, 6], [7, 8]] = {JSON.stringify(result)}
91 |
92 | ); 93 | } 94 | 95 | export default App; 96 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /src/locateFile.js: -------------------------------------------------------------------------------- 1 | // this file overrides the wasm file path from scriptDirectory (./static/js) to the server's base URL 2 | Module["locateFile"] = (path, scriptDirectory_unused) => { 3 | return path; 4 | }; 5 | -------------------------------------------------------------------------------- /src/matrixMultiply.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | EMSCRIPTEN_KEEPALIVE float *matrixMultiply(float *arg1, float *arg2, float *result, int length) 5 | /** 6 | * matrixMultiply multiplies two square matrices (arg1, arg2) and places the result in (result). 7 | * 8 | * arg1, arg2: pointers to NxN input matrices, as a flat array of N^2 floats 9 | * result: pointer to NxN output matrix 10 | * length: the side-length N of the NxN matrices to multiply 11 | */ 12 | { 13 | // zero out the result array 14 | for (unsigned int i = 0; i < length * length; i++) 15 | { 16 | result[i] = 0; 17 | } 18 | 19 | // naive matrix multiply (on square inputs) 20 | for (unsigned int i = 0; i < length; i++) 21 | { 22 | 23 | for (unsigned int j = 0; j < length; j++) 24 | { 25 | 26 | for (unsigned int k = 0; k < length; k++) 27 | { 28 | result[i * length + j] += (arg1[i * length + k] * arg2[k * length + j]); 29 | } 30 | } 31 | } 32 | 33 | return result; 34 | } 35 | 36 | EMSCRIPTEN_KEEPALIVE int add(int a, int b) 37 | { 38 | // trivial function for testing 39 | return a + b; 40 | } -------------------------------------------------------------------------------- /stressit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbiec/react-wasm-demo/81f76bcf17d9ff78f762d2156dcb4c9e51920f5f/stressit.png --------------------------------------------------------------------------------