什么是wasm虚拟机
在区块链系统开中,目前主流的虚拟机有两大类
- 以太坊虚拟机:开发语言是特殊定制的solidity
- wasm虚拟机:开发语言可以是C++,Rest等,通俗讲就是可以编译成wasm目标格式的语言都可以
在目前的区块链系统中,有的系统支持以太坊虚拟久,有些支持wasm虚拟机,也有系统两种虚拟机都支持的。
有些系统还有另外的虚拟实现,如迅雷链早期就是用过c++与lua实现过,后面更新成wasm虚拟机了。
Wasm虚拟机是现代区块链系统开发中的首选虚拟机,它相比其他虚拟机具有更高的安全性,更好的执行效率,同时合约语言的选择也大大的增加了,可以选择C++,也可以选择Rust,后续还可以选择wasm支持的其他语言。
那么Wasm到底是什么呢?WebAssembly简称WASM,它是可以运行在Web浏览器中的一种可移植、体积小、加载快的编码格式,Wasm 被设计为可供类似C/C++/Rust等高级语言的平台编译目标。
简单来说,在区块链中Wasm是系统运行智能合约的沙盒
关于Wasm更多的详细信息可以参考如下链接:
Wasm官网
EOSIO中Wasm的初始化
在EOSIO中,Wasm的初始化,是在wasm_interface类中完成的,核心代码如下
wasm_interface_impl(wasm_interface::vm_type vm, bool eosvmoc_tierup, const chainbase::database& d, const boost::filesystem::path data_dir, const eosvmoc::config& eosvmoc_config) : db(d), wasm_runtime_time(vm) {
#ifdef EOSIO_EOS_VM_RUNTIME_ENABLED
if(vm == wasm_interface::vm_type::eos_vm)
runtime_interface = std::make_unique<webassembly::eos_vm_runtime::eos_vm_runtime<eosio::vm::interpreter>>();
#endif
}
注意:在C++中,Wasm的运行使用的是wbat开发库
这段代码中,Wasm的初始化实在构造函数中实现的,在wasm_interface类中,除了实例化runtime_interface对象,同时还建立了code的缓存列表,在合约调用时,可以直接读取code句柄。
当然整个Wasm的初始化肯定不止这么一点,在EOSIO中最核心的初始化,是虚拟机与主机之间调用API的初始化,虚拟机要通过主机提供的接口,访问链上的数据,具体过程如下
// 主机提供给虚拟机访问合约action数据的接口,虚拟机就是通过read_action_data读取了action的二进制数据,然后在虚拟机中反序列化后,最终传递给具体的action执行函数
REGISTER_INTRINSICS(action_api,
(read_action_data, int(int, int) )
(action_data_size, int() )
(current_receiver, int64_t() )
);
// 主机提供的console日志输出API
REGISTER_INTRINSICS(console_api,
(prints, void(int) )
(prints_l, void(int, int) )
(printi, void(int64_t) )
(printui, void(int64_t) )
(printi128, void(int) )
(printui128, void(int) )
(printsf, void(float) )
(printdf, void(double) )
(printqf, void(int) )
(printn, void(int64_t) )
(printhex, void(int, int) )
);
在EOSIO中,除了上面的两组接口,还提供了以下一系列接口
- compiler_builtins:内置操作运算API
- privileged_api,producer_api:生产控制相关高级API
- transaction_context:交易运行超时相关API
- database_api:数据库操作相关API,我们在合约开发中实际调用最多的API,合约通过这一类API访问chainbase数据库
- crypto_api:加密安全相关API
- context_free_system_api:断言相关API,在合约中断言失败,会中断合约的运行
- softfloat_api:浮点运算相关的API,因为区块链运行与不同的平台,如ubuntu,Mac等,所以在wasm中运行的合约特别是服点运算这一块,必须要精度一致
如后续我们要在EOSIO上进行二次开发,扩展我们自己的API,就可以在这里增加我们的开发
那么他具体是如何初始化的呢? 连续展开宏REGISTER_INTRINSICS,我们可以得到如下结果
#define REGISTER_INTRINSICS(CLS, MEMBERS)\
BOOST_PP_SEQ_FOR_EACH(_REGISTER_INTRINSIC, CLS, _WRAPPED_SEQ(MEMBERS))
#define _REGISTER_INTRINSIC_EXPLICIT(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)\
_REGISTER_WAVM_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG) \
_REGISTER_WABT_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG) \
_REGISTER_EOS_VM_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG) \
_REGISTER_EOSVMOC_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)
#define _REGISTER_WABT_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)\
static eosio::chain::webassembly::wabt_runtime::intrinsic_registrator _INTRINSIC_NAME(__wabt_intrinsic_fn, __COUNTER__) (\
MOD,\
NAME,\
eosio::chain::webassembly::wabt_runtime::wabt_function_type_provider<WASM_SIG>::type(),\
eosio::chain::webassembly::wabt_runtime::intrinsic_function_invoker_wrapper<SIG>::type::fn<&CLS::METHOD>()\
);\
在这里很清晰的展示出,生成名为_INTRINSIC_NAME静态类,并将对应的函数函数回调注入此类,intrinsic_registrator的结构如下
struct intrinsic_registrator {
using intrinsic_fn = TypedValue(*)(wabt_apply_instance_vars&, const TypedValues&);
struct intrinsic_func_info {
FuncSignature sig;
intrinsic_fn func;
};
static auto& get_map(){
static map<string, map<string, intrinsic_func_info>> _map;
return _map;
};
intrinsic_registrator(const char* mod, const char* name, const FuncSignature& sig, intrinsic_fn fn) {
get_map()[string(mod)][string(name)] = intrinsic_func_info{
sig, fn};
}
};
注册的最终结果将宿主函数相关信息存入结构变量_map中。
至此,在EOSIO中,Wasm虚拟机最重要的初始模块完成,接下来等待合约调用既可以。
职能合约调用
在前文中讲到,职能合约最终会调入Wasm虚拟机中,入口如下:
control.get_wasm_interface().apply( receiver_account->code_hash, receiver_account->vm_type, receiver_account->vm_version, *this );
接下来我们进入相关函数,一步步看合约action最终是怎么被调用的
// 调用wasm_interface::apply函数
void wasm_interface::apply( const digest_type& code_hash, const uint8_t& vm_type, const uint8_t& vm_version, apply_context& context ) {
// 函数get_instantiated_module根据code_hash,vm_type,vm_version获取正确的code,并且加载进wasm虚拟机中,在加载的过程中会对对应的code进行验证,验证其是否合法,最后成功加载完成之后,会将对应的句柄加入cache,方便下次调用
// 获取对应的code的instance之后,调用apply函数
my->get_instantiated_module(code_hash, vm_type, vm_version, context.trx_context)->apply(context);
}
接下来我们进入apply函数,继续细看
// 进入wabt_instantiated_module类的apply函数
void apply(apply_context& context) override {
//reset mutable globals
for(const auto& mg : _initial_globals)
mg.first->typed_value = mg.second;
// 注意context变量的保存,最终保存在静态变量static_wabt_vars
// 想想前面宿主函数的形式,是不是也和context变量关联
wabt_apply_instance_vars this_run_vars{
nullptr, context};
static_wabt_vars = &this_run_vars;
//reset memory to inital size & copy back in initial data
//这里主要初始化调用时内存信息的相关数据
if(_env->GetMemoryCount()) {
Memory* memory = this_run_vars.memory = _env->GetMemory(0);
memory->page_limits = _initial_memory_configuration;
memory->data.resize(_initial_memory_configuration.initial * WABT_PAGE_SIZE);
memcpy(memory->data.data(), _initial_memory.data(), _initial_memory.size());
memset(memory->data.data() + _initial_memory.size(), 0, memory->data.size() - _initial_memory.size());
}
// 传递最终调用的函数参数,三个参数都是uint64
// 这里为什么参数类型都是uint64,是因为在传递中uint64是可以直接传递给wasm的,如果是字符串是需要通过内存拷贝进入的,具体参看action中data参数的传递,使用read_action_data函数完成
// 因为name与uint64是可以相互转化的,所以可以这样使用
_params[0].set_i64(context.get_receiver().to_uint64_t());
_params[1].set_i64(context.get_action().account.to_uint64_t());
_params[2].set_i64(context.get_action().name.to_uint64_t());
ExecResult res = _executor.RunStartFunction(_instatiated_module);
EOS_ASSERT( res.result == interp::Result::Ok, wasm_execution_error, "wabt start function failure (${s})", ("s", ResultToString(res.result)) );
// 最终调用wasm合约导出函数apply
// 这里产生一个疑问,我们在写合约时并没有看到apply函数,我们观察cdt的中关于eosio的库,也没有apply函数, 那么apply函数是怎么出现的,合约编译时又有哪些趣事呢,我们后面再讲
res = _executor.RunExportByName(_instatiated_module, "apply", _params);
EOS_ASSERT( res.result == interp::Result::Ok, wasm_execution_error, "wabt execution failure (${s})", ("s", ResultToString(res.result)) );
}
至此合约调用,真正进入了wasm虚拟机中,再进入就需要大家细读wbat的代码了。
关于合约对宿主函数的调用
我们在分析宿主函数的初始化时,注意到,宿主函数最终注册到了一个map结构中,而且宿主函数都和apply_context类型,那么合约对宿主函数的调用,肯定和这些相关。最后根据跟踪我们发现,调用代码如下
template<>
struct intrinsic_invoker_impl<void_type, std::tuple<>> {
using next_method_type = void_type (*)(wabt_apply_instance_vars&, const TypedValues&, int);
template<next_method_type Method>
static TypedValue invoke(wabt_apply_instance_vars& vars, const TypedValues& args) {
Method(vars, args, args.size() - 1);
return TypedValue(Type::Void);
}
template<next_method_type Method>
static const auto fn() {
return invoke<Method>;
}
};
在这里定义了好些个intrinsic_invoker_impl实现,他们分别对应不同的参数,也对应不同的调用,最终宿主函数的调用会进入apply_context类,进行对应的业务处理。
总结
总的来说,在EOSIO中合约的运行相对来说是透明的,它借助了wasm虚拟机,如果我们继续深入,就需要进入wbat的源代码去看,我们的目的是了解wasm在eosio中的使用,我们可以在上面进行二次开发,进而可以开发属于我们自己的区块链系统。
上面的知识点,我们核心可以归结为以下几点:
- wasm在初始化时,最核心的是初始化宿主函数,也就是提供合约调用链相关的API
- 合约最终的调用入口是合约导出函数apply
- 在链上apply_context,wasm_interface是和wasm虚拟机打交道最深的两个类,一个提供初始化接口,一个负责具体的业务逻辑
- apply函数的参数是三个uint64类型的数值,在合约中会和name类互转
- action中data的数值是通过read_action_data宿主函数传递的,而后在合约中反序列化为具体的参数
扩展总结
如果我们要对EOSIO进行二次开发,通过这篇文章的理解,我们可以思考如下的改动
- 增加属于我们的宿主函数
- 如果我们在熟悉CDT源代码,改变action中data的序列化规则
- 在CDT中开发封装属于我们自己的合约库
- 甚至模拟以太坊的合约事件机制,为eosio增加我们自己的事件
同时我们也引入了新的问题
- 在wasm格式的文件中,导出函数是个啥?
- 在eosio中,apply导出函数是怎么出现的
这些是需要我们继续细读CDT的代码,并且了解wasm文件相关信息才能解答的。