[关闭]
@Fangzheng1992 2018-11-14T09:55:37.000000Z 字数 5030 阅读 1177

EOS ABI介绍

本文对EOS ABI进行简单介绍。

Wasm介绍

EOS底层使用了Wasm(WebAssembly)技术,所以为了更好的理解EOS ABI,这里先对Wasm虚拟机进行一些介绍。Wasm这个词实际上有多重含义,既可以指W3C发布的Wasm规范,也可以指Wasm二进制文件(由Binaryen等编译器生成,以.wasm为后缀),还可以指Wasm虚拟机。本文出现的Wasm主要指Wasm虚拟机或者二进制格式,具体因上下文而异。下面列出Wasm虚拟机的一些要点,更详细的信息可以从Wasm规范获取:

生成ABI

EOS智能合约使用C/C++语言编写,可以使用EOS提供的CDT(Contract Development Toolkit)对合约进行编译。以EOS开发文档里的hello合约为例:

  1. #include <eosiolib/eosio.hpp>
  2. #include <eosiolib/print.hpp>
  3. using namespace eosio;
  4. class hello : public contract {
  5. public:
  6. using contract::contract;
  7. [[eosio::action]]
  8. void hi( name user ) {
  9. print( "Hello, ", name{user});
  10. }
  11. };
  12. EOSIO_DISPATCH( hello, (hi))

使用eosio-cpp -o hello.wasm hello.cpp --abigen命令可以编译出hello.wasm和hello.abi文件。下面是hello.abi文件的内容:

  1. {
  2. "____comment": "This file was generated with eosio-abigen. DO NOT EDIT Wed Nov 14 14:20:08 2018",
  3. "version": "eosio::abi/1.0",
  4. "structs": [
  5. {
  6. "name": "hi",
  7. "base": "",
  8. "fields": [
  9. {
  10. "name": "user",
  11. "type": "name"
  12. }
  13. ]
  14. }
  15. ],
  16. "types": [],
  17. "actions": [
  18. {
  19. "name": "hi",
  20. "type": "hi",
  21. "ricardian_contract": ""
  22. }
  23. ],
  24. "tables": [],
  25. "ricardian_clauses": [],
  26. "abi_extensions": []
  27. }

从上面的信息可以看出:

那么EOS智能合约到底是怎么开始执行的呢?下面来详细讨论一下。

EOSIO_DISPATCH魔法

ABI文档可知,每一个EOS智能合约都必须提供一个叫做apply的action handler,这个apply先接收到action,然后再把action分发给具体的handler进行处理。上面的hello合约并没有直接定义这个apply handler,那么必然是EOSIO_DISPATCH帮我们干了这件事。我们可以从eosio.cdt的源代码中找到这个宏,具体在dispatcher.hpp文件里,下面是它的代码:

  1. // dispatcher.hpp#L123
  2. #define EOSIO_DISPATCH( TYPE, MEMBERS ) \
  3. extern "C" { \
  4. void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
  5. if( code == receiver ) { \
  6. switch( action ) { \
  7. EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
  8. } \
  9. /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
  10. } \
  11. } \
  12. } \

可见apply()函数的确是由这个宏生成的。虽然并不能完全理解这段代码的含义,但大致也可以看出是根据code和action参数去选择具体的handler。我们接着看一下EOSIO_DISPATCH_HELPER这个宏:

  1. // dispatcher.hpp#L102
  2. // Helper macro for EOSIO_DISPATCH
  3. #define EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
  4. BOOST_PP_SEQ_FOR_EACH( EOSIO_DISPATCH_INTERNAL, TYPE, MEMBERS )

这个宏又用了EOSIO_DISPATCH_INTERNAL宏,只好顺藤摸瓜继续看:

  1. // dispatcher.hpp#L96
  2. // Helper macro for EOSIO_DISPATCH_INTERNAL
  3. #define EOSIO_DISPATCH_INTERNAL( r, OP, elem ) \
  4. case eosio::name( BOOST_PP_STRINGIZE(elem) ).value: \
  5. eosio::execute_action( eosio::name(receiver), eosio::name(code), &OP::elem ); \
  6. break;

可见最后调用的是execute_action()函数,为了更清楚的了解事实,我们可以使用-E选项(告诉eosio-cpp编译器仅进行预处理)重新编译hello合约。下面是预处理之后的apply()方法代码:

  1. extern "C" {
  2. void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
  3. if( code == receiver ) {
  4. switch( action ) {
  5. case eosio::name( "hi" ).value:
  6. eosio::execute_action(
  7. eosio::name(receiver),
  8. eosio::name(code),
  9. &hello::hi );
  10. break;
  11. }
  12. }
  13. }
  14. }

预处理器生成的apply()方法基本上跟我们理解的一致,接下来看一下execute_action()方法。

eosio::execute_action()方法

下面是execute_action()的代码,这是一个模版方法:

  1. // dispatcher.hpp#L65
  2. template<typename T, typename... Args>
  3. bool execute_action( name self, name code, void (T::*func)(Args...) ) {
  4. size_t size = action_data_size();
  5. //using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptions
  6. constexpr size_t max_stack_buffer_size = 512;
  7. void* buffer = nullptr;
  8. if( size > 0 ) {
  9. buffer = max_stack_buffer_size < size ? malloc(size) : alloca(size);
  10. read_action_data( buffer, size );
  11. }
  12. std::tuple<std::decay_t<Args>...> args;
  13. datastream<const char*> ds((char*)buffer, size);
  14. ds >> args;
  15. T inst(self, code, ds);
  16. auto f2 = [&]( auto... a ){
  17. ((&inst)->*func)( a... );
  18. };
  19. boost::mp11::tuple_apply( f2, args );
  20. if ( max_stack_buffer_size < size ) {
  21. free(buffer);
  22. }
  23. return true;
  24. }

虽然代码比较难懂,但还是可以看出,execute_action()函数调用了action_data_size()函数获取action数据的字节数,还调用了read_action_data()函数读取action数据。从action.h头文件中可以找到这两个函数的声明:

  1. #pragma once
  2. #include <eosiolib/system.h>
  3. extern "C" {
  4. uint32_t read_action_data( void* msg, uint32_t len );
  5. uint32_t action_data_size();
  6. ... // 其他函数省略
  7. }

我们用Wagon提供的wasm-dump工具查看hello合约编译出来的hello.wasm,由import段可以确认上面两个函数的确是由外部提供的,也就是说是由EOS的Wasm实现提供的:

  1. import:
  2. - function[0] sig=1 <- env.action_data_size
  3. - function[1] sig=2 <- env.read_action_data
  4. - function[2] sig=3 <- env.eosio_assert
  5. - function[3] sig=4 <- env.memcpy
  6. - function[4] sig=5 <- env.prints
  7. - function[5] sig=6 <- env.printn
  8. - function[6] sig=3 <- env.set_blockchain_parameters_packed
  9. - function[7] sig=2 <- env.get_blockchain_parameters_packed
  10. - function[8] sig=4 <- env.memset

另外从export段也可以看到,的确有apply()函数:

  1. export:
  2. - global[2] -> "__data_end"
  3. - global[1] -> "__heap_base"
  4. - function[10] -> "apply"
  5. - memory[0] -> "memory"

到这里基本上可以得出结论,EOS智能合约的action数据是由合约字节码通过外部函数读入的,而且合约字节码也已经包含了action数据的解码逻辑,ABI仅仅在编码action数据时使用。

总结

  1. 使用eosio-cpp编译EOS智能合约时,可以通过--abi选项生成合约的ABI描述文件;ABI描述采用JSON格式。
  2. EOS智能合约需要一个apply()入口函数,这个函数使用一个switch-case语句进行action分派,可以通过EOS提供的EOSIO_DISPATCH宏生成。
  3. 解码action数据的逻辑由eosio::execute_action()函数实现,这个函数通过两个外部函数读入action数据,然后进行数据解码,最后调用具体的handler函数。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注