javascriptc++crustwebassembly

Webassembly: possible to have shared structs?


I am wondering if, using C (or C++ or Rust) and javascript, I am able to do CRUD operations to a shared data structure. Using the most basic example, here would be an example or each of the operations:

#include <stdio.h>
typedef struct Person {
    int age;
    char* name;
} Person;

int main(void) {

    // init
    Person* sharedPersons[100];
    int idx=0;

    // create
    sharedPersons[idx] = (Person*) {12, "Tom"};

    // read
    printf("{name: %s, age: %d}", sharedPersons[idx]->name, sharedPersons[idx]->age);

    // update
    sharedPersons[idx]->age = 11;

    // delete
    sharedPersons[idx] = NULL;

}

Then, I would like to be able to do the exact same thing in Javascript, and both be able to write to the same shared sharedPersons object. How could this be done? Or does the setup need to be something like a 'master-slave' where one just needs to pass back information to the other and the master does all the relevant actions? I'm hoping that there's a way do CRUD on a shared data object in webassembly, and any help would be greatly appreciated.

As a reference: https://rustwasm.github.io/wasm-bindgen/contributing/design/js-objects-in-rust.html


Solution

  • Creating the object

    Let's create the object in C and return it:

    typedef struct Person {
        int age;
        char* name;
    } Person;
    
    Person *get_persons(void) {
        Person* sharedPersons[100];
        return sharedPersons;
    }
    

    You could also create the object in JS, but it's harder. I'll come back to this later.

    In order for JS to get the object, we've defined a function (get_persons) that returns (a pointer to) it. In this case it's an array, but of course it could have been a single object. The thing is, there must be a function that will be called from JS and that provides the object.

    Compiling the program

    emcc \
        -s "SINGLE_FILE=1" \
        -s "MODULARIZE=1" \
        -s "ALLOW_MEMORY_GROWTH=1" \
        -s "EXPORT_NAME=createModule" \
        -s "EXPORTED_FUNCTIONS=['_get_persons', '_malloc', '_free']" \
        -s "EXPORTED_RUNTIME_METHODS=['cwrap', 'setValue', 'getValue', 'AsciiToString', 'writeStringToMemory']" \
        -o myclib.js
        person.c
    

    I don't remember why we have a leading underscore in _get_persons, but that's how Emscripten works.

    Getting the object in JS

    const createModule = require('./myclib');
    
    let myclib;
    let Module;
    
    export const myclibRuntime = createModule().then((module) => {
      get_persons: Module.cwrap('get_persons', 'number', []),
    });
    

    What this does is create a get_persons() JS function that is a wrapper of the C get_persons() function. The return value of the JS function is "number". Emscripten knows that the C get_persons() function returns a pointer, and the wrapper will convert that pointer to a JS number. (Pointers in WASM are 32-bit.)

    Manipulating the object in JS

    const persons = get_persons();
    Module.getValue(persons, 'i32');  // Returns the age of the first person
    Module.AsciiToString(Module.getValue(persons + 4, 'i32'));  // Name of first person
    
    // Set the second person to be "Alice", age 18
    const second_person = persons + 8;
    Module.setValue(second_person, 18, 'i32');
    const buffer = Module._malloc(6);  // Length of "Alice" plus the null terminator
    Module.writeStringToMemory("Alice", buffer);
    Module.setValue(second_person + 4, buffer, 'i32');
    

    This is a fairly low level way of doing it, although there seems to be an even lower level way. As other people have suggested, there may be higher level tools to help in C++ and Rust.

    Creating the object in JS

    You can create objects in JS by using _malloc() (and free them with _free()) as we did with the string above, and then pass their pointers to C functions. But, as I said, creating them in C is probably easier. In any case, anything _malloc()ed must eventually be freed (so the string creation above is incomplete). The FinalizationRegistry can help with this.