CKB 脚本编程简介[8]: 高性能 WASM

Xuejie Xiao

Xuejie Xiao

Nervos Core Team

先前的文章中,我已经向你展示过在 CKB 上运行 WASM 程序,但需要注意的是,WASM 程序的性能可能并不那么出色。我也提到了,有一个潜在的解决方案可以解决这个问题。就在今天,我们发布了一个新项目,这个项目可以用来生成高效的 WASM 程序!让我们赶紧来看看它具体是如何操作的。

背景

(如果你迫不及待了,可以跳过本节,直接跳至示例部分)

在先前的文章中,我们是先将 WebAssembly 程序转换成了 C 代码,然后再将其从 C 代码编译成了 RISC-V平台的代码。但这有很多缺点:

  • 使用 C 代码做中间层时,我们并不能总是可以保留所有的 能对代码进行优化的信息。
  • 由于 C 语言的限制,无法完全自定义内存分配以实现最佳性能。
  • 有时候C 语言层会变得非常奇怪,不容易调试。

这里,我们尝试一种不同的解决方案:WAVM 是一种高性能的转换层(基准测试表明,这是迄今为止最高性能的 WASM 实现),可以通过 LLVM 将 WASM 代码直接编译成机器码。因为LLVM 9+的官方直接支持RISC-V,因此我们可以将 WAVM 编译成 RISC-V 代码,然后它就可以直接将 WASM 程序转换成在 RISC-V 上可以运行的机器码。

还有一个问题:WAVM 需要一个运行时,使得本机环境和周围环境互补。目前它被包含在WAVM中,并且依赖于LLVM,这使得二进制文件非常大。有一天,我突然想到构建运行时所需的所有信息,已经都包含在了原始的 WASM 文件中,因此我们可以单独构建一个项目,用于处理原始的 WASM 文件并以纯C代码输出一个最小的运行时,然后将其与生成的机器码链接在一起,这样我们就可以得到一个由 WASM 代码编译的,并且可以独立运行的 RISC-V 的本地程序。

示例

在这里,我们将使用与先前文章中完全相同的示例:用 AssemblyScript 写的一个斐波那契和一个纯 Rust 写的 secp256k1代码实现。我们将对生成的代码大小以及在 CKB VM 中运行所消耗的 cycles 数量进行比较。为了完整起见,每个示例中还将包括用纯 C 编写的原生版本。 正如我们将在下面看到的那样,即使我们当前的 WASM 解决方案仍然和纯 C 版本有一些差距,但已经非常接近了,并且已经可以满足很多用例的需求了。

首先让我们复制所需的代码库并进行一些必要的准备:

$ export TOP=$(pwd)
$ git clone https://github.com/AssemblyScript/assemblyscript.git
$ cd assemblyscript
$ git checkout b433bc425633c3df6a4a30c735c91c78526a9eb7
$ npm install
$ cd $TOP
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ git checkout bec78eafbc203d81b9a6d1ce81f5a80dd7bf692a
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
$ cd $TOP
$ git clone https://github.com/xxuejie/WAVM
$ cd WAVM
$ git checkout cb35225feeb4ba1b5a9c73cbbdb07f4cace9b359
$ mkdir build
$ cd build
# Make sure you are using LLVM 9+, you might need to tweak this path depending
# on your environment
$ cmake .. -DLLVM_DIR=/usr/lib/llvm-9/lib/cmake/llvm
$ cmake --build .
$ cd $TOP
$ git clone https://github.com/xxuejie/wavm-aot-generator
$ cd wavm-aot-generator
$ git checkout 8c818747eb19494fc9c5e0289810aa7ad484a22e
$ cargo build --release
$ cd $TOP
$ git clone https://github.com/xxuejie/ckb-standalone-debugger
$ cd ckb-standalone-debugger
$ git checkout 15e8813b8cb886e95e2c81bbee9f26d47a831850
$ cd bins
$ cargo build --release
$ cd $TOP
$ git clone https://github.com/xxuejie/ckb-binary-patcher
$ cd ckb-binary-patcher
$ git checkout 930f0b468a8f426ebb759d9da735ebaa1e2f98ba
$ cd ckb-binary-patcher
$ cargo build --release
$ cd $TOP
$ git clone https://github.com/nervosnetwork/ckb-c-stdlib
$ cd ckb-c-stdlib
$ git checkout 693c58163fe37d6abd326c537447260a846375f0

AssemblyScript 示例

这是我们先前用 AssemblyScript 写的斐波那契的例子,让我们先将其编译成 WebAssembly 程序:

$ cd $TOP
$ cat << EOF > fib.ts
export function fib(n: i32): i32 {
var a = 0, b = 1;
for (let i = 0; i < n; i++) {
let t = a + b; a = b; b = t;
}
return b;
}
EOF
$ assemblyscript/bin/asc fib.ts -b fib.wasm -O3

我们将这个 WASM 编译成两个版本:

$ cd $TOP
$ wabt/build/wasm2c fib.wasm -o fib.c
$ WAVM/build/bin/wavm compile --target-triple riscv64 fib.wasm fib_precompiled.wasm
$ wavm-aot-generator/target/release/wavm-aot-generator fib_precompiled.wasm fib_precompiled

你可能会注意到,我们不是生成 RISC-V 的机器码,而是使用 WAVM 来生成一个 precomplied object 的格式化文件。这本质上就是原来的 WASM 文件,其中嵌入了自定义部分中的机器码,这样我们就可以方便地将单个文件提供给生成器。

让我们附加 2 个不同的包装文件到 2 个 WASM 中去,同时也提供一个 C 的原生实现:

$ cd $TOP
$ cat << EOF > fib_wabt_main.c
#include <stdio.h>
#include <stdlib.h>
#include "ckb_syscalls.h"
#include "fib.h"
void (*Z_envZ_abortZ_viiii)(u32, u32, u32, u32);
void env_abort(u32 a, u32 b, u32 c, u32 d) {
abort();
}
int main() {
uint32_t value;
uint64_t len = 4;
int ret = ckb_load_witness((void*) &value, &len, 0, 0,
CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 4) {
return -1;
}
init();
u8 result = Z_fibZ_ii(value);
return result;
}
EOF
$ cat << EOF > fib_wavm_main.c
#include "fib_precompiled_glue.h"
#include "abi/ckb_vm_wasi_abi.h"
#include "ckb_syscalls.h"
void* wavm_env_abort(void* dummy, int32_t code, int32_t a, int32_t b, int32_t c)
{
ckb_exit(code);
return dummy;
}
int main() {
uint32_t value;
uint64_t len = 4;
int ret = ckb_load_witness((void*) &value, &len, 0, 0,
CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 4) {
return -1;
}
wavm_ret_int32_t wavm_ret = wavm_exported_function_fib(NULL, value);
return wavm_ret.value;
}
EOF
$ cat << EOF > fib_native_main.c
#include "ckb_syscalls.h"
int32_t fib(int32_t n) {
int32_t a = 0;
int32_t b = 1;
for (int32_t i = 0; i < n; i++) {
int32_t t = a + b;
a = b;
b = t;
}
return b;
}
int main() {
uint32_t value;
uint64_t len = 4;
int ret = ckb_load_witness((void*) &value, &len, 0, 0,
CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 4) {
return -1;
}
return fib(value);
}
EOF

你可能会注意到,我们改变了先前文章中使用的 wabt 包装器,所以这里的三个版本都会从 witness data 加载 input 到斐波那契函数,这样我们就可以设置相同的比较标准。

让我们先来编译这 3 个文件:

$ cd $TOP
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191209 bash
root@7f24745ca702:/# cd /code
root@7f24745ca702:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c fib_wabt_main.c fib.c wabt/wasm2c/wasm-rt-impl.c -o fib_wabt
root@7f24745ca702:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c fib_wavm_main.c wavm-aot-generator/abi/riscv64_runtime.S fib_precompiled.o -o fib_wavm -Wl,-T wavm-aot-generator/abi/riscv64.lds
root@7f24745ca702:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c fib_native_main.c -o fib_native
root@7f24745ca702:/code# exit
exit
$ ckb-binary-patcher/target/release/ckb-binary-patcher -i fib_wavm -o fib_wavm_patched

由于一个 虚拟机的bug,这里已经提供了一个补丁程序来解决可能生成 bug 的 RISC-V 代码。虽然我们只观察到 LLVM 会受到这个 bug 的影响(GCC 有一些优化,会生成不同的代码),我们还是建议你,如果在 CKB 上跑任何脚本,都应该运行一下这个补丁程序。

我们还准备了一个运行程序来运行脚本:

$ cd $TOP
$ cat << EOF > runner.rb
#!/usr/bin/env ruby
require "rbnacl"
def bin_to_hex(bin)
"0x#{bin.unpack1('H*')}"
end
def blake2b(data)
RbNaCl::Hash::Blake2b.digest(data,
personal: "ckb-default-hash",
digest_size: 32)
end
if ARGV.length != 2
STDERR.puts "Usage: runner.rb <script file> <witness args>"
exit 1
end
script_binary = File.read(ARGV[0])
script_hash = blake2b(script_binary)
tx = DATA.read.sub("@FIB_CODE", bin_to_hex(script_binary))
.sub("@FIB_HASH", bin_to_hex(script_hash))
.sub("@FIB_ARG", ARGV[1])
File.write("tx.json", tx)
commandline = "ckb-standalone-debugger/bins/target/release/ckb-debugger --tx-file tx.json --script-group-type type -i 0 -e input"
STDERR.puts "Executing: #{commandline}"
exec(commandline)
__END__
{
"mock_info": {
"inputs": [
{
"input": {
"previous_output": {
"tx_hash": "0xa98c57135830e1b91345948df6c4b8870828199a786b26f09f7dec4bc27a73da",
"index": "0x0"
},
"since": "0x0"
},
"output": {
"capacity": "0x4b9f96b00",
"lock": {
"args": "0x",
"code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"hash_type": "data"
},
"type": {
"args": "0x",
"code_hash": "@FIB_HASH",
"hash_type": "data"
}
},
"data": "0x"
}
],
"cell_deps": [
{
"cell_dep": {
"out_point": {
"tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
"index": "0x0"
},
"dep_type": "code"
},
"output": {
"capacity": "0x702198d000",
"lock": {
"args": "0x",
"code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"hash_type": "data"
},
"type": null
},
"data": "@FIB_CODE"
}
],
"header_deps": []
},
"tx": {
"version": "0x0",
"cell_deps": [
{
"out_point": {
"tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
"index": "0x0"
},
"dep_type": "code"
}
],
"header_deps": [
],
"inputs": [
{
"previous_output": {
"tx_hash": "0xa98c57135830e1b91345948df6c4b8870828199a786b26f09f7dec4bc27a73da",
"index": "0x0"
},
"since": "0x0"
}
],
"outputs": [
{
"capacity": "0x0",
"lock": {
"args": "0x",
"code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"hash_type": "data"
},
"type": null
}
],
"witnesses": [
"@FIB_ARG"
],
"outputs_data": [
"0x"
]
}
}
EOF
$ chmod +x runner.rb

现在我们可以看看每个版本的二进制文件的大小,以及运行 3 个版本的斐波那契:

$ ls -lh fib_wabt fib_wavm_patched fib_native
-rwxr-xr-x 1 root 11K Mar 3 03:27 fib_native*
-rwxr-xr-x 1 root 53K Mar 3 03:26 fib_wabt*
-rwxr-xr-x 1 root 88K Mar 3 03:26 fib_wavm_patched*
$ ./runner.rb fib_wabt 0x10000000
Run result: Ok(61)
Total cycles consumed: 549478
Transfer cycles: 6530, running cycles: 542948
$ ./runner.rb fib_wabt 0x20000000
Run result: Ok(-30)
Total cycles consumed: 549590
Transfer cycles: 6530, running cycles: 543060
$ ./runner.rb fib_wabt 0x00010000
Run result: Ok(29)
Total cycles consumed: 551158
Transfer cycles: 6530, running cycles: 544628
$ ./runner.rb fib_wavm_patched 0x10000000
Run result: Ok(61)
Total cycles consumed: 22402
Transfer cycles: 19696, running cycles: 2706
$ ./runner.rb fib_wavm_patched 0x20000000
Run result: Ok(-30)
Total cycles consumed: 22578
Transfer cycles: 19696, running cycles: 2882
$ ./runner.rb fib_wavm_patched 0x00010000
Run result: Ok(29)
Total cycles consumed: 25042
Transfer cycles: 19696, running cycles: 5346
$ ./runner.rb fib_native 0x10000000
Run result: Ok(61)
Total cycles consumed: 3114
Transfer cycles: 1137, running cycles: 1977
$ ./runner.rb fib_native 0x20000000
Run result: Ok(-30)
Total cycles consumed: 3226
Transfer cycles: 1137, running cycles: 2089
$ ./runner.rb fib_native 0x00010000
Run result: Ok(29)
Total cycles consumed: 4794
Transfer cycles: 1137, running cycles: 3657

在 witness 部分,输入值被编译成 32 位的小端无符号整数,这里的 0x100000000x200000000x00010000 分别代表 1632256

由于 CKB VM 会将 8 位有符号数值作为输出,所以计算值在这里会被截断。但是我们并不关心实际的斐波那契数(当然,假设 3 个版本生成相同的结果),我们关心的是这里消耗的 cycle 数。

从这些值中可以得出以下一些结论:

  • WAVM 版本生成的二进制文件最大(88 K),实际上它还需要往 VM 内加载更多的字节,这是由 transfer cycle 确定的(粗略地说,一个 transfer cycle 意味着需要往 VM 内加载 4 个字节)
  • WABT 版本和 C 原生版本都需要大约 7 个 cycles 来计算一轮的斐波那契迭代,而 WAVM 版本需要大约 11 个 cycles 来计算一轮迭代
  • 然而,WAVM 版本只需要大约 2530 个 cycles 来设置运行环境,而 WABT 版本需要大约 542836 个 cycles 来设置运行环境。

我们可以看到,WAVM 版本在初始设置中使用了更少的 cycles(这主要是由于我们可以在 WAVM 版本中使用定制的内存设计),但是 WAVM 版本在每次运行斐波纳契迭代时会稍微慢一些。这可能是因为 LLVM 仍需要一些工作来赶上 GCC 为 RISC-V 生成代码的质量,也可能是因为斐波那契函数非常简单,所以 GCC 可以从还原的 C 代码中完美地获得计算结构。对于更复杂的示例,可能就不是这种情况了。

我个人确实对 WAVM 二进制文件的大小做了一些调查,问题是,在 WAVM 中生成的所有符号都是声明的公共符号。这意味着我们不能依靠无用代码删除(dead code elimination DCE)来清除那些我们没有使用的变量和函数,因此这里生成了一个更大的二进制文件。如果原始的 WASM 程序是由 Rust 或 LLVM 直接生成的,就不会出现这样的问题了,因为已经执行了 DCE,但是 AssemblyScript 执行的 DCE 很少,因此我们会有一个更大的二进制文件。稍后,我可能会看看 WAVM 是否有办法将未导出的函数调整为本地模块,如果可以解决这个问题,我们应该能够将 WAVM 版本的二进制文件大小降低到与其他解决方案相同的级别。

Rust Secp256k1 示例

让我们也尝试一下更复杂的基于 Rust 的 secp256k1 的例子:

$ cd $TOP
$ git clone https://github.com/nervosnetwork/wasm-secp256k1-test
$ cd wasm-secp256k1-test
$ cargo build --release --target=wasm32-unknown-unknown
$ cd $TOP
$ wabt/bin/wasm2c wasm-secp256k1-test/target/wasm32-unknown-unknown/release/wasm-secp256k1-test.wasm -o secp.c
# There's a symbol confliction in the latest versioni of gcc with wabt here, this
# can serve as a temporary solutin
$ sed -i s/bcmp/bcmp1/g secp.c
$ WAVM/build/bin/wavm compile --target-triple riscv64 wasm-secp256k1-test/target/wasm32-unknown-unknown/release/wasm-secp256k1-test.wasm secp_precompiled.wasm
$ wavm-aot-generator/target/release/wavm-aot-generator secp_precompiled.wasm secp_precompiled
$ cd $TOP
$ cat << EOF > secp_wabt_main.c
#include <stdio.h>
#include <stdlib.h>
#include "ckb_syscalls.h"
#include "secp.h"
int main() {
uint32_t value;
uint64_t len = 4;
int ret = ckb_load_witness((void*) &value, &len, 0, 0,
CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 4) {
return -1;
}
init();
uint32_t times = value >> 8;
value = value & 0xFF;
uint8_t result = 0;
for (int i = 0; i < times; i++) {
result += Z_runZ_ii(value);
}
return result;
}
EOF
$ cat << EOF > secp_wavm_main.c
#include "secp_precompiled_glue.h"
#include "abi/ckb_vm_wasi_abi.h"
#include "ckb_syscalls.h"
int main() {
uint32_t value;
uint64_t len = 4;
int ret = ckb_load_witness((void*) &value, &len, 0, 0,
CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 4) {
return -1;
}
uint32_t times = value >> 8;
value = value & 0xFF;
uint8_t result = 0;
for (int i = 0; i < times; i++) {
ckb_debug("One run!");
wavm_ret_int32_t wavm_ret = wavm_exported_function_run(NULL, value);
result += wavm_ret.value;
}
return result;
}
EOF

现在我们可以编译代码,然后比较二进制文件大小和运行的 cycles 数:

$ cd $TOP
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191209 bash
root@a237c0d00b1c:/# cd /code/
root@a237c0d00b1c:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c secp_wabt_main.c secp.c wabt/wasm2c/wasm-rt-impl.c -o secp_wabt
root@a237c0d00b1c:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c secp_wavm_main.c wavm-aot-generator/abi/riscv64_runtime.S secp_precompiled.o -o secp_wavm -Wl,-T wavm-aot-generator/abi/riscv64.lds
root@a237c0d00b1c:/code# exit
exit
$ ckb-binary-patcher/target/release/ckb-binary-patcher -i secp_wavm -o secp_wavm_patched
$ ls -l secp_wabt secp_wavm_patched
-rwxrwxr-x 1 ubuntu 1791744 Mar 3 05:27 secp_wabt*
-rw-rw-r-- 1 ubuntu 1800440 Mar 3 05:29 secp_wavm_patched
$ ./runner.rb secp_wabt 0x01010000
Run result: Ok(0)
Total cycles consumed: 35702943
Transfer cycles: 438060, running cycles: 35264883
$ ./runner.rb secp_wabt 0x01050000
Run result: Ok(0)
Total cycles consumed: 90164183
Transfer cycles: 438060, running cycles: 89726123
$ ./runner.rb secp_wavm_patched 0x01010000
Run result: Ok(0)
Total cycles consumed: 10206568
Transfer cycles: 428764, running cycles: 9777804
$ ./runner.rb secp_wavm_patched 0x01050000
Run result: Ok(0)
Total cycles consumed: 49307936
Transfer cycles: 428764, running cycles: 48879172

和前面的例子一样,我们也可以从这些数据中得到一些判断:

  • 用两个版本生成的二进制文件的大小差别不大,WAVM 的二进制文件稍微大一些,但是需要加载到 VM 中的字节会更少

  • 在这种情况下,单个 secp256k1 验证在 WAVM 版本中需要 9775342 个 cycles,而在 WABT 版本中需要 13615310 个 cycles。在这里,我们可以看到在 LLVM 中直接进行编译,确实比 WABT还原成C代码的版本提供了更好的性能

  • 对于如此复杂的程序,WAVM 版本仍然只需要 2462 个 cycles ,而 WABT 版本则需要 21649573 个 cycles。在这里,WAVM 已经胜出了。

由于从 Rust 到 RISC-V 的当前直接路径不允许使用 std。我们不能直接提供类似的 C 的原生版本。但是对于那些好奇的朋友,我仍然在纯 C 代码中提供了一个类似的函数,我们可以测试直接编译成 RISC-V 的 C 版本所花费的周期:

$ cd $TOP
$ git clone --recursive https://github.com/nervosnetwork/ckb-vm-bench-scripts
$ cd ckb-vm-bench-scripts
$ git checkout f7ab37c055b1a59bbc4f931c732331642c728c1d
$ cd $TOP
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191209 bash
root@ad22c452cb54:/# cd /code/ckb-vm-bench-scripts
root@ad22c452cb54:/code/ckb-vm-bench-scripts# make
(omitted ...)
root@ad22c452cb54:/code/ckb-vm-bench-scripts# exit
exit
$ ./runner.rb ckb-vm-bench-scripts/build/secp256k1_bench 0x01010000
Run result: Ok(0)
Total cycles consumed: 1621594
Transfer cycles: 272630, running cycles: 1348964
$ ./runner.rb ckb-vm-bench-scripts/build/secp256k1_bench 0x01050000
Run result: Ok(0)
Total cycles consumed: 7007598
Transfer cycles: 272630, running cycles: 6734968

正如我们在这里看到的,C 原生版本每个 secp256k1 花费了 1346501 个 cycles,初始的统计工作需要 2463 个 cycles。二进制文件的大小和加载所需的字节都更小。

我先在这里提一下,我们并不是在比较相同的代码,C 版本和 Rust 版本使用的是不同的实现,而且我们还没有直接对这两种实现的质量进行评估。话虽如此,假设这两个版本有相似的性能,先编译成 WASM,再编译成 RISC-V 的 Rust 代码的性能大约是 C 代码的 1/7。考虑到 Rust 版本也可能执行边界检查,我认为这对于很多用例来说是很好的性能。 有很多脚本可以处理这种级别的性能。更重要的是,你总是可以将 C 实现的为提高性能的代码和 Rust 实现的逻辑代码结合在一起,以享受两者的最佳效果。 我们还没有提到这条新路线的优点。最后同样重要的是,所有涉及的项目,包括Rust,LLVM,WAVM和我们的生成器都是活跃的项目,正在进行开发工作,随着优秀工程师们不断的改进,这种差距会很快缩小。

WASI

我一直在说通过 WASM 在 CKB 上做 Rust,我的同事已经证明有一条路可以直接从 Rust 到 RISC-V 的路径,WASM 作为中间路径在这里有什么帮助? 直接路径的问题是,RISC-V port不支持 Rust 的 std,更糟糕的是,libc 绑定也不存在。这意味着你将不得不使用 core Rust,它是一种最小且功能有限的 Rust。请不要误解我的意思,使用 core Rust 并没有什么不好,如果你的用例已经足够支持Rust的std,那么你顺其自然就好。 但是我确实想提供一个不同的道路,这里 std 是可以用的,所以大多数 crates 上的 Rust 库都可以用来构建CKB 脚本。这样就可以通过 WASM 来实现 WASI。

你没有听过WASI?WASI 是一种与 WebAssembly 程序的运行环境进行交互的标准方法。已经 证明,Rust 在 WASM 的未来取决于一个新的 wasm32-wasi 的平台。

通过执行 WASM 的中间步骤,我们可以将对 WASI 的支持直接构建到 CKB 脚本中,享受未来 Rust 的 wasm32-wasi!事实上,WAVM 已经提供了一个使用 WASI 的 API 的例子,让我们看看是否可以在 CKB 上实现:

$ cd $TOP
$ WAVM/build/bin/wavm compile --target-triple riscv64 WAVM/Examples/helloworld.wast helloworld_precompiled.wasm
$ wavm-aot-generator/target/release/wavm-aot-generator helloworld_precompiled.wasm helloworld_precompiled
$ cat << EOF > helloworld_wavm_main.c
#include "helloworld_precompiled_glue.h"
#include "abi/ckb_vm_wasi_abi.h"
/* main is already generated via wavm-aot-generator */
EOF
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191209 bash
root@d28602dba318:/# cd /code
root@d28602dba318:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c helloworld_wavm_main.c wavm-aot-generator/abi/riscv64_runtime.S
helloworld_precompiled.o -o helloworld_wavm -Wl,-T wavm-aot-generator/abi/riscv64.lds
root@d28602dba318:/code# exit
exit
$ ckb-binary-patcher/target/release/ckb-binary-patcher -i helloworld_wavm -o helloworld_wavm_patched
$ RUST_LOG=debug ./runner.rb helloworld_wavm_patched 0x
DEBUG:<unknown>: script group: Byte32(0x86cfac3b49b8f97f913aa5a09d02ad1e5b1ab5be0be793815e9cb714ba831948) DEBUG OUTPUT: Hello World!
Run result: Ok(0)
Total cycles consumed: 20260
Transfer cycles: 17728, running cycles: 2532

我们可以看到 WASI API 运行的非常好! 这是因为我已经提供了此处使用的两个 API 的实现

虽然现在还不完整,但我将会为所有的 WASI API 添加 shims。之后,我们可以使用支持std的Rust程序,编译成 wasm32-wasi 平台的 WASM 代码,然后完美地转换成 RISC-V。 你可以看到很多不同的区块链项目声称他们时刻都在使用 WebAssembly,但他们没有告诉你的是,WebAssembly 被设计成多种风格,他们只是选择支持其中的一种。 事实上,许多 著名的 区块链项目 往往只支持最简单的 WebAssembly 程序。然后他们中的大多数让你使用 Rust,他们只使用奇怪的和可能被弃用的 wasm32-unknown-unknown 平台。 结果,他们要么直接禁用 Rust std,说你不需要它,要么提供不可靠的支持,将来可能会中断这种支持,或者由于兼容性的原因,他们无力修改代码。 另一方面,你可以在CKB 上享受 WASI 和全功能的 Rust。许多人问我们为什么不直接使用 WebAssembly,我想说我们是第一个,或者至少是最先在区块链上正确使用 WebAssembly 的项目之一。

“Vice Verca”并不总是有效的

我们经常听到的一个话题是,如果你能把 WASM 翻译成 RISC-V,你也能把 RISC-V 翻译成WASM!从某种意义上说,这是对的,但是在一种可行的方法和另一种可行的方法之间是有区别的。

由于 RISC-V 的设计,它是一个非常简单的规范,可以很好地映射到现代的 CPU。如果检查我们的 VM 实现,你可能会注意到大多数 RISC-V 指令都直接映射到 12 个 x86-64 CPU 指令中。我们只是构建了一个最小的安全层,它在你机器的 CPU上工作。 而另一方面,WASM 是一个像 JVM 一样的庞然大物,规范中已经有大量的特性,而且每天都有大量的特性被添加到规范中。 许多新特性在 CPU 上没有直接的映射,底层指令如 memory.grow 或者 call indirect,到更高层次的功能,如垃圾回收或者线程。当你选择 WebAssembly 作为区块链的引擎时,你将不得不选择一组特性,并确定新功能不断出现时你如何/是否要迁移。使问题复杂化的是,你不能仅仅更改当前 WebAssembly 引擎中的某些功能的实现,因为这可能会带来不兼容的更改。

当你选择 RISC-V 作为底层引擎时,你不会有这样的顾虑。RISC-V 是为永不改变的硬件设计的。规范修订后,它将永远修复,所有编译器都必须遵守规范中的错误。当你在 RISC-V 上实现 WebAssembly 程序时,你可以自由地改变更高结构在WebAssembly中的实现。例如,你可能会发现一个新的垃圾回收算法,它有助于你的智能合约,你可以通过升级一个不同的智能合约来部署算法,不需要任何硬分叉来支持它。 如果从WebAssembly引擎开始解决问题,所有这些都将变得非常困难,甚至是不可能的。这就是我认为的CKB独特设计所带来的美。

回顾

这里有一个建议:如果有人告诉你他/她的区块链在使用 WebAssembly,为了你自己着想,询问一下他/她的 WebAssembly 引擎使用的具体规范是什么,以及当更多特性添加到 WebAssembly 规范中时,他/她计划如何处理这个问题。WebAssembly 是一个不断增长的规范,由于它的 Web 根源,选择一个规范并冻结它,从来就不是一个在堆栈中使用 WebAssembly 的好策略。在区块链的世界中,依赖 WebAssembly 没有什么错,但是如何以一个正确的方式使用 WebAssembly 就很重要了。对我来说,CKB 就是一个示范,通过正确的方式来使用 WebAssembly的例子,并考虑到未来的问题。我相信,如果你付出额外的努力,确保你选择的区块链以正确的方式部署 WebAssembly,多年后你会感谢自己的。

原文链接