如何从WebAssembly函数返回JavaScript字符串

58

如何从WebAssembly函数返回JavaScript字符串?

以下模块是否可以用C(++)编写?

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

还有一个问题:我能把它传递给JS引擎进行垃圾回收吗?

5个回答

67
WebAssembly本身不支持字符串类型,而是支持 / / / 值类型以及 / 用于存储。
您可以使用以下方式与WebAssembly实例进行交互:
  • exports,在JavaScript中调用WebAssembly,并返回单个值类型。
  • imports,WebAssembly调用JavaScript,可以使用任意多个值类型(注意:计数必须在模块编译时已知,这不是一个数组,也不是可变参数)。
  • Memory.buffer,它是一个可以使用(包括)Uint8Array进行索引的ArrayBuffer

这取决于您想要做什么,但直接访问缓冲区似乎是最简单的方法:

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

如果您的模块具有一个 start 函数,则它将在实例化时执行。否则,您可能会有一个导出项来调用,例如 instance.exports.doIt()
完成后,您需要获取字符串大小和内存中的索引,这也可以通过导出项公开。
const size = instance.exports.myStringSize();
const index = instance.exports.myStringIndex();

你会从缓冲区中读取它:
let s = "";
for (let i = index; i < index + size; ++i)
  s += String.fromCharCode(buffer[i]);

请注意,我从缓冲区中读取的是8位值,因此我假设字符串是ASCII编码的。这就是使用std::string时会得到的结果(内存中的索引将是.c_str()返回的内容),但如果要暴露其他编码,如UTF-8,您需要使用支持UTF-8的C++库,然后从JavaScript中自己读取UTF-8,获取代码点,并使用String.fromCodePoint。
您还可以依赖字符串以空字符结尾,但我在这里没有这样做。
一旦TextDecoder API在浏览器中更广泛地可用,您还可以通过创建一个ArrayBufferView来使用它,该视图指向WebAssembly.Memorybuffer(它是一个ArrayBuffer)。
如果你正在从WebAssembly记录到JavaScript,那么你可以按照上述方式暴露内存,并且从WebAssembly中声明一个导入,调用JavaScript并传递大小和位置。你可以这样实例化你的模块:
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);
        }
    }
});

这里有一个注意事项,如果你通过JavaScript使用Memory.prototype.grow或者使用grow_memory操作码来增加内存,那么ArrayBuffer将会被禁用,你需要重新创建它。
关于垃圾回收:WebAssembly.Module / WebAssembly.Instance / WebAssembly.Memory 都由 JavaScript 引擎进行垃圾回收,但这是一个相当大的工具。您可能希望对字符串进行垃圾回收,但目前无法对位于 WebAssembly.Memory 内部的对象进行垃圾回收。我们已经讨论过在将来添加垃圾回收支持的问题。

3
您也可以使用TextDecoderAPI将UTF-8的Uint8Array解码为字符串:https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode - Maciej
1
在邻居问题中:Module.AsciiToString(ptr) - nzeemin
3
如何从C/C++实际访问此内存/缓冲区? - Daniel Fekete
@DanielFekete WebAssembly.Memory 是普通的 C++ 堆。你传递的 i32 是指向该堆的普通 C++ 指针。 - JF Bastien
1
这太不可思议了。WebAssembly被誉为互联网的未来,却连字符串都无法处理。JavaScript之所以如此受欢迎,其中一个重要原因就是它强大的字符串处理能力。我现在可以看出为什么WebAssembly如此受欢迎,因为没有人在真实世界的场景中使用它。这只是开发人员在等待真正的工作分配时玩耍的东西。 - Paul McCarthy

21

2020 更新

自其他答案发布以来,情况已经发生了变化。

今天我会押注于WebAssembly接口类型——请参见下文。

由于您特别询问了C++,请查看:

nbind-神奇的头文件,使您的C++库可以从JavaScript中访问

nbind是一组标头,可使您的C++11库可以从JavaScript中访问。通过单个#include语句,您的C++编译器生成必要的绑定,无需任何其他工具。然后可以将您的库用作Node.js附加组件,或者如果使用Emscripten编译为asm.js,则可以直接在网页中使用,无需任何插件。

Embind用于将C++函数和类绑定到JavaScript,以便“普通”JavaScript可以自然地使用编译后的代码。 Embind还支持从C++调用JavaScript类。

请参阅以下WebAssembly提案:

该提案向WebAssembly添加了一组新的接口类型,用于描述高级值(例如字符串、序列、记录和变体),而不承诺单个内存表示或共享方案。接口类型只能在模块的接口中使用,并且只能由声明性接口适配器生成或消费。

有关更多信息和详细解释,请参见:

您已经可以使用一些实验性功能,详情请参见:

如果需要一个使用另一种方法的好的现实世界示例,请参见:

libsodium.js - 这是使用Emscripten编译为WebAssembly和纯JavaScript的钠加密库,自动生成包装器以便在Web应用程序中轻松使用。
另请参见:
- Wasmer: Wasmer是一个开源运行时,用于在服务器上执行WebAssembly。我们的使命是使所有软件普遍可用。我们支持在我们的运行时中独立运行Wasm模块,但也可以使用我们的语言集成嵌入到多种语言中。 - 特别是Wasmer-JS: Wasmer-JS使得Node.js和浏览器中可以使用服务器端编译的WebAssembly模块。该项目设置为多个JavaScript包的单一存储库。 此外,在Hacker News的这篇文章中还有一些很好的信息。

7

给定:

  • memWebAssembly.Memory 对象(来自模块导出)
  • p,字符串第一个字符的地址
  • len,字符串的长度(以字节为单位)

您可以使用以下方法读取字符串:

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

这假定该字符串是 UTF-8 编码的。


不错!Emscripten在https://github.com/emscripten-core/emscripten/blob/cbc9742/src/runtime_strings.js中确实做了同样的事情。字符串的长度是通过扫描空字符来确定的。 - Michael Franzl

0

我发现了一种像混合应用程序那样的hack方法,而且非常简单。

只需注入window.alert函数,然后将其放回即可:

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

而在本地端,只需:

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

你可以在我的 GitHub 示例中查看:https://github.com/phodal/rust-wasm-d3js-sample


请随意参考此指南。如果您在最小可重现示例中包含至少一个简要说明,通常会更有帮助。 - JerodG

-2

有一种更简单的方法来解决这个问题。首先,您需要获取二进制文件的实例:

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

然后,如果你运行console.log(instance),几乎在这个对象的顶部,你会看到函数AsciiToString。传递一个返回字符串的C++函数,你就会看到输出结果。对于这种情况,查看这个库


2
“AsciiToString” 似乎是 Emscripten 的东西。 - Vladimir Panteleev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接