add C++ wrappers, simplify linking external functions (#71)

* add a set of C++ wrappers, simplify linking external functions

* m3_config_platforms.h: don't define min/max for C++
extensions
Ivan Grokhotkov 4 years ago committed by GitHub
parent 5b60775b76
commit b0874355d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -328,3 +328,23 @@ jobs:
run: |
diff -q platforms/esp32-idf/main/main.cpp platforms/esp32-pio/src/main.cpp
# TODO: also check that the build flags are in sync
build-cpp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Run CMake
run: |
cd platforms/cpp
mkdir build
cd build
cmake ..
- name: Build
run: |
cd platforms/cpp
cmake --build build
- name: Run
run: |
cd platforms/cpp/build
./wasm3_cpp_example

@ -0,0 +1,3 @@
build
cmake-build-debug
wasm/test_prog.wasm

@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.9)
project(wasm3_cpp_example)
set(target ${CMAKE_PROJECT_NAME})
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../source ${CMAKE_BINARY_DIR}/m3)
add_executable(${target} main.cpp)
target_link_libraries(${target} PRIVATE m3)
add_subdirectory(wasm3_cpp)
target_link_libraries(${target} PRIVATE wasm3_cpp)
target_compile_options(${target} PUBLIC -g)
target_compile_options(m3 PUBLIC -g)
target_compile_options(m3 PRIVATE -Dd_m3LogOutput=0)

@ -0,0 +1,77 @@
## C++ wrapper
This example shows how to embed WASM3 into a C++ application. It uses a header-only library, `wasm3_cpp.h`, provided in `wasm3_cpp` subdirectory. Like WASM3 itself, this library can be included into CMake projects using `add_subdirectory` function.
The main code of the example in `main.cpp` initializes WASM3, loads a WebAssembly module, links two external functions to the module, and executes two functions defined in WebAssembly.
The WebAssembly module source code is inside `wasm` subdirectory.
### `wasm3_cpp.h` reference
All the classes are located in `wasm3` namespace.
#### Class `environment`
`environment::environment()` — create a new WASM3 environment. Runtimes, modules are owned by an environment.
`runtime environment::new_runtime(size_t stack_size_bytes)` — create new runtime inside the environment.
`module environment::parse_module(std::istream &in)` or `module environment::parse_module(const uint8_t *data, size_t size)` — parse a WASM binary module.
#### Class `runtime`
`runtime` objects are created using `environment::new_runtime` method, see above.
`void runtime::load(module &m)` — load a parsed module into the runtime.
`function runtime::find_function(const char *name)` — find a function defined in one of the loaded modules, by name. Raises a `wasm3::error` exception if the function is not found.
#### Class `module`
`module` objects are created by `environment::parse_module`. Parsed modules can be loaded into a `runtime` object. One module can only be loaded into one runtime.
Before loading a module, you may need to link some external functions to it:
`template <auto Fn> void module::link<Fn>(const char *mod, const char *func)` — link a function `Fn` to module named `mod` under the name `func`. To link to any module, use `mod="*"`.
`Fn` has to be either a non-member function or a static member function. At the moment WASM3 doesn't pass any context along with the external function call, so binding non-static member functions is not possible.
Currently, the following types of arguments can be passed to functions linked this way:
* int32_t
* int64_t
* float
* double
* const/non-const pointers
Automatic conversion of other integral types may be implemented in the future.
If the module doesn't reference an imported function named `func`, an exception is thrown. To link a function "optionally", i.e. without throwing an exception if the function is not imported, use `module::link_optional` instead.
#### Class `function`
`function` object can be obtained from a `runtime`, looking up the function by name. Function objects are used to call WebAssembly functions.
`template <typename Ret> Ret function::call()` — call a WebAssembly function which doesn't take any arguments. The return value of the function is automatically converted to the type `Ret`. Note that you need to specify the return type when using this template function, and the type has to match the type returned by the WebAssembly function.
`template <typename Ret, typename ...Args> Ret function::call(Args...)` — same as above, but also allows passing arguments to the WebAssembly function. Note that due to a limitation of WASM3 API, the arguments are first converted to strings, and then passed to WASM3. The strings are then converted to the appropriate types based on the WebAssembly function signature. This conversion is limited to the following types: `int32_t`, `int64_t`, `float`, `double`.
`template <typename Ret, typename ...Args> Ret function::call_argv(Args...)` — same as above, except that this function takes arguments as C strings (`const char*`).
### Building and running
This directory is a CMake project, and can be built as follows:
```bash
mkdir build
cd build
cmake ..
cmake --build .
```
Then run the example:
```bash
./wasm3_cpp_example
```

@ -0,0 +1,44 @@
#include <cstdio>
#include "wasm3_cpp.h"
#include "wasm/test_prog.wasm.h"
int sum(int a, int b)
{
return a + b;
}
void ext_memcpy(void* dst, const void* arg, int64_t size)
{
memcpy(dst, arg, (size_t) size);
}
int main(void)
{
std::cout << "Loading WebAssembly..." << std::endl;
try {
wasm3::environment env;
wasm3::runtime runtime = env.new_runtime(1024);
wasm3::module mod = env.parse_module(test_prog_wasm, test_prog_wasm_len);
runtime.load(mod);
mod.link<sum>("*", "sum");
mod.link<ext_memcpy>("*", "ext_memcpy");
{
wasm3::function test_fn = runtime.find_function("test");
auto res = test_fn.call<int>(20, 10);
std::cout << "result: " << res << std::endl;
}
{
wasm3::function memcpy_test_fn = runtime.find_function("test_memcpy");
auto res = memcpy_test_fn.call<int64_t>();
std::cout << "result: 0x" << std::hex << res << std::dec << std::endl;
}
}
catch(wasm3::error &e) {
std::cerr << "WASM3 error: " << e.what() << std::endl;
return 1;
}
return 0;
}

@ -0,0 +1,18 @@
NAME=test_prog
SRC=$(NAME).c
WASM=$(NAME).wasm
HEADER=$(NAME).wasm.h
EMCC_FLAGS=-s STANDALONE_WASM -s ERROR_ON_UNDEFINED_SYMBOLS=0 -O3
all: $(HEADER)
clean:
rm -f $(HEADER) $(WASM)
.PHONY: all clean
$(WASM): $(SRC)
emcc $< -o $@ $(EMCC_FLAGS)
$(HEADER): $(WASM)
xxd -i $< >$@

@ -0,0 +1,24 @@
#include <stddef.h>
#include <stdint.h>
extern int sum(int, int);
extern int ext_memcpy(void*, const void*, size_t);
#define WASM_EXPORT __attribute__((used)) __attribute__((visibility ("default")))
int WASM_EXPORT test(int32_t arg1, int32_t arg2)
{
int x = arg1 + arg2;
int y = arg1 - arg2;
return sum(x, y) / 2;
}
int64_t WASM_EXPORT test_memcpy(void)
{
int64_t x = 0;
int32_t low = 0x01234567;
int32_t high = 0x89abcdef;
ext_memcpy(&x, &low, 4);
ext_memcpy(((uint8_t*)&x) + 4, &high, 4);
return x;
}

@ -0,0 +1,25 @@
unsigned char test_prog_wasm[] = {
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x15, 0x04, 0x60,
0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x60, 0x03, 0x7f, 0x7f,
0x7f, 0x01, 0x7f, 0x60, 0x00, 0x01, 0x7e, 0x02, 0x1c, 0x02, 0x03, 0x65,
0x6e, 0x76, 0x0a, 0x65, 0x78, 0x74, 0x5f, 0x6d, 0x65, 0x6d, 0x63, 0x70,
0x79, 0x00, 0x02, 0x03, 0x65, 0x6e, 0x76, 0x03, 0x73, 0x75, 0x6d, 0x00,
0x00, 0x03, 0x04, 0x03, 0x01, 0x03, 0x00, 0x05, 0x06, 0x01, 0x01, 0x80,
0x02, 0x80, 0x02, 0x06, 0x09, 0x01, 0x7f, 0x01, 0x41, 0x80, 0x8c, 0xc0,
0x02, 0x0b, 0x07, 0x28, 0x04, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79,
0x02, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x04, 0x0b, 0x74, 0x65,
0x73, 0x74, 0x5f, 0x6d, 0x65, 0x6d, 0x63, 0x70, 0x79, 0x00, 0x03, 0x06,
0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x02, 0x0a, 0x71, 0x03, 0x03,
0x00, 0x01, 0x0b, 0x59, 0x02, 0x01, 0x7f, 0x01, 0x7e, 0x23, 0x00, 0x41,
0x10, 0x6b, 0x22, 0x00, 0x24, 0x00, 0x20, 0x00, 0x42, 0x00, 0x37, 0x03,
0x08, 0x20, 0x00, 0x41, 0xe7, 0x8a, 0x8d, 0x09, 0x36, 0x02, 0x04, 0x20,
0x00, 0x41, 0xef, 0x9b, 0xaf, 0xcd, 0x78, 0x36, 0x02, 0x00, 0x20, 0x00,
0x41, 0x08, 0x6a, 0x20, 0x00, 0x41, 0x04, 0x6a, 0x41, 0x04, 0x10, 0x00,
0x1a, 0x20, 0x00, 0x41, 0x08, 0x6a, 0x41, 0x04, 0x72, 0x20, 0x00, 0x41,
0x04, 0x10, 0x00, 0x1a, 0x20, 0x00, 0x29, 0x03, 0x08, 0x21, 0x01, 0x20,
0x00, 0x41, 0x10, 0x6a, 0x24, 0x00, 0x20, 0x01, 0x0b, 0x11, 0x00, 0x20,
0x00, 0x20, 0x01, 0x6a, 0x20, 0x00, 0x20, 0x01, 0x6b, 0x10, 0x01, 0x41,
0x02, 0x6d, 0x0b, 0x0b, 0x0a, 0x01, 0x00, 0x41, 0x80, 0x0c, 0x0b, 0x03,
0xa0, 0x06, 0x50
};
unsigned int test_prog_wasm_len = 255;

@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.5)
add_library(wasm3_cpp INTERFACE)
target_include_directories(wasm3_cpp INTERFACE include)
target_compile_features(wasm3_cpp INTERFACE cxx_std_17)

@ -0,0 +1,414 @@
#pragma once
#include <tuple>
#include <algorithm>
#include <type_traits>
#include <iostream>
#include <vector>
#include <memory>
#include <iterator>
#include <m3_api_defs.h>
#include "wasm3.h"
/* FIXME: remove when there is a public API to get function return value */
#include "m3_env.h"
namespace wasm3 {
/** @cond */
namespace detail {
typedef uint64_t *stack_type;
typedef void *mem_type;
template<typename T, typename...> struct first_type { typedef T type; };
typedef const void *(*m3_api_raw_fn)(IM3Runtime, uint64_t *, void *);
template<typename T>
void arg_from_stack(T &dest, stack_type &psp, mem_type mem) {
dest = *(T *) (psp);
psp++;
}
template<typename T>
void arg_from_stack(T* &dest, stack_type &psp, mem_type _mem) {
dest = (void*) m3ApiOffsetToPtr(* ((u32 *) (psp++)));
};
template<typename T>
void arg_from_stack(const T* &dest, stack_type &psp, mem_type _mem) {
dest = (void*) m3ApiOffsetToPtr(* ((u32 *) (psp++)));
};
template<char c>
struct m3_sig {
static const char value = c;
};
template<typename T> struct m3_type_to_sig;
template<> struct m3_type_to_sig<i32> : m3_sig<'i'> {};
template<> struct m3_type_to_sig<i64> : m3_sig<'I'> {};
template<> struct m3_type_to_sig<f32> : m3_sig<'f'> {};
template<> struct m3_type_to_sig<f64> : m3_sig<'F'> {};
template<> struct m3_type_to_sig<void> : m3_sig<'v'> {};
template<> struct m3_type_to_sig<void *> : m3_sig<'*'> {};
template<> struct m3_type_to_sig<const void *> : m3_sig<'*'> {};
template<typename Ret, typename ... Args>
struct m3_signature {
constexpr static size_t n_args = sizeof...(Args);
constexpr static const char value[n_args + 4] = {
m3_type_to_sig<Ret>::value,
'(',
m3_type_to_sig<Args>::value...,
')',
0
};
};
template <typename ...Args>
static void get_args_from_stack(stack_type &sp, mem_type mem, std::tuple<Args...> &tuple) {
std::apply([&](auto &... item) {
(arg_from_stack(item, sp, mem), ...);
}, tuple);
}
template<auto func>
struct wrap_helper;
template <typename Ret, typename ...Args, Ret (*Fn)(Args...)>
struct wrap_helper<Fn> {
static const void *wrap_fn(IM3Runtime rt, stack_type sp, mem_type mem) {
Ret *ret_ptr = (Ret *) (sp);
std::tuple<Args...> args;
get_args_from_stack(sp, mem, args);
Ret r = std::apply(Fn, args);
*ret_ptr = r;
return m3Err_none;
}
};
template <typename ...Args, void (*Fn)(Args...)>
struct wrap_helper<Fn> {
static const void *wrap_fn(IM3Runtime rt, stack_type sp, mem_type mem) {
std::tuple<Args...> args;
get_args_from_stack(sp, mem, args);
std::apply(Fn, args);
return m3Err_none;
}
};
template<auto value>
class m3_wrapper;
template<typename Ret, typename ... Args, Ret (*Fn)(Args...)>
class m3_wrapper<Fn> {
public:
static M3Result link(IM3Module io_module,
const char *const i_moduleName,
const char *const i_functionName) {
return m3_LinkRawFunction(io_module, i_moduleName, i_functionName, m3_signature<Ret, Args...>::value,
&wrap_helper<Fn>::wrap_fn);
}
};
} // namespace detail
/** @endcond */
class module;
class runtime;
class function;
/**
* Exception thrown for wasm3 errors.
*
* Use error:what() to get the reason as a string.
*/
class error : public std::runtime_error {
public:
explicit error(M3Result err) : std::runtime_error(err) {}
};
/** @cond */
namespace detail {
void check_error(M3Result err) {
if (err != m3Err_none) {
throw error(err);
}
}
} // namespace detail
/** @endcond */
/**
* Wrapper for WASM3 environment.
*
* Runtimes, modules are owned by an environment.
*/
class environment {
public:
environment() {
m_env.reset(m3_NewEnvironment(), m3_FreeEnvironment);
if (m_env == nullptr) {
throw std::bad_alloc();
}
}
/**
* Create new runtime
*
* @param stack_size_bytes size of the WASM stack for this runtime
* @return runtime object
*/
runtime new_runtime(size_t stack_size_bytes);
/**
* Parse a WASM module from file
*
* The parsed module is not loaded into any runtime. Use runtime::load to
* load the module after parsing it.
*
* @param in file (WASM binary)
* @return module object
*/
module parse_module(std::istream &in);
/**
* Parse a WASM module from binary data
*
* @param data pointer to the start of the binary
* @param size size of the binary
* @return module object
*/
module parse_module(const uint8_t *data, size_t size);
protected:
std::shared_ptr<struct M3Environment> m_env;
};
/**
* Wrapper for the runtime, where modules are loaded and executed.
*/
class runtime {
public:
/**
* Load the module into runtime
* @param mod module parsed by environment::parse_module
*/
void load(module &mod);
/**
* Get a function handle by name
*
* If the function is not found, throws an exception.
* @param name name of a function, c-string
* @return function object
*/
function find_function(const char *name);
protected:
friend class environment;
runtime(const std::shared_ptr<M3Environment> &env, size_t stack_size_bytes)
: m_env(env) {
m_runtime.reset(m3_NewRuntime(env.get(), stack_size_bytes, nullptr), &m3_FreeRuntime);
if (m_runtime == nullptr) {
throw std::bad_alloc();
}
}
/* runtime extends the lifetime of the environment */
std::shared_ptr<M3Environment> m_env;
std::shared_ptr<M3Runtime> m_runtime;
};
/**
* Module object holds a webassembly module
*
* It can be constructed by parsing a WASM binary using environment::parse_module.
* Functions can be linked to the loaded module.
* Once constructed, modules can be loaded into the runtime.
*/
class module {
public:
/**
* Link an external function.
*
* Throws an exception if the module doesn't reference a function with the given name.
*
* @tparam func Function to link (a function pointer)
* @param module Name of the module to link the function to, or "*" to link to any module
* @param function Name of the function (as referenced by the module)
*/
template<auto func>
void link(const char *module, const char *function);
/**
* Same as module::link, but doesn't throw an exception if the function is not referenced.
*/
template<auto func>
void link_optional(const char *module, const char *function);
protected:
friend class environment;
friend class runtime;
module(const std::shared_ptr<M3Environment> &env, std::istream &in_wasm) {
std::vector<uint8_t> in_bytes;
std::copy(std::istream_iterator<uint8_t>(in_wasm),
std::istream_iterator<uint8_t>(),
std::back_inserter(in_bytes));
parse(env.get(), in_bytes.data(), in_bytes.size());
}
module(const std::shared_ptr<M3Environment> &env, const uint8_t *data, size_t size) : m_env(env) {
parse(env.get(), data, size);
}
void parse(IM3Environment env, const uint8_t *data, size_t size) {
IM3Module p;
M3Result err = m3_ParseModule(env, &p, data, size);
detail::check_error(err);
m_module.reset(p, [this](IM3Module module) {
if (!m_loaded) {
m3_FreeModule(module);
}
});
}
void load_into(IM3Runtime runtime) {
M3Result err = m3_LoadModule(runtime, m_module.get());
detail::check_error(err);
m_loaded = true;
}
std::shared_ptr<M3Environment> m_env;
std::shared_ptr<M3Module> m_module;
bool m_loaded = false;
};
/**
* Handle of a function. Can be obtained from runtime::find_function method by name.
*/
class function {
public:
/**
* Call the function with the provided arguments, expressed as strings.
*
* Arguments are passed as strings. WASM3 automatically converts them into the types expected
* by the function being called.
*
* Note that the type of the return value must be explicitly specified as a template argument.
*
* @return the return value of the function.
*/
template<typename Ret, typename ... Args>
typename detail::first_type<Ret,
typename std::enable_if<std::is_convertible<Args, const char*>::value>::type...>::type
call_argv(Args... args) {
/* std::enable_if above checks that all argument types are convertible const char* */
const char* argv[] = {args...};
M3Result res = m3_CallWithArgs(m_func, sizeof...(args), argv);
detail::check_error(res);
Ret ret;
/* FIXME: there should be a public API to get the return value */
auto sp = (detail::stack_type) m_runtime->stack;
detail::arg_from_stack(ret, sp, nullptr);
return ret;
}
/**
* Call the function with the provided arguments (int/float types).
*
* WASM3 only accepts string arguments when calling WASM functions, and automatically converts them
* into the correct type based on the called function signature.
*
* This function provides a way to pass integer/float types, by first converting them to strings,
* and then letting WASM3 do the reverse conversion. This is to be fixed once WASM3 gains an equivalent
* of m3_CallWithArgs which can accept arbitrary types, not just strings.
*
* Note that the type of the return value must be explicitly specified as a template argument.
*
* @return the return value of the function.
*/
template<typename Ret, typename ... Args>
Ret call(Args... args) {
std::string argv_str[] = {std::to_string(args)...};
const char* argv[sizeof...(Args)];
for (size_t i = 0; i < sizeof...(Args); ++i) {
argv[i] = argv_str[i].c_str();
}
M3Result res = m3_CallWithArgs(m_func, sizeof...(args), argv);
detail::check_error(res);
Ret ret;
/* FIXME: there should be a public API to get the return value */
auto sp = (detail::stack_type) m_runtime->stack;
detail::arg_from_stack(ret, sp, nullptr);
return ret;
}
/**
* Call the function which doesn't take any arguments.
* Note that the type of the return value must be explicitly specified as a template argument.
* @return the return value of the function.
*/
template<typename Ret>
Ret call() {
M3Result res = m3_Call(m_func);
detail::check_error(res);
Ret ret;
/* FIXME: there should be a public API to get the return value */
auto sp = (detail::stack_type) m_runtime->stack;
detail::arg_from_stack(ret, sp, nullptr);
return ret;
}
protected:
friend class runtime;
function(const std::shared_ptr<M3Runtime> &runtime, const char *name) : m_runtime(runtime) {
M3Result err = m3_FindFunction(&m_func, runtime.get(), name);
detail::check_error(err);
assert(m_func != nullptr);
}
std::shared_ptr<M3Runtime> m_runtime;
M3Function *m_func = nullptr;
};
runtime environment::new_runtime(size_t stack_size_bytes) {
return runtime(m_env, stack_size_bytes);
}
module environment::parse_module(std::istream &in) {
return module(m_env, in);
}
module environment::parse_module(const uint8_t *data, size_t size) {
return module(m_env, data, size);
}
void runtime::load(module &mod) {
mod.load_into(m_runtime.get());
}
function runtime::find_function(const char *name) {
return function(m_runtime, name);
}
template<auto func>
void module::link(const char *module, const char *function) {
M3Result ret = detail::m3_wrapper<func>::link(m_module.get(), module, function);
detail::check_error(ret);
}
template<auto func>
void module::link_optional(const char *module, const char *function) {
M3Result ret = detail::m3_wrapper<func>::link(m_module.get(), module, function);
if (ret == m3Err_functionLookupFailed) {
return;
}
detail::check_error(ret);
}
} // namespace wasm3

@ -178,11 +178,16 @@
# define M3_WEAK __attribute__((weak))
# endif
# ifndef min
# define min(A,B) (((A) < (B)) ? (A) : (B))
# endif
# ifndef max
# define max(A,B) (((A) > (B)) ? (A) : (B))
/* Don't define min and max when compiling C++ sources;
* this causes issues in STL where min() and max() member functions are defined.
*/
# ifndef __cplusplus
# ifndef min
# define min(A,B) (((A) < (B)) ? (A) : (B))
# endif
# ifndef max
# define max(A,B) (((A) > (B)) ? (A) : (B))
# endif
# endif
#define M3_INIT(field) memset(&field, 0, sizeof(field))

Loading…
Cancel
Save