-
Notifications
You must be signed in to change notification settings - Fork 119
Description
TL;DR
- returning class instance by value creates too many copies of it (mostly irrelevant)
- returning class instance by shared_ptr doesn't "register" it in v8
electron v9.1
node.js v12.14.1
V8 v8.3
v8pp v1.7.0
Am i doing something wrong here? Is this behaviour intended? Is there a better solution?
I have a project that was using nbind for several years, but since it was abandoned, decision was made to upgrade to latest LTS node.js release & adopt another V8 binding library.
Please forgive me if code smells, I write in JS most of the time.
I've set up a playground with v8pp + electron to check things out and immediately stumbled upon this weird behaviour:
// objects.hpp
...
class A {
private:
int x_;
static int instance_counter;
public:
typedef std::shared_ptr<A> Ptr;
int id;
A(int x)
: id(instance_counter++),
x_(x)
{
std::cout << "C++: " << "A#" << id << " constructed" << std::endl;
}
A(const A& obj)
: id(instance_counter++)
{
x_ = obj.x_;
std::cout << "C++: " << "A#" << id << " copy-constructed from " << obj.id << std::endl;
}
~A()
{
std::cout << "C++: " << "A#" << id << " destructed" << std::endl;
}
int get_x() { return x_; }
void set_x(int value) { x_ = value; }
};
int A::instance_counter = 1;
class B {
public:
// this works just as expected, no additional copies of A created
void do_something_with_a(A::Ptr obj) {
obj->set_x(obj->get_x() + 1);
}
// this creates 3(!) instances of A, 1 "original" + 2 copies
A get_a_by_value() {
A obj(1);
return obj;
}
// this doesn't work when using shared_ptr_traits, but that's to be expected i guess
A* get_a_by_rptr() {
A* obj = new A(10);
return obj;
}
// doesn't work, doesn't need to work
A& get_a_by_ref() {
A obj(100);
return obj;
}
// this one doesn't work while expected to
A::Ptr get_a_by_sptr() {
A::Ptr obj = std::make_shared<A>(1000);
return obj;
}
};// bindings.cpp
...
void InitAll(
v8::Local<v8::Object> exports
) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8pp::module module(isolate);
v8pp::class_<A, v8pp::shared_ptr_traits> A_class(isolate);
A_class
.ctor<int>()
// .auto_wrap_objects()
.set("x", v8pp::property(&A::get_x, &A::set_x));
module.set("A", A_class);
v8pp::class_<B, v8pp::shared_ptr_traits> B_class(isolate);
B_class
.ctor()
// .auto_wrap_objects()
.set("do_something_with_a", &B::do_something_with_a)
.set("get_a_by_value", &B::get_a_by_value)
// .set("get_a_by_rptr", &B::get_a_by_rptr)
// .set("get_a_by_ref", &B::get_a_by_ref)
.set("get_a_by_sptr", &B::get_a_by_sptr);
module.set("B", B_class);
exports->SetPrototype(isolate->GetCurrentContext(), module.new_instance());
}// main.js
...
// exactly 1 isntance of A created, as expected
const a = new addon.A(1, "a name");
// exactly 1 instance of B created, as expected
const b = new addon.B();
console.log(`b = ${b}`); // b = [object B]
console.log();
// creates no additional copies of A, passes A by shared_ptr
b.do_something_with_a(a);
// creates 2 additional copies of A
console.log('### BY VALUE ###')
try {
const a_value = b.get_a_by_value();
console.log(`a_value = ${a_value}`);
} catch (error) {
console.error(error);
}
console.log();
// doesn't work
console.log('### BY RAW PTR ###')
try {
const a_rptr = b.get_a_by_rptr();
console.log(`a_rptr = ${a_rptr}`);
} catch (error) {
console.error(error);
}
console.log();
// doesn't work
console.log('### BY REF ###')
try {
const a_ref = b.get_a_by_ref();
console.log(`a_ref = ${a_ref}`);
} catch (error) {
console.error(error);
}
console.log();
// now this is interesting
// while C++ compiles and doesn't seem to complain about anything
// instance of A is created and immediately disposed of, so on JS side i'm getting undefined
console.log('### BY SMART PTR ###')
try {
const a_sptr = b.get_a_by_sptr();
console.log(`a_sptr = ${a_sptr}`); // a_sptr = undefined
} catch (error) {
console.error(error);
}
console.log();As far as i understand, this happens because shared_ptr<A> instance from get_a_by_sptr is not "registered" anywhere in v8.
That's why to_v8 call results in undefined.
But i have far too little experience with C++ to understand exactly why this happens, or if there's anything i'm doing that caused such a behaviour. I managed to come up with a workaround like this:
// bindings.cpp
// Parts of this workaround were ripped straight from v8pp source code
template<typename T, typename Method>
MethodFunctor make_method_for_shared(Method mem_func) {
return [mem_func] (const v8::FunctionCallbackInfo<v8::Value>& args) {
using class_type = T;
using class_pointer_type = std::shared_ptr<class_type>;
using mem_func_traits = v8pp::detail::function_traits<Method>;
using mem_func_type = typename mem_func_traits::template pointer_type<class_type>;
v8::Isolate* isolate = args.GetIsolate();
mem_func_type mf(mem_func);
class_pointer_type self = v8pp::class_<T, v8pp::shared_ptr_traits>::unwrap_object(isolate, args.This());
if (!self) {
args.GetReturnValue().Set(v8::Null(isolate));
return;
}
v8::EscapableHandleScope scope(isolate);
mem_func_traits::return_type result = v8pp::detail::call_from_v8<
v8pp::detail::call_from_v8_traits<mem_func_type, 0>,
class_type,
mem_func_type
>
(*self, std::forward<mem_func_type>(mf), args);
v8::Local<v8::Object> local = v8pp::class_<mem_func_traits::return_type::element_type, v8pp::shared_ptr_traits>::reference_external(isolate, result);
args.GetReturnValue().Set(scope.Escape(local));
};
}
...
// Used like this
v8pp::class_<B, v8pp::shared_ptr_traits> B_class(isolate);
B_class
.ctor()
....
.set("get_a_by_sptr", make_method_for_shared<B>(&B::get_a_by_sptr));