探索前沿:移动端WebAssembly虚拟机

所谓“移动端动态执行WebAssembly”就是要找个移动端可用的WebAssembly虚拟机。所谓虚拟机简单说就是能执行Wasm二进制格式文件的程序。

初步搜索

  • https://github.com/appcypher/awesome-wasm-runtimes
  • https://github.com/appcypher/awesome-wasm-langs

上面两个 Awesome 链接里基本上包含比较全的开源的虚拟机(VM)了,例如:

  • https://github.com/WAVM/WAVM
  • https://github.com/wasmerio/wasmer
  • https://github.com/perlin-network/life
  • https://github.com/paritytech/wasmi
  • https://github.com/fluencelabs/wagon
  • https://github.com/EOSIO/eos-vm
  • https://github.com/wasm3/wasm3

经过对比和试用,最后发现wasm3是目前最好的选择,理由如下:

wasm3链接:https://github.com/wasm3/wasm3

  1. 使用C语言开发。其他vm有Rust和Golang等语言开发,用于移动端的话,相对没有C语言简单。
  2. 纯解释执行,没有JIT。其他vm有的有JIT功能,有点有AOT功能,存在运行时编译为Native代码的行为,不是为移动端(尤其是iOS)设计。
  3. 开发活跃。最近一次更新是18天之前,作者也经常在Twitter活跃。
  4. 天生支持移动端,且不只是移动端。见下图。


当看到上图时,就知道选wasm3没错了。

Wasm3 上手

wasm3提供了比较全的 demo,对新手来说可谓是十分周到。
https://github.com/wasm3/wasm3/tree/master/platforms

目前我们只关心iOS,可以直接打开iOS的例子直接build即可。
iOS例子:https://github.com/wasm3/wasm3/tree/master/platforms/ios

最关键的入门代码见这里:
https://github.com/wasm3/wasm3/blob/master/platforms/ios/wasm3/main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void run_wasm()
{
    M3Result result = m3Err_none;

    uint8_t* wasm = (uint8_t*)fib32_wasm;
    uint32_t fsize = fib32_wasm_len - 1;

    printf("Loading WebAssembly...\
");

    IM3Environment env = m3_NewEnvironment ();
    if (!env) FATAL("m3_NewEnvironment failed");

    IM3Runtime runtime = m3_NewRuntime (env, 8192, NULL);
    if (!runtime) FATAL("m3_NewRuntime failed");

    IM3Module module;
    result = m3_ParseModule (env, &module, wasm, fsize);
    if (result) FATAL("m3_ParseModule: %s", result);

    result = m3_LoadModule (runtime, module);
    if (result) FATAL("m3_LoadModule: %s", result);

    IM3Function f;
    result = m3_FindFunction (&f, runtime, "fib");
    if (result) FATAL("m3_FindFunction: %s", result);

    printf("Running fib(" FIB_ARG_VALUE ") on WebAssembly...\
");

    const char* i_argv[2] = { FIB_ARG_VALUE, NULL };

    clock_t start = clock();
    result = m3_CallWithArgs (f, 1, i_argv);
    clock_t end = clock();

    if (result) FATAL("m3_CallWithArgs: %s", result);
    printf("Elapsed: %ld ms\
\
", (end - start) * 1000 / CLOCKS_PER_SEC);

    // uint64_t value = *(uint64_t*)(runtime->stack);
    // printf("Result: %llu\
", value);
}

代码十分简单直接,本文就不详细解释啦。

进一步使用

有了可用的虚拟机,大家可以自己操作一下。

首先用WasmFiddle 写个简单的代码:
https://wasdk.github.io/WasmFiddle/

点击下载Wasm文件,然后就可以用如下代码运行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Load wasm data
std::string path = "path to add.wasm";
std::string buffer;
if (!wasmfix::read_buffer_from_file(path, buffer)) {
return;
}
   
uint8_t* wasm = (uint8_t*)buffer.data();
uint32_t fsize = (uint32_t)buffer.size();

// Load wasm
M3Result result = m3Err_none;
IM3Environment env = m3_NewEnvironment ();
if (!env) FATAL("m3_NewEnvironment failed");

IM3Runtime runtime = m3_NewRuntime (env, 8192, NULL);
if (!runtime) FATAL("m3_NewRuntime failed");

IM3Module module;
result = m3_ParseModule (env, &module, wasm, fsize);
if (result) FATAL("m3_ParseModule: %s", result);

result = m3_LoadModule (runtime, module);
if (result) FATAL("m3_LoadModule: %s", result);
   
const char* func_name = "add";
const char* i_argv[3] = {"11", "5", NULL };

printf("\
calling : %s\
", func_name);

IM3Function f;
result = m3_FindFunction (&f, runtime, func_name);
if (result) FATAL("m3_FindFunction: %s", result);

result = m3_CallWithArgs (f, sizeof(i_argv)/sizeof(const char*) - 1, i_argv);

if (result) FATAL("m3_CallWithArgs: %s", result);

u32 value = *(u32*)(runtime->stack);
printf("Result: %u\
", value);

再进一步使用

Native提供方法给Wasm用

1
2
3
4
5
6
7
const char* module_name = "env";
const char* func_name = "bind_sum";
const char* func_signature = "i(ii)";
M3RawCall func_raw = bind_sum_raw;

result = m3_LinkRawFunction(module, module_name, func_name, func_signature, func_raw);
if (result) FATAL("m3_LinkRawFunction(%s): %s", func_name, result);

Native与Wasm操作同一片内存

1
2
3
4
5
6
7
char* vram = (char*)(m3_GetMemory(runtime, 0, 0));
   
int * data = (int*)(char*)(vram + dataOffset);
for (int i=0; i<10; i++) {
    printf("%d\
",data[i]);
}

m3_GetMemory 的使用确实麻烦,具体使用方法就后续文章介绍,这里大家先自行研究下哈。

用什么语言来生存Wasm呢

我们的目的是要实现iOS热修复,如果像上面的例子那样,还用Javascript生成Wasm,那还不如直接用JavascriptCore或者QuickJS,毕竟Javascript的虚拟机提供的接口更友好、好用。

但由于Wasm这个中间层,理论上我们的热修复可以用任何语言,只要这个代码可以转换成Wasm。这也是我想探索一下的原因吧,如果我们可以实现用C语言去修复,或者ObjC语言,甚至可以用Swift修复。再甚至可以用Rust(那就是太闲了哈)。

C语言生存Wasm,大家肯定会想到 Emscripten ,但调研后 Emscripten 生成的Wasm文件太大了,Emscripten 太过于强大。我们想要个更“单纯”的Wasm。就像WasmFiddle那样生成的Wasm文件很小。

找了一番资料,研究了一番之后,找到了这两篇文章:

  • https://surma.dev/things/c-to-webassembly/
  • https://depth-first.com/articles/2019/10/16/compiling-c-to-webassembly-and-running-it-without-emscripten/

其中第二篇是第一篇的续集,两篇文章都很简单易懂,大家跟着上手操作即可。

这里比较明确了,我们用C语言来生成Wasm。后期如果有需要,可以把ObjC代码转换成C语言,间接实现了ObjC语言热修复的功能。当然甚至可以用Swift。

C代码生成Wasm

首先安装llvm

1
2
brew install llvm
brew link --force llvm

然后确认支持wasm(操作没问题的话,肯定是支持了)

1
llc --version

然后写个C代码

1
2
3
4
// Filename: add.c
int add(int a, int b) {
  return a*a + b;
}

运行

1
clang --target=wasm32 -O3 -flto --no-standard-libraries -Wl,--export-all -Wl,--no-entry -Wl,--allow-undefined -Wl,--lto-O3 -o add.wasm add.c

即可产出 wasm文件。

参数解释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# sample:
#
# clang \\
#    --target=wasm32 \\
# +  -O3 \\ # Agressive optimizations
# +  -flto \\ # Add metadata for link-time optimizations
#    -nostdlib \\
#    -Wl,--no-entry \\
#    -Wl,--export-all \\
#    -Wl,--allow-undefined \\
# +  -Wl,--lto-O3 \\ # Aggressive link-time optimizations
#    -o add.wasm \\
#    add.c

更详细的解释就见那两篇文章吧。

AssemblyScript

https://www.assemblyscript.org/

这个语言我也玩耍了好久,曾经想用来实现使用TypeScript来热修复。但想来大家还得学习这个“非官方”的TypeScript,就不如直接用C语言好了。

参考

  • https://github.com/wasm3/wasm3
  • https://www.assemblyscript.org/
  • https://github.com/appcypher/awesome-wasm-langs
  • https://depth-first.com/articles/2019/10/16/compiling-c-to-webassembly-and-running-it-without-emscripten/
  • https://depth-first.com/articles/2019/05/15/compiling-inchi-to-webassembly-part-1/
  • https://surma.dev/things/c-to-webassembly/
  • https://00f.net/2019/04/07/compiling-to-webassembly-with-llvm-and-clang/
  • https://medium.com/perlin-network/life-a-secure-blazing-fast-cross-platform-webassembly-vm-in-go-ea3b31fa6e09
  • https://eos.io/news/eos-virtual-machine-a-high-performance-blockchain-webassembly-interpreter/

总结

好了,我们能用C语言生存Wasm了,也能用移动端执行Wasm了。