Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to call a webassembly method in vue.js? #10114

Closed
marcoippolito opened this issue Dec 27, 2019 · 15 comments
Closed

How to call a webassembly method in vue.js? #10114

marcoippolito opened this issue Dec 27, 2019 · 15 comments

Comments

@marcoippolito
Copy link

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}');
    }
  }
@cggallant
Copy link

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'

@marcoippolito
Copy link
Author

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’

@cggallant
Copy link

cggallant commented Dec 28, 2019

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}`);
});

@marcoippolito
Copy link
Author

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?

@marcoippolito
Copy link
Author

marcoippolito commented Dec 28, 2019

@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;
          });
        }
      }
    }

@cggallant
Copy link

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.

@marcoippolito
Copy link
Author

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;

@cggallant
Copy link

cggallant commented Dec 30, 2019

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);
                        }
                    });
                });
            }
        }   
    }
}

@marcoippolito
Copy link
Author

marcoippolito commented Dec 30, 2019

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

@cggallant
Copy link

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

@marcoippolito
Copy link
Author

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

@marcoippolito
Copy link
Author

marcoippolito commented Dec 31, 2019

@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

@cggallant
Copy link

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]);
            }
        }
    };

@marcoippolito
Copy link
Author

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"

@zzmao
Copy link

zzmao commented Oct 15, 2020

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

dapplion added a commit to dapplion/bls that referenced this issue Oct 28, 2020
Follows the advice from @cggallant in  emscripten-core/emscripten#10114

Note that this will require coordinate changes in the consumer of the library

Check this diff of bls_c.js to see the impact of this flag
https://www.diffchecker.com/nLKh3P65
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants