我最近写了一个关于我最近在WebAssembly(WASM)的经历的blog post。在那篇文章中,我触摸了一些如何设置C/C ++编写代码的SDK,以及如何编译一个简单的C ++函数,该功能可以从JavaScript中获取几个数字值,在浏览器中运行WASM二进制,并且返回值。
对于那些刚跳进去的人,WebAssembly是酷炫的跨平台二进制格式,汇编语言和虚拟机,可以在浏览器中运行此二进制文件。它能做什么?好吧,当您浏览自己喜欢的网页时,它的内部是can mine crypto currency。猜猜谁为电力付款?
好吧,除了加密货币滥用外,这是一项有趣的技术,可以运行以合理的性能来运行繁重的客户端。
从哪儿开始
我正在与Emscipten一起玩,该Emscipten将clang
包裹在WASM Binary中编译C/C ++代码,并提供一些胶合代码API,将WASM Binary嵌入JavaScript中。查看MDN Docs和Emscripten SDK以开始。
用emscripten管理记忆
之前,我研究了诸如Embind之类的高级Emscripten东西,我决定研究其低级内存模型。
这是一个玩具问题。我们有一个c功能,它需要一系列的double
,对它们做点事并返回一个数字。看起来很简单。
// malloc_testing.c
#include <assert.h>
#include <math.h>
#include <stdio.h>
double vector_norm(double* p, int n) {
int i;
double norm2 = 0;
assert(n > 0 && "number of elements must be positive");
assert(p && "must be a valid pointer");
printf("received: n = %d\n", n);
for (i = 0; i < n; i++) {
printf("processed: p[%d] = %.3f\n", i, p[i]);
norm2 += p[i] * p[i];
}
return sqrt(norm2);
}
为了方便起见,我还在其中洒了一些断言和老式的打印出去。如果要将其视为C ++代码,请不要忘记将其包装在extern "C" {}
中。
我们已经知道可以使用ccall()
或cwrap()
方法从JavaScript调用此函数,但是如何在此处通过JavaScript传递数组?
让我们使用emscripten emcc
编译器将此函数编译为二进制。
$emcc malloc_testing.c -o malloc_testing.js -O0 -sASSERTIONS=2 -sEXPORTED_FUNCTIONS=_vector_norm,_malloc,_free,setValue -sEXPORTED_RUNTIME_METHODS=cwrap,ccall
在这里,我告诉编译器通过设置低优化级别的-O0
来保持断言,并为我们导出一些有用的东西,例如_malloc
,_free
和setValue
,以及我们的C函数kude10(请注意领先的下注效果)。
现在,我们有几个文件:包含二进制文件的malloc_testing.wasm
和malloc_testing.js
是JavaScript胶水代码,允许从网页使用它。您也可以在node.js中运行,但是在这种情况下,它应该与-sMODULARIZE
一起编译。
从JavaScript分配内存
WASM VM的内存模型看起来如何?好吧,对于C/C ++代码,它看起来很正常:代码,堆,堆栈。我们可以在堆中分配东西,并通过指针。对我们来说幸运的是,我们还要求emcc
导出内存分配到JavaScript胶水代码,因此现在我们可以从外部分配堆上的内存。
从理论上讲,整个过程看起来很容易:在堆上分配内存,并将指针输入JavaScript代码,将某些内容写入此内存,然后将此指针传递给C函数。这样的东西:
让我们尝试一下。我将使用一个简单的网页设置来通过按下按钮在浏览器内运行我们的C功能。
<!DOCTYPE html>
<html lang="en">
<body>
<button id="mybutton">Run</button>
<script>
document.getElementById("mybutton").addEventListener("click", ()=>{
const vectorNorm = Module.cwrap(
'vector_norm', // no underscore
'number', // return type
['number', 'number']); // param types;
const myTypedArray = new Float64Array([0, 1, 2, 3, 5]);
// allocate empty buffer
let buf = Module._malloc(myTypedArray.length * myTypedArray.BYTES_PER_ELEMENT);
// fill this buffer with our stuff
Module.HEAPF64.set(myTypedArray, buf / myTypedArray.BYTES_PER_ELEMENT);
// call our function and pass pointer to buffer
const result = vectorNorm(buf, myTypedeArray.length);
console.log(`result = ${result}`);
Module._free(buf); // no leaks!
});
</script>
<script src="malloc_testing.js"></script>
</body>
在这里,我首先使用float64
连续的内存视图创建一个JavaScript键入数组。
之后,我通过致电_malloc
在库德7中在堆内创建一个空的缓冲区,然后我们在编译c文件时导出了该缓冲区。它将指针buf
返回到分配的内存段,在JavaScript代码中仅将其视为number
(非常“安全”,嗯?)。
下一步是用某些东西填充分配的内存。我使用的Module.HEAPF64.set(myTypedArray, buf / myTypedArray.BYTES_PER_ELEMENT)
采用了两个参数:我的数组和一个指向缓冲区的指针。注意对齐!在这种情况下,指针必须以8个字数计数。实际上,我花了一个多小时才能弄清楚这一点,因为在这一点上,empscipten api文档非常,hm,emscryptic 。感谢Chatgpt和this post。
要查看其工作原理,我们可以通过手动分配替换对HEAPF64.set
的调用。我想出了这样的事情(不要在生产附近任何地方做!):
function setMemoryManually(myArray, ptr) {
for (const x of myArray) {
Module.setValue(ptr, x, 'double');
ptr += myArray.BYTES_PER_ELEMENT;
}
}
看起来很糟糕,但是有效。低级功能Module.setValue(ptr, value, 'double')
可用于在ptr
指向的地址手动设置value
。在这种情况下,没有技巧。对于double
,指针由BYTES_PER_ELEMENT = 8
递增。因此,现在我可以在我的JavaScript代码中编写类似setMemoryManually(myTypedArray, buf)
的东西,它将填充缓冲区的myTypedArray
的内容。
设置所有内存时,我们可以从JavaScript调用C函数。我更喜欢先结束。
const vectorNorm = Module.cwrap('vector_norm', // no underscore
'number', // return type
['number', 'number']); // param types;
我们告诉cwrap
我们返回了number
,然后通过了几个number
。好吧,是的,指向分配内存的缓冲区的指针以number
的形式传递(看起来非常safe
和portable
,eh?)。因此,我们可以从我们的脚本中调用vectorNorm
。
const result = vectorNorm(buf, myTypedArray.length);
最后一步。打开浏览器,从本地主机(我刚运行python -m http.server
)
服务我们的HTTP-WEB页面
http://localhost:8000/wasm_testing/malloc_testing.html
硬重载,按Run
,我们去这里
received: n = 5
p[0] = 0.000
p[1] = 1.000
p[2] = 2.000
p[3] = 3.000
p[4] = 5.000
result = 6.244997998398398
总之...
使用低级东西玩很有趣,但是我不会在可生产的代码中使用它。好吧,至少没有丰富的经验和对Emscripten code base的理解。