2

Is there a simple built-in way to pass JS objects to C++?

I tried doing it the obvious way:

echo.cpp:

#include <iostream>

#include <emscripten.h>
#include <emscripten/val.h>

using emscripten::val;

#ifdef __cplusplus
extern "C"  {
#endif

EMSCRIPTEN_KEEPALIVE void echo(val x){
    val::global("console").call<void>("log", x);
}

int main(int argc, char **argv){
    return 0;
}


#ifdef __cplusplus
}
#endif

script.mjs:

import initEM from "./echo.mjs";

var mod = await initEM();

export function echo(x){
  mod.ccall("echo", "void", ["object"], [x]);
}

echo({attr: 9});

Compiled using:

emcc ./echo.cpp -o ./echo.mjs \
  -sEXPORTED_FUNCTIONS=_main,_echo \
  -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,registeredTypes \
  -lembind --bind

But I got an error:

Uncaught TypeError: Cannot read properties of undefined (reading 'refcount')
    at __emval_incref (echo.mjs:2622:29)
    at echo.wasm:0x1957
    at echo.wasm:0x1842
    at echo.wasm:0x121c
    at echo.wasm:0x110f
    at echo.wasm:0x104a
    at echo.mjs:1639:22
    at Object.ccall (echo.mjs:845:18)
    at echo (script.mjs:6:7)
    at script.mjs:9:1

After some trial and error, I got it to work:

echo.cpp:

#include <iostream>

#include <emscripten.h>
#include <emscripten/val.h>

using emscripten::val;
using emscripten::internal::EM_VAL;

#ifdef __cplusplus
extern "C"  {
#endif

EMSCRIPTEN_KEEPALIVE void echo(EM_VAL x_ptr){
    // converts it to object from the pointer
    val x = val::take_ownership(x_ptr);
    val::global("console").call<void>("log", x);
}

int main(int argc, char **argv){
    return 0;
}


#ifdef __cplusplus
}
#endif

script.mjs:

import initEM from "./echo.mjs";

var mod = await initEM();

let objToC = null;
for(let tp of Object.values(mod.registeredTypes)){
  if(tp.name=="emscripten::val"){
    // turns it into a pointer (I think)
    objToC = (v) => tp.toWireType(null, v);
    break;
  }
}
if(objToC==null){
  throw new ReferenceError("val.toWireType not found");
}

export function echo(x){
  mod.ccall("echo", "void", ["number"], [objToC(x)]);
}

echo({attr: 9});

(Compiled using same thing as the other one)

Questions:

  1. Why isn't this already in the ccall/cwrap functions?
  2. Why doesn't emscripten expose the val.toWireType as an attribute of the module object (i.e. why do I have to loop through all types to find it), or is there something I've missed?

1 Answer 1

1

Via the docs :

EMSCRIPTEN_BINDINGS(my_module) {
  function("lerp", &lerp);
}

my_module is just a (globally) unique name you have to add, it doesn't have any other purpose.

void echo(EM_VAL x_ptr){
  // converts it to object from the pointer
  val x = val::take_ownership(x_ptr);
  val::global("console").call<void>("log", x);
}
EMSCRIPTEN_BINDINGS(my_bindings) {
  function("echo", echo);
}

now you can invoke echo from JS without using ccall:

Module.echo({addr: 9});

note that this doesn't work well from a webworker; the registration of the echo method in Module is done as part of WASM initialization, and only in the initializing thread.

While EMSCRITEN_BINDINGS looks magical, it basically just makes a static global function and calls it at static construction time. It is

function("echo", echo);

that does all of the work; it determines the arguments of echo, and builds a JS wrapper that converts arguments and calls the C++ function echo, using the name "echo".

Sign up to request clarification or add additional context in comments.

2 Comments

I don't even need to use val::take_ownership(), I can just use void echo(val x){ ... }
@MarcellPerger Yep! You can even teach EMBIND about types passed if you want.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.