作者:Xuejie
原文链接:https://xuejie.space/2019_10_09_introduction_to_ckb_script_programming_wasm_on_ckb/
Nervos CKB 脚本编程简介[4]:在 CKB 上实现 WebAssembly
自从我们选择使用 RISC-V 构建 CKB-VM(Virtual Machine 虚拟机)以来,我们几乎每一天都会被问及这样一个问题:为什么不像别人那样在 WebAssembly 上构建你的虚拟机呢?
在这个选择的背后其实有非常多的原因,可能需要另一篇文章或者一次演讲才能解释完其中的原因。从根本上来说,有一个相当重要的原因:构建软件最主要的就是找到正确的抽象概念,我们相信对于无需许可的区块链而言 RISC-V 比 WebAssembly 是一个更好抽象概念。
当然 WebAssembly 相比于其他更高级的编程语言和第一代区块链虚拟机而言已经是一个巨大的进步了,但 RISC-V 的运行级别还要比 WebAssembly 低得多,这使得它非常适合用于那些希望未来在运行几十年的公有链。
但还有一个问题没有得到回答:目前区块链行业很大一部分人都押注在 WebAssembly 上,(可以说) 基于 WebAssembly 的 dapps 构建了一个很好的生态系统。那么 CKB 如何与之竞争呢?正如上面所提到的,RISC-V 实际上位于一个比 WebAssembly 更低的抽象级别上,我们可以移植现有的 WebAssembly 的程序,并直接在 CKB-VM 上运行他们。通过这种方式,我们既可以享受 RISC-V 提供的灵活性和稳定性,同时也可以拥抱 WebAssembly 的生态系统。
在本文中,我们将展示如何在 CKB-VM 中运行 WebAssembly 程序,我们还会展示通过这种方式运行(程序),会比直接使用 WebAssembly VM 具有更多的优势。
就我个人而言,虽然我相信 WebAssembly 会有一些有趣的特性来支持不同的用例,但是我不相信 WebAssembly 会在区块链领域创建一个更好的生态。环顾四周,在基于 WebAssembly 的区块链中构建 DApp 可能只有两种成熟的选择:Rust 和 AssemblyScript。
人们一直在吹嘘 WebAssembly 在单个抽象的 VM 中支持任意语言的能力(我个人拒绝将WebAssembly 称为 low-level 的虚拟机),但是在这里创建一个真正的 DApp,只能在这两种语言内选择其一。我认为,如果将仅支持 2 种的编程语言的虚拟机称为良好的 VM 生态系统,那么我们可能会有不同的定义。当然还有一些其他语言也在迎头赶上,但它们还没有稳定到可以被认为是一个繁荣的生态系统的阶段。虽然一些有趣的语言在基于 WebAssembly 的环境中具有潜力,但是还没有人注意到,并去支持它们。
如果你仔细观察,就会发现,使用 WebAssembly 的两个不同的区块链项目之间是否可以彼此共享合约,这目前仍然是个问题。当然,有人可能会说:「嗯,这只是一个时间问题,随着时间的推移,更有活力的 WebAssembly 生态系统将会发芽。」但同样的论点也适用于任何地方:为什么随着时间的推移,RISC-V 的生态系统不会变得更好?
咆哮到此为止,现在我们只是假设,WebAssembly 确实拥有一个区块链生态系统,我们可以证明,其中两个被广泛使用的语言:AssemblyScript 和 Rust,都可以在 CKB-VM 环境中得到支持。
AssemblyScript
我相信没有比演示更能说明问题的了。所以,让我们试试官方的 AssemblyScript,然后在 CKB 上运行编译好的程序。我们将只使用 AssemblyScript 简介页面中的官方示例:
$ cat 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;
}
关于如何安装,请参考 AssemblyScripts 的文档。为了方便起见,我提供了一些步骤,您可以在这里复制粘贴。
$ git clone https://github.com/AssemblyScript/assemblyscript.git
$ cd assemblyscript
$ npm install
$ bin/asc ../fib.ts -b ../fib.wasm -O3
$ cd ..
这样我们就有了一个编译好的 WebAssembly 程序,我们可以调用一个名为 wasm2c 的程序将它编译成 C 语言的源文件,然后通过 RISC-V 编译器将它编译成 RISC-V 程序,并在 CKB-VM 上运行。
我敢肯定你会说:这是一个黑客行为!它这里对 WASM 程序进行了反编译,然后使它可以运行,你这是在作弊。这个问题的答案是,是但又不是:
一方面,我是在作弊,但我要提出的问题是:我们应该关心的是最终的结果,如果结果足够好,我们为什么要关心这是否是作弊呢?另外,现代编译器已经足够复杂了,就像一个完全的黑盒,我们怎么能确定这个反编译会得到更糟糕的结果呢?
另一方面,这只是将 WebAssembly 转换为 RISC-V 的一种方法。还有许多其他方法都可以实现相同的结果。我们将在后面的重述部分再次讨论这一点。
启动 wasm2c 然后转换 WebAssembly 程序:
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
$ cd ../..
$ wabt/bin/wasm2c fib.wasm -o fib.c
您将在当前目录中看到一对 fib.c 和 fib.h 文件,它们包含 WebAssembly 程序转换的结果,当编译和调用正确时,它们将实现与 WebAssembly 程序相同的功能。
我们可以使用一个小的包装器 C 文件来调用 WebAssembly 程序:
$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include "fib.h"
int main(int argc, char** argv) {
if (argc < 2) return 2;
u8 x = atoi(argv[1]);
init();
u8 result = Z_fibZ_ii(x);
return result;
}
这只是从 CLI 参数中读取一个整数,在 WebAssembly 程序中调用 Fibonacci 函数,然后返回结果。让我们先来编译它:
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
(docker) $ cd /code
(docker) $ riscv64-unknown-elf-gcc -o fib_riscv64 -O3 -g main.c fib.c /code/wabt/wasm2c/wasm-rt-impl.c -I /code/wabt/wasm2c
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `__retain':
/code/fib.c:1602: undefined reference to `Z_envZ_abortZ_viiii'
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `i32_load':
/code/fib.c:42: undefined reference to `Z_envZ_abortZ_viiii'
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `f17':
/code/fib.c:1564: undefined reference to `Z_envZ_abortZ_viiii'
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/fib.c:1564: undefined reference to `Z_envZ_abortZ_viiii'
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `f6':
/code/fib.c:1011: undefined reference to `Z_envZ_abortZ_viiii'
/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o:/code/fib.c:1012: more undefined references to `Z_envZ_abortZ_viiii' follow
collect2: error: ld returned 1 exit status
(docker) $ exit
如上所示,这里有一个报错。它告诉我们有一个 Z_ENVZ_ABORTZ_VIII 函数没有定义。让我们深入了解为什么会发生这种情况。
首先,让我们将原始的 WebAssembly 文件转换为可读的形式:
$ wabt/bin/wasm2wat fib.wasm -o fib.wast
$ cat fib.wast | grep "(import"
(import "env" "abort" (func (;0;) (type 2)))
那么问题来了,WebAssembly 可以导入外部函数,在调用的时候,提供了额外的功能,事实上,著名的 WASI 就是基于 import 功能实现的,后面我们会看到 import 可以用来实现更多基于 WebAssembly 的区块链虚拟机不可能实现的有趣功能。
现在,让我们尝试一个 abort 执行,来修复报错:
$ cat main.c
#include <stdio.h>
#include <stdlib.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(int argc, char** argv) {
if (argc < 2) return 2;
u8 x = atoi(argv[1]);
Z_envZ_abortZ_viiii = &env_abort;
init();
u8 result = Z_fibZ_ii(x);
return result;
}
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
(docker) $ cd /code
(docker) $ riscv64-unknown-elf-gcc -o fib_riscv64 -O3 -g main.c fib.c /code/wabt/wasm2c/wasm-rt-impl.c -I /code/wabt/wasm2c
(docker) $ exit
当然,您可以在 CKB 上测试已编译好的 fib_riscv64 程序。但是,有一个小技巧,在测试套件中有一个简单的 CKB-VM 二进制文件,我们可以使用它来运行这个特定的程序。值得一提的是,这个 CKB-VM 二进制文件的工作方式与 CKB 中的 VM 略有不同。在当前示例中测试 WebAssembly 程序已经足够了。但是为了测试正确的 CKB 脚本,您可能希望使用新构建的独立调试器,它遵循所有 CKB 的语义。后面的文章将解释调试器是如何工作的。
让我们在测试套件中编译二进制文件并运行程序:
$ git clone --recursive https://github.com/nervosnetwork/ckb-vm-test-suite
$ cd ckb-vm-test-suite
$ git clone https://github.com/nervosnetwork/ckb-vm
$ cd binary
$ cargo build --release
$ cd ../..
$ ckb-vm-test-suite/binary/target/release/asm64 fib_riscv64 5
Error result: Ok(8)
$ ckb-vm-test-suite/binary/target/release/asm64 fib_riscv64 10
Error result: Ok(89)
这里的报错稍微有点误导,二进制将把程序中的任何非零结果都视为错误。由于测试的程序返回斐波那契计算结果作为返回值,二进制会把返回值(很可能不是零)视为错误,但是我们可以看到实际的错误值包含正确的斐波那契值。
现在我们证明 AssemblyScript 程序确实可以在 CKB-VM 上工作!我确信更复杂的程序可能会遇到需要单独调整的错误,但是您已经了解了整个流程,并且知道在发生错误时应该去哪里查找