pmed / v8pp

Bind C++ functions and classes into V8 JavaScript engine
http://pmed.github.io/v8pp/
Other
901 stars 121 forks source link

Support std::function as function parameter #142

Open ivalylo opened 3 years ago

ivalylo commented 3 years ago

Hello,

Seems this is not supported currently, but it's quite handy to have (for example callbacks). I tried implementing it, but I'm very new to both v8 and v8pp. Also not that good with template programming either. But here is something I got working, and maybe can be used to build somewhat better implementation.

template<typename T, typename ...Args>
struct V8CallFunc
{
   V8CallFunc(v8::Isolate* isolate, v8::Local<v8::Function> func)
      : isolate(isolate), func(isolate, func)
   {
   }

   T operator()(Args&&... args) const
   {
      v8::EscapableHandleScope scope(isolate);

      int const arg_count = sizeof...(Args);
      // +1 to allocate array for arg_count == 0
      v8::Local<v8::Value> v8_args[arg_count + 1] =
      {
         to_v8(isolate, std::forward<Args>(args))...
      };

      v8::TryCatch try_catch(isolate);
      v8::Local<v8::Value> result;
      v8::Local<v8::Function> funcLocal = func.Get(isolate);
      bool const not_empty = funcLocal->Call(isolate->GetCurrentContext(), funcLocal, arg_count, v8_args).ToLocal(&result);

      if (try_catch.HasCaught())
      {
         std::string const msg = v8pp::from_v8<std::string>(isolate,
            try_catch.Exception()->ToString(isolate->GetCurrentContext()).ToLocalChecked());
         THROW(std::runtime_error, msg);
      }

      assert(not_empty);
      return from_v8<T>(isolate, scope.Escape(result));
   }

   v8::Isolate* isolate;
   v8::Eternal<v8::Function> func;
};

template<typename ...Args>
struct V8CallFunc<void, Args...>
{
   V8CallFunc(v8::Isolate* isolate, v8::Local<v8::Function> func)
      : isolate(isolate), func(isolate, func)
   {
   }

   void operator()(Args&&... args) const
   {
      v8::EscapableHandleScope scope(isolate);

      int const arg_count = sizeof...(Args);
      // +1 to allocate array for arg_count == 0
      v8::Local<v8::Value> v8_args[arg_count + 1] =
      {
         to_v8(isolate, std::forward<Args>(args))...
      };

      v8::TryCatch try_catch(isolate);
      v8::Local<v8::Function> funcLocal = func.Get(isolate);
      funcLocal->Call(isolate->GetCurrentContext(), funcLocal, arg_count, v8_args);

      if (try_catch.HasCaught())
      {
         std::string const msg = v8pp::from_v8<std::string>(isolate,
            try_catch.Exception()->ToString(isolate->GetCurrentContext()).ToLocalChecked());
         THROW(std::runtime_error, msg);
      }
   }

   v8::Isolate* isolate;
   v8::Eternal<v8::Function> func;
};

template <typename Ret, typename... Args> __forceinline
std::function<Ret(Args...)> createV8CallFunc(std::function<Ret(Args...)> f, v8::Isolate* isolate, v8::Local<v8::Function> func)
{
    return V8CallFunc<Ret, Args...>(isolate, func);
}

template<typename T>
struct convert<std::function<T>>
{
    using from_type = std::function<T>;
    using to_type = v8::Local<v8::Function>;

    static bool is_valid(v8::Isolate* isolate, v8::Local<v8::Value> value)
    {
        return !value.IsEmpty() && value->IsFunction();
    }

    static from_type from_v8(v8::Isolate* isolate, v8::Local<v8::Value> value)
    {
        if (!is_valid(isolate, value))
        {
            THROW(invalid_argument, isolate, value, "Function");
        }
        auto func = value.As<v8::Function>();
        return createV8CallFunc(from_type(), isolate, func);
    }

    static to_type to_v8(v8::Isolate* isolate, const from_type& func)
    {
        using T_type = typename std::decay<from_type>::type;
        T_type func_copy = func;

        v8::Local<v8::Function> fn = v8::Function::New(isolate->GetCurrentContext(),
            &detail::forward_function<raw_ptr_traits, T_type>,
            detail::set_external_data(isolate, std::forward<T_type>(func_copy))).ToLocalChecked();
        return fn;
    }
};
pmed commented 3 years ago

Hi @ivalylo

there is a wrap_function() helper in v8pp/function.hpp that allows to wrap any callable C++ object (pointer to free function, lambda, maybe also std::function) to a V8 Function value. So you could try to use the wrapped function as a callback argument.

Btw, there is also a call_v8() helper in v8pp/call_v8.hpp that seems to be very similar to your V8CallFunc::operator().

YarikTH commented 3 years ago

It seems that proper support for std::function overload for v8pp::convert require including full content of call_from_v8.hpp and call_v8.hpp. It can be easily done from the technical point of view, but I have no idea about the architectural point of view.

I made my ugly version using forward declaration, but it doesn't work if used doesn't include call_v8.hpp or call_from_v8.hpp or both while using std::function bindings. And there are not so clear messages "undefined reference to v8::Local<v8::Value> v8pp::call_v8<double>()"

What do you think, @pmed?

pmed commented 1 year ago

Hi @YarikTH

do you mean PR #169? Thanks, it looks viable, maybe with a test case added.

I hope call_v8() can be used somehow inside convert<std::function<>> specialization. Anyways call_v8() i just a helper for v8::Function::Call(), that we can use in the specialization.

YarikTH commented 1 year ago

Yes. But I gave up using V8 a long time ago so I can't collaborate more.