Merge pull request #353 from AntelopeIO/call_abi

SC: Generate `call` and `call_results` sections in ABI files for sync calls and bump ABI version to `1.3`
This commit is contained in:
Lin Huang
2025-07-09 17:00:01 -04:00
committed by GitHub
28 changed files with 413 additions and 35 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"antelope-spring-dev":{
"target":"return_status",
"target":"sync_call",
"prerelease":false
}
}
-21
View File
@@ -126,27 +126,6 @@ BOOST_AUTO_TEST_CASE(void_func_test) { try {
check(trx_trace);
} FC_LOG_AND_RETHROW() }
// Verify a function tagged as both `action` and `call` works
BOOST_AUTO_TEST_CASE(mixed_action_call_tags_test) { try {
call_tester t({
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
// `sum` in `callee` contract is tagged as `action` and `call`
// Make sure we can make a sync call to `sum` (`mulparamtest` in `caller` does
// a sync call to `sum`)
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstmulprmtst"_n, "caller"_n, {}));
// Make sure we can push an action using `sum`.
BOOST_REQUIRE_NO_THROW(t.push_action("callee"_n, "sum"_n, "callee"_n,
mvo()
("a", 1)
("b", 2)
("c", 3)));
} FC_LOG_AND_RETHROW() }
// Verify the receiver contract with only one sync call function works
// (for testing the sync_call entry point dispatcher)
BOOST_AUTO_TEST_CASE(single_function_test) { try {
@@ -1,6 +1,7 @@
{
"tests" : [
{
"compile_flags": ["--abi-version=1.2"],
"expected" : {
"abi-file" : "action_results_test.abi"
}
@@ -1,6 +1,7 @@
{
"tests": [
{
"compile_flags": ["--abi-version=1.2"],
"expected": {
"abi-file": "aliased_type_variant_template_arg.abi"
}
@@ -1,6 +1,7 @@
{
"tests" : [
{
"compile_flags": ["--abi-version=1.2"],
"expected" : {
"abi-file" : "nested_container.abi"
}
@@ -1,7 +1,7 @@
{
"tests" : [
{
"compile_flags" : ["-R={cwd}"],
"compile_flags" : ["--abi-version=1.2", "-R={cwd}"],
"expected" : {
"abi-file" : "ricardian_contract_test.abi"
}
@@ -1,10 +1,10 @@
{
"tests" : [
{
"compile_flags": ["--abi-version=1.2"],
"expected" : {
"abi-file" : "singleton_contract.abi"
}
}
]
}
@@ -1,6 +1,7 @@
{
"tests": [
{
"compile_flags": ["--abi-version=1.2"],
"expected": {
"abi-file": "struct_base_typedefd.abi"
}
@@ -0,0 +1,114 @@
{
"____comment": "This file was generated with eosio-abigen. DO NOT EDIT ",
"version": "eosio::abi/1.3",
"types": [],
"structs": [
{
"name": "call_data_header",
"base": "",
"fields": [
{
"name": "version",
"type": "uint32"
},
{
"name": "func_name",
"type": "uint64"
}
]
},
{
"name": "noparam",
"base": "",
"fields": [
{
"name": "header",
"type": "call_data_header"
}
]
},
{
"name": "sum",
"base": "",
"fields": [
{
"name": "header",
"type": "call_data_header"
},
{
"name": "a",
"type": "uint32"
},
{
"name": "b",
"type": "uint32"
},
{
"name": "c",
"type": "uint32"
}
]
},
{
"name": "voidfunc",
"base": "",
"fields": [
{
"name": "header",
"type": "call_data_header"
}
]
},
{
"name": "withparam",
"base": "",
"fields": [
{
"name": "header",
"type": "call_data_header"
},
{
"name": "in",
"type": "uint32"
}
]
}
],
"actions": [],
"tables": [],
"ricardian_clauses": [],
"variants": [],
"action_results": [],
"calls": [
{
"name": "noparam",
"type": "noparam"
},
{
"name": "sum",
"type": "sum"
},
{
"name": "voidfunc",
"type": "voidfunc"
},
{
"name": "withparam",
"type": "withparam"
}
],
"call_results": [
{
"name": "noparam",
"result_type": "uint32"
},
{
"name": "sum",
"result_type": "uint32"
},
{
"name": "withparam",
"result_type": "uint32"
}
]
}
@@ -0,0 +1,27 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
class [[eosio::contract]] sync_call_test : public eosio::contract {
public:
using contract::contract;
[[eosio::call]]
uint32_t noparam() {
return 10;
}
[[eosio::call]]
uint32_t withparam(uint32_t in) {
return in;
}
[[eosio::call]]
void voidfunc() {
int i = 10;
}
[[eosio::call]]
uint32_t sum(uint32_t a, uint32_t b, uint32_t c) {
return a + b + c;
}
};
@@ -0,0 +1,9 @@
{
"tests" : [
{
"expected" : {
"abi-file" : "sync_call_test.abi"
}
}
]
}
@@ -1,6 +1,7 @@
{
"tests" : [
{
"compile_flags": ["--abi-version=1.2"],
"expected" : {
"abi-file" : "tagged_number_test.abi"
}
@@ -1,6 +1,7 @@
{
"tests" : [
{
"compile_flags": ["--abi-version=1.2"],
"expected" : {
"abi-file" : "using_std_array.abi"
}
@@ -0,0 +1,19 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
// Test the validation that action and call tags cannot be mixed.
class [[eosio::contract]] invalid_call_tags : public eosio::contract{
public:
using contract::contract;
[[eosio::action, eosio::call]]
uint32_t act1() {
return 0;
}
[[eosio::call, eosio::action]]
uint32_t act2() {
return 0;
}
};
@@ -0,0 +1,11 @@
{
"tests" : [
{
"compile_flags": [],
"expected" : {
"exit-code": 255,
"stderr": "codegen error (cannot be tagged as both action and call)"
}
}
]
}
@@ -0,0 +1,17 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
// Test the validation that action and call tags cannot be mixed.
class [[eosio::contract]] invalid_call_tags : public eosio::contract{
public:
using contract::contract;
[[eosio::action]]
uint32_t act();
};
[[eosio::call]]
uint32_t invalid_call_tags::act() {
return 0;
}
@@ -0,0 +1,11 @@
{
"tests" : [
{
"compile_flags": [],
"expected" : {
"exit-code": 255,
"stderr": "codegen error (cannot be tagged as both action and call)"
}
}
]
}
@@ -0,0 +1,17 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
// Test the validation that action and call tags cannot be mixed.
class [[eosio::contract]] invalid_call_tags : public eosio::contract{
public:
using contract::contract;
[[eosio::call]]
uint32_t act();
};
[[eosio::action]]
uint32_t invalid_call_tags::act() {
return 0;
}
@@ -0,0 +1,11 @@
{
"tests" : [
{
"compile_flags": [],
"expected" : {
"exit-code": 255,
"stderr": "codegen error (cannot be tagged as both action and call)"
}
}
]
}
@@ -16,7 +16,7 @@ void sync_call_callee::voidfunc() {
eosio::print("I am a void function");
}
[[eosio::action, eosio::call]]
[[eosio::call]]
uint32_t sync_call_callee::sum(uint32_t a, uint32_t b, uint32_t c) {
return a + b + c;
}
@@ -26,7 +26,7 @@ public:
[[eosio::call]]
void voidfunc();
[[eosio::action, eosio::call]]
[[eosio::call]]
uint32_t sum(uint32_t a, uint32_t b, uint32_t c);
// pass in a struct and return it
@@ -6,7 +6,7 @@ class [[eosio::contract]] sync_call_single_func : public eosio::contract{
public:
using contract::contract;
[[eosio::action, eosio::call]]
[[eosio::call]]
uint32_t getten() {
return 10;
}
+5 -1
View File
@@ -948,9 +948,13 @@ static Options CreateOptions(bool add_defaults=true) {
#endif
// 1.2 -- action results
// 1.3 -- sync calls, bitset type, action and sync call names in C++
// identifier format
/* TODO add some way of defaulting these to the current highest version */
int abi_version_major = 1;
int abi_version_minor = 2;
int abi_version_minor = 3;
if (!abi_version_opt.empty()) {
abi_version_major = std::stoi(abi_version_opt);
+19
View File
@@ -30,6 +30,12 @@ struct abi_action {
bool operator<(const abi_action& s) const { return name < s.name; }
};
struct abi_call {
std::string name;
std::string type;
bool operator<(const abi_call& s) const { return name < s.name; }
};
struct abi_table {
std::string name;
std::string type;
@@ -61,6 +67,17 @@ struct abi_action_result {
bool operator<(const abi_action_result& ar) const { return name < ar.name; }
};
struct abi_call_result {
std::string name;
std::string type;
bool operator<(const abi_call_result& ar) const { return name < ar.name; }
};
// The version when sync call was first introduced.
constexpr int abi_call_version_major = 1;
constexpr int abi_call_version_minor = 3;
constexpr int abi_call_version = abi_call_version_major * 10 + abi_call_version_minor;
/// From eosio libraries/chain/include/eosio/chain/abi_def.hpp
struct abi {
int version_major = 1;
@@ -69,11 +86,13 @@ struct abi {
std::set<abi_struct> structs;
std::set<abi_typedef> typedefs;
std::set<abi_action> actions;
std::set<abi_call> calls;
std::set<abi_table> tables;
std::set<abi_variant> variants;
std::vector<abi_ricardian_clause_pair> ricardian_clauses;
std::vector<abi_error_message> error_messages;
std::set<abi_action_result> action_results;
std::set<abi_call_result> call_results;
};
inline void dump( const abi& abi ) {
+104 -2
View File
@@ -131,6 +131,43 @@ namespace eosio { namespace cdt {
}
}
void add_call( const clang::CXXRecordDecl* decl ) {
abi_call ret;
auto call_name = decl->getEosioCallAttr()->getName();
if (call_name.empty()) {
validate_name(decl->getName().str(), [&](auto s) { CDT_ERROR("abigen_error", decl->getLocation(), s); });
ret.name = decl->getName().str();
}
else {
validate_name( call_name.str(), [&](auto s) { CDT_ERROR("abigen_error", decl->getLocation(), s); });
ret.name = call_name.str();
}
ret.type = decl->getName().str();
_abi.calls.insert(ret);
}
void add_call( const clang::CXXMethodDecl* decl ) {
abi_call ret;
auto call_name = decl->getEosioCallAttr()->getName();
if (call_name.empty()) {
validate_name( decl->getNameAsString(), [&](auto s) { CDT_ERROR("abigen_error", decl->getLocation(), s); } );
ret.name = decl->getNameAsString();
}
else {
validate_name( call_name.str(), [&](auto s) { CDT_ERROR("abigen_error", decl->getLocation(), s); } );
ret.name = call_name.str();
}
ret.type = decl->getNameAsString();
_abi.calls.insert(ret);
if (translate_type(decl->getReturnType()) != "void") {
add_type(decl->getReturnType());
_abi.call_results.insert({get_call_name(decl), translate_type(decl->getReturnType())});
}
}
void add_tuple(const clang::QualType& type) {
auto pt = llvm::dyn_cast<clang::ElaboratedType>(type.getTypePtr());
auto tst = llvm::dyn_cast<clang::TemplateSpecializationType>((pt) ? pt->desugar().getTypePtr() : type.getTypePtr());
@@ -216,6 +253,21 @@ namespace eosio { namespace cdt {
void add_struct( const clang::CXXMethodDecl* decl ) {
abi_struct new_struct;
new_struct.name = decl->getNameAsString();
if (decl->isEosioCall()) {
// Add call_data_header definition to structs set
abi_struct data_header;
data_header.name = "call_data_header";
if (_abi.structs.count(data_header) == 0) {
data_header.fields.push_back({"version", "uint32"});
data_header.fields.push_back({"func_name", "uint64"});
_abi.structs.insert(data_header);
}
// Add header field as the first field to the method struct
new_struct.fields.push_back({"header", "call_data_header"});
}
for (auto param : decl->parameters() ) {
auto param_type = param->getType().getNonReferenceType().getUnqualifiedType();
new_struct.fields.push_back({param->getNameAsString(), get_type(param_type)});
@@ -551,6 +603,13 @@ namespace eosio { namespace cdt {
return o;
}
ojson call_to_json( const abi_call& c ) {
ojson o;
o["name"] = c.name;
o["type"] = c.type;
return o;
}
ojson clause_to_json( const abi_ricardian_clause_pair& clause ) {
ojson o;
o["id"] = clause.id;
@@ -575,6 +634,13 @@ namespace eosio { namespace cdt {
return o;
}
ojson call_result_to_json( const abi_call_result& result ) {
ojson o;
o["name"] = result.name;
o["result_type"] = result.type;
return o;
}
bool is_empty() {
std::set<abi_table> set_of_tables;
for ( auto t : ctables ) {
@@ -593,7 +659,7 @@ namespace eosio { namespace cdt {
set_of_tables.insert(t);
}
return _abi.structs.empty() && _abi.typedefs.empty() && _abi.actions.empty() && set_of_tables.empty() && _abi.ricardian_clauses.empty() && _abi.variants.empty();
return _abi.structs.empty() && _abi.typedefs.empty() && _abi.actions.empty() && _abi.calls.empty() && set_of_tables.empty() && _abi.ricardian_clauses.empty() && _abi.variants.empty();
}
ojson to_json() {
@@ -658,6 +724,10 @@ namespace eosio { namespace cdt {
if (as.name == _translate_type(a.type))
return true;
}
for ( auto a : _abi.calls ) {
if (as.name == _translate_type(a.type))
return true;
}
for( auto t : set_of_tables ) {
if (as.name == _translate_type(t.type))
return true;
@@ -670,6 +740,10 @@ namespace eosio { namespace cdt {
if (as.name == _translate_type(ar.type))
return true;
}
for( auto ar : _abi.call_results ) {
if (as.name == _translate_type(ar.type))
return true;
}
return false;
};
@@ -695,6 +769,9 @@ namespace eosio { namespace cdt {
for ( auto a : _abi.actions )
if ( a.type == td.new_type_name )
return true;
for ( auto a : _abi.calls )
if ( a.type == td.new_type_name )
return true;
for ( auto _td : _abi.typedefs )
if ( remove_suffix(_td.type) == td.new_type_name )
return true;
@@ -702,6 +779,9 @@ namespace eosio { namespace cdt {
if ( ar.type == td.new_type_name )
return true;
}
for ( auto ar : _abi.call_results )
if ( ar.type == td.new_type_name )
return true;
return false;
};
@@ -719,6 +799,18 @@ namespace eosio { namespace cdt {
for ( auto a : _abi.actions ) {
o["actions"].push_back(action_to_json( a ));
}
if (_abi.version_major > abi_call_version_major ||
_abi.version_major == abi_call_version_major && _abi.version_minor >= abi_call_version_minor) { // sync call
o["calls"] = ojson::array();
for ( auto a : _abi.calls ) {
o["calls"].push_back(call_to_json( a ));
}
o["call_results"] = ojson::array();
for ( auto ar : _abi.call_results ) {
o["call_results"].push_back(call_result_to_json( ar ));
}
}
o["tables"] = ojson::array();
for ( auto t : set_of_tables ) {
o["tables"].push_back(table_to_json( t ));
@@ -778,6 +870,14 @@ namespace eosio { namespace cdt {
ag.add_type( param->getType() );
}
}
if (decl->isEosioCall() && ag.is_eosio_contract(decl, ag.get_contract_name())) {
ag.add_struct(decl);
ag.add_call(decl);
for (auto param : decl->parameters()) {
ag.add_type( param->getType() );
}
}
return true;
}
virtual bool VisitCXXRecordDecl(clang::CXXRecordDecl* decl) {
@@ -786,10 +886,12 @@ namespace eosio { namespace cdt {
ag.add_contracts(ag.parse_contracts());
has_added_clauses = true;
}
if ((decl->isEosioAction() || decl->isEosioTable()) && ag.is_eosio_contract(decl, ag.get_contract_name())) {
if ((decl->isEosioAction() || decl->isEosioCall() || decl->isEosioTable()) && ag.is_eosio_contract(decl, ag.get_contract_name())) {
ag.add_struct(decl);
if (decl->isEosioAction())
ag.add_action(decl);
if (decl->isEosioCall())
ag.add_call(decl);
if (decl->isEosioTable())
ag.add_table(decl);
for (auto field : decl->fields()) {
+25
View File
@@ -37,6 +37,10 @@ class ABIMerger {
if (std::stod(vers.substr(vers.size()-3))*10 >= 12) {
ret["action_results"] = merge_action_results(other);
}
if (std::stod(vers.substr(vers.size()-3))*10 >= abi_call_version) {
ret["calls"] = merge_calls(other);
ret["call_results"] = merge_call_results(other);
}
return ret;
}
private:
@@ -82,6 +86,10 @@ class ABIMerger {
a["type"] == b["type"];
}
static bool call_is_same(ojson a, ojson b) {
return a["name"] == b["name"] &&
a["type"] == b["type"];
}
static bool variant_is_same(ojson a, ojson b) {
for (auto tya : a["types"].array_range()) {
@@ -114,6 +122,11 @@ class ABIMerger {
a["result_type"] == b["result_type"];
}
static bool call_result_is_same(ojson a, ojson b) {
return a["name"] == b["name"] &&
a["result_type"] == b["result_type"];
}
template <typename F>
void add_object_to_array(ojson& ret, ojson a, ojson b, std::string type, std::string id, F&& is_same_func) {
for (auto obj_a : a[type].array_range()) {
@@ -181,6 +194,12 @@ class ABIMerger {
return acts;
}
ojson merge_calls(ojson b) {
ojson calls = ojson::array();
add_object_to_array(calls, abi, b, "calls", "name", call_is_same);
return calls;
}
ojson merge_tables(ojson b) {
ojson tabs = ojson::array();
add_object_to_array(tabs, abi, b, "tables", "name", table_is_same);
@@ -199,6 +218,12 @@ class ABIMerger {
return res;
}
ojson merge_call_results(ojson b) {
ojson res = ojson::array();
add_object_to_array(res, abi, b, "call_results", "name", call_result_is_same);
return res;
}
ojson abi;
};
#pragma GCC diagnostic pop
+6 -4
View File
@@ -317,6 +317,11 @@ namespace eosio { namespace cdt {
static std::set<std::string> _action_set; //used for validations
static std::set<std::string> _notify_set; //used for validations
static std::set<std::string> _call_set; //used for validations
if (decl->isEosioAction() && decl->isEosioCall()) {
CDT_ERROR("codegen_error", decl->getLocation(), "cannot be tagged as both action and call");
}
if (decl->isEosioAction()) {
name = generation_utils::get_action_name(decl);
validate_name(name, [&](auto s) {
@@ -363,10 +368,7 @@ namespace eosio { namespace cdt {
create_notify_dispatch(decl);
cg.notify_handlers.insert(full_notify_name); // insert the method action, so we don't create the dispatcher twice
}
}
// We allow a method to be tagged as both `action` and `call`
if (decl->isEosioCall()) {
} else if (decl->isEosioCall()) {
name = generation_utils::get_call_name(decl);
validate_name(name, [&](auto s) {
CDT_ERROR("codegen_error", decl->getLocation(), std::string("call name (")+s+") is not a valid eosio name");
+6 -1
View File
@@ -192,12 +192,17 @@ struct generation_utils {
return decl->getNameAsString();
}
static inline std::string get_call_name( const clang::CXXMethodDecl* decl ) {
std::string call_name = "";
auto tmp = decl->getEosioCallAttr()->getName();
if (!tmp.empty())
return tmp;
return decl->getNameAsString();
}
static inline std::string get_call_name( const clang::CXXRecordDecl* decl ) {
auto tmp = decl->getEosioCallAttr()->getName();
if (!tmp.empty())
return tmp;
return decl->getName();
}
static inline std::string get_notify_pair( const clang::CXXMethodDecl* decl ) {
std::string notify_pair = "";
auto tmp = decl->getEosioNotifyAttr()->getName();