Compare commits
8 Commits
main
...
antler-run-draft
| Author | SHA1 | Date | |
|---|---|---|---|
| d1912a39b5 | |||
| b54a28c2d6 | |||
| c92664d5b8 | |||
| 004569022b | |||
| 389cb1d432 | |||
| e9b221c176 | |||
| e246a9f21e | |||
| 4a359eb77b |
@@ -13,3 +13,6 @@
|
||||
[submodule "tools/external/eos-vm"]
|
||||
path = tools/external/eos-vm
|
||||
url = https://github.com/AntelopeIO/eos-vm
|
||||
[submodule "tools/external/antler-interfaces"]
|
||||
path = tools/external/antler-interfaces
|
||||
url = git@github.com:AntelopeIO/antler-interfaces
|
||||
|
||||
@@ -560,6 +560,37 @@ datastream<Stream>& operator >> ( datastream<Stream>& ds, std::array<T,N>& v ) {
|
||||
return ds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize capi_checksum160, capi_checksum256, capi_checksum512 struct
|
||||
*
|
||||
* @param ds - The stream to write
|
||||
* @param v - The value to serialize
|
||||
* @tparam Stream - Type of datastream buffer
|
||||
* @return datastream<Stream>& - Reference to the datastream
|
||||
*/
|
||||
template<typename Stream, typename T, typename = std::enable_if_t<std::is_array_v<decltype(T::hash)>>>
|
||||
datastream<Stream>& operator << ( datastream<Stream>& ds, const T& v ) {
|
||||
for( int i = 0; i < sizeof(v.hash)/sizeof(v.hash[0]); ++i )
|
||||
ds << v.hash[i];
|
||||
return ds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserialize capi_checksum160, capi_checksum256, capi_checksum512 struct
|
||||
*
|
||||
* @param ds - The stream to read
|
||||
* @param v - The destination for deserialized value
|
||||
* @tparam Stream - Type of datastream buffer
|
||||
* @return datastream<Stream>& - Reference to the datastream
|
||||
*/
|
||||
template<typename Stream, typename T, typename = std::enable_if_t<std::is_array_v<decltype(T::hash)>>>
|
||||
datastream<Stream>& operator >> ( datastream<Stream>& ds, T& v ) {
|
||||
for( int i = 0; i < sizeof(v.hash)/sizeof(v.hash[0]); ++i )
|
||||
ds >> v.hash[i];
|
||||
return ds;
|
||||
}
|
||||
|
||||
namespace _datastream_detail {
|
||||
/**
|
||||
* Check if type T is a pointer
|
||||
|
||||
@@ -85,85 +85,7 @@ extern "C" {
|
||||
___has_failed = false;
|
||||
___earlier_unit_test_has_failed = false;
|
||||
|
||||
// preset the print functions
|
||||
intrinsics::set_intrinsic<intrinsics::prints_l>([](const char* cs, uint32_t l) {
|
||||
_prints_l(cs, l, eosio::cdt::output_stream_kind::std_out);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::prints>([](const char* cs) {
|
||||
_prints(cs, eosio::cdt::output_stream_kind::std_out);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printi>([](int64_t v) {
|
||||
printf("%lli", v);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printui>([](uint64_t v) {
|
||||
printf("%llu", v);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printi128>([](const int128_t* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printui128>([](const uint128_t* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printsf>([](float v) {
|
||||
char buff[512] = {0};
|
||||
std::string ret = std::to_string((int)v);
|
||||
memcpy(buff, ret.c_str(), ret.size());
|
||||
v -= (int)v;
|
||||
buff[ret.size()] = '.';
|
||||
size_t size = ret.size();
|
||||
for (size_t i=size+1; i < size+10; i++) {
|
||||
v *= 10;
|
||||
buff[i] = ((int)v)+'0';
|
||||
v -= (int)v;
|
||||
}
|
||||
prints(buff);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printdf>([](double v) {
|
||||
char buff[512] = {0};
|
||||
std::string ret = std::to_string((long)v);
|
||||
memcpy(buff, ret.c_str(), ret.size());
|
||||
v -= (long)v;
|
||||
buff[ret.size()] = '.';
|
||||
size_t size = ret.size();
|
||||
for (size_t i=size+1; i < size+10; i++) {
|
||||
v *= 10;
|
||||
buff[i] = ((int)v)+'0';
|
||||
v -= (int)v;
|
||||
}
|
||||
prints(buff);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printqf>([](const long double* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printn>([](uint64_t nm) {
|
||||
std::string s = eosio::name(nm).to_string();
|
||||
prints_l(s.c_str(), s.length());
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printhex>([](const void* data, uint32_t len) {
|
||||
constexpr static uint32_t max_stack_buffer_size = 512;
|
||||
const char* hex_characters = "0123456789abcdef";
|
||||
|
||||
uint32_t buffer_size = 2*len;
|
||||
if(buffer_size < len) eosio_assert( false, "length passed into printhex is too large" );
|
||||
|
||||
void* buffer = (max_stack_buffer_size < buffer_size) ? malloc(buffer_size) : alloca(buffer_size);
|
||||
|
||||
char* b = reinterpret_cast<char*>(buffer);
|
||||
const uint8_t* d = reinterpret_cast<const uint8_t*>(data);
|
||||
for( uint32_t i = 0; i < len; ++i ) {
|
||||
*b = hex_characters[d[i] >> 4];
|
||||
++b;
|
||||
*b = hex_characters[d[i] & 0x0f];
|
||||
++b;
|
||||
}
|
||||
|
||||
prints_l(reinterpret_cast<const char*>(buffer), buffer_size);
|
||||
|
||||
if(max_stack_buffer_size < buffer_size) free(buffer);
|
||||
});
|
||||
set_print_intrinsics();
|
||||
|
||||
jmp_ret = setjmp(env);
|
||||
if (jmp_ret == 0) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "native/eosio/crt.hpp"
|
||||
#include "native/eosio/intrinsics.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
@@ -42,4 +43,26 @@ void _prints(const char* cstr, uint8_t which) {
|
||||
if (!___disable_output)
|
||||
std::putc(cstr[i], which == eosio::cdt::output_stream_kind::std_out ? stdout : stderr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template<eosio::native::intrinsics::intrinsic_name ID, typename Fn, typename Ret, typename TupleArgs, size_t... Is>
|
||||
void set_intrinsic(Fn fn, std::index_sequence<Is...>) {
|
||||
eosio::native::intrinsics::set_intrinsic<ID>(reinterpret_cast<Ret(*)(typename std::tuple_element<Is, TupleArgs>::type...)>(fn));
|
||||
}
|
||||
|
||||
#define REGISTER_INTRINSIC(ID) \
|
||||
case eosio::native::intrinsics::ID: \
|
||||
set_intrinsic<eosio::native::intrinsics::ID, \
|
||||
decltype(fn), \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::res_t, \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::deduced_full_ts> \
|
||||
(fn, eosio::native::intrinsics::__ ## ID ## _types::is); \
|
||||
break;
|
||||
|
||||
|
||||
void register_intrinsic(int64_t id, void(*fn)()) {
|
||||
switch(id) {
|
||||
INTRINSICS(REGISTER_INTRINSIC);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <eosio/system.h>
|
||||
#include <eosio/transaction.h>
|
||||
#include <eosio/types.h>
|
||||
#include <eosio/name.hpp>
|
||||
#include "native/eosio/intrinsics.hpp"
|
||||
#include "native/eosio/crt.hpp"
|
||||
#include <softfloat.hpp>
|
||||
@@ -905,9 +906,92 @@ extern "C" {
|
||||
uint32_t get_active_security_group(char* data, uint32_t datalen){
|
||||
return intrinsics::get().call<intrinsics::get_active_security_group>(data, datalen);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace eosio { namespace native {
|
||||
void set_print_intrinsics() {
|
||||
// preset the print functions
|
||||
intrinsics::set_intrinsic<intrinsics::prints_l>([](const char* cs, uint32_t l) {
|
||||
_prints_l(cs, l, eosio::cdt::output_stream_kind::std_out);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::prints>([](const char* cs) {
|
||||
_prints(cs, eosio::cdt::output_stream_kind::std_out);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printi>([](int64_t v) {
|
||||
printf("%lli", v);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printui>([](uint64_t v) {
|
||||
printf("%llu", v);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printi128>([](const int128_t* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printui128>([](const uint128_t* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printsf>([](float v) {
|
||||
char buff[512] = {0};
|
||||
std::string ret = std::to_string((int)v);
|
||||
memcpy(buff, ret.c_str(), ret.size());
|
||||
v -= (int)v;
|
||||
buff[ret.size()] = '.';
|
||||
size_t size = ret.size();
|
||||
for (size_t i=size+1; i < size+10; i++) {
|
||||
v *= 10;
|
||||
buff[i] = ((int)v)+'0';
|
||||
v -= (int)v;
|
||||
}
|
||||
prints(buff);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printdf>([](double v) {
|
||||
char buff[512] = {0};
|
||||
std::string ret = std::to_string((long)v);
|
||||
memcpy(buff, ret.c_str(), ret.size());
|
||||
v -= (long)v;
|
||||
buff[ret.size()] = '.';
|
||||
size_t size = ret.size();
|
||||
for (size_t i=size+1; i < size+10; i++) {
|
||||
v *= 10;
|
||||
buff[i] = ((int)v)+'0';
|
||||
v -= (int)v;
|
||||
}
|
||||
prints(buff);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printqf>([](const long double* v) {
|
||||
int* tmp = (int*)v;
|
||||
printf("0x%04x%04x%04x%04x", tmp[0], tmp[1], tmp[2], tmp[3]);
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printn>([](uint64_t nm) {
|
||||
std::string s = eosio::name(nm).to_string();
|
||||
prints_l(s.c_str(), s.length());
|
||||
});
|
||||
intrinsics::set_intrinsic<intrinsics::printhex>([](const void* data, uint32_t len) {
|
||||
constexpr static uint32_t max_stack_buffer_size = 512;
|
||||
const char* hex_characters = "0123456789abcdef";
|
||||
|
||||
uint32_t buffer_size = 2*len;
|
||||
if(buffer_size < len) eosio_assert( false, "length passed into printhex is too large" );
|
||||
|
||||
void* buffer = (max_stack_buffer_size < buffer_size) ? malloc(buffer_size) : alloca(buffer_size);
|
||||
|
||||
char* b = reinterpret_cast<char*>(buffer);
|
||||
const uint8_t* d = reinterpret_cast<const uint8_t*>(data);
|
||||
for( uint32_t i = 0; i < len; ++i ) {
|
||||
*b = hex_characters[d[i] >> 4];
|
||||
++b;
|
||||
*b = hex_characters[d[i] & 0x0f];
|
||||
++b;
|
||||
}
|
||||
|
||||
prints_l(reinterpret_cast<const char*>(buffer), buffer_size);
|
||||
|
||||
if(max_stack_buffer_size < buffer_size) free(buffer);
|
||||
});
|
||||
}
|
||||
}} // eosio::native
|
||||
|
||||
int32_t blake2_f( uint32_t rounds, const char* state, uint32_t state_len, const char* msg, uint32_t msg_len,
|
||||
const char* t0_offset, uint32_t t0_len, const char* t1_offset, uint32_t t1_len, int32_t final, char* result, uint32_t result_len) {
|
||||
return intrinsics::get().call<intrinsics::blake2_f>(rounds, state, state_len, msg, msg_len, t0_offset, t0_len, t1_offset, t1_len, final, result, result_len);
|
||||
|
||||
@@ -24,6 +24,10 @@ namespace eosio { namespace cdt {
|
||||
};
|
||||
}} //ns eosio::cdt
|
||||
|
||||
namespace eosio { namespace native {
|
||||
void set_print_intrinsics();
|
||||
}} //eosio::native
|
||||
|
||||
extern eosio::cdt::output_stream std_out;
|
||||
extern eosio::cdt::output_stream std_err;
|
||||
|
||||
@@ -39,4 +43,8 @@ extern "C" {
|
||||
void __reset_env();
|
||||
void _prints_l(const char* cstr, uint32_t len, uint8_t which);
|
||||
void _prints(const char* cstr, uint8_t which);
|
||||
|
||||
//used only in shared object
|
||||
void register_intrinsic(int64_t, void(*)());
|
||||
void initialize();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <eosio/action.hpp>
|
||||
#include "intrinsics_def.hpp"
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
project(antler-run)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/antler-run.cpp ${CMAKE_BINARY_DIR}/antler-run.cpp)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.hpp.in ${CMAKE_BINARY_DIR}/version.hpp)
|
||||
find_package(eos-vm)
|
||||
find_package(cdt_libraries)
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(Protobuf REQUIRED)
|
||||
|
||||
set(TOOLS_DIR ${CMAKE_SOURCE_DIR})
|
||||
set(LIB_DIR ${CMAKE_SOURCE_DIR}/../libraries)
|
||||
set(CDT_NO_START TRUE)
|
||||
|
||||
include_directories(${LIB_DIR})
|
||||
include_directories(${LIB_DIR} ${CMAKE_BINARY_DIR}/include ${Protobuf_INCLUDE_DIRS})
|
||||
|
||||
add_tool(antler-run)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -ldl)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -ldl -fpermissive -ftemplate-backtrace-limit=0)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-rpath,\"\\$ORIGIN/../lib\"")
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${TOOLS_DIR}/external/eos-vm/include
|
||||
${TOOLS_DIR}/external/spdlog/include
|
||||
${LIB_DIR}/eosiolib/contracts
|
||||
${LIB_DIR}/eosiolib/core
|
||||
${LIB_DIR}/eosiolib/capi
|
||||
${LIB_DIR}/eosiolib/native
|
||||
${LIB_DIR}/native
|
||||
${LIB_DIR}/meta_refl/include)
|
||||
target_link_libraries(${PROJECT_NAME} native_eosio native)
|
||||
${LIB_DIR}/meta_refl/include
|
||||
)
|
||||
target_link_libraries(${PROJECT_NAME} native_eosio native antler_interfaces ${Protobuf_LIBRARIES})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "options.hpp"
|
||||
#include "file-utils.hpp"
|
||||
#include "run.hpp"
|
||||
#include "native-runner.hpp"
|
||||
#include "wasm-runner.hpp"
|
||||
#include "version.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <string.h>
|
||||
@@ -14,37 +18,44 @@
|
||||
#include "llvm/Support/CommandLine.h"
|
||||
|
||||
using namespace eosio;
|
||||
using namespace eosio::testing;
|
||||
using namespace eosio::cdt;
|
||||
using namespace eosio::native;
|
||||
using namespace eosio::vm;
|
||||
using namespace llvm;
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
|
||||
cl::SetVersionPrinter([](llvm::raw_ostream& os) {
|
||||
os << "Antler-run version ${VERSION_FULL}" << "\n";
|
||||
os << "Antler-run version " << app_version() << "\n";
|
||||
});
|
||||
|
||||
cl::ParseCommandLineOptions(argc, argv);
|
||||
|
||||
const auto& contract_path = contract_path_opt.getValue();
|
||||
auto contract_type = utils::get_file_type(contract_path.c_str());
|
||||
|
||||
if (test_so_opt) {
|
||||
auto contract_type = utils::get_file_type(contract_path.c_str());
|
||||
if ( contract_type != utils::file_type::elf_shared_object ) {
|
||||
fprintf(stderr, "not a shared object file: %s\n", file_type_str(contract_type).c_str());
|
||||
}
|
||||
// TODO: add check for neccesary exports
|
||||
|
||||
//loads shared object and throws if any export function is missing
|
||||
testing::native::runner runner(contract_path);
|
||||
ANTLER_INFO("{} is a valid native contract shared library", std::filesystem::path(contract_path).filename());
|
||||
// TODO: add unit test that generates shared object and executes runner with this flag
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto& node_url = nodeos_url_opt.getValue();
|
||||
const auto& node_port = nodeos_port_opt.getValue();
|
||||
const auto& action = action_name_opt.getValue();
|
||||
const auto& account = register_opt.getValue();
|
||||
const auto& action = eosio::name(action_name_opt.getValue());
|
||||
const auto& account = eosio::name(register_opt.getValue());
|
||||
|
||||
//TODO add runner implementation here
|
||||
if (contract_type == utils::file_type::elf_shared_object) {
|
||||
run( testing::native::runner(contract_path), node_url, node_port, account );
|
||||
} else if (contract_type == utils::file_type::wasm) {
|
||||
run( testing::wasm::runner(contract_path), node_url, node_port, account );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/eosio.hpp>
|
||||
|
||||
#include <type_traits>
|
||||
#include <string>
|
||||
#include <cstddef>
|
||||
#include <variant>
|
||||
|
||||
namespace eosio { namespace testing {
|
||||
enum class object_type {
|
||||
shared_object,
|
||||
wasm
|
||||
};
|
||||
template <typename Impl>
|
||||
struct runner_interface {
|
||||
inline void init() {
|
||||
return static_cast<Impl*>(this)->init();
|
||||
}
|
||||
inline void apply(eosio::name receiver, eosio::name code, eosio::name action) {
|
||||
return static_cast<Impl*>(this)->apply(receiver, code, action);
|
||||
}
|
||||
inline object_type get_type() {
|
||||
return static_cast<Impl*>(this)->get_type();
|
||||
}
|
||||
};
|
||||
|
||||
}} // eosio::testing
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <native/eosio/intrinsics.hpp>
|
||||
#include <native/eosio/crt.hpp>
|
||||
#include "node-client.hpp"
|
||||
|
||||
namespace eosio { namespace testing { namespace native {
|
||||
|
||||
template<eosio::native::intrinsics::intrinsic_name ID, typename Ret, typename TupleArgs, size_t... Is>
|
||||
inline auto get_intrinsic_target(std::index_sequence<Is...>) {
|
||||
return *eosio::native::intrinsics::get_intrinsic<ID>().template target<Ret(*)(typename std::tuple_element<Is, TupleArgs>::type...)>();
|
||||
}
|
||||
|
||||
template<int64_t ID, typename Ret, typename... Args>
|
||||
inline auto redirect_to_node(Args... args) {
|
||||
return testing::node_client::get().call_intrinsic<ID, Ret>(args...);
|
||||
}
|
||||
|
||||
template<int64_t ID, typename Ret, typename TupleArgs, size_t... Is>
|
||||
inline auto create_redirect_function(std::index_sequence<Is...>) {
|
||||
return std::function<Ret(typename std::tuple_element<Is, TupleArgs>::type ...)>{
|
||||
&redirect_to_node<ID, Ret, typename std::tuple_element<Is, TupleArgs>::type ...>
|
||||
};
|
||||
}
|
||||
|
||||
#define RPC_INTRINSIC(ID) \
|
||||
eosio::native::intrinsics::set_intrinsic<eosio::native::intrinsics::ID>( \
|
||||
create_redirect_function<eosio::native::intrinsics::ID, \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::res_t, \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::deduced_full_ts > \
|
||||
(eosio::native::intrinsics::__ ## ID ## _types::is));
|
||||
|
||||
void setup_rpc_intrinsics() {
|
||||
|
||||
INTRINSICS(RPC_INTRINSIC);
|
||||
|
||||
//override print intrinsics to be non-rpc
|
||||
eosio::native::set_print_intrinsics();
|
||||
}
|
||||
}}} //eosio::testing::native
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils.hpp"
|
||||
#include "file-utils.hpp"
|
||||
#include "interface.hpp"
|
||||
#include "intrinsics_setup.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
namespace eosio { namespace testing { namespace native {
|
||||
using apply = std::add_pointer_t<void(uint64_t, uint64_t, uint64_t)>;
|
||||
using generic_intrinsic = std::add_pointer_t<void()>;
|
||||
using register_intrinsic = std::add_pointer_t<void (int64_t, const generic_intrinsic&)>;
|
||||
using initialize = std::add_pointer_t<void()>;
|
||||
|
||||
#define REGISTER_LIB_INTRINSIC(ID) \
|
||||
exports.register_intrinsic((int64_t)eosio::native::intrinsics::ID, \
|
||||
reinterpret_cast<generic_intrinsic>(\
|
||||
get_intrinsic_target<eosio::native::intrinsics::ID, \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::res_t, \
|
||||
eosio::native::intrinsics::__ ## ID ## _types::deduced_full_ts>\
|
||||
(eosio::native::intrinsics::__ ## ID ## _types::is)));
|
||||
|
||||
template <typename Fn>
|
||||
struct function {
|
||||
Fn f = nullptr;
|
||||
|
||||
inline function& operator = ( void* raw ) {
|
||||
f = (Fn)raw;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool operator == (Fn other) {
|
||||
return f == other;
|
||||
}
|
||||
inline bool operator != (Fn other) {
|
||||
return !operator==(other);
|
||||
}
|
||||
template<typename... Args>
|
||||
inline auto operator ()(Args... args) {
|
||||
return f(args...);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief native runner, loads shared object with contract, performs intrinsics setup and exposes interface to execute apply
|
||||
struct runner : testing::runner_interface<runner> {
|
||||
runner(const std::string& path) {
|
||||
load(path);
|
||||
}
|
||||
void apply(eosio::name receiver, eosio::name code, eosio::name action) {
|
||||
exports.apply(receiver.value, code.value, action.value);
|
||||
}
|
||||
inline object_type get_type() {
|
||||
return object_type::shared_object;
|
||||
}
|
||||
|
||||
void init() {
|
||||
// this call assigns rpc handlers for every intrinsic
|
||||
setup_rpc_intrinsics();
|
||||
|
||||
// this macro executes exports.register_intrinsic for every intrinsic
|
||||
// we need this because of shared object has its own native library internal variables
|
||||
// so this call is to supply current intrinsics pointers to shared object
|
||||
INTRINSICS(REGISTER_LIB_INTRINSIC);
|
||||
|
||||
//let library override neccesary intrinsics
|
||||
exports.initialize();
|
||||
}
|
||||
private:
|
||||
// if closing handle before main finishes, it gives segmentation fault
|
||||
// function with destructor attribute will be executed after main finishes, when static objects destroyed
|
||||
inline static void* so_handle = 0;
|
||||
static void close_so_handle(void) __attribute__ ((destructor)) {
|
||||
dlclose(so_handle);
|
||||
}
|
||||
|
||||
struct exports {
|
||||
native::function<native::apply> apply;
|
||||
native::function<native::register_intrinsic> register_intrinsic;
|
||||
native::function<native::initialize> initialize;
|
||||
};
|
||||
exports exports;
|
||||
|
||||
void load(const std::string& path) {
|
||||
ANTLER_ASSERT(so_handle == nullptr, "object already loaded");
|
||||
|
||||
auto contract_type = utils::get_file_type(path.c_str());
|
||||
if ( contract_type != utils::file_type::elf_shared_object ) {
|
||||
ANTLER_THROW("not a shared object file: {}", file_type_str(contract_type));
|
||||
}
|
||||
|
||||
so_handle = dlopen(path.c_str(), RTLD_LOCAL | RTLD_LAZY);
|
||||
if (so_handle == nullptr) {
|
||||
ANTLER_THROW("failed to load {} : {}", path, dlerror());
|
||||
}
|
||||
|
||||
exports.apply = dlsym(so_handle, "apply");
|
||||
if (exports.apply == nullptr) {
|
||||
ANTLER_THROW("failed to find `apply`: ", dlerror());
|
||||
}
|
||||
|
||||
exports.register_intrinsic = dlsym(so_handle, "register_intrinsic");
|
||||
if (exports.register_intrinsic == nullptr) {
|
||||
ANTLER_THROW("failed to find `register_intrinsic`: ", dlerror());
|
||||
}
|
||||
|
||||
exports.initialize = dlsym(so_handle, "initialize");
|
||||
if (exports.apply == nullptr) {
|
||||
ANTLER_THROW("failed to find `initialize`: ", dlerror());
|
||||
}
|
||||
}
|
||||
};
|
||||
}}} // eosio::testing::native
|
||||
@@ -0,0 +1,475 @@
|
||||
#pragma once
|
||||
|
||||
#include "version.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "rpc-utils.hpp"
|
||||
|
||||
#include <native/eosio/intrinsics.hpp>
|
||||
#include <eosio/datastream.hpp>
|
||||
#include <bluegrass/meta/for_each.hpp>
|
||||
#include <pb/antler-run.pb.h>
|
||||
|
||||
// macro defined in eosiolib collides with enum value in boost::beast
|
||||
#ifdef TABLE
|
||||
#pragma push_macro("TABLE")
|
||||
#undef TABLE
|
||||
#endif
|
||||
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <future>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <condition_variable>
|
||||
#include <optional>
|
||||
|
||||
// restoring eosiolib macro
|
||||
#ifndef TABLE
|
||||
#pragma pop_macro("TABLE")
|
||||
#endif
|
||||
|
||||
namespace eosio { namespace testing {
|
||||
|
||||
class sync_call_traits {
|
||||
public:
|
||||
template<typename Callback>
|
||||
using callback = std::packaged_task<Callback>;
|
||||
};
|
||||
|
||||
/// @brief connection class that connects to nodeos and processes debug execution messages
|
||||
/// interface is synchroneous, actual reads and writes are done in a separate thread
|
||||
/// class designed to always listen for incoming messages and write them to read queue
|
||||
/// every request and response must have same request_id
|
||||
/// in case server sends response for a different request, class leaves it in the read queue
|
||||
/// @tparam CallbackTraits
|
||||
template<typename CallbackTraits = sync_call_traits>
|
||||
class connection : public std::enable_shared_from_this<connection<CallbackTraits>>{
|
||||
using io_context = boost::asio::io_context;
|
||||
using asio_strand = boost::asio::strand<boost::asio::io_context::executor_type>;
|
||||
using tcp_stream = boost::beast::websocket::stream<boost::beast::tcp_stream>;
|
||||
using ip_resolver = boost::asio::ip::tcp::resolver;
|
||||
using error_code = boost::beast::error_code;
|
||||
using stream_base = boost::beast::websocket::stream_base;
|
||||
using data_buffer = boost::beast::flat_buffer;
|
||||
using request_type = boost::beast::websocket::request_type;
|
||||
using read_queue_type = std::unordered_map<uint64_t, antler_rpc::antler_run_response>;
|
||||
using on_event = typename CallbackTraits::callback<void()>;
|
||||
|
||||
//order is important as fields are using one another in constructor
|
||||
io_context context;
|
||||
ip_resolver resolver;
|
||||
tcp_stream stream;
|
||||
data_buffer write_buffer;
|
||||
data_buffer read_buffer;
|
||||
std::string host;
|
||||
std::string port;
|
||||
on_event connected;
|
||||
std::mutex reading;
|
||||
std::condition_variable new_response;
|
||||
std::mutex writing;
|
||||
read_queue_type read_queue;
|
||||
std::mutex read_queue_mutex;
|
||||
public:
|
||||
using on_connected = on_event;
|
||||
|
||||
connection(const std::string& host_, const std::string& port_, on_connected&& connected_)
|
||||
: resolver(context.get_executor())
|
||||
, stream(context.get_executor())
|
||||
, host(host_)
|
||||
, port(port_)
|
||||
, connected(std::move(connected_)) {}
|
||||
|
||||
template<typename ReqType, typename = std::enable_if_t<std::is_base_of_v<google::protobuf::Message, ReqType>> >
|
||||
auto sync_request(ReqType&& req) {
|
||||
ANTLER_ASSERT( !in_context_thread(), "code must be called outside connection thread" );
|
||||
|
||||
auto req_id = req.req_id();
|
||||
boost::asio::post(context.get_executor(),
|
||||
[r = std::move(req), c = shared_from_this()](){
|
||||
c->write(r);
|
||||
});
|
||||
return get_response(req_id);
|
||||
}
|
||||
|
||||
std::optional<antler_rpc::antler_run_response> wait_for_divert_flow() {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ANTLER_ASSERT( !in_context_thread(), "code must be called outside connection thread" );
|
||||
|
||||
while (!closing()) {
|
||||
{
|
||||
std::lock_guard<decltype(read_queue_mutex)> lock(read_queue_mutex);
|
||||
for (auto it = read_queue.begin(); it != read_queue.end(); ++it) {
|
||||
if (it->second.has_divert_flow_result()) {
|
||||
antler_rpc::antler_run_response ret = std::move(it->second);
|
||||
read_queue.erase(it);
|
||||
return std::optional(std::move(ret));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::unique_lock<decltype(read_queue_mutex)> wait_lock(read_queue_mutex);
|
||||
new_response.wait_for(wait_lock, 1s, [&](){ return !read_queue.empty(); });
|
||||
}
|
||||
|
||||
// we are here if connection was closed
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
inline bool closing() {
|
||||
return stream.is_open();
|
||||
}
|
||||
|
||||
void run() {
|
||||
// blocking until closed
|
||||
context.run();
|
||||
}
|
||||
void start_connection() {
|
||||
boost::asio::post(context.get_executor(),
|
||||
[c = shared_from_this()](){
|
||||
c->connect();
|
||||
});
|
||||
}
|
||||
private:
|
||||
|
||||
template<typename Handler>
|
||||
inline auto bind_front_this(Handler&& f) {
|
||||
return boost::beast::bind_front_handler(std::forward<Handler>(f), shared_from_this());
|
||||
}
|
||||
|
||||
void on_resolve(error_code ec, ip_resolver::results_type results){
|
||||
ANTLER_ASSERT(!ec, "{}", ec.message());
|
||||
|
||||
// Set the timeout for the operation
|
||||
boost::beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Make the connection on the IP address we get from a lookup
|
||||
boost::beast::get_lowest_layer(stream).async_connect(results,
|
||||
bind_front_this(&connection::on_connect));
|
||||
}
|
||||
void on_connect(error_code ec, ip_resolver::results_type::endpoint_type ep){
|
||||
ANTLER_ASSERT(!ec, "{}", ec.message());
|
||||
|
||||
// Turn off the timeout on the tcp_stream, because
|
||||
// the websocket stream has its own timeout system.
|
||||
boost::beast::get_lowest_layer(stream).expires_never();
|
||||
|
||||
// Set suggested timeout settings for the websocket
|
||||
stream.set_option(stream_base::timeout::suggested(boost::beast::role_type::client));
|
||||
|
||||
// Set a decorator to change the User-Agent of the handshake
|
||||
stream.set_option(stream_base::decorator(
|
||||
[](request_type& req)
|
||||
{
|
||||
req.set(boost::beast::http::field::user_agent,
|
||||
eosio::utils::log_to_str("antler-run client {}", app_version()));
|
||||
}));
|
||||
|
||||
// Perform the websocket handshake
|
||||
stream.async_handshake(eosio::utils::log_to_str("{}:{}", host, ep.port()),
|
||||
"/",
|
||||
bind_front_this(&connection::on_handshake));
|
||||
}
|
||||
void on_handshake(error_code ec) {
|
||||
ANTLER_ASSERT(!ec, "{}", ec.message());
|
||||
|
||||
start_read();
|
||||
connected();
|
||||
}
|
||||
template <typename ReqType>
|
||||
void write(const ReqType& req) {
|
||||
ANTLER_ASSERT( in_context_thread(), "code must be called inside connection thread" );
|
||||
ANTLER_ASSERT( writing.try_lock(), "current implementation is single-threaded so \
|
||||
only one write operation is permitted at the same time. \
|
||||
If you need this functionality you should implement write queue \
|
||||
or multi-socket connection (new thread = new socket)" );
|
||||
|
||||
const auto msg_size = req.ByteSizeLong();
|
||||
{
|
||||
zero_copy_output_stream<decltype(write_buffer)> ostream(write_buffer, msg_size);
|
||||
req.SerializeToZeroCopyStream(&ostream);
|
||||
}
|
||||
|
||||
stream.async_write(write_buffer.data(), bind_front_this(&connection::on_write));
|
||||
}
|
||||
void on_write(error_code ec, size_t) {
|
||||
std::unique_lock<decltype(writing)>(writing, std::adopt_lock);
|
||||
ANTLER_ASSERT(!ec, "{}", ec.message());
|
||||
|
||||
write_buffer.consume(write_buffer.size());
|
||||
}
|
||||
void on_read(error_code ec, std::size_t) {
|
||||
antler_rpc::antler_run_response response;
|
||||
{
|
||||
std::unique_lock<decltype(reading)>(reading, std::adopt_lock);
|
||||
ANTLER_ASSERT(!ec, "{}", ec.message());
|
||||
|
||||
zero_copy_input_stream<decltype(read_buffer)> istream(read_buffer);
|
||||
response.ParseFromZeroCopyStream(&istream);
|
||||
}
|
||||
{
|
||||
std::lock_guard<decltype(read_queue_mutex)> lock(read_queue_mutex);
|
||||
read_queue[response.req_id()] = std::move(response);
|
||||
}
|
||||
new_response.notify_all();
|
||||
|
||||
start_read();
|
||||
}
|
||||
void start_read() {
|
||||
ANTLER_ASSERT( in_context_thread(), "code must be called inside connection thread" );
|
||||
ANTLER_ASSERT( reading.try_lock(), "current implementation is single-threaded so \
|
||||
only one read operation is permitted at the same time. \
|
||||
If you need this functionality you should implement read queue \
|
||||
or multi-socket connection (new thread = new socket)" );
|
||||
stream.async_read(read_buffer, bind_front_this(&connection::on_read));
|
||||
}
|
||||
template<typename ReqType>
|
||||
auto post_request(ReqType&& req) {
|
||||
return boost::asio::post(context.get_executor(),
|
||||
[r = std::move(req), c = shared_from_this()](){
|
||||
c->write(r);
|
||||
});
|
||||
}
|
||||
void connect() {
|
||||
ANTLER_ASSERT( !stream.is_open(), "already connected" );
|
||||
ANTLER_ASSERT( in_context_thread(), "code must be called inside connection thread" );
|
||||
// Look up the domain name
|
||||
resolver.async_resolve(
|
||||
host,
|
||||
port,
|
||||
bind_front_this(&connection::on_resolve));
|
||||
}
|
||||
inline bool in_context_thread() {
|
||||
return context.get_executor().running_in_this_thread();
|
||||
}
|
||||
|
||||
std::optional<antler_rpc::antler_run_response> get_response(uint64_t id) {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ANTLER_ASSERT( !in_context_thread(), "code must be called outside connection thread" );
|
||||
|
||||
while (!closing()) {
|
||||
{
|
||||
std::lock_guard<decltype(read_queue_mutex)> lock(read_queue_mutex);
|
||||
auto it = read_queue.find(id);
|
||||
if (it != read_queue.end()) {
|
||||
antler_rpc::antler_run_response ret = std::move(it->second);
|
||||
read_queue.erase(it);
|
||||
return std::optional(std::move(ret));
|
||||
}
|
||||
}
|
||||
std::unique_lock<decltype(read_queue_mutex)> wait_lock(read_queue_mutex);
|
||||
new_response.wait_for(wait_lock, 1s, [&](){ return !read_queue.empty(); });
|
||||
}
|
||||
|
||||
// we are here if connection was closed
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief client that exposes debug RPC.
|
||||
/// it maintains single thread for network operations where connection class is running
|
||||
class node_client {
|
||||
private:
|
||||
using connection = connection<>;
|
||||
std::shared_ptr<connection> node_connection;
|
||||
std::thread context_thread;
|
||||
|
||||
node_client() {}
|
||||
|
||||
static inline uint64_t generate_req_id() {
|
||||
static uint64_t counter = 0;
|
||||
return ++counter;
|
||||
}
|
||||
void run() {
|
||||
context_thread = std::thread([&](){
|
||||
// blocks until connection is closed
|
||||
node_connection->run();
|
||||
});
|
||||
}
|
||||
public:
|
||||
|
||||
void init(const std::string& host, const std::string& port) {
|
||||
connection::on_connected connected;
|
||||
auto future = connected.get_future();
|
||||
|
||||
node_connection.reset( new connection(host, port, std::move(connected)) );
|
||||
run();
|
||||
node_connection->start_connection();
|
||||
|
||||
future.get();
|
||||
}
|
||||
static inline node_client& get() {
|
||||
static node_client client;
|
||||
return client;
|
||||
}
|
||||
|
||||
void register_account(eosio::name acc_name) {
|
||||
auto request = create_generic_request();
|
||||
|
||||
request.mutable_register_message()->set_account(acc_name.value);
|
||||
|
||||
//sync_request will check return code, as long as it doesn't throw we good to return
|
||||
std::ignore = sync_request(std::move(request));
|
||||
}
|
||||
|
||||
void unregister_account(eosio::name acc_name) {
|
||||
auto request = create_generic_request();
|
||||
|
||||
request.mutable_unregister_message()->set_account(acc_name.value);
|
||||
|
||||
//sync_request will check return code, as long as it doesn't throw we good to return
|
||||
std::ignore = sync_request(std::move(request));
|
||||
}
|
||||
|
||||
inline std::optional<antler_rpc::antler_run_response> wait_for_divert_flow() {
|
||||
return node_connection->wait_for_divert_flow();
|
||||
}
|
||||
|
||||
void set_time(int64_t new_time) {
|
||||
auto request = create_generic_request();
|
||||
|
||||
request.mutable_set_time_message()->set_time(new_time);
|
||||
|
||||
//sync_request will check return code, as long as it doesn't throw we good to return
|
||||
std::ignore = sync_request(std::move(request));
|
||||
}
|
||||
|
||||
int64_t get_time() {
|
||||
auto request = create_generic_request();
|
||||
|
||||
std::ignore = request.mutable_get_time_message();
|
||||
|
||||
auto response = sync_request(std::move(request));
|
||||
ANTLER_ASSERT(response.has_get_time_result(), "get time result should be in place");
|
||||
|
||||
return response.get_time_result().time();
|
||||
}
|
||||
|
||||
template<int64_t ID, typename Ret, typename... Args>
|
||||
Ret call_intrinsic(Args... args) {
|
||||
auto request = create_generic_request();
|
||||
|
||||
request.set_req_id(generate_req_id());
|
||||
request.mutable_call_intrinsic_message()->set_id(ID);
|
||||
request.mutable_call_intrinsic_message()->set_data(serialize_args(args...));
|
||||
|
||||
//call CallHostFunction RPC synchronously and get response
|
||||
auto response = sync_request(std::move(request));
|
||||
ANTLER_ASSERT(response.has_call_intrinsic_result(), "call intrinsic result is absent");
|
||||
|
||||
const auto& intrinsic_result = response.call_intrinsic_result();
|
||||
const auto& output_params = intrinsic_result.data();
|
||||
if (!output_params.empty()) {
|
||||
//deserialize response back into args if any
|
||||
deserialize_output_parameters(output_params.data(), std::make_tuple(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
if constexpr (!std::is_same_v<void, Ret>) {
|
||||
//deserialize intrinsic return value if any
|
||||
return deserialize_return_value<Ret>(intrinsic_result.ret_val());
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Ret, typename... Args>
|
||||
Ret call_action(Args... args) {
|
||||
auto request = create_generic_request();
|
||||
request.mutable_call_action_message()->set_data(serialize_args(args...));
|
||||
|
||||
//call CallAction RPC synchronously and get response
|
||||
auto response = sync_request(std::move(request));
|
||||
ANTLER_ASSERT(response.has_call_action_result(), "call action result is absent");
|
||||
|
||||
const auto& action_result = response.call_action_result();
|
||||
const auto& output_params = action_result.data();
|
||||
if (!output_params.empty()) {
|
||||
//deserialize response back into args if any
|
||||
deserialize_output_parameters(output_params.data(), std::make_tuple(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
if constexpr (!std::is_same_v<void, Ret>) {
|
||||
//deserialize intrinsic return value if any
|
||||
return deserialize_return_value<Ret>(action_result.ret_val());
|
||||
}
|
||||
}
|
||||
|
||||
void return_control_flow(uint64_t id) {
|
||||
antler_rpc::antler_run_request request;
|
||||
// id should match req_id we got from divert_flow
|
||||
request.set_req_id(id);
|
||||
|
||||
std::ignore = sync_request(std::move(request));
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename ReqType>
|
||||
antler_rpc::antler_run_response sync_request(ReqType&& request) {
|
||||
auto response = node_connection->sync_request(std::move(request));
|
||||
|
||||
ANTLER_QUIT(!node_connection->closing(), "connection to node was closed, exiting");
|
||||
ANTLER_ASSERT(response, "error getting response from node");
|
||||
ANTLER_ASSERT(response->ret_code() == 0, "request {} call has failed", response->DebugString());
|
||||
|
||||
return *response;
|
||||
}
|
||||
|
||||
antler_rpc::antler_run_request create_generic_request() const {
|
||||
antler_rpc::antler_run_request request;
|
||||
request.set_req_id(generate_req_id());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
std::string serialize_args(Args... args) {
|
||||
std::string tmp_buf;
|
||||
size_t buf_size = 0;
|
||||
if constexpr (std::index_sequence_for<Args...>::size()) {
|
||||
buf_size = (eosio::pack_size(args) + ...);
|
||||
tmp_buf.resize(buf_size);
|
||||
|
||||
eosio::datastream<const char*> ds(tmp_buf.data(), tmp_buf.size());
|
||||
//serialize args into vector of bytes
|
||||
(ds << ... << args);
|
||||
}
|
||||
|
||||
return tmp_buf;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void deserialize_output_parameters(const std::string& data, std::tuple<Args...>&& t) {
|
||||
eosio::datastream<const char*> ds(data.data(), data.size());
|
||||
bluegrass::meta::for_each(t, [&](auto v){
|
||||
if constexpr (std::is_pointer_v<decltype(v)> && !std::is_const_v<std::remove_pointer_t<decltype(v)>>) {
|
||||
//void* and char* are exceptions, everything else is serialized via datastream
|
||||
// if you'll encounter errors here, update datastream to deserialize type you have issues with
|
||||
if constexpr (std::is_same_v<void*, decltype(v)> || std::is_same_v<char*, decltype(v)>) {
|
||||
std::vector<char> tmp;
|
||||
ds >> tmp;
|
||||
memcpy((void*)v, tmp.data(), tmp.size());
|
||||
} else {
|
||||
ds >> *v;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
template<typename Ret>
|
||||
Ret deserialize_return_value(const std::string& serialized_data) {
|
||||
eosio::datastream<const char*> ds(serialized_data.data(), serialized_data.size());
|
||||
Ret ret_val;
|
||||
ds >> ret_val;
|
||||
|
||||
return std::move(ret_val);
|
||||
}
|
||||
};
|
||||
|
||||
}} //eosio::testing
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils.hpp"
|
||||
|
||||
#include <google/protobuf/io/zero_copy_stream.h>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/core/buffer_traits.hpp>
|
||||
|
||||
|
||||
namespace eosio { namespace testing {
|
||||
/**
|
||||
* idea taken from https://github.com/boostorg/beast/issues/1180
|
||||
* Implements ZeroCopyOutputStream around a AsioBuf.
|
||||
* AsioBuf matches the public interface defined by boost::asio::streambuf.
|
||||
* @tparam AsioBuf A type meeting the requirements of AsioBuf.
|
||||
*/
|
||||
template <class AsioBuf>
|
||||
class zero_copy_output_stream : public google::protobuf::io::ZeroCopyOutputStream {
|
||||
private:
|
||||
using mutable_buffers_type = typename AsioBuf::mutable_buffers_type;
|
||||
using iterator = typename boost::beast::buffers_iterator_type<mutable_buffers_type>;
|
||||
|
||||
AsioBuf& streambuf;
|
||||
std::size_t mutable_buf_size;
|
||||
google::protobuf::int64 commited = 0;
|
||||
std::size_t bytes_to_commit = 0;
|
||||
iterator cur_pos = nullptr;
|
||||
|
||||
inline void commit() {
|
||||
if (bytes_to_commit != 0) {
|
||||
streambuf.commit(bytes_to_commit);
|
||||
commited += bytes_to_commit;
|
||||
bytes_to_commit = 0;
|
||||
}
|
||||
}
|
||||
public:
|
||||
explicit zero_copy_output_stream(AsioBuf& streambuf, std::size_t blockSize):
|
||||
streambuf(streambuf)
|
||||
, mutable_buf_size(blockSize) {}
|
||||
~zero_copy_output_stream() { commit(); }
|
||||
|
||||
bool Next(void** data, int* size) override {
|
||||
ANTLER_ASSERT(data, "data pointer is null");
|
||||
ANTLER_ASSERT(size, "{}: size pointer is null");
|
||||
|
||||
commit();
|
||||
|
||||
auto cur_pos = boost::asio::buffer_sequence_begin(streambuf.prepare(mutable_buf_size));
|
||||
|
||||
*data = boost::asio::buffer_cast<void*>(*cur_pos);
|
||||
*size = static_cast<int>(boost::asio::buffer_size(*cur_pos));
|
||||
bytes_to_commit = *size;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackUp(int count) override {
|
||||
ANTLER_ASSERT(count <= bytes_to_commit, "count greater than bytes to commit");
|
||||
bytes_to_commit -= count;
|
||||
commit();
|
||||
}
|
||||
|
||||
google::protobuf::int64 ByteCount() const override {
|
||||
return commited;
|
||||
}
|
||||
};
|
||||
|
||||
template <class AsioBuf>
|
||||
class zero_copy_input_stream : public google::protobuf::io::ZeroCopyInputStream {
|
||||
private:
|
||||
using mutable_buffers_type = typename AsioBuf::mutable_buffers_type;
|
||||
using iterator = typename boost::beast::buffers_iterator_type<mutable_buffers_type>;
|
||||
|
||||
AsioBuf& streambuf;
|
||||
size_t bytes_to_consume = 0;
|
||||
google::protobuf::int64 total_consumed = 0;
|
||||
inline void consume(bool skip = false) {
|
||||
if (bytes_to_consume) {
|
||||
streambuf.consume(bytes_to_consume);
|
||||
if (!skip)
|
||||
total_consumed += bytes_to_consume;
|
||||
bytes_to_consume = 0;
|
||||
}
|
||||
}
|
||||
public:
|
||||
explicit zero_copy_input_stream(AsioBuf& buf) : streambuf(buf) {}
|
||||
~zero_copy_input_stream() { consume(); }
|
||||
|
||||
bool Next(const void** data, int* size) override {
|
||||
ANTLER_ASSERT(data, "data pointer is null");
|
||||
ANTLER_ASSERT(size, "{}: size pointer is null");
|
||||
|
||||
consume();
|
||||
|
||||
auto cur_pos = boost::asio::buffer_sequence_begin(streambuf.data());
|
||||
|
||||
*data = boost::asio::buffer_cast<void*>(*cur_pos);
|
||||
*size = static_cast<int>(boost::asio::buffer_size(*cur_pos));
|
||||
bytes_to_consume = *size;
|
||||
|
||||
if (++cur_pos == boost::asio::buffer_sequence_end(streambuf.data())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
void BackUp(int count) override {
|
||||
ANTLER_ASSERT(count <= bytes_to_consume, "count greater than bytes to consume");
|
||||
bytes_to_consume -= count;
|
||||
consume();
|
||||
}
|
||||
bool Skip(int count) override {
|
||||
consume();
|
||||
bytes_to_consume = count;
|
||||
consume(true);
|
||||
}
|
||||
int64_t ByteCount() const override {
|
||||
return total_consumed;
|
||||
}
|
||||
};
|
||||
|
||||
}} // eosio::testing
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils.hpp"
|
||||
#include "options.hpp"
|
||||
#include "interface.hpp"
|
||||
#include "node-client.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace eosio { namespace testing {
|
||||
|
||||
/// @brief function that perform entire debugging lifecycle
|
||||
/// @tparam Impl contract execution implementation, currently can be either native or wasm contract runner.
|
||||
/// passed as r-value as it is not supposed to live outside run function
|
||||
/// @param runner contract runner
|
||||
/// @param host nodeos debug plugin host name
|
||||
/// @param port nodeos debug plugin port number
|
||||
/// @param register_account_name account for debugging
|
||||
template <typename Impl>
|
||||
void run(runner_interface<Impl>&& runner,
|
||||
const std::string& host = "",
|
||||
const std::string& port = "",
|
||||
eosio::name register_account_name = {}) {
|
||||
|
||||
runner.init();
|
||||
// all calls to node_client are synchroneous, if error occurs exception is raised
|
||||
// init starts separate connection thread and returns after successful handshake with nodeos
|
||||
node_client::get().init(host, port);
|
||||
node_client::get().register_account(register_account_name);
|
||||
|
||||
// TODO: add signal interruption processing
|
||||
//if response is empty that means connection was closed, exiting
|
||||
while (auto response = node_client::get().wait_for_divert_flow()) {
|
||||
eosio::name receiver( response->divert_flow_result().receiver() );
|
||||
ANTLER_ASSERT( receiver, "receiver name must be set" );
|
||||
eosio::name code( response->divert_flow_result().code() );
|
||||
ANTLER_ASSERT( code, "code name must be set" );
|
||||
eosio::name action( response->divert_flow_result().action() );
|
||||
ANTLER_ASSERT( action, "action name must be set" );
|
||||
|
||||
runner.apply(receiver, code, action);
|
||||
|
||||
// letting nodeos know that action debugging was done
|
||||
node_client::get().return_control_flow(response->req_id());
|
||||
}
|
||||
}
|
||||
|
||||
}} //eosio::testing
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/ostream_sink.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/fmt/ostr.h>
|
||||
|
||||
namespace eosio { namespace utils {
|
||||
|
||||
// log to str and return it
|
||||
template<typename... Args>
|
||||
std::string log_to_str(const Args &... args)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
static auto oss_sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(oss);
|
||||
static spdlog::logger oss_logger("ostringstream", oss_sink);
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
//this means simply use string itself
|
||||
oss_logger.set_pattern("%v");
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
oss_logger.info(args...);
|
||||
auto ret_val = oss.str();
|
||||
oss.str("");
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger> get_std_logger() {
|
||||
static auto logger = spdlog::stdout_color_mt("antler logger");
|
||||
bool initialized = false;
|
||||
if (!initialized) {
|
||||
//format is <color>message level</color> message
|
||||
logger->set_pattern("%^%l%$ %v");
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
#define ANTLER_ERROR(...) \
|
||||
SPDLOG_LOGGER_ERROR(eosio::utils::get_std_logger(), __VA_ARGS__)
|
||||
#define ANTLER_INFO(...) \
|
||||
SPDLOG_LOGGER_INFO(eosio::utils::get_std_logger(), __VA_ARGS__)
|
||||
|
||||
|
||||
#define ANTLER_MULTILINE_MACRO_BEGIN do {
|
||||
#define ANTLER_MULTILINE_MACRO_END } while (0)
|
||||
|
||||
#define ANTLER_EXCEPTION std::runtime_error
|
||||
|
||||
#define ANTLER_THROW(...) \
|
||||
ANTLER_MULTILINE_MACRO_BEGIN \
|
||||
throw ANTLER_EXCEPTION( eosio::utils::log_to_str(__VA_ARGS__) ); \
|
||||
ANTLER_MULTILINE_MACRO_END
|
||||
|
||||
#define ANTLER_ASSERT(cond, ...) \
|
||||
ANTLER_MULTILINE_MACRO_BEGIN \
|
||||
if (!(cond)) \
|
||||
ANTLER_THROW("{}: {}", __func__, eosio::utils::log_to_str(__VA_ARGS__)); \
|
||||
ANTLER_MULTILINE_MACRO_END
|
||||
|
||||
#define ANTLER_QUIT(cond, ...) \
|
||||
ANTLER_MULTILINE_MACRO_BEGIN \
|
||||
if (!(cond)) \
|
||||
ANTLER_INFO("{}: {}", __func__, eosio::utils::log_to_str(__VA_ARGS__)); \
|
||||
ANTLER_MULTILINE_MACRO_END
|
||||
|
||||
std::ostream &operator<<(std::ostream &ost, const std::filesystem::path& p)
|
||||
{
|
||||
std::string tmp = fmt::format("{}", p.native());
|
||||
ost << tmp;
|
||||
return ost;
|
||||
}
|
||||
|
||||
}} // eosio::utils
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
inline const char* app_version() {
|
||||
return "${VERSION_FULL}";
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils.hpp"
|
||||
#include "file-utils.hpp"
|
||||
#include "interface.hpp"
|
||||
#include "intrinsics_setup.hpp"
|
||||
|
||||
#include <eosio/action.h>
|
||||
#include <eosio/chain.h>
|
||||
#include <eosio/crypto.h>
|
||||
#include <eosio/db.h>
|
||||
#include <eosio/permission.h>
|
||||
#include <eosio/print.h>
|
||||
#include <eosio/privileged.h>
|
||||
#include <eosio/system.h>
|
||||
#include <eosio/transaction.h>
|
||||
|
||||
#include <eosio/vm/host_function.hpp>
|
||||
#include <eosio/vm/backend.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
|
||||
namespace eosio { namespace testing { namespace wasm {
|
||||
struct intrinsics_type_converter : eosio::vm::type_converter<eosio::vm::standalone_function_t> {
|
||||
using type_converter::type_converter;
|
||||
using type_converter::from_wasm;
|
||||
EOS_VM_FROM_WASM(int*, (void* ptr)) { return static_cast<int*>(ptr); }
|
||||
EOS_VM_FROM_WASM(long int*, (void* ptr)) { return static_cast<long int*>(ptr); }
|
||||
EOS_VM_FROM_WASM(long unsigned int*, (void* ptr)) { return static_cast<long unsigned int*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const long unsigned int*, (void* ptr)) { return static_cast<long unsigned int*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const __int128 unsigned*, (void* ptr)) { return static_cast<__int128 unsigned*>(ptr); }
|
||||
EOS_VM_FROM_WASM(__int128 unsigned*, (void* ptr)) { return static_cast<__int128 unsigned*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const __int128*, (void* ptr)) { return static_cast<__int128*>(ptr); }
|
||||
EOS_VM_FROM_WASM(__int128*, (void* ptr)) { return static_cast<__int128*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const double*, (void* ptr)) { return static_cast<double*>(ptr); }
|
||||
EOS_VM_FROM_WASM(double*, (void* ptr)) { return static_cast<double*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const long double*, (void* ptr)) { return static_cast<long double*>(ptr); }
|
||||
EOS_VM_FROM_WASM(long double*, (void* ptr)) { return static_cast<long double*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const capi_checksum512*, (void* ptr)) { return static_cast<capi_checksum512*>(ptr); }
|
||||
EOS_VM_FROM_WASM(capi_checksum512*, (void* ptr)) { return static_cast<capi_checksum512*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const capi_checksum256*, (void* ptr)) { return static_cast<capi_checksum256*>(ptr); }
|
||||
EOS_VM_FROM_WASM(capi_checksum256*, (void* ptr)) { return static_cast<capi_checksum256*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const capi_checksum160*, (void* ptr)) { return static_cast<capi_checksum160*>(ptr); }
|
||||
EOS_VM_FROM_WASM(capi_checksum160*, (void* ptr)) { return static_cast<capi_checksum160*>(ptr); }
|
||||
EOS_VM_FROM_WASM(bool, (uint32_t value)) { return value ? 1 : 0; }
|
||||
EOS_VM_FROM_WASM(char*, (void* ptr)) { return static_cast<char*>(ptr); }
|
||||
EOS_VM_FROM_WASM(const char*, (void* ptr)) { return static_cast<char*>(ptr); }
|
||||
};
|
||||
|
||||
/// @brief wasm runner, loads wasm object, performs intrinsics setup and exposes interface to execute apply
|
||||
struct runner : testing::runner_interface<runner> {
|
||||
using rhf_t = eosio::vm::registered_host_functions<eosio::vm::standalone_function_t, eosio::vm::execution_interface, intrinsics_type_converter>;
|
||||
using backend_t = eosio::vm::backend<rhf_t, eosio::vm::interpreter>;
|
||||
|
||||
#define REGISTER_WASM_INTRINSIC(ID) \
|
||||
rhf_t::add<&::ID>("host", #ID);
|
||||
|
||||
runner(const std::string& path) {
|
||||
load(path);
|
||||
}
|
||||
|
||||
void apply(eosio::name receiver, eosio::name code, eosio::name action) {
|
||||
backend->call("env", "apply", receiver.value, code.value, action.value);
|
||||
}
|
||||
inline object_type get_type() {
|
||||
return object_type::wasm;
|
||||
}
|
||||
void init() {
|
||||
eosio::testing::native::setup_rpc_intrinsics();
|
||||
|
||||
INTRINSICS(REGISTER_WASM_INTRINSIC);
|
||||
}
|
||||
private:
|
||||
eosio::vm::wasm_allocator wasm_allocator;
|
||||
std::unique_ptr<backend_t> backend;
|
||||
|
||||
void load(const std::string& path) {
|
||||
auto contract_type = utils::get_file_type(path.c_str());
|
||||
if ( contract_type != utils::file_type::wasm ) {
|
||||
ANTLER_THROW("not a wasm file: {}", file_type_str(contract_type));
|
||||
}
|
||||
|
||||
auto wasm_data = read_wasm(path);
|
||||
backend.reset( new backend_t(wasm_data, &wasm_allocator) );
|
||||
}
|
||||
|
||||
std::vector<uint8_t> read_wasm( const std::string& fname ) {
|
||||
std::ifstream wasm_file(fname, std::ios::binary);
|
||||
ANTLER_ASSERT( wasm_file.is_open(), "error opening file: {}", fname );
|
||||
|
||||
wasm_file.seekg(0, std::ios::end);
|
||||
int len = wasm_file.tellg();
|
||||
ANTLER_ASSERT( len, "wasm file is empty", fname );
|
||||
ANTLER_ASSERT( len != -1, "file error: {}", wasm_file.rdstate() );
|
||||
|
||||
std::vector<uint8_t> wasm;
|
||||
wasm.resize(len);
|
||||
|
||||
wasm_file.seekg(0, std::ios::beg);
|
||||
wasm_file.read((char*)wasm.data(), wasm.size());
|
||||
wasm_file.close();
|
||||
|
||||
return std::move(wasm);
|
||||
}
|
||||
};
|
||||
}}} // eosio::testing::wasm
|
||||
Vendored
+2
-1
@@ -1,2 +1,3 @@
|
||||
add_subdirectory(wabt)
|
||||
add_subdirectory(eos-vm)
|
||||
add_subdirectory(eos-vm)
|
||||
add_subdirectory(antler-interfaces)
|
||||
+1
Submodule tools/external/antler-interfaces added at 46954ebe83
Reference in New Issue
Block a user