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/"
}
}
]
},
};
@skandonkumar
Copy link

Hello,
I can't understand out how this code is supposed to work:

locateFile(path) {
    if(path.endsWith('.wasm')) {
      return fibonacciModule;
    }
    return path;
  }

fibonacciModule is not string, and emscripten doesn't find the wasm module.
Please help to fix this issue.

locateFile(path) is a hook for the Emscripten generated Javascript file to find the .WASM file. By default without the hook, the JS file looks for WASM file in the root directory. Your code looks correct to look for .wasm file or just return the path specified in the locateFile() hook.

@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