emscripten-core / emscripten-core/emscripten
Is there any better way to bind a function that using std::function or lambda as argument or return value?
- Dominant language
- C++
- Stars
- 27.6k
- Forks
- 3.6k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 105
Description
I may have a c++ function like that:
```c++
std::function function_callback_function()
{
return [](double d, bool b)
{
return d;
};
}
```
Then I use embind to bind it to js:
```c++
emscripten::function("function_callback_function", &function_callback_function);
```
But something was wrong because embind does not support the converter for c++ class std::function or lambda;
I have to write the converter by myself.
First, check template is std::function or not:
```c++
template
struct is_function : std::false_type {};
template
struct is_function> : std::true_type {};
```
Define typeID for that function template:
```c++
template
struct TypeID::type>::value>>
{
static constexpr TYPEID get() { return TypeID::get(); }
};
```
Define bindingType for that function template:
```c++
template
struct BindingType>
{
using ValBinding = BindingType;
using WireType = ValBinding::WireType;
static WireType toWireType(const std::function &f) {
// converter code
}
static std::function fromWireType(WireType value) {
// converter code
}
}
```
BindingType::fromWireType is very easy, returning a lambda that call the jsvalue using operator() is the end:
```c++
static std::function fromWireType(WireType value) {
return [=](Args...args) -> R
{
val &&v = ValBinding::fromWireType(value);
val r = v(args...);
if constexpr (std::is_same_v)
{
// do nothing
}
else
{
return r.template as();
}
};
}
```
BindingType::toWireType is not easy, because the emscripten::val cannot be created using a c function.
I create a C function wrapper to wrap the std::function or lambda template for a shared function signature.
And I use a std::map to save the pointer of the C function wrapper.
Then I bind a helper function to find the specified C function wrapper from the std::map above and call the target C function.
Finally I use a string to new a emscripten::val that represents a function in js, this function will pass the function id (as key for std::map above) and arguments to the helper function above, then the helper function could find the target C function to call and get the result.
```c++
// define C function wrapper
typedef std::function cfunction_wrapper;
// the map to store the C function id and wrapper
std::map cfunction_map;
// helper function, used to find and call the target C function, and delete it?
emscripten::val cfunction_helper(std::string key, emscripten::val args)
{
auto it = cfunction_map.find(key);
if (it != cfunction_map.end())
{
auto func_ptr = it->second;
emscripten::val r = (*func_ptr)(args);
// delete it? should I?
// cfunction_map.erase(it);
// delete func_ptr;
return r;
}
return emscripten::val::undefined();
}
EMSCRIPTEN_BINDINGS(😅)
{
// register the helper function first
function("cfunction_helper", &cfunction_helper);
// register the function that would return a lambda value
function("function_callback_function", &function_callback_function);
}
template
struct StringTypeReplace {
using type = std::string;
};
template
static val NewFunctionVal(std::tuple tuple)
{
return std::apply(&val::new_, std::tuple_cat(std::tuple(val::global("Function")), tuple));
}
template
Tuple GetJsFunctionArgsDeclare(std::index_sequence)
{
return Tuple(std::tuple_element_t(std::string("a") + std::to_string(I))...);
}
template
auto JsArgumentToCppObject(val args)
{
return args[I].as();
}
template
Tuple ArgumentsToTuple_impl(val args, std::index_sequence)
{
return Tuple{JsArgumentToCppObject, I>(args)...};
}
template
std::tuple...> ArgumentsToTuple(val args)
{
return ArgumentsToTuple_impl...>>(args, std::make_index_sequence());
}
template
struct BindingType>
{
using ValBinding = BindingType;
using WireType = ValBinding::WireType;
static WireType toWireType(const std::function &f) {
// wrap the std::function or lambda
cfunction_wrapper wrap_func = [=](val args)
{
if (args.isArray()) {
// convert argument array to std::tuple
auto &&tuple = ArgumentsToTuple(args);
if constexpr (std::is_same_v)
{
std::apply(f, tuple);
return val::undefined();
}
else
{
R &&r = std::apply(f, tuple);
return val(r);
}
}
return val::undefined();
};
// use function pointer as function key, make it be unique
auto func_ptr = new cfunction_wrapper(wrap_func);
std::string key = std::to_string(reinterpret_cast(func_ptr));
cfunction_map[key] = func_ptr;
// convert Args... to string 'a0,a1,a2...' as js function arguments declaration
using tuple_type = std::tuple::type...>;
auto tuple = GetJsFunctionArgsDeclare(std::make_index_sequence());
std::ostringstream oss;
std::apply([&oss](const auto&... args) {
((oss << args << ","), ...);
}, tuple);
std::string arg_array_str = oss.str();
if (!arg_array_str.empty()) {
arg_array_str.erase(arg_array_str.size() - 1);
}
// create a function val that would call the helper function
val &&func = NewFunctionVal(std::tuple_cat(tuple, std::tuple(std::string("return Module.call_cfunction(\"") + key + "\", [" + arg_array_str + "]);")));
return ValBinding::toWireType(func);
}
static std::function fromWireType(WireType value) {
// converter code
}
};
```
In that way, I could use lambda or std::function as function argument, return value, and even if class property. But I don't think it is a good way, I'm not sure when the function pointer should be released? It bothered me very much.
Is there any way to know when the emscripten::val would be released, or any better way to bind someone using std::function or lambda?
Glad to read your thoughts!🥺
Contributor guide
Assessment
This issue has not been assessed yet.