emscripten-core/emscripten

Tutorial Request: Creating a canvas component in React/Other

mariorodriguespt opened this issue · 8 comments

I've been trying to create my own React component that I can plug into my existing application but failed so far.

Creating a standalone emscripten application works perfectly but I have been facing several errors when trying to inject it dynamically.

Example with React hooks:

const loadWASM = async () => {
      const script = document.createElement("script");
      script.src = "/public/wasm/browser.js";
      script.async = true;

      document.body.appendChild(script);
 }

This will fail and will result in:

browser.js:1 Uncaught TypeError: Cannot read property 'addEventListener' of null
    at Object.registerOrRemoveHandler (browser.js:1)
    at __registerMouseEventCallback (browser.js:1)
    at _emscripten_set_mousemove_callback_on_thread (browser.js:1)
    at :3000/public/wasm/browser.wasm:wasm-function[1562]:0xa86bf
    at :3000/public/wasm/browser.wasm:wasm-function[1538]:0xa77d9
    at :3000/public/wasm/browser.wasm:wasm-function[1606]:0xabd2f
    at :3000/public/wasm/browser.wasm:wasm-function[898]:0x6661b
    at :3000/public/wasm/browser.wasm:wasm-function[1192]:0x8378a
    at Gd (:3000/public/wasm/browser.wasm:wasm-function[1034]:0x70874)
    at Module._main (browser.js:1)

However, doing this will work perfectly:

<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>

<script type='text/javascript'>
    var Module = {
        canvas: (function() { return document.getElementById('canvas'); })()
    };
</script>

<script src="index.js"></script>

Creating a guide/tutorial/example-repo on how to build a module that we can inject into any web application, would be something very interesting to have.

Although it is possible to inject it before react loads, after is loads the canvas gets stretched and I've no way of doing state management via redux/react.

I'm working on this scenario and I should be publishing a code sample within the month.

The main challenge here is that Emscripten isn't fully designed for component reuse, so it requires patches. Some of the code relies on globals in window (hence the addEventListener error you received), so I've been writing polyfills to re-scope those to the component.

Look at this WIP Svelte code for inspiration:

Although the simple demo works, I haven't yet stress-tested the scenario with worker threads, advanced GL, etc. etc.

(edit: updated links)

Thank you @devappd and please post the link to the tutorial here once completed.

I was able to get this working with React:

const loadWASM = async () => {
        const script = document.createElement("script");
        script.src = "/public/wasm/someModule.js";
        script.async = true;

        document.body.appendChild(script);

        script.onload = () => {
            Module({ canvas : document.getElementById("canvas")});
        }

     }

The documentation on the Module is a bit hard to understand.

@mariorodriguespt The soultion you have mentioned doesnt work for me

I load the wasm file like the below

emscriptenProject({
locateFile: f => {
if(f.endsWith(".wasm")) return "http://localhost:8080/src/" + f;
return f;
}
}).then(Module => {
Module({ canvas : document.getElementById("canvas")});
});

I get the same error as mentioned.

Any ideas on this?

@amarav I'm using the generated js file, not the wasm directly.

My compile command:

em++ -s WASM=1 -s ENVIRONMENT=web\
     -s MODULARIZE=1 -s FORCE_FILESYSTEM=1 \
     -o filesystem.js filesystem.cpp \
     --preload-file assets --use-preload-plugins \
     -s EXTRA_EXPORTED_RUNTIME_METHODS='[cwrap]'

Copy the generated files to your public folder, this includes the .js, .wasm, and others.

On the generated javascript file, search for var REMOTE_PACKAGE_BASE and change to the URL where you're serving the generated file.

There may be a better way of doing this, this seems a bit sloppy to me, but it is working for me on any browser so far.

@mariorodriguespt Thanku for your response!

I use a similar command except for not preloading the assets. I tried the same by keeping it in public folder and another thing is i dont have var REMOTE_PACKAGE_BASE in the js file.

Also, where did you create the canvas?

In the provided compilation line, it will generate filesystem.wasm and filesystem.js. In the filesystem.js, you'll find the REMOTE_PACKAGE_BASE variable that you can change to the location of your static server.

I've this file that is working without any issue in my react/webpack setup:

import React, {useEffect} from 'react';

export const FileSystemTest = () => {

    const loadWASM = async () => {
        const script = document.createElement("script");
        script.src = "/public/wasm/filesystem.js";
        script.async = true;

        document.body.appendChild(script);

        script.onload = async() => {
            window.Module = await Module({ canvas : document.getElementById("canvas")});
        }
     }

    useEffect(() => {
        loadWASM();
    }, []);

    return (
        <div>
            <h1> FileSystemTest</h1>
            <canvas id="canvas" style={{ backgroundColor : 'blue'}} onContextMenu={ (event) => event.preventDefault() } tabIndex="-1"/>            
        </div>
    );
}

In my case, I change the REMOTE_PACKAGE_BASE to "/public/wasm/filesystem.wasm" in the /public/wasm/filesystem.js

Note: Maybe there is a better of doing this in the docs, hope it helps.

@mariorodriguespt Thanks for the help!

stale commented

This issue has been automatically marked as stale because there has been no activity in the past year. It will be closed automatically if no further activity occurs in the next 30 days. Feel free to re-open at any time if this issue is still relevant.