Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you call a C function that takes (or returns) a struct by value from JS via Emscripen/Wasm?

Passing structs, arrays, and strings by reference from Javascript to C is pretty well documented in the Emscripten docs (https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-direct-function-calls).

But what about passing structures by value? If I have a C function like this:

typedef struct {double a, b, c;} MyStruct;

MyStruct Foo(const MyStruct x, double y);

How would I call Foo and decode the result? (Either using Module.cwrap or calling Module._Foo directly). Would I need to get access to the Emscripten stack to do that? Where is that documented?

like image 671
griffin2000 Avatar asked May 31 '18 01:05

griffin2000


People also ask

Can you use C with JavaScript?

Implement a C API in JavaScript. It is possible to implement a C API in JavaScript! This is the approach used in many of Emscripten's libraries, like SDL1 and OpenGL. You can use it to write your own APIs to call from C/C++.

What is Embind?

Embind is used to bind C++ functions and classes to JavaScript, so that the compiled code can be used in a natural way by “normal” JavaScript. Embind also supports calling JavaScript classes from C++. Embind has support for binding most C++ constructs, including those introduced in C++11 and C++14.

What is module Ccall?

Module.ccall() calls a compiled C function from JavaScript and returns the result of that function. The function signature for Module.ccall() is as follows: ccall(ident, returnType, argTypes, args, opts) You must specify a type name for the returnType and argTypes parameters.

What is module Cwrap?

Module.cwrap() is similar to ccall() in that it calls a compiled C function. However, rather than returning a value, it returns a JavaScript function that can be reused as many times as needed.


2 Answers

Module._malloc(), Module.writeArrayToMemory() and Module.ccall() is available, but it's very complicated.

It is easier to wrap it with C++ and embind.

// em++ --bind test.cpp -o test.js
#include <emscripten.h>
#include <emscripten/bind.h>

using namespace emscripten;

struct MyStruct {
  double a, b, c;
};

MyStruct Foo(const MyStruct x, double y) {
  MyStruct r;
  r.a = x.a;
  r.b = x.b;
  r.c = y;
  return r;
}

EMSCRIPTEN_BINDINGS(my_struct) {
  class_<MyStruct>("MyStruct")
    .constructor<>()
    .property("a", &MyStruct::a)
    .property("b", &MyStruct::b)
    .property("c", &MyStruct::c)
    ;

  function("Foo", &Foo);
}

And call from JavaScript.

var x = new Module.MyStruct();
x.a = 10;
x.b = 20;
x.c = 30;
var y = Module.Foo(x, 21);
console.log(y, y.a, y.b, y.c);

x.delete();
y.delete();

Also you can allocate memory on stack, and ccall.

var sp = Module.Runtime.stackSave();
var ret = Module.allocate(24, 'i8', Module.ALLOC_STACK);
var ptr_a = Module.allocate(24, 'i8', Module.ALLOC_STACK);
Module.HEAPF64[(ptr_a >> 3) + 0] = Math.random();
Module.HEAPF64[(ptr_a >> 3) + 1] = Math.random();
Module.HEAPF64[(ptr_a >> 3) + 2] = Math.random();
Module.ccall("Foo",
             'number',
             ['number', 'number', 'number'],
             [ret, ptr_a, 21]
            );
console.log(
    sp,
    Module.HEAPF64[(ret >> 3) + 0],
    Module.HEAPF64[(ret >> 3) + 1],
    Module.HEAPF64[(ret >> 3) + 2]);
Module.Runtime.stackRestore(sp);
like image 76
zakki Avatar answered Sep 23 '22 05:09

zakki


It is possible and here how it works:

  1. When C structure is passed by value, it is proceeded as if it is passed by pointer.

Structure:

typedef struct {
    size_t a;
    double b;
} my_struct_t;

Pass by value (C):

size_t my_struct_get_a(my_struct_t my) {
    return my.a;
}

Pass by value (Wasm):

  (func $func3 (param $var0 i32) (result i32)
    get_local $var0
    i32.load
  )

Pass by pointer (C):

size_t my_struct_ptr_get_a(const my_struct_t* my) {
    return my->a;
}

Pass by pointer (Wasm):

  (func $func5 (param $var0 i32) (result i32)
    get_local $var0
    i32.load
  )

WebAssembly code is the same!

  1. When C structure is returned by value, then it is expected to be passed as a first function parameter!

Return by value (C):

my_struct_t my_struct_create(size_t a, double b) {
    return (my_struct_t){a, b};
}

Return by value (Wasm):

  (func $func1 (param $var0 i32) (param $var1 i32) (param $var2 f64)
    get_local $var0
    get_local $var2
    f64.store offset=8
    get_local $var0
    get_local $var1
    i32.store
  )

Pay attention that Wasm does not contain result and has 3 params.

For comparison lets check alternative function:

void my_struct_fill(my_struct_t* my, size_t a, double b) {
    my->a = a;
    my->b = b;
}

Wasm is equal to the previous function:

  (func $func2 (param $var0 i32) (param $var1 i32) (param $var2 f64)
    get_local $var0
    get_local $var2
    f64.store offset=8
    get_local $var0
    get_local $var1
    i32.store
  )

Full C code sample and its Wasm

Note, this approach works well for WebAssembly, asm.js was not checked.

like image 27
user1763487 Avatar answered Sep 21 '22 05:09

user1763487