编译 c 到 wasm

2021年05月01日 09:27GMT+8

编译 c 代码到 wasm, 不使用 emscripten sdk.

参考

https://surma.dev/things/c-to-webassembly/

wasm 指令集 显示了 wasm 所能执行的操作(但似乎并不完整)

clang Basic Builtins.def

clang Basic TokenKinds.def

BuiltinsWebAssembly.def

一些需要用到的工具库, 使用 7zip 打开安装包或压缩档进行提取.

  • clang.exe(编译器), wasm-ld.exe(链接器), llvm-ar.exe(归档工具) : 提取自 LLVM

  • wasm-opt.exe : 优化器, 提取自 WebAssembly/binaryen 用于 wasm 文件

  • wasm2wat.exe : (可选), 因为使用 wasm-opt.exe --print NAME.wasm 也能查看

我用的是 cygwin, 主要是因为我用 Makefile 但由于只是这些 exe 工具只接受 windows 路径, 因此传递路径时需要转换

wasm 内存模型

可以使用 -z stack-size=<bytes> 指定 stack 的大小

或者使用 --initial-memory=<bytes> 指定的是整个内存的大小.

例如在 Makefile 中, -z stack-size=$$((1024*64*10)) 将指定 640K 大小的 stack.

可以使用 --stack-first 将 stack 移到第一个区块, 这样能更好地监测到堆栈溢出.

// |__memory_base
// |    |__global_base
// |    |          |__data_end     |__heap_base
// --------------------------------------------------
// | 空 |          |               |                |
// | 白 |   Data   |    Stack <--- |  ---> Heap     |
// | 处 |          |               |                |
// --------------------------------------------------

// wasm 有内置几个变量, 可以使用 & 的形式访问, 如
extern int __data_end;
extern int __heap_base;
printf("data end: %d, heap base: %d, stack size: %d\n",
	&__data_end, &__heap_base, &__heap_base - &__data_end
);


// 当使用 --export-all --allow-undefined 链接时, js 端看到这几个内置变量, 如下:
__global_base : Global  // 数据段的起始点(我猜的), 这个值一般是 1024
__data_end    : Global  // 数据段的结束点, 即 stack 的最顶端
__heap_base   : Global  // 堆的起始点
__memory_base : Global  // 这个值一直是 0, 似乎表示的是整个内存的起始点?
__table_base  : Global  // 未知.
__dso_handle  : Global  // 出现在 emscripten_tls_init.h 中, 似乎与多线程有关

__wasm_call_ctors : fn  // 未知, 这个函数也许是 wasm 用于做某些初使化,

如果要实现自己的 malloc/free, 有几个需要注意的变量, 以及内置函数,

  • &__heap_base: 你的内存管理程序应该将这个值作为 “起始值”

  • int __builtin_wasm_memory_size(int index); : 返回指定”内存块”的页面数, 每页为 64K 大小.

    注: 参数 index 目前为 0, 因为 wasm 目前只使用了 1 个内存块.

  • void __builtin_wasm_memory_grow(int index, int pages); : 扩展指定的”内存块”到指定的页面数

我自己实现了一个 200 行左右的 malloc/free

指令集里还有个 __builtin_memcpy_inline, 我猜测应该是 clang 提供的一个宏, 用于复制已知大小的小结构

  __builtin_memcpy_inline(dst, src, const int);

交互

wasm 默认使用 env 字段用于传递 js 数据到 c,

#define EM_IMPORT(NAME) __attribute__((import_module("env"), import_name(#NAME)))
// 如果没有 import_module, 则默认为 "env"
// 这个宏说明了我们可以自定义 module name.

当有编译参数: --import-memory 时, 需要由 js 提供 Memory 并传递给 env.memory.

var mem = new Memory({initial: 10}); // 64K * 10
var lib = new Instance(mod, {
	env : {
		memory : mem,
	}
});

还有一个用于导出 c 函数到 js 的宏为:

#define EM_EXPORT(name) __attribute__((used, export_name(#name)))

// e.g:
EM_EXPORT(square) int arbitrary_name(int n) {
	return n * n;
}

数据类型转换:

wasm 只允行将 i32, f32, f64 这 3 种类型与 js 交互