Merge pull request #351 from AntelopeIO/call_wrapper

SC: Implement call wrapper to simplify making sync calls
This commit is contained in:
Lin Huang
2025-07-09 11:08:35 -04:00
committed by GitHub
21 changed files with 766 additions and 246 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"antelope-spring-dev":{
"target":"new_entry_point_validation",
"target":"return_status",
"prerelease":false
}
}
+1 -79
View File
@@ -6,11 +6,11 @@
#include <cstdlib>
#include <type_traits>
#include "detail.hpp"
#include "../../core/eosio/serialize.hpp"
#include "../../core/eosio/datastream.hpp"
#include "../../core/eosio/name.hpp"
#include "../../core/eosio/fixed_bytes.hpp"
#include "../../core/eosio/ignore.hpp"
#include "../../core/eosio/time.hpp"
namespace eosio {
@@ -409,84 +409,6 @@ namespace eosio {
};
namespace detail {
/// @cond INTERNAL
template <typename T>
struct unwrap { typedef T type; };
template <typename T>
struct unwrap<ignore<T>> { typedef T type; };
template <typename R, typename Act, typename... Args>
auto get_args(R(Act::*p)(Args...)) {
return std::tuple<std::decay_t<typename unwrap<Args>::type>...>{};
}
template <typename R, typename Act, typename... Args>
auto get_args_nounwrap(R(Act::*p)(Args...)) {
return std::tuple<std::decay_t<Args>...>{};
}
template <auto Action>
using deduced = decltype(get_args(Action));
template <auto Action>
using deduced_nounwrap = decltype(get_args_nounwrap(Action));
template <typename T>
struct convert { typedef T type; };
template <>
struct convert<const char*> { typedef std::string type; };
template <>
struct convert<char*> { typedef std::string type; };
template <typename T, typename U>
struct is_same { static constexpr bool value = std::is_convertible<T,U>::value; };
template <typename U>
struct is_same<bool,U> { static constexpr bool value = std::is_integral<U>::value; };
template <typename T>
struct is_same<T,bool> { static constexpr bool value = std::is_integral<T>::value; };
template <size_t N, size_t I, auto Arg, auto... Args>
struct get_nth_impl { static constexpr auto value = get_nth_impl<N,I+1,Args...>::value; };
template <size_t N, auto Arg, auto... Args>
struct get_nth_impl<N, N, Arg, Args...> { static constexpr auto value = Arg; };
template <size_t N, auto... Args>
struct get_nth { static constexpr auto value = get_nth_impl<N,0,Args...>::value; };
template <auto Action, size_t I, typename T, typename... Rest>
struct check_types {
static_assert(detail::is_same<typename convert<T>::type, typename convert<typename std::tuple_element<I, deduced<Action>>::type>::type>::value);
using type = check_types<Action, I+1, Rest...>;
static constexpr bool value = true;
};
template <auto Action, size_t I, typename T>
struct check_types<Action, I, T> {
static_assert(detail::is_same<typename convert<T>::type, typename convert<typename std::tuple_element<I, deduced<Action>>::type>::type>::value);
static constexpr bool value = true;
};
template <auto Action, typename... Ts>
constexpr bool type_check() {
static_assert(sizeof...(Ts) == std::tuple_size<deduced<Action>>::value);
if constexpr (sizeof...(Ts) != 0)
return check_types<Action, 0, Ts...>::value;
return true;
}
/// @endcond
}
/**
* Wrapper for an action object.
*
+104 -57
View File
@@ -5,6 +5,7 @@
#include <cstdlib>
#include <type_traits>
#include "detail.hpp"
#include "../../core/eosio/serialize.hpp"
#include "../../core/eosio/datastream.hpp"
#include "../../core/eosio/name.hpp"
@@ -34,6 +35,10 @@ namespace eosio {
* @note There are some methods from the @ref call that can be used directly from C++
*/
inline int64_t call(eosio::name receiver, uint64_t flags, const char* data, size_t data_size) {
return internal_use_do_not_use::call(receiver.value, flags, data, data_size);
}
inline uint32_t get_call_return_value( void* mem, uint32_t len ) {
return internal_use_do_not_use::get_call_return_value(mem, len);
}
@@ -46,65 +51,107 @@ namespace eosio {
internal_use_do_not_use::set_call_return_value(mem, len);
}
/**
* This is the packed representation of a call
*
* @ingroup call
*/
struct call {
/**
* Name of the account the call is intended for
*/
const name receiver{};
// Indicate whether a sync call is read_write or read_only. Default is read_write
enum class access_mode { read_write = 0, read_only = 1 };
/**
* indicating if the call is read only or not
*/
const bool read_only = false;
// Indicate the action to take if the receiver does not support sync calls.
// Default is abort_op
enum class support_mode { abort_op = 0, no_op = 1 };
/**
* if the receiver contract does not have sync_call entry point or its signature
* is invalid, when no_op_if_receiver_not_support_sync_call is set to true,
* the sync call is no op, otherwise the call is aborted and an exception is raised.
*/
const bool no_op_if_receiver_not_support_sync_call = false;
/**
* Payload data
*/
const std::vector<char> data{};
/**
* Construct a new call object with receiver, name, and payload data
*
* @tparam T - Type of call data, must be serializable by `pack(...)`
* @param receiver - The name of the account this call is intended for
* @param flags - The flags
* @param payload - The call data that will be serialized via pack into data
*/
template<typename T>
call( struct name receiver, T&& payload, bool read_only = false, bool no_op = false )
: receiver(receiver)
, read_only(read_only)
, no_op_if_receiver_not_support_sync_call(no_op)
, data(pack(std::forward<T>(payload))) {}
/// @cond INTERNAL
EOSLIB_SERIALIZE( call, (receiver)(read_only)(no_op_if_receiver_not_support_sync_call)(data) )
/// @endcond
/**
* Make a call using the functor operator
*/
int64_t operator()() const {
uint64_t flags = read_only ? 0x01 : 0x00; // last bit indicating read only
auto retval = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size());
if (retval == -2) { // sync call is not supported by the receiver contract
check(no_op_if_receiver_not_support_sync_call, "receiver does not support sync call but no_op_if_receiver_not_support_sync_call flag is not set");
}
return retval;
}
// For a void function, when support_mode is set to no_op, the call_wrapper.
// returns `std::optional<void_call>`. If the optional has no value, it indicates
// the call was op-op; if the optional has a value of `void_call`, the call
// executed successfully.
struct void_call {
};
struct call_data_header {
uint32_t version = 0;
uint64_t func_name = 0; // At WASM level, function name is an uint64_t. We do not use eosio::name here to make the decoding function name simpler in sync_call entry point function.
EOSLIB_SERIALIZE(call_data_header, (version)(func_name))
};
/**
* Wrapper for simplifying making a sync call
*
* @brief Used to wrap a particular sync call to simplify the process of other contracts making sync calls to the "wrapped" call.
* Example:
* @code
* // defined by contract writer of the sync call functions
* using get_func = call_wrapper<"get"_n, &callee::get, uint32_t>;
* // usage by different contract writer
* get_func{"callee"_n}();
* // or
* get_func get{"callee"_n};
* get();
* @endcode
*/
template <eosio::name::raw Func_Name, auto Func_Ref, access_mode Access_Mode=access_mode::read_write, support_mode Support_Mode = support_mode::abort_op>
struct call_wrapper {
template <typename Receiver>
constexpr call_wrapper(Receiver&& receiver)
: receiver(std::forward<Receiver>(receiver))
{}
static constexpr eosio::name function_name = eosio::name(Func_Name);
eosio::name receiver {};
using orig_ret_type = typename detail::function_traits<decltype(Func_Ref)>::return_type;
using return_type = std::conditional_t<
Support_Mode == support_mode::abort_op,// if Support_Mode is abort_op
orig_ret_type, // use the original return type
std::conditional_t< // else
std::is_void<orig_ret_type>::value, // original return type is void
std::optional<void_call>, // use optional of empty struct
std::optional<orig_ret_type> // use optional of original return type
>
>;
template <typename... Args>
return_type operator()(Args&&... args)const {
static_assert(detail::type_check<Func_Ref, Args...>());
uint64_t flags = 0x00;
if constexpr (Access_Mode == access_mode::read_only) {
flags = 0x01;
}
call_data_header header{ .version = 0,
.func_name = function_name.value };
const std::vector<char> data{ pack(std::forward_as_tuple(header, detail::deduced<Func_Ref>{std::forward<Args>(args)...})) };
auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size());
if (ret_val_size < 0) { // the receiver does not support sync calls
if constexpr (Support_Mode == support_mode::abort_op) {
check(false, "receiver does not support sync call but support_mode is set to abort_op");
} else {
return std::nullopt;
}
}
// The sync call has been executed by the receiver
if constexpr (std::is_void<return_type>::value) {
return;
} else {
if constexpr (Support_Mode == support_mode::no_op && std::is_void<orig_ret_type>::value) {
return void_call{};
} else {
constexpr size_t max_stack_buffer_size = 512;
char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be freed at the end of callers wasm execution.
internal_use_do_not_use::get_call_return_value(buffer, ret_val_size);
orig_ret_type ret_val = unpack<orig_ret_type>(buffer, ret_val_size);
if constexpr (Support_Mode == support_mode::no_op) {
return std::make_optional(ret_val);
} else {
return ret_val;
}
}
}
}
};
} // namespace eosio
@@ -0,0 +1,94 @@
#pragma once
#include "../../core/eosio/ignore.hpp"
namespace eosio { namespace detail {
/// @cond INTERNAL
template <typename T>
struct unwrap { typedef T type; };
template <typename T>
struct unwrap<ignore<T>> { typedef T type; };
template <typename R, typename Act, typename... Args>
auto get_args(R(Act::*p)(Args...)) {
return std::tuple<std::decay_t<typename unwrap<Args>::type>...>{};
}
template <typename R, typename Act, typename... Args>
auto get_args_nounwrap(R(Act::*p)(Args...)) {
return std::tuple<std::decay_t<Args>...>{};
}
template <auto Function>
using deduced = decltype(get_args(Function));
template <auto Function>
using deduced_nounwrap = decltype(get_args_nounwrap(Function));
template <typename T>
struct convert { typedef T type; };
template <>
struct convert<const char*> { typedef std::string type; };
template <>
struct convert<char*> { typedef std::string type; };
template <typename T, typename U>
struct is_same { static constexpr bool value = std::is_convertible<T,U>::value; };
template <typename U>
struct is_same<bool,U> { static constexpr bool value = std::is_integral<U>::value; };
template <typename T>
struct is_same<T,bool> { static constexpr bool value = std::is_integral<T>::value; };
template <size_t N, size_t I, auto Arg, auto... Args>
struct get_nth_impl { static constexpr auto value = get_nth_impl<N,I+1,Args...>::value; };
template <size_t N, auto Arg, auto... Args>
struct get_nth_impl<N, N, Arg, Args...> { static constexpr auto value = Arg; };
template <size_t N, auto... Args>
struct get_nth { static constexpr auto value = get_nth_impl<N,0,Args...>::value; };
template <auto Function, size_t I, typename T, typename... Rest>
struct check_types {
static_assert(detail::is_same<typename convert<T>::type, typename convert<typename std::tuple_element<I, deduced<Function>>::type>::type>::value);
using type = check_types<Function, I+1, Rest...>;
static constexpr bool value = true;
};
template <auto Function, size_t I, typename T>
struct check_types<Function, I, T> {
static_assert(detail::is_same<typename convert<T>::type, typename convert<typename std::tuple_element<I, deduced<Function>>::type>::type>::value);
static constexpr bool value = true;
};
template <auto Function, typename... Ts>
constexpr bool type_check() {
static_assert(sizeof...(Ts) == std::tuple_size<deduced<Function>>::value);
if constexpr (sizeof...(Ts) != 0)
return check_types<Function, 0, Ts...>::value;
return true;
}
// For non-function-pointers (function_traits is undefined)
template <typename T>
struct function_traits;
// For non-const member function
template <typename Class, typename Ret, typename... Args>
struct function_traits<Ret (Class::*)(Args...)> {
using return_type = Ret;
};
// For const member function
template <typename Class, typename Ret, typename... Args>
struct function_traits<Ret (Class::*)(Args...) const> {
using return_type = Ret;
};
/// @endcond
}} // eosio detail
+119 -29
View File
@@ -41,7 +41,11 @@ BOOST_AUTO_TEST_CASE(return_value_test) { try {
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "retvaltest"_n, "caller"_n, {}));
// Using host function directly
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstretvaltst"_n, "caller"_n, {}));
// Using call_wrapper
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrpretvaltst"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify one parameter passing works correctly
@@ -51,7 +55,11 @@ BOOST_AUTO_TEST_CASE(param_basic_test) { try {
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "paramtest"_n, "caller"_n, {}));
// Using host function directly
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstoneprmtst"_n, "caller"_n, {}));
// Using call_wrapper
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrponeprmtst"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify multiple parameters passing works correctly
@@ -61,7 +69,31 @@ BOOST_AUTO_TEST_CASE(multiple_params_test) { try {
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "mulparamtest"_n, "caller"_n, {}));
// Using host function directly
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstmulprmtst"_n, "caller"_n, {}));
// Using call_wrapper
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrpmulprmtst"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify passing a struct parameter works correctly
BOOST_AUTO_TEST_CASE(struct_param_test) { try {
call_tester t({
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "structtest"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify passing a mix of structs and integer works correctly
BOOST_AUTO_TEST_CASE(mix_struct_int_params_test) { try {
call_tester t({
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "structinttst"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify a sync call to a void function works properly.
@@ -71,18 +103,27 @@ BOOST_AUTO_TEST_CASE(void_func_test) { try {
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
auto trx_trace = t.push_action("caller"_n, "voidfunctest"_n, "caller"_n, {});
auto& atrace = trx_trace->action_traces;
auto check = [] (const transaction_trace_ptr& trx_trace) {
auto& atrace = trx_trace->action_traces;
auto& call_traces = atrace[0].call_traces;
BOOST_REQUIRE_EQUAL(call_traces.size(), 1u);
auto& call_traces = atrace[0].call_traces;
BOOST_REQUIRE_EQUAL(call_traces.size(), 1u);
// Verify the print from the void function is correct.
// The test contract checks the return value size is 0.
auto& call_trace = call_traces[0];
BOOST_REQUIRE_EQUAL(call_trace.call_ordinal, 1u);
BOOST_REQUIRE_EQUAL(call_trace.sender_ordinal, 0u);
BOOST_REQUIRE_EQUAL(call_trace.console, "I am a void function");
// Verify the print from the void function is correct.
// The test contract checks the return value size is 0.
auto& call_trace = call_traces[0];
BOOST_REQUIRE_EQUAL(call_trace.call_ordinal, 1u);
BOOST_REQUIRE_EQUAL(call_trace.sender_ordinal, 0u);
BOOST_REQUIRE_EQUAL(call_trace.console, "I am a void function");
};
// Using host function directly
auto trx_trace = t.push_action("caller"_n, "hstvodfuntst"_n, "caller"_n, {});
check(trx_trace);
// Using call_wrapper
trx_trace = t.push_action("caller"_n, "wrpvodfuntst"_n, "caller"_n, {});
check(trx_trace);
} FC_LOG_AND_RETHROW() }
// Verify a function tagged as both `action` and `call` works
@@ -96,14 +137,14 @@ BOOST_AUTO_TEST_CASE(mixed_action_call_tags_test) { try {
// 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, "mulparamtest"_n, "caller"_n, {}));
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()
("in1", 1)
("in2", 2)
("in3", 3)));
("a", 1)
("b", 2)
("c", 3)));
} FC_LOG_AND_RETHROW() }
// Verify the receiver contract with only one sync call function works
@@ -116,31 +157,80 @@ BOOST_AUTO_TEST_CASE(single_function_test) { try {
// The single_func_wasm contains only one function and the caller contract
// hooks up with it
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "retvaltest"_n, "caller"_n, {}));
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstretvaltst"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify no_op_if_receiver_not_support_sync_call flag works
BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try {
// Verify support_mode for void and non-void sync calls if calls are a failure
BOOST_AUTO_TEST_CASE(sync_call_support_mode_failure_test) { try {
call_tester t({
{"caller"_n, contracts::not_supported_wasm(), contracts::not_supported_abi().data()}
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::not_supported_wasm(), contracts::not_supported_abi().data()}
});
// * sync_call_not_supported contract only has actions
// * no_op_if_receiver_not_support_sync_call is set
// so the call is just a no-op
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "noopset"_n, "caller"_n, {}));
// voidfncnoop uses support_mode::no_op
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "voidfncnoop"_n, "caller"_n, {}));
// voidfncabort uses default support_mode::abort
BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "voidfncabort"_n, "caller"_n, {}),
eosio_assert_message_exception,
fc_exception_message_contains("receiver does not support sync call but support_mode is set to abort"));
// intfuncnoop uses support_mode::no_op
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "intfuncnoop"_n, "caller"_n, {}));
// intfuncabort uses default support_mode::abort
BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "intfuncabort"_n, "caller"_n, {}),
eosio_assert_message_exception,
fc_exception_message_contains("receiver does not support sync call but support_mode is set to abort"));
} FC_LOG_AND_RETHROW() }
// Verify calling an unknown function will result in an eosio_assert
// Verify support_mode for void and non-void sync calls if call is successful
BOOST_AUTO_TEST_CASE(sync_call_support_mode_success_test) { try {
call_tester t({
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
// voidnoopsucc uses support_mode::no_op
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "voidnoopsucc"_n, "caller"_n, {}));
// sumnoopsucc uses support_mode::no_op
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "sumnoopsucc"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify header validation
BOOST_AUTO_TEST_CASE(unknown_function_test) { try {
call_tester t({
{"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()},
{"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()}
});
BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "unknwnfuntst"_n, "caller"_n, {}),
eosio_assert_code_exception,
eosio_assert_code_is(8000000000000000003));
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hdrvaltest"_n, "caller"_n, {}));
} FC_LOG_AND_RETHROW() }
// Verify adding/reading entries to/from a table, and read-only enforcement work
BOOST_AUTO_TEST_CASE(addr_book_tests) { try {
call_tester t({
{"caller"_n, contracts::addr_book_caller_wasm(), contracts::addr_book_caller_abi().data()},
{"callee"_n, contracts::addr_book_callee_wasm(), contracts::addr_book_callee_abi().data()}
});
// Try to add an entry using a read-only sync call
BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "upsertrdonly"_n, "caller"_n, mvo()
("user", "alice")
("first_name", "alice")
("street", "123 Main St.")),
unaccessible_api,
fc_exception_message_contains("this API is not allowed in read only action/call"));
// Add an entry using a read-write sync call
t.push_action("caller"_n, "upsert"_n, "caller"_n, mvo()
("user", "alice")
("first_name", "alice")
("street", "123 Main St."));
// Read the inserted entry. "get"_n action will check the return value from the sync call
BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "get"_n, "caller"_n, mvo() ("user", "alice")));
} FC_LOG_AND_RETHROW() }
BOOST_AUTO_TEST_SUITE_END()
+4
View File
@@ -48,5 +48,9 @@ namespace eosio::testing {
static std::vector<char> not_supported_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_not_supported.abi"); }
static std::vector<uint8_t> single_func_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_single_func.wasm"); }
static std::vector<char> single_func_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_single_func.abi"); }
static std::vector<uint8_t> addr_book_callee_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_callee.wasm"); }
static std::vector<char> addr_book_callee_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_callee.abi"); }
static std::vector<uint8_t> addr_book_caller_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.wasm"); }
static std::vector<char> addr_book_caller_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.abi"); }
};
} //ns eosio::testing
@@ -0,0 +1,31 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
// Test the validation of the number of arguments passed in call_wrapper.
// Expected error:
// .../build/bin/../include/eosiolib/contracts/eosio/detail.hpp:72:7: error: static_assert failed due to requirement 'sizeof...(Ts) == std::tuple_size<std::__1::tuple<unsigned int, unsigned int, unsigned int>>::value'
// static_assert(sizeof...(Ts) == std::tuple_size<deduced<Function>>::value);
class [[eosio::contract]] sync_call_invalid_arg_nums : public eosio::contract{
public:
using contract::contract;
[[eosio::call]]
uint32_t sum(uint32_t a, uint32_t b, uint32_t c) {
return a + b + c;
}
using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_invalid_arg_nums::sum>;
// Fewer number of arguments
[[eosio::action]]
void fewerargs() {
sum_func{"callee"_n}(1, 2);
}
// More number of arguments
[[eosio::action]]
void moreargs() {
sum_func{"callee"_n}(1, 2, 3, 4);
}
};
@@ -0,0 +1,11 @@
{
"tests" : [
{
"compile_flags": [],
"expected" : {
"exit-code": 255,
"stderr": "error: static_assert failed due to requirement 'sizeof"
}
}
]
}
@@ -0,0 +1,28 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
// Test the validation of the types of arguments passed in call_wrapper.
// Expected error:
// build/bin/../include/eosiolib/contracts/eosio/detail.hpp:60:7: error: static_assert failed due to requirement 'detail::is_same<sync_call_invalid_arg_nums::empty &, unsigned int>::value'
// static_assert(detail::is_same<typename convert<T>::type, typename convert<typename std::tuple_element<I, deduced<Function>>::type>::type>::value);
class [[eosio::contract]] sync_call_invalid_arg_nums : public eosio::contract{
public:
using contract::contract;
[[eosio::call]]
uint32_t sum(uint32_t a, uint32_t b, uint32_t c) {
return a + b + c;
}
using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_invalid_arg_nums::sum>;
struct empty {
};
// Invalid first argument
[[eosio::action]]
void wrongrettype() {
empty bad;
sum_func{"callee"_n}(bad, 2, 3);
}
};
@@ -0,0 +1,11 @@
{
"tests" : [
{
"compile_flags": [],
"expected" : {
"exit-code": 255,
"stderr": "error: static_assert failed due to requirement 'detail::is_same"
}
}
]
}
+2
View File
@@ -17,6 +17,8 @@ add_contract(sync_call_caller sync_call_caller sync_call_caller.cpp)
add_contract(sync_call_callee sync_call_callee sync_call_callee.cpp)
add_contract(sync_call_not_supported sync_call_not_supported sync_call_not_supported.cpp)
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(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)
@@ -0,0 +1,37 @@
#include "sync_call_addr_book_callee.hpp"
#include <eosio/eosio.hpp>
using namespace eosio;
void sync_call_addr_book_callee::upsert(name user, std::string first_name, std::string street) {
// Intentionally leave out require_auth(user) to test upsert cannot be called as a read_only
// sync call
address_index addresses(get_first_receiver(), get_first_receiver().value);
auto iterator = addresses.find(user.value);
if( iterator == addresses.end() )
{
addresses.emplace(user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.street = street;
});
}
else {
addresses.modify(iterator, user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.street = street;
});
}
}
person_info sync_call_addr_book_callee::get(name user) {
address_index addresses(get_first_receiver(), get_first_receiver().value);
auto iterator = addresses.find(user.value);
check(iterator != addresses.end(), "Record does not exist");
return person_info{ .first_name = iterator->first_name,
.street = iterator->street };
}
@@ -0,0 +1,33 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
struct person_info {
std::string first_name;
std::string street;
};
class [[eosio::contract]] sync_call_addr_book_callee : public eosio::contract {
public:
sync_call_addr_book_callee(eosio::name receiver, eosio::name code, eosio::datastream<const char*> ds): contract(receiver, code, ds) {}
[[eosio::call]]
void upsert(eosio::name user, std::string first_name, std::string street);
[[eosio::call]]
person_info get(eosio::name user);
using upsert_read_only_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert, eosio::access_mode::read_only>;
using upsert_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert>;
using get_func = eosio::call_wrapper<"get"_n, &sync_call_addr_book_callee::get>;
private:
struct [[eosio::table]] person {
eosio::name key;
std::string first_name;
std::string street;
uint64_t primary_key() const { return key.value; }
};
using address_index = eosio::multi_index<"people"_n, person>;
};
@@ -0,0 +1,30 @@
#include "sync_call_addr_book_callee.hpp"
#include <eosio/eosio.hpp>
#include <eosio/call.hpp>
class [[eosio::contract]] sync_call_addr_book_caller : public eosio::contract{
public:
using contract::contract;
// Insert an entry using read_only, which will fail
[[eosio::action]]
void upsertrdonly(eosio::name user, std::string first_name, std::string street) {
sync_call_addr_book_callee::upsert_read_only_func{"callee"_n}(user, first_name, street);
}
// Insert an entry
[[eosio::action]]
void upsert(eosio::name user, std::string first_name, std::string street) {
sync_call_addr_book_callee::upsert_func{"callee"_n}(user, first_name, street);
}
// Read an entry
[[eosio::action]]
person_info get(eosio::name user) {
auto user_info = sync_call_addr_book_callee::get_func{"callee"_n}(user);
eosio::check(user_info.first_name == "alice", "first name not alice");
eosio::check(user_info.street == "123 Main St.", "street not 123 Main St.");
return user_info;
}
};
+27 -22
View File
@@ -1,28 +1,33 @@
#include <eosio/call.hpp>
#include "sync_call_callee.hpp"
#include <eosio/print.hpp>
#include <eosio/eosio.hpp>
class [[eosio::contract]] sync_call_callee : public eosio::contract{
public:
using contract::contract;
[[eosio::call]]
uint32_t sync_call_callee::getten() {
return 10;
}
[[eosio::call]]
uint32_t getten() {
return 10;
}
[[eosio::call]]
uint32_t sync_call_callee::getback(uint32_t in) {
return in;
}
[[eosio::call]]
uint32_t getback(uint32_t in) {
return in;
}
[[eosio::call]]
void sync_call_callee::voidfunc() {
eosio::print("I am a void function");
}
[[eosio::call]]
void voidfunc() {
eosio::print("I am a void function");
}
[[eosio::action, eosio::call]]
uint32_t sync_call_callee::sum(uint32_t a, uint32_t b, uint32_t c) {
return a + b + c;
}
[[eosio::call]]
struct1_t sync_call_callee::structonly(struct1_t s) {
return s;
}
[[eosio::call]]
struct1_t sync_call_callee::structmix(struct1_t s1, int32_t m, struct2_t s2) {
return { .a = s1.a * m + s2.c, .b = s1.b * m + s2.d };
}
[[eosio::action, eosio::call]]
uint32_t sum(uint32_t in1, uint32_t in2, uint32_t in3) {
return in1 + in2 + in3;
}
};
@@ -0,0 +1,50 @@
#include <eosio/call.hpp>
#include <eosio/eosio.hpp>
struct struct1_t {
int64_t a;
uint64_t b;
};
struct struct2_t {
char a;
bool b;
int64_t c;
uint64_t d;
};
class [[eosio::contract]] sync_call_callee : public eosio::contract{
public:
using contract::contract;
[[eosio::call]]
uint32_t getten();
[[eosio::call]]
uint32_t getback(uint32_t in);
[[eosio::call]]
void voidfunc();
[[eosio::action, eosio::call]]
uint32_t sum(uint32_t a, uint32_t b, uint32_t c);
// pass in a struct and return it
[[eosio::call]]
struct1_t structonly(struct1_t s);
// pass in two structs and an integer, multiply each field in the struct by
// the integer, add last two fields of the second struct, and return the result
[[eosio::call]]
struct1_t structmix(struct1_t s1, int32_t m, struct2_t s2);
using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten>;
using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback>;
using voidfunc_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc>;
using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum>;
using structonly_func = eosio::call_wrapper<"structonly"_n, &sync_call_callee::structonly>;
using structmix_func = eosio::call_wrapper<"structmix"_n, &sync_call_callee::structmix>;
using void_no_op_success_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>;
using sum_no_op_success_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum, eosio::access_mode::read_write, eosio::support_mode::no_op>;
};
+131 -11
View File
@@ -1,13 +1,21 @@
#include "sync_call_callee.hpp"
#include "sync_call_not_supported.hpp"
#include <eosio/eosio.hpp>
#include <eosio/call.hpp>
using namespace eosio;
class [[eosio::contract]] sync_call_caller : public eosio::contract{
public:
using contract::contract;
// Using host function directly
[[eosio::action]]
void retvaltest() {
auto expected_size = eosio::call("callee"_n, "getten"_n)();
void hstretvaltst() {
call_data_header header{ .version = 0, .func_name = "getten"_n.value };
const std::vector<char> data{ eosio::pack(header) };
auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size());
eosio::check(expected_size >= 0, "call did not return a positive value");
std::vector<char> return_value;
@@ -17,10 +25,20 @@ public:
eosio::check(eosio::unpack<uint32_t>(return_value) == 10u, "return value not 10"); // getten always returns 10
}
// Using call_wrapper
[[eosio::action]]
void paramtest() {
void wrpretvaltst() {
sync_call_callee::getten_func getten{ "callee"_n };
eosio::check(getten() == 10u, "return value not 10");
}
// Using host function directly, testing one parameter passing
[[eosio::action]]
void hstoneprmtst() {
// `getback(uint32_t p)` returns p
auto expected_size = eosio::call("callee"_n, std::make_tuple("getback"_n, 5))();
call_data_header header{ .version = 0, .func_name = "getback"_n.value };
const std::vector<char> data{ eosio::pack(std::make_tuple(header, 5)) };
auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size());
eosio::check(expected_size >= 0, "call did not return a positive value");
std::vector<char> return_value;
@@ -30,26 +48,128 @@ public:
eosio::check(eosio::unpack<uint32_t>(return_value) == 5u, "return value not 5"); // getback returns back the same value of parameter
}
// Using call_wrapper, testing one parameter passing
[[eosio::action]]
void mulparamtest() {
auto expected_size = eosio::call("callee"_n, std::make_tuple("sum"_n, 10, 20, 30))();
void wrponeprmtst() {
sync_call_callee::getback_func getback{ "callee"_n };
eosio::check(getback(5) == 5u, "return value not 5");
}
// Using host function directly, testing multiple parameters passing
[[eosio::action]]
void hstmulprmtst() {
call_data_header header{ .version = 0, .func_name = "sum"_n.value };
const std::vector<char> data{ eosio::pack(std::make_tuple(header, 10, 20, 30)) };
auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size());
eosio::check(expected_size >= 0, "call did not return a positive value");
std::vector<char> return_value;
return_value.resize(expected_size);
auto actual_size = eosio::get_call_return_value(return_value.data(), return_value.size());
eosio::check(actual_size == expected_size, "actual_size not equal to expected_size");
eosio::check(eosio::unpack<uint32_t>(return_value) == 60u, "return value not 60"); // sum returns the sum of the 3 arguments
eosio::check(eosio::unpack<uint32_t>(return_value) == 60u, "sum of 10, 20, and 30 not 60"); // sum returns the sum of the 3 arguments
}
// Using call_wrapper, testing multiple parameters passing
[[eosio::action]]
void wrpmulprmtst() {
sync_call_callee::sum_func sum{ "callee"_n };
eosio::check(sum(10, 20, 30) == 60u, "sum of 10, 20, and 30 not 60");
}
// Verify single struct parameter passing
[[eosio::action]]
void structtest() {
sync_call_callee::structonly_func func{ "callee"_n };
struct1_t input = { 10, 20 };
auto output = func(input); // structonly_func returns the input as is
eosio::check(output.a == input.a, "field a in output is not equal to a in input");
eosio::check(output.b == input.b, "field b in output is not equal to b in input");
}
// Verify mix of struct and integer parameters passing
[[eosio::action]]
void structinttst() {
sync_call_callee::structmix_func func{ "callee"_n };
struct1_t input1 = { 10, 20 };
struct2_t input2 = { 'a', true, 50, 100 };
int32_t m = 2;
// structmix_func multiply each field of input1 by m,
// add last two fields of input2, and return a struct1_t
auto output = func(input1, m, input2);
eosio::check(output.a == m * input1.a + input2.c, "field a of output is not correct");
eosio::check(output.b == m * input1.b + input2.d, "field b of output is not correct");
}
[[eosio::action]]
void voidfunctest() {
auto expected_size = eosio::call("callee"_n, "voidfunc"_n)();
void hstvodfuntst() {
call_data_header header{ .version = 0, .func_name = "voidfunc"_n.value };
const std::vector<char> data{ eosio::pack(header) };
auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size());
eosio::check(expected_size == 0, "call did not return 0"); // void function. return value size should be 0
}
[[eosio::action]]
void unknwnfuntst() {
eosio::call("callee"_n, "unknwnfunc"_n)(); // unknwnfunc will never be in "callee"_n contract
void wrpvodfuntst() {
sync_call_callee::voidfunc_func voidfunc{ "callee"_n };
voidfunc();
}
// Verify void call. void_func uses default support_mode::abort
[[eosio::action]]
void voidfncabort() {
sync_call_not_supported::void_func void_func_abort{ "callee"_n };
void_func_abort(); // Will throw. Tester will verify that.
}
// void_func uses support_mode::no_op
[[eosio::action]]
void voidfncnoop() {
sync_call_not_supported::void_no_op_func void_func_no_op{ "callee"_n };
check(void_func_no_op() == std::nullopt, "void_func_no_op did not return std::nullopt");
}
// verify non-void call. int_func uses default support_mode::abort
[[eosio::action]]
void intfuncabort() {
sync_call_not_supported::int_func int_func_abort{ "callee"_n };
int_func_abort(); // Will throw. Tester will verify that.
}
// int_func uses support_mode::no_opabort
[[eosio::action]]
void intfuncnoop() {
sync_call_not_supported::int_no_op_func int_func_no_op{ "callee"_n };
check(int_func_no_op() == std::nullopt, "void_func_no_op did not return std::nullopt");
}
// void_no_op_success_func uses support_mode::no_op
[[eosio::action]]
void voidnoopsucc() {
sync_call_callee::void_no_op_success_func f{ "callee"_n };
check(f().has_value(), "void_no_op_success_func did not return a value");
}
// void_no_op_success_func uses support_mode::no_op
[[eosio::action]]
void sumnoopsucc() {
sync_call_callee::sum_no_op_success_func f{ "callee"_n };
check(*f(7, 8, 9) == 24, "sum_no_op_success_func did not return a value");
}
[[eosio::action]]
void hdrvaltest() {
// Verify function name validation works
call_data_header unkwn_func_header{ .version = 0, .func_name = "unknwnfunc"_n.value };
const std::vector<char> unkwn_func_data{ eosio::pack(unkwn_func_header) }; // unknwnfunc is not in "callee"_n contract
auto status = eosio::call("callee"_n, 0, unkwn_func_data.data(), unkwn_func_data.size());
eosio::check(status == -10001, "call did not return -10001 for unknown function");
// Verify version validation works
call_data_header bad_version_header{ .version = 1, .func_name = "sum"_n.value }; // version 1 is not supported
const std::vector<char> bad_version_data{ eosio::pack(bad_version_header) };
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");
}
};
@@ -1,34 +1,10 @@
#include <eosio/eosio.hpp>
#include <eosio/call.hpp>
#include "sync_call_not_supported.hpp"
class [[eosio::contract]] sync_call_not_supported : public eosio::contract{
public:
using contract::contract;
[[eosio::action]]
void sync_call_not_supported::voidfunc() {
}
// * sync call is not supported as no method is taged by `call`
// * no_op_if_receiver_no_support_sync_call is set
[[eosio::action]]
void noopset() {
std::vector<char> data{};
// For now, because sync_call entry point has not been implemented yet and
// no_op_if_receiver_no_support_sync_call is set to true, call should return -1
auto rc = eosio::call("caller"_n, data, false /* read_only */, true /* no_op_if_receiver_no_support_sync_call */)();
eosio::check(rc == -1, "call did not return -1");
// call was not executed. return value size should be 0
std::vector<char> value(10);
auto size = eosio::get_call_return_value(value.data(), value.size());
eosio::check(size == 0, "return value size is not 0");
}
// sync call not supported, no_op_if_receiver_no_support_sync_call not set
[[eosio::action]]
void noopnotset() {
std::vector<char> data{};
// For now, because sync_call entry point has not been implemented yet and
// no_op_if_receiver_no_support_sync_call is not set, call should fail
eosio::call("caller"_n, data)();
}
};
[[eosio::action]]
int sync_call_not_supported::intfunc() {
return 1;
}
@@ -0,0 +1,23 @@
#include <eosio/eosio.hpp>
#include <eosio/call.hpp>
// Because this contract does not tag any functions by `call` attribute,
// `sync_call` entry point is not generated.
// Any sync calls to this contract will return a status indicating
// sync calls are not supported by the receiver.
class [[eosio::contract]] sync_call_not_supported : public eosio::contract{
public:
using contract::contract;
[[eosio::action]]
void voidfunc();
[[eosio::action]]
int intfunc();
using void_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_not_supported::voidfunc>; // default behavior: abort when called
using void_no_op_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_not_supported::voidfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; // no op when called
using int_func = eosio::call_wrapper<"intfunc"_n, &sync_call_not_supported::intfunc>; // default behavior: abort when called
using int_no_op_func = eosio::call_wrapper<"intfunc"_n, &sync_call_not_supported::intfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; // no op when called
};
+20 -14
View File
@@ -232,7 +232,7 @@ namespace eosio { namespace cdt {
std::string nm = decl->getNameAsString()+"_"+decl->getParent()->getNameAsString();
if (cg.is_eosio_contract(decl, cg.contract_name)) {
ss << "\n\n#include <eosio/datastream.hpp>\n";
ss << "#include <eosio/name.hpp>\n";
ss << "#include <eosio/call.hpp>\n";
ss << "extern \"C\" {\n";
const auto& return_ty = decl->getReturnType().getAsString();
if (return_ty != "void") {
@@ -245,7 +245,7 @@ namespace eosio { namespace cdt {
ss << func_name << nm;
ss << "\"))) void " << func_name << nm << "(unsigned long long sender, unsigned long long receiver, size_t data_size, void* data) {\n";
ss << "eosio::datastream<const char*> ds{(char*)data, data_size};\n";
ss << "unsigned long long func_name; ds >> func_name;\n"; // skip called function name
ss << "eosio::call_data_header header; ds >> header;\n"; // skip header
int i=0;
for (auto param : decl->parameters()) {
clang::LangOptions lang_opts;
@@ -275,32 +275,38 @@ namespace eosio { namespace cdt {
call_function();
if (return_ty != "void") {
ss << "const auto& packed_result = eosio::pack(result);\n";
ss << "set_call_return_value((void*)packed_result.data(), packed_result.size());\n";
ss << "::set_call_return_value((void*)packed_result.data(), packed_result.size());\n";
}
ss << "}}\n";
}
}
// Generate get_sync_call_func_name which returns called function name
static void create_get_sync_call_func_name(std::stringstream& ss) {
// Generate get_sync_call_data_version which returns the version of call data.
// In version 0, call data is packed as header + arguments, where
// header is `struct header { uint32_t version; uint64_t func_name }`
static void create_get_sync_call_data_header(std::stringstream& ss) {
ss << "\n\n#include <eosio/datastream.hpp>\n";
ss << "#include <eosio/name.hpp>\n";
ss << "#include <eosio/call.hpp>\n";
ss << "extern \"C\" {\n";
ss << "__attribute__((weak)) unsigned long long __eos_get_sync_call_func_name_(void* data) {\n";
ss << "eosio::datastream<const char*> ds{(char*)data, sizeof(unsigned long long)};\n";
ss << "unsigned long long func_name; ds >> func_name;\n";
ss << "return func_name;\n";
ss << "__attribute__((weak)) void* __eos_get_sync_call_data_header_(void* data) {\n";
ss << "size_t size = sizeof(eosio::call_data_header);\n";
ss << "eosio::datastream<const char*> ds{(char*)data, size};\n";
ss << "eosio::call_data_header header; ds >> header;\n";
ss << "void* ptr = malloc(size);\n";
ss << "memcpy(ptr, &header, size);\n";
ss << "return ptr;\n";
ss << "}}\n";
}
// Generate get_sync_call_data which returns call data
// Generate get_sync_call_data which returns call data which consists of
// header and arguments
static void create_get_sync_call_data(std::stringstream& ss) {
ss << "\n\n#include <eosio/datastream.hpp>\n";
ss << "#include <eosio/name.hpp>\n";
ss << "extern \"C\" {\n";
ss << "__attribute__((eosio_wasm_import)) uint32_t get_call_data(void*, uint32_t);\n";
ss << "__attribute__((weak)) void* __eos_get_sync_call_data_(unsigned long size) {\n";
ss << "void* data = malloc(size);\n";
ss << "void* data = malloc(size);\n"; // store data in linear memory
ss << "::get_call_data(data, size);\n";
ss << "return data;\n";
ss << "}}\n";
@@ -366,10 +372,10 @@ namespace eosio { namespace cdt {
CDT_ERROR("codegen_error", decl->getLocation(), std::string("call name (")+s+") is not a valid eosio name");
});
// Genereate create_get_sync_call_data and get_sync_call_func_name only once
// Genereate create_get_sync_call_data and create_get_sync_call_data_header only once
if (_call_set.empty()) {
create_get_sync_call_data(ss);
create_get_sync_call_func_name(ss);
create_get_sync_call_data_header(ss);
}
if (!_call_set.count(name))