CKB 脚本编程简介[8]: 高性能 WASM
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.tsexport 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 bashroot@7f24745ca702:/# cd /coderoot@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_wabtroot@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.ldsroot@7f24745ca702:/code# riscv64-unknown-elf-gcc -O3 -I ckb-c-stdlib -I wavm-aot-generator -I wabt/wasm2c fib_native_main.c -o fib_nativeroot@7f24745ca702:/code# exitexit$ 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 1end
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 0x10000000Run result: Ok(61)Total cycles consumed: 549478Transfer cycles: 6530, running cycles: 542948$ ./runner.rb fib_wabt 0x20000000Run result: Ok(-30)Total cycles consumed: 549590Transfer cycles: 6530, running cycles: 543060$ ./runner.rb fib_wabt 0x00010000Run result: Ok(29)Total cycles consumed: 551158Transfer cycles: 6530, running cycles: 544628$ ./runner.rb fib_wavm_patched 0x10000000Run result: Ok(61)Total cycles consumed: 22402Transfer cycles: 19696, running cycles: 2706$ ./runner.rb fib_wavm_patched 0x20000000Run result: Ok(-30)Total cycles consumed: 22578Transfer cycles: 19696, running cycles: 2882$ ./runner.rb fib_wavm_patched 0x00010000Run result: Ok(29)Total cycles consumed: 25042Transfer cycles: 19696, running cycles: 5346$ ./runner.rb fib_native 0x10000000Run result: Ok(61)Total cycles consumed: 3114Transfer cycles: 1137, running cycles: 1977$ ./runner.rb fib_native 0x20000000Run result: Ok(-30)Total cycles consumed: 3226Transfer cycles: 1137, running cycles: 2089$ ./runner.rb fib_native 0x00010000Run result: Ok(29)Total cycles consumed: 4794Transfer cycles: 1137, running cycles: 3657
在 witness 部分,输入值被编译成 32 位的小端无符号整数,这里的 0x10000000
,0x20000000
和 0x00010000
分别代表 16
,32
和 256
。
由于 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 bashroot@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_wabtroot@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.ldsroot@a237c0d00b1c:/code# exitexit$ 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 0x01010000Run result: Ok(0)Total cycles consumed: 35702943Transfer cycles: 438060, running cycles: 35264883$ ./runner.rb secp_wabt 0x01050000Run result: Ok(0)Total cycles consumed: 90164183Transfer cycles: 438060, running cycles: 89726123$ ./runner.rb secp_wavm_patched 0x01010000Run result: Ok(0)Total cycles consumed: 10206568Transfer cycles: 428764, running cycles: 9777804$ ./runner.rb secp_wavm_patched 0x01050000Run result: Ok(0)Total cycles consumed: 49307936Transfer 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 bashroot@ad22c452cb54:/# cd /code/ckb-vm-bench-scriptsroot@ad22c452cb54:/code/ckb-vm-bench-scripts# make(omitted ...)root@ad22c452cb54:/code/ckb-vm-bench-scripts# exitexit
$ ./runner.rb ckb-vm-bench-scripts/build/secp256k1_bench 0x01010000Run result: Ok(0)Total cycles consumed: 1621594Transfer cycles: 272630, running cycles: 1348964
$ ./runner.rb ckb-vm-bench-scripts/build/secp256k1_bench 0x01050000Run result: Ok(0)Total cycles consumed: 7007598Transfer 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 bashroot@d28602dba318:/# cd /coderoot@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.Shelloworld_precompiled.o -o helloworld_wavm -Wl,-T wavm-aot-generator/abi/riscv64.ldsroot@d28602dba318:/code# exitexit$ ckb-binary-patcher/target/release/ckb-binary-patcher -i helloworld_wavm -o helloworld_wavm_patched$ RUST_LOG=debug ./runner.rb helloworld_wavm_patched 0xDEBUG:<unknown>: script group: Byte32(0x86cfac3b49b8f97f913aa5a09d02ad1e5b1ab5be0be793815e9cb714ba831948) DEBUG OUTPUT: Hello World!
Run result: Ok(0)Total cycles consumed: 20260Transfer 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,多年后你会感谢自己的。