@Fangzheng1992
2018-11-14T09:55:37.000000Z
字数 5030
阅读 1250
EOS底层使用了Wasm(WebAssembly)技术,所以为了更好的理解EOS ABI,这里先对Wasm虚拟机进行一些介绍。Wasm这个词实际上有多重含义,既可以指W3C发布的Wasm规范,也可以指Wasm二进制文件(由Binaryen等编译器生成,以.wasm为后缀),还可以指Wasm虚拟机。本文出现的Wasm主要指Wasm虚拟机或者二进制格式,具体因上下文而异。下面列出Wasm虚拟机的一些要点,更详细的信息可以从Wasm规范获取:
Wasm虚拟机只支持四种内置类型:i32(32比特整数)、i64(64比特整数)、f32(32比特浮点数)、f64(64比特浮点数)。其他比如布尔、字符串、结构体、数组、指针等类型需要由编译器提供支持。
Wasm虚拟机能够操作的存储空间主要包括三部分:
i32.const、 i32.add、 i32.sub、 drop等。memory.grow 使可访问内存增加一页memory.size 把当前内存字节数推入栈顶load系列指令(比如i32.load)把内存数据载入栈顶store 系列指令(比如i32.store)把栈顶数据写回内存get_global 获取指定索引处的全局变量值,并推入栈顶set_global 从栈顶弹出一个值,并用它设置指定索引处的全局变量EOS智能合约使用C/C++语言编写,可以使用EOS提供的CDT(Contract Development Toolkit)对合约进行编译。以EOS开发文档里的hello合约为例:
#include <eosiolib/eosio.hpp>#include <eosiolib/print.hpp>using namespace eosio;class hello : public contract {public:using contract::contract;[[eosio::action]]void hi( name user ) {print( "Hello, ", name{user});}};EOSIO_DISPATCH( hello, (hi))
使用eosio-cpp -o hello.wasm hello.cpp --abigen命令可以编译出hello.wasm和hello.abi文件。下面是hello.abi文件的内容:
{"____comment": "This file was generated with eosio-abigen. DO NOT EDIT Wed Nov 14 14:20:08 2018","version": "eosio::abi/1.0","structs": [{"name": "hi","base": "","fields": [{"name": "user","type": "name"}]}],"types": [],"actions": [{"name": "hi","type": "hi","ricardian_contract": ""}],"tables": [],"ricardian_clauses": [],"abi_extensions": []}
从上面的信息可以看出:
那么EOS智能合约到底是怎么开始执行的呢?下面来详细讨论一下。
由ABI文档可知,每一个EOS智能合约都必须提供一个叫做apply的action handler,这个apply先接收到action,然后再把action分发给具体的handler进行处理。上面的hello合约并没有直接定义这个apply handler,那么必然是EOSIO_DISPATCH帮我们干了这件事。我们可以从eosio.cdt的源代码中找到这个宏,具体在dispatcher.hpp文件里,下面是它的代码:
// dispatcher.hpp#L123#define EOSIO_DISPATCH( TYPE, MEMBERS ) \extern "C" { \void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \if( code == receiver ) { \switch( action ) { \EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \} \/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \} \} \} \
可见apply()函数的确是由这个宏生成的。虽然并不能完全理解这段代码的含义,但大致也可以看出是根据code和action参数去选择具体的handler。我们接着看一下EOSIO_DISPATCH_HELPER这个宏:
// dispatcher.hpp#L102// Helper macro for EOSIO_DISPATCH#define EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \BOOST_PP_SEQ_FOR_EACH( EOSIO_DISPATCH_INTERNAL, TYPE, MEMBERS )
这个宏又用了EOSIO_DISPATCH_INTERNAL宏,只好顺藤摸瓜继续看:
// dispatcher.hpp#L96// Helper macro for EOSIO_DISPATCH_INTERNAL#define EOSIO_DISPATCH_INTERNAL( r, OP, elem ) \case eosio::name( BOOST_PP_STRINGIZE(elem) ).value: \eosio::execute_action( eosio::name(receiver), eosio::name(code), &OP::elem ); \break;
可见最后调用的是execute_action()函数,为了更清楚的了解事实,我们可以使用-E选项(告诉eosio-cpp编译器仅进行预处理)重新编译hello合约。下面是预处理之后的apply()方法代码:
extern "C" {void apply( uint64_t receiver, uint64_t code, uint64_t action ) {if( code == receiver ) {switch( action ) {case eosio::name( "hi" ).value:eosio::execute_action(eosio::name(receiver),eosio::name(code),&hello::hi );break;}}}}
预处理器生成的apply()方法基本上跟我们理解的一致,接下来看一下execute_action()方法。
下面是execute_action()的代码,这是一个模版方法:
// dispatcher.hpp#L65template<typename T, typename... Args>bool execute_action( name self, name code, void (T::*func)(Args...) ) {size_t size = action_data_size();//using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptionsconstexpr size_t max_stack_buffer_size = 512;void* buffer = nullptr;if( size > 0 ) {buffer = max_stack_buffer_size < size ? malloc(size) : alloca(size);read_action_data( buffer, size );}std::tuple<std::decay_t<Args>...> args;datastream<const char*> ds((char*)buffer, size);ds >> args;T inst(self, code, ds);auto f2 = [&]( auto... a ){((&inst)->*func)( a... );};boost::mp11::tuple_apply( f2, args );if ( max_stack_buffer_size < size ) {free(buffer);}return true;}
虽然代码比较难懂,但还是可以看出,execute_action()函数调用了action_data_size()函数获取action数据的字节数,还调用了read_action_data()函数读取action数据。从action.h头文件中可以找到这两个函数的声明:
#pragma once#include <eosiolib/system.h>extern "C" {uint32_t read_action_data( void* msg, uint32_t len );uint32_t action_data_size();... // 其他函数省略}
我们用Wagon提供的wasm-dump工具查看hello合约编译出来的hello.wasm,由import段可以确认上面两个函数的确是由外部提供的,也就是说是由EOS的Wasm实现提供的:
import:- function[0] sig=1 <- env.action_data_size- function[1] sig=2 <- env.read_action_data- function[2] sig=3 <- env.eosio_assert- function[3] sig=4 <- env.memcpy- function[4] sig=5 <- env.prints- function[5] sig=6 <- env.printn- function[6] sig=3 <- env.set_blockchain_parameters_packed- function[7] sig=2 <- env.get_blockchain_parameters_packed- function[8] sig=4 <- env.memset
另外从export段也可以看到,的确有apply()函数:
export:- global[2] -> "__data_end"- global[1] -> "__heap_base"- function[10] -> "apply"- memory[0] -> "memory"
到这里基本上可以得出结论,EOS智能合约的action数据是由合约字节码通过外部函数读入的,而且合约字节码也已经包含了action数据的解码逻辑,ABI仅仅在编码action数据时使用。
eosio-cpp编译EOS智能合约时,可以通过--abi选项生成合约的ABI描述文件;ABI描述采用JSON格式。apply()入口函数,这个函数使用一个switch-case语句进行action分派,可以通过EOS提供的EOSIO_DISPATCH宏生成。eosio::execute_action()函数实现,这个函数通过两个外部函数读入action数据,然后进行数据解码,最后调用具体的handler函数。