本文从最简单函数调用开始,逐步探索c库的调用,多文件/模块链接,WASI,函数指针参数,wasm引用js对象,c函数作为js回调等话题。所有代码均不生成胶水代码,更能体现wasm的本质,所有代码都放github上,都能在node v16.16.0上运行。
如下C代码:
int add(int a, int b) {
return a + b;
}
编译:
emcc --no-entry -O3 adder.c -o adder.wasm -s EXPORTED_FUNCTIONS="['_add']"
js调用代码:
const fs = require('fs');
const wasmSource = new Uint8Array(fs.readFileSync("adder.wasm"));
const wasmModule = new WebAssembly.Module(wasmSource);
const wasmInstance = new WebAssembly.Instance(wasmModule, {
env: {
}
});
const result = wasmInstance.exports.add(2, 40);
console.log(result);
运行:
node test_adder.js #输出42
作为一个调包侠,我很自然的问到:怎么调其它库?如果是第三方库,需要自行编译成wasm或者找现成的wasm版本,如果是c/c++标准库,emscripten已经内置了支持,直接使用即可。以malloc和free为例子:
//call_malloc.c
#include <stdlib.h>
void* allocStr(int len) {
int i;
char* p = (char*)malloc(len + 1);
for(i = 0; i < len; i++) {
p[i] = 'a' + (char)i;
}
p[len] = '\n';
return p;
}
void freeStr(char* p) {
if (p) free(p);
}
调用的js代码:
//。。。省略的加载代码
const ptr = wasmInstance.exports.allocStr(6);
console.log(`str(${6}) = ${ptr}`);
wasmInstance.exports.freeStr(ptr);
输出
str(6) = 67080
怎么是个数字?不是字符串么?原来WebAssembly(1.0)只有四种类型:int32,int64,float32,float64,而指针,在wasm32用int32表达,代表的是线性内存偏移,我们在allocStr后加两行代码验证下:
const ptr = wasmInstance.exports.allocStr(6);
const heap = new Uint8Array(wasmInstance.exports.memory.buffer);
console.log(`arr[0]=${heap[ptr]}, arr[1]=${heap[ptr + 1]}`);
打印如下,符合预期,0,1号字节分别是'a','b'的ascii码:
arr[0]=97, arr[1]=98
我们可以写个函数,去memory读取内容转成js字符串,篇幅的关系就不贴了,看附带的代码仓库链接。
foo.c代码
#include <stdlib.h>
extern int bar(int* array, int size);
int foo(int size) {
int i, ret;
int* p = (int*)malloc(size * 4);
for(i = 0; i < size; i++) {
p[i] = i;
}
ret = bar(p, size);
free(p);
return ret;
}
直接emcc多个文件
emcc foo.c bar.c --no-entry -O3 -o muti_src.wasm -s EXPORTED_FUNCTIONS="['_foo']"
也可以加-c,先编译成.o文件,然后链接在一起:
emcc foo.c -c -O3 -o foo.o
emcc bar.c -c -O3 -o bar.o
emcc foo.o bar.o --no-entry -O3 -o muti_src.wasm -s EXPORTED_FUNCTIONS="['_foo']"
有没可能类似动态库那样,运行时链接呢?我尝试了下:
emcc bar.c -O3 -s SIDE_MODULE=1 -o bar.wasm
emcc foo.c --no-entry -O3 -o foo.wasm -s EXPORTED_FUNCTIONS="['_foo']" -s WARN_ON_UNDEFINED_SYMBOLS=0
然后在js代码中
const fs = require('fs');
const fooWasmSource = new Uint8Array(fs.readFileSync("foo.wasm"));
const fooWasmModule = new WebAssembly.Module(fooWasmSource);
const fooWasmInstance = new WebAssembly.Instance(fooWasmModule, {
env: {
bar: function() {
return barWasmInstance.exports.bar.apply(null, arguments);
}
}
});
const barWasmSource = new Uint8Array(fs.readFileSync("bar.wasm"));
const barWasmModule = new WebAssembly.Module(barWasmSource);
const barWasmInstance = new WebAssembly.Instance(barWasmModule, {env: {memory: fooWasmInstance.exports.memory}});
const result = fooWasmInstance.exports.foo(100);
console.log(result);
碰到的难点是:SIDE_MODULE模块的内存由外部传入,为了实现和主模块用同一块内存,所以需要传主模块的memory,但主模块的实例化又依赖于bar模块。这问题通过一个封装的bar函数解决。
应该还有别的方式解决,比如两个文件都以SIDE_MODULE编译,统一使用一个外部构造的Memory,但会带来新问题:SIDE_MODULE似乎不会把c/c++库链接进来,运行会报找不到malloc方法。
Docker创始人Solomon Hykes说:如果2008年的时候,WASM和 WASI这两个东西已经存在了的话,他就没有必要创立 Docker了。
这WASI是何方神圣?让我们一探究竟。
如下wasi_copy.c代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char **argv) {
ssize_t n, m;
char buf[BUFSIZ];
if (argc != 3) {
fprintf(stderr, "usage: %s <from> <to>\n, argc expect 3, got %d", argv[0], argc);
exit(1);
}
int in = open(argv[1], O_RDONLY);
if (in < 0) {
fprintf(stderr, "error opening input %s: %s\n", argv[1], strerror(errno));
exit(1);
}
int out = open(argv[2], O_WRONLY | O_CREAT, 0660);
if (out < 0) {
fprintf(stderr, "error opening output %s: %s\n", argv[2], strerror(errno));
exit(1);
}
while ((n = read(in, buf, BUFSIZ)) > 0) {
char *ptr = buf;
while (n > 0) {
m = write(out, ptr, (size_t)n);
if (m < 0) {
fprintf(stderr, "write error: %s\n", strerror(errno));
exit(1);
}
n -= m;
ptr += m;
}
}
if (n < 0) {
fprintf(stderr, "read error: %s\n", strerror(errno));
exit(1);
}
return EXIT_SUCCESS;
}
代码里有文件的读写,参数的获取,这也能跑在wasm上?
上面代码用emcc能编译,也能加载,但执行会报文件读取没有权限,我还以为是我代码的问题,各种查阅资料,问ChatGPT也没解决。后面我换了一个编译工具就跑成功了。到这里 下载wasi-sdk并按照其首页介绍方式编译成wasi_copy.wasm。
在js中加载wasm。
const wasi = require( 'wasi');
const fs = require('fs');
const process = require('process');
const wasiInstance = new wasi.WASI({
args: process.argv.slice(1),
env: process.env,
preopens: {
'/sandbox': '.'
}
});
const imports = {
wasi_snapshot_preview1: wasiInstance.wasiImport
};
const wasmSource = new Uint8Array(fs.readFileSync("wasi_copy.wasm"));
const wasmModule = new WebAssembly.Module(wasmSource);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
wasiInstance.start(wasmInstance);
然后执行如下命令:
node --experimental-wasi-unstable-preview1 test_wasi_copy.js /sandbox/buid.sh /sandbox/buid.sh.bak
即可把当前目录的buid.sh拷贝到buid.sh.bak。
代码说明:
函数指针调用在wasm翻译成call_indirect指令,指针会翻译为table的索引,这个table会被导出到__indirect_function_table,我们只要往这个table插入函数,然后调用时,用索引作为函数指针参数就可以了。。吗?试试调用如下c代码:
#include <stdlib.h>
typedef int (*compare_t)(const void *, const void*);
void sort_int_array(int *array, size_t len, compare_t compar) {
qsort(array, len, sizeof(int), compar);
}
编译需加ALLOW_TABLE_GROWTH=1,这可以让我们在外部添加table条目
emcc --no-entry -O3 call_qsort.c -o call_qsort.wasm -s EXPORTED_FUNCTIONS="['_sort_int_array']" -s ALLOW_TABLE_GROWTH=1
js调用代码如下:
//。。。省略的加载代码
function call_qsort(arr) {
const savedStack = wasmInstance.exports.stackSave();
const ptr = wasmInstance.exports.stackAlloc(arr.length * 4);
const start = ptr >> 2;
const heap = new Uint32Array(wasmInstance.exports.memory.buffer);
for (let i = 0; i < arr.length; ++i) {
heap[start + i] = arr[i];
}
function cmp(pa, pb) {
return heap[pa >> 2] - heap[pb >> 2];
}
let cmpIndex = wasmInstance.exports.__indirect_function_table.grow(1);
wasmInstance.exports.__indirect_function_table.set(cmpIndex, cmp);
wasmInstance.exports.sort_int_array(ptr, arr.length, cmpIndex);
const result = [];
for (let i = 0; i < arr.length; ++i) {
result.push(heap[start + i]);
}
wasmInstance.exports.stackRestore(savedStack);
return result;
}
const numbers = [14, 3, 7, 42];
console.log(numbers, 'becomes', call_qsort(numbers));
上述代码会报错,经过一番周折才得知__indirect_function_table只能插入wasm方法,最后在emcc的胶水代码中找到解决方案:通过一个动态生成的wasm,把js方法先import,然后再export出来即可,最终完整代码见附带的代码仓库链接。
WebAssembly 1.0只有数字类型,而1.1加入了引用类型:externref和funcref,externref可以用于引用wasm之外的对象,比如js。funcref的例子我们放到下一小节讲。emscripten对externref有一定的支持:在c/c++可以有限使用(比如接下来的例子),不过我尝试定义一个externref全局变量,或者stl容器,会编译失败。 一个能编译的externref c例子如下:
typedef char __attribute__((address_space(10)))* externref;
externref pass_externref(externref p) {
return p;
}
编译需要加-mreference-types参数:
emcc --no-entry -O3 pass_externref.c -o pass_externref.wasm -s EXPORTED_FUNCTIONS="['_pass_externref']" -mreference-types
调用pass_externref的nodejs代码:
const input = { message: 'Hello, WebAssembly!' };
const output = wasmInstance.exports.pass_externref(input);
console.log('Input:', input);
console.log('Output:', output);
nodejs运行上述脚本需要加--experimental-wasm-reftypes参数:
node --experimental-wasm-reftypes test_pass_externref.js
输出结果:
Input: { message: 'Hello, WebAssembly!' }
Output: { message: 'Hello, WebAssembly!' }
这里需要用到另一个引用类型:funcref。这个例子是我无意中找到的,出处在这里 。这例子实现了一个c函数作为回调调用setTimeout。
被调用的funcref_example.c:
typedef char __attribute__((address_space(20)))* funcref;
typedef void* funcref_ptr;
// imported from javascript
extern int setTimeout(funcref callback, int timeout);
extern funcref funcptr_to_funcref(funcref_ptr);
extern void printSomeInfo();
void some_proc() {
printSomeInfo();
}
void setTimeoutByCFunc(int i) {
setTimeout(funcptr_to_funcref((funcref_ptr)&some_proc), i);
}
funcptr_to_funcref通过汇编文件funcref_example.support.S实现:
.globl __indirect_function_table
.tabletype __indirect_function_table, funcref
.globl funcptr_to_funcref
funcptr_to_funcref:
.functype funcptr_to_funcref(i32) -> (funcref)
local.get 0
table.get __indirect_function_table
end_function
编译:
emcc --no-entry -O3 funcref_example.c funcref_example.support.S -o funcref_example.wasm -s EXPORTED_FUNCTIONS="['_setTimeoutByCFunc']" -mreference-types -s WARN_ON_UNDEFINED_SYMBOLS=0
nodejs调用代码:
//。。。省略的加载代码
const wasmInstance = new WebAssembly.Instance(wasmModule, {
env: {
printSomeInfo: function() {
console.log('printSomeInfo')
},
setTimeout: setTimeout
}
});
wasmInstance.exports.setTimeoutByCFunc(2000);
同样,得加--experimental-wasm-reftypes参数:
node --experimental-wasm-reftypes test_funcref_example.js
运行2秒后打印printSomeInfo