Skip to content

Instantly share code, notes, and snippets.

@surma
Last active March 8, 2024 12:06
Show Gist options
  • Save surma/b2705b6cca29357ebea1c9e6e15684cc to your computer and use it in GitHub Desktop.
Save surma/b2705b6cca29357ebea1c9e6e15684cc to your computer and use it in GitHub Desktop.
webpack-emscripten-wasm

Minimal example making webpack and wasm/Emscripten work together.

Build instructions:

  • Clone this gist
  • npm install
  • npm start
  • Open http://localhost:8080
  • Look at console

Note: Docker is required to build this project.

I filed a bug with webpack to make this integration easier.

More questions? Hit me up on Twitter.

License Apache-2.0

<3 Surma

/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
int i, t, a = 0, b = 1;
for (i = 0; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
<!doctype html>
<script src="./dist/bundle.js"></script>
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fibonacci from './fibonacci.js';
import fibonacciModule from './fibonacci.wasm';
// Since webpack will change the name and potentially the path of the
// `.wasm` file, we have to provide a `locateFile()` hook to redirect
// to the appropriate URL.
// More details: https://kripken.github.io/emscripten-site/docs/api_reference/module.html
const module = fibonacci({
locateFile(path) {
if(path.endsWith('.wasm')) {
return fibonacciModule;
}
return path;
}
});
module.onRuntimeInitialized = () => {
console.log(module._fib(12));
};
{
"name": "lol",
"scripts": {
"build:codec": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"fibonacci\"' -o ./fibonacci.js fibonacci.c",
"build:bundle": "webpack",
"build": "npm run build:codec && npm run build:bundle",
"serve": "http-server",
"start": "npm run build && npm run serve"
},
"devDependencies": {
"exports-loader": "^0.7.0",
"file-loader": "^1.1.11",
"http-server": "^0.11.1",
"webpack": "^4.8.3",
"webpack-cli": "^2.1.3"
}
}
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const webpack = require("webpack");
const path = require("path");
module.exports = {
mode: "development",
context: path.resolve(__dirname, "."),
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
// This is necessary due to the fact that emscripten puts both Node and web
// code into one file. The node part uses Node’s `fs` module to load the wasm
// file.
// Issue: https://github.com/kripken/emscripten/issues/6542.
browser: {
"fs": false
},
module: {
rules: [
// Emscripten JS files define a global. With `exports-loader` we can
// load these files correctly (provided the global’s name is the same
// as the file name).
{
test: /fibonacci\.js$/,
loader: "exports-loader"
},
// wasm files should not be processed but just be emitted and we want
// to have their public URL.
{
test: /fibonacci\.wasm$/,
type: "javascript/auto",
loader: "file-loader",
options: {
publicPath: "dist/"
}
}
]
},
};
@programer-s
Copy link

Thank you for your answer! This code is not mine. (from the example above)
fibonacciModule is javascript object, and late I get an error in the next code (Emscripten generated Javascript file):

function hasPrefix(str, prefix) {
  return String.prototype.startsWith ?
      str.startsWith(prefix) :
      str.indexOf(prefix) === 0;
}

startsWith is not a function, because str is not a string type. (fibonacciModule)

If I think correctly then the locateFile function returns a string type and fibonacciModule is not the string type.

@Modjular
Copy link

Modjular commented Feb 5, 2021

@surma Should this work in a Web Worker? I've been told webpack now supports wasm out-of-the-box
https://github.com/webpack/webpack/tree/master/examples/wasm-simple but I haven't had any luck, with this method or anything else on google.

I'm not sure if I'm using outdated methods, or I shouldn't even be attempting this in a worker. Can you shine some light on this?

@surma
Copy link
Author

surma commented Feb 5, 2021

I haven’t looked into Wasm + Webpack or Workers + Webpack with Webpack 5. So I don’t really know.

@igormcoelho
Copy link

This original gist helped me years ago, but it stopped working (for me...). Some advices that worked here:

  • updates on package.json:
    • "build:codec": "docker run --rm -v $(pwd):/src emscripten/emsdk emcc -O3 -s WASM=1 -s EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"fibonacci\"' -o ./fibonacci.js fibonacci.c"
      • Because of warning: emcc: warning: EXTRA_EXPORTED_RUNTIME_METHODS is deprecated, please use EXPORTED_RUNTIME_METHODS instead [-Wdeprecated]
      • Also because image trzeci/emscripten seems to be officially replaced by emscripten/emsdk
    • fixed devDependencies because some packages were old, specially http-server (webpack is still v4 and couldn't manage to make it work on v5):
      • "exports-loader": "^0.7.0",
      • "file-loader": "^1.1.11",
      • "http-server": "^13.0.2",
      • "webpack": "^4.8.3",
      • "webpack-cli": "^3.1.2"
  • updates on webpack.config.js:
    • Added: library to module.exports.output:
      • library: 'TestLib'
  • updates on index.js:
    • need to process async promise to get module working
    • did not manage to use onRuntimeInitialized as in the original example

index.js

 import fibonacci from './fibonacci.js';
 import fibonacciModule from './fibonacci.wasm';
 
 export var loadedModule = null;  // storing it on TestLib module

const module = fibonacci({
   locateFile(path) {
     if(path.endsWith('.wasm')) {
       return fibonacciModule;
     }
     return path;
   }
 }).then(instance => {
    console.log("use it!");
    console.log("_fib -> "+instance._fib(5));
    loadedModule = instance;
  });

On the browser, TestLib.loadedModule._fib(5) correctly invokes the function fib.

index.html

<!doctype html>
<script src="./dist/bundle.js"></script>

webpack.config.js

 const webpack = require("webpack");
 const path = require("path");

 module.exports = {
   mode: "development",
   context: path.resolve(__dirname, "."),
   entry: "./index.js",
   output: {
     path: path.resolve(__dirname, "dist"),
     filename: "bundle.js",
     library: 'TestLib',
     globalObject: 'this' // https://webpack.js.org/configuration/output/
   },
   node: {
    fs: "empty"
    },
   module: {
     rules: [
       {
         test: /fibonacci\.js$/,
         loader: "exports-loader"
       },
       {
         test: /fibonacci\.wasm$/,
         type: "javascript/auto",
         loader: "file-loader",
         options: {
           publicPath: "dist/"
         }
       }
     ]
   },
 };

packages.json:

{
    "name": "lol",
    "scripts": {
      "build:codec": "docker run --rm -v $(pwd):/src emscripten/emsdk  emcc -O3  -s WASM=1  -s ASSERTIONS=1 -s EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"fibonacci\"'  -o ./fibonacci.js fibonacci.c",
      "build:bundle": "webpack",
      "build": "npm run build:codec && npm run build:bundle",
      "serve": "http-server",
      "start": "npm run build && npm run serve"
    },
    "devDependencies": {
      "exports-loader": "^0.7.0",
      "file-loader": "^1.1.11",
      "http-server": "^13.0.2",
      "webpack": "^4.8.3",
      "webpack-cli": "^3.1.2"
    }
  }

fibbonacci.c (original file)

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
  int i, t, a = 0, b = 1;
  for (i = 0; i < n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  return b;
}

@dhdaines
Copy link

dhdaines commented May 6, 2022

No longer works with Webpack 5 - instead of the node configuration, you need something like:

module.exports = {
    resolve: {
        fallback: {
            crypto: false,
            fs: false,
            path: false
        }
    }
};

For the wasm files, one solution is just to use copy-webpack-plugin, which is annoying since it makes it non-transparent for people to use your module, but then again, they're already adding the magic resolve incantation...

const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
    plugins: [
        new CopyPlugin({
          patterns: [
              { from: "node_modules/MYPACKAGE/MYPACKAGE.wasm*",
                to: "[name][ext]" },
          ],
      }),
    ],
};

@9oelM
Copy link

9oelM commented May 10, 2022

Hi guys, I felt extremely lost while I was digging into this issue, so after I got everything working, I made a example repo containing detailed walkthroughs:

Hope it helps someone.

@acransac
Copy link

With Webpack 5, I think you can remove the locateFile hook, the file-loader dependency and simplify

    import fibonacciModule from './fibonacci.wasm';

to

    import './fibonacci.wasm';

provided that you change the .wasm rule in the Webpack config to:

    {
        test: /fibonacci\.wasm$/,
        type: "asset/resource",
        generator: {
            filename: "[name].wasm"
        }
    }

It might help with issues locating the wasm asset when loading the page.

@spencer-sherk-ai
Copy link

Hi guys, I felt extremely lost while I was digging into this issue, so after I got everything working, I made a example repo containing detailed walkthroughs:

Hope it helps someone.

Thanks! You just saved me a ton of time with this. Confirmed still working as of 2/2/2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment