从eos v1.3.0更新开始,eosio提倡使用eosio.cdt工具来编译智能合约,以提高智能合约的执行性能等。其中相较于旧的工具,eosio.cdt更新了不少语法。
定义的合约类,数据表,对外动作接口格式有所变化。
具体可以参考eosio.cdt/exmaples/abigen_test中的例子。
CONTRACT hello : public eosio::contract { ... };
ACTION hi (name user) { ... }
TABLE testtable {
uint64_t owner;
uint64_t third;
uint64_t primary_key() const { return owner; }
};
capi_name取代了以前所有uint64_t的别名,如:
实现完善了不少,使得代币相关的实现更加严谨。
默认初始化的asset内的symbol为空,值为零。在作asset比较和赋值的时候,一不小新就经常遇到符号错误。在智能合约编写过程中,应该指定所设计的代码的符号与精度。初始值的asset,可以写成这样:
#define XXX_TOKEN_SYMBOL 'SYS'
#define XXX_TOKEN_PRICISION 4
eosio::asset asswecan(0, eosio::symbol(XXX_TOKEN_SYMBOL, XXX_TOKEN_PRICISION));
打印asset时,因为目前仍唯有to_string之类的相关函数,只能使用asset::print来打印。
新的contract构造函数定义变化挺大的。
// action的接受者
// action的所属域code
// action的参数字节流对象,即便有参数,一般情况下也为空.
contract(
name receiver,
name code,
datastream<const char*> ds
)
: _self(receiver),_code(code),_ds(ds)
{}
数据流,在EOS中但凡涉及到trx或action的数据存取,都会用到datastream相关的接口。
从实现上来看,datastream也不会很复杂,从某种程度上来说,他就是重载了<<和>>操作符的handle类。
其中,重载了多个<<和>>操作符,主要为不同类型的transaction、action参数的序列化与反序列化提供支持。
template<typename DataStream, typename... Args>
DataStream& operator<<( DataStream& ds, const std::tuple<Args...>& t ) {
boost::fusion::for_each( t, [&]( const auto& i ) {
ds << i;
});
return ds;
}
template<typename DataStream, typename... Args>
DataStream& operator>>( DataStream& ds, std::tuple<Args...>& t ) {
boost::fusion::for_each( t, [&]( auto& i ) {
ds >> i;
});
return ds;
}
为什么对持久化存储的multi_index和singleton的数据结构进行拓展时,一不小心就会出现read错误?这里给出了答案。
也正因如此,EOS系统合约中,有不少用于持久化存储的数据结构预备了多个reserved字段。
inline bool read( char* d, size_t s ) {
eosio_assert( size_t(_end - _pos) >= (size_t)s, "read" );
memcpy( d, _pos, s );
_pos += s;
return true;
}
由衷地叹服eosio技术大佬们的c++功力。
从代码实现上禁止用户序列化指针与隔离原语类型模板定义。
template<typename DataStream, typename T, std::enable_if_t<_datastream_detail::is_pointer<T>()>* = nullptr>
DataStream& operator >> ( DataStream& ds, T ) {
static_assert(!_datastream_detail::is_pointer<T>(), "Pointers should not be serialized" );
return ds;
}
template<typename DataStream, typename T, std::size_t N,
std::enable_if_t<!_datastream_detail::is_primitive<T>() &&
!_datastream_detail::is_pointer<T>()>* = nullptr>
DataStream& operator << ( DataStream& ds, const T (&v)[N] ) {
ds << unsigned_int( N );
for( uint32_t i = 0; i < N; ++i )
ds << v[i];
return ds;
}
action分发相关的实现都在这里。
eosio.cdt不再使用宏EOSIO_ABI了,改为使用EOSIO_DISPATCH。
#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;
// Helper macro for EOSIO_DISPATCH
#define EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
BOOST_PP_SEQ_FOR_EACH( EOSIO_DISPATCH_INTERNAL, TYPE, MEMBERS )
#define EOSIO_DISPATCH( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
// 只接受自己的action,其他人发来的receipt不会再接收
if( code == receiver ) { \
switch( action ) { \
EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
} \
wabt虚拟机是如何分发action的呢?这里可以略窥其一二。
template<typename Contract, typename FirstAction>
bool dispatch( uint64_t code, uint64_t act ) {
if( code == FirstAction::get_account() && FirstAction::get_name() == act ) {
Contract().on( unpack_action_data<FirstAction>() );
return true;
}
return false;
}
template<typename Contract, typename FirstAction, typename SecondAction, typename... Actions>
bool dispatch( uint64_t code, uint64_t act ) {
if( code == FirstAction::get_account() && FirstAction::get_name() == act ) {
Contract().on( unpack_action_data<FirstAction>() );
return true;
}
return eosio::dispatch<Contract,SecondAction,Actions...>( code, act );
}
template<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 exceptions
constexpr 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... );
};
// 此处会调用f2(args...)
boost::mp11::tuple_apply( f2, args );
if ( max_stack_buffer_size < size ) {
free(buffer);
}
return true;
}
template<name::raw TableName, typename T, typename... Indices>
class multi_index;
因为在定义multi_index时候,TableName需要使用常量表达式,所以现在一般的写法是
// N("testtab") 变为 "testtab"_n
typedef eosio::multi_index< "testtab"_n, abitest::testtable > testtable_t;
也是实际上也是一个multi_index,是一个不对外声明的multi_index。在想要用到map, set, tuple等数据结构存储持久化数据,而又不必要对外公开该数据记录,笔者认为采用singleton未尝不是一个好的选择。
template<name::raw SingletonName, typename T>
class singleton
{
constexpr static uint64_t pk_value = static_cast<uint64_t>(SingletonName);
struct row {
T value;
uint64_t primary_key() const { return pk_value; }
EOSLIB_SERIALIZE( row, (value) )
};
typedef eosio::multi_index<SingletonName, row> table;
public:
singleton( name code, uint64_t scope ) : _t( code, scope ) {}
// ...
private:
table _t;
}
接口相较于旧版本并无太大改动,只是其中的发送延迟交易的接口添加了一个字段。
sender_id 字面上意思是发送者的ID,但结合其他源码的分析,这实际上是需要指定一个唯一的交易ID,只是通常的做法是把这个交易ID指定为发送者的用户ID。
用户ID是uint64_t,sender_id为uint128_t,static_cast类型转换一下就可以了。
void send_deferred(const uint128_t& sender_id, capi_name payer, const char *serialized_transaction, size_t size, uint32_t replace_existing = 0);
class transaction : public transaction_header {
public:
// ...
void send(const uint128_t& sender_id, name payer, bool replace_existing = false) const {
auto serialize = pack(*this);
send_deferred(sender_id, payer.value, serialize.data(), serialize.size(), replace_existing);
}
// ...
};