Emscripten: How to call a webassembly method in vue.js?

Created on 27 Dec 2019  Â·  15Comments  Â·  Source: emscripten-core/emscripten

I'm trying to transpose to vue.js this simple html page add.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <input type="button" value="Add" onclick="callAdd()" />

    <script>
      function callAdd() {
        const result = Module.ccall('Add',
            'number',
            ['number', 'number'],
            [1, 2]);

        console.log(`Result: ${result}`);
      }
    </script>
    <script src="js_plumbing.js"></script>
  </body>
</html>

which calls the Add function defined in add.c :

#include <stdlib.h>
#include <emscripten.h>

// If this is an Emscripten (WebAssembly) build then...
#ifdef __EMSCRIPTEN__
  #include <emscripten.h>
#endif

#ifdef __cplusplus
extern "C" { // So that the C++ compiler does not rename our function names
#endif

EMSCRIPTEN_KEEPALIVE
int Add(int value1, int value2) 
{
  return (value1 + value2); 
}

#ifdef __cplusplus
}
#endif

and converted to js_plumbing and js_plumbling.wasm files through the command:

emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s 
ENVIRONMENT='web','worker'

In console of google chrome I get these errors:

GET http://localhost:8080/dist/js_plumbing.wasm 404 (Not Found)  @  js_plumbing.js?2b2c:1653

Where in js_plumbing_js :

// Prefer streaming instantiation if available.
  function instantiateAsync() {
    if (!wasmBinary &&
        typeof WebAssembly.instantiateStreaming === 'function' &&
        !isDataURI(wasmBinaryFile) &&
        typeof fetch === 'function') {
      fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) {  // <---------------!!!
        var result = WebAssembly.instantiateStreaming(response, info);
        return result.then(receiveInstantiatedSource, function(reason) {
            // We expect the most common failure cause to be a bad MIME type for the binary,
            // in which case falling back to ArrayBuffer instantiation should work.
            err('wasm streaming compile failed: ' + reason);
            err('falling back to ArrayBuffer instantiation');
            instantiateArrayBuffer(receiveInstantiatedSource);
          });
      });
    } else {
      return instantiateArrayBuffer(receiveInstantiatedSource);
    }
  }

In Google Chrome: createWasm @ js_plumbing.js?2b2c:1680

line 1680 of js_plumbing.js:

instantiateAsync();

in Google Chrome: eval @ js_plumbing.js?2b2c:1930

line 1930 of js_plumbing.js:

<pre><font color="#4E9A06">var</font> asm = createWasm();</pre>

And many other errors related to wasm :

https://drive.google.com/open?id=1-aY2Iae1BRPjiLsslQ9P5khUKzuVZJLm
https://drive.google.com/open?id=1tlhlp38XNXUp61Vc0pZagz8hWN9eCKpb

So... how should I modify the callAdd() method in Result.vue in order to correctly execute the Add function in js_plumbing.js and in js_plumbing.wasm files?

  methods: {
    callAdd() {
      const result = Module.ccall('Add',
          'number',
          ['number', 'number'],
          [1, 2]);
      console.log('Result: ${result}');
    }
  }

Most helpful comment

My thought was that, rather than download and instantiate the module in main.js _(when the app first loads)_, the first component that needs it creates it. Once instantiated, the module is available to all components that need it.

Rather than having a global object, the following worked for me, where the component uses a local variable for the module instance:

    import Module from '../js_plumbing.js';
    let moduleInstance = null;

    export default {
        beforeCreate() {
            new Module().then(myModule => {
                moduleInstance = myModule;
            });
        },
        data() {
            return {
                result: null
            }
        },
        methods: {
            callAdd() {
                this.result = moduleInstance.ccall('Add',
                    'number',
                    ['number', 'number'],
                    [2, 3]);
            }
        }
    };

All 15 comments

Testing this on my side, the issue appears to be that an error gets thrown when compiling the module due to the ENVIRONMENT flag. If you change the command line to the following, the module is generated and the web page runs:

emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s 
ENVIRONMENT='web,worker'

Hi @cggallant Gerard!
I solved that error importing in this way: import * as js_plumbing from ‘./js_plumbing’
and then

methods: {
  callAdd() {
    const result = js_plumbing.Module.ccall('Add',  // <---------------
        'number',
        ['number', 'number'],
        [1, 2]);
    console.log('Result: ${result}');
    console.log(result);
  }
}

Now I’m facing this problem: “Cannot read property ‘ccall’ of undefined” :

errorsInChome-06

But I compiled the add.c file, creating js_plumbing.js and js_plumbing.wasm files, with this command, which exports the methods ‘ccall’ and ‘cwrap’ :

emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=[‘ccall’,‘cwrap’] -s ENVIRONMENT=‘web’,‘worker’

In order to import the Emscripten code like an ES6 module, you'll need to compile the module with the -s EXPORT_ES6=1 -s MODULARIZE=1 flags:
emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1

Because the Modularize flag is used, you'll need to create an instance of the Module object before you can call into the module (it doesn't get downloaded and instantiated until you create an instance of the object):

import Module from './js_plumbing.js'
Module().then(myModule => {  
  const result = myModule.ccall('Add',
      'number',
      ['number', 'number'],
      [1, 2]);
  console.log(`Result: ${result}`);
});

I "solved" through a sort of an hack, which I do not like at all.

This is the Result.vue file:

<template>
  <div>
    <p button @click="callAdd">Add!</p>
    <p>Result: {{ result }}</p>
  </div>
</template>

<script>
    import * as js_plumbing from './js_plumbing'
    import Module  from './js_plumbing'
    export default {
      data () {
        return {
          result: null
        }
      },
      methods: {
        callAdd () {
          const result = js_plumbing.Module.ccall('Add',
            'number',
            ['number', 'number'],
            [1, 2]);
          this.result = result;
        }
      }
    }
</script>

which is exactly the same as the one used before, as you can see.

The only thing I've done to make it working, is to add export to the definition of Module in js_plumbing.js :

js_plumbing.js

// Copyright 2010 The Emscripten Authors.  All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License.  Both these licenses can be
// found in the LICENSE file.

// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
export var Module = typeof Module !== 'undefined' ? Module : {};

image

But, as I said, I do not like this hack.
Any suggestions on how to make the Module exportable, thus importable, without manually adding 'export' in js_plumbing.js file?

@cggallant Gerard I'm trying to you use your elegant solution, but I'm encountering some problems.

I complied add.c , as you suggested, in this way:

emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s 
ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1

Then, I modified Result.vue as follows:

<template>
  <div>
    <p button @click="callAdd">Add!</p>
    <p>Result: {{ result }}</p>
  </div>
</template>

<script>
    import Module  from './js_plumbing'
    export default {
      data () {
        return {
          result: null
        }
      },
      methods: {
        callAdd() {
          Module().then(Module => {
            const result = Module.ccall('Add',
                'number',
                ['number', 'number'],
                [1, 2]);
            console.log(`Result: ${result}`);
            this.result = result;
          });
        }
      }
    }
</script>

But I'm getting this error message:

Failed to compile.

./src/components/js_plumbing.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: Unexpected token, expected ( (3:25)

  1 | 
  2 | var Module = (function() {
> 3 |   var _scriptDir = import.meta.url;
      |                          ^
   4 |   
   5 |   return (
   6 | function(Module) {

image

Aside from this error message, I do not understand what myModule should be in

      methods: {
        callAdd() {
          Module().then(myModule => {
            const result = myModule.ccall('Add',
                'number',
                ['number', 'number'],
                [1, 2]);
            console.log(`Result: ${result}`);
            this.result = result;
          });
        }
      }
    }

I haven't used Vue before so I'm not sure where you'd place the variable but you only want to create an instance of the Module object, that was imported from the js_plumbing.js file, the once because it's creating a new instance of the WebAssembly module each time you do Module().

I called it myModule but you can call it whatever you'd like. Once you have that object, you can call into the module.

I'll see if I can get Vue going on my system to see if I can help you out better. Might be a few hours though, my brother and his family just arrived in town.

very kind of you @cggallant Gerard, thank you very much. Much appreciated.
As far as I understand, the error message regards the very first lines of js_plumbing.js created by the command emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1

var Module = (function() {
var _scriptDir = import.meta.url;

Update (Dec 30, 2019): I was able to get this working. I've updated the following with my results.

There's a USE_ES6_IMPORT_META flag that you can set to 0 when compiling the WebAssembly module that will use an older version of the import.meta.url line of code for systems that don't recognize the import style:
emcc add.c -o js_plumbing.js -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1 -s USE_ES6_IMPORT_META=0

In my main.js file, I created a variable on the Vue object ($myModule) so that the module is only downloaded and initialized the once:

import Vue from 'vue';
import App from './App.vue';

Vue.config.productionTip = true;
Vue.prototype.$myModule = null; // Will hold the module's instance when loaded the first time

new Vue({
  render: h => h(App)
}).$mount('#app');

In my controller, I check to see if $myModule is null and, if so, the module is loaded:

import Module from '../js_plumbing.js';

    export default {
        beforeCreate() {
            if (this.$myModule === null) {
                new Module().then(myModule => {
                    this.$myModule = myModule;
                });
            }
        },
        data() {
            return {
                result: null
            }
        },
        methods: {
            callAdd() {
                this.result = this.$myModule.ccall('Add',
                    'number',
                    ['number', 'number'],
                    [2, 3]);
            }
        }
    };

This might be because I'm running on a Windows machine, Visual Studio, and just the development web server but, to get the content-type to work, I needed to adjust my vue.config.js file as follows:

const path = require('path');
const contentBase = path.resolve(__dirname, '..', '..');

module.exports = {
    configureWebpack: config => {
        config.devServer = {
            before(app) {
                // use proper mime-type for wasm files
                app.get('*.wasm', function (req, res, next) {
                    var options = {
                        root: contentBase,
                        dotfiles: 'deny',
                        headers: {
                            'Content-Type': 'application/wasm'
                        }
                    };
                    res.sendFile(req.url, options, function (err) {
                        if (err) {
                            next(err);
                        }
                    });
                });
            }
        }   
    }
}

I'm not sure that enclosing the Vue instance within the Module in main.js has no side-effects, since the vue instance created in main.js has global coverage on the entire webapp, not just the part related to wasm

It's now working on my machine. I've updated my comment above.

Hi @cggallant Gerard!
I asked help also in vue forum, and yesterday night, thanks to @AnthumChris , another solution was reached. ( https://forum.vuejs.org/t/wasm-how-to-correctly-call-a-webassembly-method-in-vue-js/83422/31 )

I compiled add.c in this way:

emcc add.c -o js_plumbing.js -s EXPORTED_FUNCTIONS="[’_Add’]" -s 
EXTRA_EXPORTED_RUNTIME_METHODS=[‘ccall’,‘cwrap’] -s MODULARIZE=1

And, thanks to @AnthumChris , I modified Result.vue as follows:

Result.vue :

<template>
  <div>
    <p button @click="callAdd()">Add!</p>
    <p>Result: {{ result }}</p>
  </div>
</template>

<script>
    import Module from './js_plumbing'

    let instance = {
      ready: new Promise(resolve => {
        Module({
          onRuntimeInitialized() {
            instance = Object.assign(this, {
              ready: Promise.resolve()
            });
            resolve();
          }
        });
      })
    };

    export default {
      data () {
        return {
          result: null
        }
      },
      methods: {
        callAdd() {
          instance.ready.then(_ => {
            console.log(instance._Add(1,2));
            this.result = instance._Add(1,2)
          });
        }
      }
    }
</script>

errorsInChome-18

@cggallant Gerard your solution works fine with Ubuntu 18.04.02 without needing to modify vue.config.js . Thank you very much for your kind help!!!

errorsInChome-19

I do not understand why in Result.vue we need to create a hook beforeCreate() to check if $myModule is null and, if so, to load the module.
Doing in main.js Vue.prototype.$myModule = null; makes myModule null available for all Vue instances before creation of the vue instance (https://vuejs.org/v2/cookbook/adding-instance-properties.html) .
Actually, I tried to remove these lines from Result.vue:

  beforeCreate () {
    if (this.$myModule === null) {
      new Module().then(myModule => {
        this.$myModule = myModule;
      });
    }
  },

And got this error message: Cannot read property 'ccall' of null

errorsInChome-20

but I do not understand this requirement

My thought was that, rather than download and instantiate the module in main.js _(when the app first loads)_, the first component that needs it creates it. Once instantiated, the module is available to all components that need it.

Rather than having a global object, the following worked for me, where the component uses a local variable for the module instance:

    import Module from '../js_plumbing.js';
    let moduleInstance = null;

    export default {
        beforeCreate() {
            new Module().then(myModule => {
                moduleInstance = myModule;
            });
        },
        data() {
            return {
                result: null
            }
        },
        methods: {
            callAdd() {
                this.result = moduleInstance.ccall('Add',
                    'number',
                    ['number', 'number'],
                    [2, 3]);
            }
        }
    };

Now I do understand... thank you very much Gerard!
And all the Best for 2020!!
(in the next days I will keep reading your book "WebAssemby in Action"

@marcoippolito @cggallant Where do you place the wasm file? Is there a way to config its path in vue?

Was this page helpful?
0 / 5 - 0 ratings