How can I return a JavaScript string from a WebAssembly function
Asked Answered
M

5

59

How can I return a JavaScript string from a WebAssembly function?

Can the following module be written in C(++) ?

export function foo() {
  return 'Hello World!';
}

Also: Can I pass this to the JS engine to be garbage collected?

Mulligrubs answered 27/12, 2016 at 23:16 Comment(0)
F
68

WebAssembly doesn't natively support a string type, it rather supports i32 / i64 / f32 / f64 value types as well as i8 / i16 for storage.

You can interact with a WebAssembly instance using:

  • exports, where from JavaScript you call into WebAssembly, and WebAssembly returns a single value type.
  • imports where WebAssembly calls into JavaScript, with as many value types as you want (note: the count must be known at Module compilation time, this isn't an array and isn't variadic).
  • Memory.buffer, which is an ArrayBuffer that can be indexed using (among others) Uint8Array.

It depends on what you want to do, but it seems like accessing the buffer directly is the easiest:

const bin = ...; // WebAssembly binary, I assume below that it imports a memory from module "imports", field "memory".
const module = new WebAssembly.Module(bin);
const memory = new WebAssembly.Memory({ initial: 2 }); // Size is in pages.
const instance = new WebAssembly.Instance(module, { imports: { memory: memory } });
const arrayBuffer = memory.buffer;
const buffer = new Uint8Array(arrayBuffer);

If your module had a start function then it got executed at instantiation time. Otherwise you'll likely have an export which you call, e.g. instance.exports.doIt().

Once that's done, you need to get string size + index in memory, which you would also expose through an export:

const size = instance.exports.myStringSize();
const index = instance.exports.myStringIndex();

You'd then read it out of the buffer:

let s = "";
for (let i = index; i < index + size; ++i)
  s += String.fromCharCode(buffer[i]);

Note that I'm reading 8-bit values from the buffer, I'm therefore assuming the strings were ASCII. That's what std::string would give you (index in memory would be what .c_str() returns), but to expose something else such as UTF-8 you'd need to use a C++ library supporting UTF-8, and then read UTF-8 yourself from JavaScript, obtain the codepoints, and use String.fromCodePoint.

You could also rely on the string being null-terminated, which I didn't do here.

You could also use the TextDecoder API once it's available more widely in browsers by creating an ArrayBufferView into the WebAssembly.Memory's buffer (which is an ArrayBuffer).


If, instead, you're doing something like logging from WebAssembly to JavaScript, then you can expose the Memory as above, and then from WebAssembly declare an import which calls JavaScript with size + position. You could instantiate your module as:

const memory = new WebAssembly.Memory({ initial: 2 });
const arrayBuffer = memory.buffer;
const buffer = new Uint8Array(arrayBuffer);
const instance = new WebAssembly.Instance(module, {
    imports: {
        memory: memory,
        logString: (size, index) => {
            let s = "";
            for (let i = index; i < index + size; ++i)
                s += String.fromCharCode(buffer[i]);
            console.log(s);
        }
    }
});

This has the caveat that if you ever grow the memory (either through JavaScript using Memory.prototype.grow, or using the grow_memory opcode) then the ArrayBuffer gets neutered and you need to create it anew.


On garbage collection: WebAssembly.Module / WebAssembly.Instance / WebAssembly.Memory are all garbage collected by the JavaScript engine, but that's a pretty big hammer. You likely want to GC strings, and that's currently not possible for objects which live inside a WebAssembly.Memory. We've discussed adding GC support in the future.

Fishwife answered 29/12, 2016 at 2:48 Comment(5)
You can also use TextDecoder api to decode UTF-8 Uint8Array into a string: developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decodeHerminiahermione
In the neighbour question: Module.AsciiToString(ptr)Domiciliate
How to actually access this memory/buffer from C/C++?Despoil
@DanielFekete the WebAssembly.Memory is the regular C++ heap. The i32 you pass around is a regular C++ pointer into that heap.Fishwife
This is unbelievable. WebAssembly is being hailed as the future of the internet and it can't even handle strings. One of the big reasons Javascript is so popular is its string handling power. I can see now why WebAssembly is so popular, nobody is using it real world scenarios. It's just something for developers to play iwth while they are waiting for real work to be assigned.Ankh
F
21

2020 Update

Things have changed since the other answers were posted.

Today I would bet on the WebAssembly Interface Types - see below.

Since you asked specifically about C++, see:

nbind - Magical headers that make your C++ library accessible from JavaScript

nbind is a set of headers that make your C++11 library accessible from JavaScript. With a single #include statement, your C++ compiler generates the necessary bindings without any additional tools. Your library is then usable as a Node.js addon or, if compiled to asm.js with Emscripten, directly in web pages without any plugins.

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++.

See the following WebAssembly proposals:

The proposal adds a new set of interface types to WebAssembly that describe high-level values (like strings, sequences, records and variants) without committing to a single memory representation or sharing scheme. Interface types can only be used in the interfaces of modules and can only be produced or consumed by declarative interface adapters.

For more info and a great explanation, see:

You can already use it with some experimental features, see:

For a good real world example using yet another approach, see:

libsodium.js - The sodium crypto library compiled to WebAssembly and pure JavaScript using Emscripten, with automatically generated wrappers to make it easy to use in web applications.

See also:

Wasmer is an open-source runtime for executing WebAssembly on the Server. Our mission is make all software universally available. We support running Wasm modules standalone in our runtime, but also can be embedded in multiple languages using our language integrations.

and specifically Wasmer-JS:

Wasmer-JS enables the use of server-side compiled WebAssembly Modules in Node.js and the Browser. The project is set up as mono-repo of multiple JavaScript packages.

There's also some good info in this article on Hacker News.

Fa answered 2/1, 2020 at 2:49 Comment(0)
M
8

Given:

  • mem, the WebAssembly.Memory object (from the module exports)
  • p, the address of the first character of the string
  • len, the length of the string (in bytes),

you can read the string using:

let str = (new TextDecoder()).decode(new Uint8Array(mem.buffer, p, len));

This assumes the string is UTF-8 encoded.

Maribelmaribelle answered 19/2, 2020 at 20:50 Comment(1)
Nice! Emscripten does exactly the same in github.com/emscripten-core/emscripten/blob/cbc9742/src/… . The length of the string is determined by scanning for the null character.Kassala
W
1

I found a hack way just like what we do in the hybird appication way, and it's very easily.

Just inject window.alert function, and then put it back:

let originAlert = window.alert;
window.alert = function(message) {
    renderChart(JSON.parse(message))
};
get_data_from_alert();
window.alert = originAlert;

and the native side, just :

// Import the `window.alert` function from the Web.
#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

...

pub fn get_data_from_alert() {
    alert(CHART_DATA);
}

you can see in examples my GitHub: https://github.com/phodal/rust-wasm-d3js-sample

Woeful answered 4/3, 2020 at 2:16 Comment(1)
Feel free to reference this guide. It is often more helpful if you include at least a brief explanation along with your minimal reproducible example.Soricine
P
-2

There's an easier way to do that. First, you need the instance of your binary:

const module = new WebAssembly.Module(bin);
const memory = new WebAssembly.Memory({ initial: 2 });
const instance = new WebAssembly.Instance(module, { imports: { memory: memory } });

Then, if you run console.log(instance), almost at the top of this object you will see function AsciiToString. Pass your function from C++ that returns string and you will see the output. For this case, check out this library.

Pore answered 11/8, 2017 at 21:12 Comment(1)
AsciiToString seems to be an Emscripten thing.Maribelmaribelle

© 2022 - 2024 — McMap. All rights reserved.