Skip to content

Return std::shared_ptr of another wrapped class from a member function #139

@poniraq

Description

@poniraq

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));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions