diff --git a/cdt-llvm b/cdt-llvm index b919709985..34c0de5404 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit b919709985457410dcc976419f4577886c3617f0 +Subproject commit 34c0de54049689382e3ca28f9defe21d31ed538b diff --git a/libraries/eosiolib/contracts/eosio/contract.hpp b/libraries/eosiolib/contracts/eosio/contract.hpp index d60d47241c..f0ef6e0568 100644 --- a/libraries/eosiolib/contracts/eosio/contract.hpp +++ b/libraries/eosiolib/contracts/eosio/contract.hpp @@ -29,6 +29,12 @@ namespace eosio { */ class contract { public: + enum class exec_type_t : uint8_t { + action, + call, + unknown + }; + /** * Construct a new contract given the contract name * @@ -75,7 +81,27 @@ class contract { */ inline const datastream& get_datastream()const { return _ds; } + /** + * Whether this contract is for a sync call + * + * @return bool - Whether this contract is for a sync call + */ + inline bool is_sync_call()const { + check(_exec_type != exec_type_t::unknown, "too early to call is_sync_call(). _exec_type has not been set yet"); + return (_exec_type == exec_type_t::call); + } + + /** + * Set the exectution type. + * + * @param type - The exectution type to be set. + */ + inline void set_exec_type(exec_type_t type) { + _exec_type = type; + } + protected: + /** * The name of the account this contract is deployed on. */ @@ -90,5 +116,10 @@ class contract { * The datastream for this contract */ datastream _ds = datastream(nullptr, 0); + + /** + * The execution type: action or sync call + */ + exec_type_t _exec_type = exec_type_t::unknown; }; } diff --git a/libraries/eosiolib/contracts/eosio/detail.hpp b/libraries/eosiolib/contracts/eosio/detail.hpp index 889bf496be..6a88af66b0 100644 --- a/libraries/eosiolib/contracts/eosio/detail.hpp +++ b/libraries/eosiolib/contracts/eosio/detail.hpp @@ -46,6 +46,11 @@ namespace eosio { namespace detail { template struct is_same { static constexpr bool value = std::is_integral::value; }; + // Full specialization to resolve ambiguity introduced by partial specializations + // of is_same and is_same + template <> + struct is_same { static constexpr bool value = true; }; + template struct get_nth_impl { static constexpr auto value = get_nth_impl::value; }; diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index be7562ed9c..3e4f0e9d9d 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -140,12 +140,11 @@ BOOST_AUTO_TEST_CASE(mixed_action_call_tags_test) { try { 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, - t.push_action("callee"_n, "sum"_n, "callee"_n, + BOOST_REQUIRE_NO_THROW(t.push_action("callee"_n, "sum"_n, "callee"_n, mvo() ("a", 1) ("b", 2) - ("c", 3)); //); + ("c", 3))); } FC_LOG_AND_RETHROW() } // Verify the receiver contract with only one sync call function works @@ -234,4 +233,43 @@ BOOST_AUTO_TEST_CASE(addr_book_tests) { try { BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "get"_n, "caller"_n, mvo() ("user", "alice"))); } FC_LOG_AND_RETHROW() } +// For a function tagged as both `action` and `call`, verify is_sync_call() +// returns true if tagged as `call` and false if tagged as `action`. +BOOST_AUTO_TEST_CASE(is_sync_call_test) { try { + call_tester t({ + {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, + {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} + }); + + // issynccall() is tagged as both `action` and `call`, and returns + // is_sync_call(). makesynccall() calls issynccall() as a sync call + // and returns its result. So the current action return value must be + // true. + auto trx_trace = t.push_action("caller"_n, "makesynccall"_n, "caller"_n, {}); + auto& action_trace = trx_trace->action_traces[0]; + bool return_value = fc::raw::unpack(action_trace.return_value); + BOOST_REQUIRE(return_value == true); + + // Call issynccall() directly as an action. Since issynccall() + // returns is_sync_call(), the current action return value must be false. + trx_trace = t.push_action("callee"_n, "issynccall"_n, "callee"_n, {}); + auto& action_trace1 = trx_trace->action_traces[0]; + return_value = fc::raw::unpack(action_trace1.return_value); + BOOST_REQUIRE(return_value == false); +} FC_LOG_AND_RETHROW() } + +// Verify customized sync call entry function, without sync call tag +BOOST_AUTO_TEST_CASE(customized_call_entry_func_test1) { try { + call_tester t({{"receiver"_n, contracts::cust_entry_wasm(), contracts::cust_entry_abi().data()}}); + + BOOST_REQUIRE_NO_THROW(t.push_action("receiver"_n, "cusentrytst1"_n, "receiver"_n, {})); +} FC_LOG_AND_RETHROW() } + +// Verify customized sync call entry function, with sync call tag +BOOST_AUTO_TEST_CASE(customized_call_entry_func_test2) { try { + call_tester t({{"receiver"_n, contracts::cust_entry_wasm(), contracts::cust_entry_abi().data()}}); + + BOOST_REQUIRE_NO_THROW(t.push_action("receiver"_n, "cusentrytst2"_n, "receiver"_n, {})); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/integration/contracts.hpp.in b/tests/integration/contracts.hpp.in index dff66add65..4fa067693c 100644 --- a/tests/integration/contracts.hpp.in +++ b/tests/integration/contracts.hpp.in @@ -54,5 +54,7 @@ namespace eosio::testing { static std::vector addr_book_callee_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_callee.abi"); } static std::vector addr_book_caller_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.wasm"); } static std::vector addr_book_caller_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.abi"); } + static std::vector cust_entry_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_cust_entry.wasm"); } + static std::vector cust_entry_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_cust_entry.abi"); } }; } //ns eosio::testing diff --git a/tests/toolchain/build-pass/separate_cpp_hpp.hpp b/tests/toolchain/build-pass/separate_cpp_hpp.hpp index 2002834021..d1685a2858 100644 --- a/tests/toolchain/build-pass/separate_cpp_hpp.hpp +++ b/tests/toolchain/build-pass/separate_cpp_hpp.hpp @@ -1,6 +1,6 @@ #include -class [[eosio::contract]] separate_cpp_hpp : eosio::contract { +class [[eosio::contract]] separate_cpp_hpp : public eosio::contract { public: using contract::contract; diff --git a/tests/unit/test_contracts/CMakeLists.txt b/tests/unit/test_contracts/CMakeLists.txt index f94d236015..feb019b0b1 100644 --- a/tests/unit/test_contracts/CMakeLists.txt +++ b/tests/unit/test_contracts/CMakeLists.txt @@ -19,6 +19,7 @@ add_contract(sync_call_not_supported sync_call_not_supported sync_call_not_suppo add_contract(sync_call_single_func sync_call_single_func sync_call_single_func.cpp) add_contract(sync_call_addr_book_callee sync_call_addr_book_callee sync_call_addr_book_callee.cpp) add_contract(sync_call_addr_book_caller sync_call_addr_book_caller sync_call_addr_book_caller.cpp) +add_contract(sync_call_cust_entry sync_call_cust_entry sync_call_cust_entry.cpp) add_contract(capi_tests capi_tests capi/capi.c capi/action.c capi/chain.c capi/crypto.c capi/db.c capi/permission.c capi/print.c capi/privileged.c capi/system.c capi/transaction.c capi/call.c) diff --git a/tests/unit/test_contracts/sync_call_callee.cpp b/tests/unit/test_contracts/sync_call_callee.cpp index 867f67dd33..883d8a4e72 100644 --- a/tests/unit/test_contracts/sync_call_callee.cpp +++ b/tests/unit/test_contracts/sync_call_callee.cpp @@ -31,3 +31,6 @@ struct1_t sync_call_callee::pass_multi_structs(struct1_t s1, int32_t m, struct2_ return { .a = s1.a * m + s2.c, .b = s1.b * m + s2.d }; } +bool sync_call_callee::issynccall() { + return is_sync_call(); +} diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp index fc867da405..2f5d5d414b 100644 --- a/tests/unit/test_contracts/sync_call_callee.hpp +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -38,6 +38,11 @@ class [[eosio::contract]] sync_call_callee : public eosio::contract{ [[eosio::call]] struct1_t pass_multi_structs(struct1_t s1, int32_t m, struct2_t s2); + // return is_sync_call() + [[eosio::action, eosio::call]] + bool issynccall(); + using issynccall_func = eosio::call_wrapper<"issynccall"_i, &sync_call_callee::issynccall>; + using return_ten_func = eosio::call_wrapper<"return_ten"_i, &sync_call_callee::return_ten>; using echo_input_func = eosio::call_wrapper<"echo_input"_i, &sync_call_callee::echo_input>; using void_func_func = eosio::call_wrapper<"void_func"_i, &sync_call_callee::void_func>; diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index ad7c8d2d33..a97a78b74a 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -172,4 +172,11 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ status = eosio::call("callee"_n, 0, bad_version_data.data(), bad_version_data.size()); eosio::check(status == -10000, "call did not return -10000 for invalid version"); } + + // Call issynccall as a sync call and return its return value + [[eosio::action]] + bool makesynccall() { + sync_call_callee::issynccall_func is_sync_call_func{ "callee"_n }; + return is_sync_call_func(); + } }; diff --git a/tests/unit/test_contracts/sync_call_cust_entry.cpp b/tests/unit/test_contracts/sync_call_cust_entry.cpp new file mode 100644 index 0000000000..08328d41d6 --- /dev/null +++ b/tests/unit/test_contracts/sync_call_cust_entry.cpp @@ -0,0 +1,83 @@ +/* + * This contract is used to verify customized sync_call entry function works. + * It verifies customized sync_call entry is exported, and `call()` host function + * can reach it. + */ + +#include +#include + +namespace eosio { + +class [[eosio::contract]] sync_call_cust_entry : public contract { +public: + using contract::contract; + + [[eosio::action]] + void cusentrytst1() { + // Make a sync call to "receiver"_n account, pass function ID in `data`, + // go to `sync_call()` entry function, and dispatch the call to `get_10()` + // based on the function ID. + const std::vector data{ eosio::pack(1) }; // function ID 1 + eosio::call("receiver"_n, 0, data.data(), data.size()); + + // Retrieve return value and unpack it + std::vector return_value(sizeof(uint32_t)); + eosio::get_call_return_value(return_value.data(), return_value.size()); + + // Verify it + eosio::check(eosio::unpack(return_value) == 10, "return value is not 10"); + } + + [[eosio::action]] + void cusentrytst2() { + // Make a sync call to "receiver"_n account, pass function ID in `data`, + // go to `sync_call()` entry function, and dispatch the call to `get_20()` + // based on the function ID. + const std::vector data{ eosio::pack(2) }; // function ID 2 + eosio::call("receiver"_n, 0, data.data(), data.size()); + + // Retrieve return value and unpack it + std::vector return_value(sizeof(uint32_t)); + eosio::get_call_return_value(return_value.data(), return_value.size()); + + // Verify it + eosio::check(eosio::unpack(return_value) == 20, "return value is not 20"); + } + + // `eosio::call` tag is optional + uint32_t get_10() { + return 10; + } + + [[eosio::call]] + uint32_t get_20() { + return 20; + } +}; +} /// namespace eosio + +extern "C" { + [[eosio::wasm_entry]] + int64_t sync_call(uint64_t sender, uint64_t receiver, uint32_t data_size) { + eosio::datastream ds(nullptr, 0); // for testing, not used + eosio::sync_call_cust_entry obj(eosio::name{receiver}, eosio::name{receiver}, ds); + + std::vector data(sizeof(uint32_t)); + eosio::get_call_data(data.data(), data.size()); + auto func_id = eosio::unpack(data); + + // Dispatch sync calls + uint32_t rv = 0; + switch (func_id) { + case 1: rv = obj.get_10(); break; + case 2: rv = obj.get_20(); break; + default: eosio::check(false, "wrong function ID"); + } + + // set return value + eosio::set_call_return_value(&rv, sizeof(uint32_t)); + + return 0; // return 0 to indicate success + } +} diff --git a/tools/include/eosio/codegen.hpp b/tools/include/eosio/codegen.hpp index 44ed8e0a1f..123ea3c631 100644 --- a/tools/include/eosio/codegen.hpp +++ b/tools/include/eosio/codegen.hpp @@ -146,11 +146,28 @@ namespace eosio { namespace cdt { return ""; } + // Return `true` if the method `decl`'s base class if `eosio::contract` + bool base_is_eosio_contract_class(const clang::CXXMethodDecl* decl) { + auto cxx_decl = decl->getParent(); + // on this point it could be just an attribute so let's check base classes + for (const auto& base : cxx_decl->bases()) { + if (const clang::Type *base_type = base.getType().getTypePtrOrNull()) { + if (const auto* cur_cxx_decl = base_type->getAsCXXRecordDecl()) { + if (cur_cxx_decl->getQualifiedNameAsString() == "eosio::contract") { + return true;; + } + } + } + } + return false; + } + template void create_dispatch(const std::string& attr, const std::string& func_name, F&& get_str, CXXMethodDecl* decl) { constexpr static uint32_t max_stack_size = 512; codegen& cg = codegen::get(); std::string nm = decl->getNameAsString()+"_"+decl->getParent()->getNameAsString(); + if (cg.is_eosio_contract(decl, cg.contract_name)) { ss << "\n\n#include \n"; ss << "#include \n"; @@ -190,8 +207,22 @@ namespace eosio { namespace cdt { ss << tn << " arg" << i << "; ds >> arg" << i << ";\n"; i++; } + + // Create contract object + ss << decl->getParent()->getQualifiedNameAsString() + << " obj {eosio::name{r},eosio::name{c},ds};\n"; + + // Call `set_exec_type()` only for contracts dervied from `eosio::contract`. + // A contract class can have only `eosio::contract` attribute + // but does not inherit from the `eosio::contract` class; + // it may not have `set_exec_type()`. We need to make sure the class derives + // from `eosio::contract` before calling set_exec_type(). + if (base_is_eosio_contract_class(decl)) { + ss << "obj.set_exec_type(eosio::contract::exec_type_t::action);\n"; + } + const auto& call_action = [&]() { - ss << decl->getParent()->getQualifiedNameAsString() << "{eosio::name{r},eosio::name{c},ds}." << decl->getNameAsString() << "("; + ss << "obj." << decl->getNameAsString() << "("; for (int i=0; i < decl->parameters().size(); i++) { ss << "arg" << i; if (i < decl->parameters().size()-1) @@ -260,8 +291,22 @@ namespace eosio { namespace cdt { ss << tn << " arg" << i << "; ds >> arg" << i << ";\n"; i++; } + + // Create contract object + ss << decl->getParent()->getQualifiedNameAsString() + << " obj {eosio::name{receiver},eosio::name{receiver},ds};\n"; + + // Call `set_exec_type()` only for contracts dervied from `eosio::contract`. + // A contract class can have only `eosio::contract` attribute + // but does not inherit from the `eosio::contract` class; + // it may not have `set_exec_type()`. We need to make sure the class derives + // from `eosio::contract` before calling set_exec_type(). + if (base_is_eosio_contract_class(decl)) { + ss << "obj.set_exec_type(eosio::contract::exec_type_t::call);\n"; + } + const auto& call_function = [&]() { - ss << decl->getParent()->getQualifiedNameAsString() << "{eosio::name{receiver},eosio::name{receiver},ds}." << decl->getNameAsString() << "("; + ss << "obj." << decl->getNameAsString() << "("; for (int i=0; i < decl->parameters().size(); i++) { ss << "arg" << i; if (i < decl->parameters().size()-1)