Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling functions taking ref or raw ptr when using shared_ptr_traits #79

Open
jcmonnin opened this issue Jul 13, 2018 · 4 comments
Open

Comments

@jcmonnin
Copy link
Contributor

I bind my C++ types with shared_ptr_traits when there is shared ownership in between C++ and JavaScript or if I need to pass ownership from JavaScript to C++. This works well, but I have to apply some workarounds. When having such a type bound with shared_ptr, it's quite common to have to call API's that expect an object reference or a raw pointer. This currently doesn't work with v8pp.

Here is a short summary of the main issue:
When a class is bound to JavaScript with shared_ptr

v8pp::class_<Test, v8pp::shared_ptr_traits> testClass(isolate);

we cannot call a function taking a reference:

void myFunction(Test const& test);

More complete example:

class Test : std::enable_shared_from_this<Test>
{
public:
    Test(int i) : i_(i) {}
    
    int i() const { return i_; }
    void setI(int i) { i_ = i; }
    
private:
    int i_;
};

std::shared_ptr<Test const> printTestSPtr(std::shared_ptr<Test const> test)
{
    if (test) {
        std::cout << test->i() << std::endl;
    } else {
        std::cout << "nullptr" << std::endl;
    }
    return test;
}

const Test* printTestRawPtr(const Test* test)
{
    if (test) {
        std::cout << test->i() << std::endl;
    } else {
        std::cout << "nullptr" << std::endl;
    }
    return test;
}

Test printTestRef(Test const& test)
{
    std::cout << test.i() << std::endl;
    return test;
}

template<typename Traits>
void bindTest(v8::Isolate* isolate)
{
    v8pp::class_<Test, Traits> testClass(isolate);
    testClass
        .template ctor<int>()
        .set("i", &Test::i)
        .set("setI", &Test::setI);

    v8pp::module myLib(isolate);
    myLib
        .set("Test", testClass)
        .set("printTestSPtr", printTestSPtr)
        .set("printTestRawPtr", printTestRawPtr)
        .set("printTestRef", printTestRef);

    isolate->GetCurrentContext()->Global()->Set(v8::String::NewFromUtf8(isolate, "myLib"), myLib.new_instance());
}

Using v8pp::shared_ptr_traits

When initialising the context with bindTest<v8pp::shared_ptr_traits>(isolate);, I get following results (note: the situation with the return values is there for completeness, but not the main issue):

Calling function taking shared_ptr

let x = new myLib.Test(123);
myLib.printTestSPtr(x).i()

gives the expected result (first line is std::cout, 2nd line is what the script expression returns)

123
123

Calling function taking raw pointer

let x = new myLib.Test(123);
myLib.printTestRawPtr(x).i()

When calling function taking raw pointer, I get a C++ exception. Ideally, this would work. Admittedly, the return value is a bit more complicated. I guess it would be ok if it throws a C++ exception when doing the return value (although theoretically it could get a shared_prt instance if the class derives from std::enable_shared_from_this).

Exception: Error: v8pp::class_<Test, v8pp::shared_ptr_traits> is already registered in isolate 0x107000000 before of v8pp::class_<Test, v8pp::raw_ptr_traits>

Calling function taking reference

let x = new myLib.Test(123);
myLib.printTestRef(x).i()

When calling function taking a reference, I get a C++ exception. This is a quite common case which should work. The return value situation is #39.

Exception: Error: v8pp::class_<Test, v8pp::shared_ptr_traits> is already registered in isolate 0x10200be00 before of v8pp::class_<Test, v8pp::raw_ptr_traits>

Using v8pp::raw_ptr_traits

As a comparison, here is what I get when initialising the context withbindTest<v8pp::raw_ptr_traits>(isolate);:

Calling function taking shared_ptr

let x = new myLib.Test(123);
myLib.printTestSPtr(x).i()

This gives a C++ exception. Generally, this call makes no sense. Strictly speaking this would be possible if the class derives from std::enable_shared_from_this, however this use case is probably not relevant in practice.

Exception: Error: v8pp::class_<Test, v8pp::raw_ptr_traits> is already registered in isolate 0x104803800 before of v8pp::class_<Test, v8pp::shared_ptr_traits>

Calling function taking raw pointer

let x = new myLib.Test(123);
myLib.printTestRawPtr(x).i()

gives the expected result

123
123

Calling function taking reference

let x = new myLib.Test(123);
myLib.printTestRef(x).i()

Calling a reference is possible. Return value issue is #39. It looks like earlier versions of v8pp were returning undefined whereas now it throws a C++ exception.

123
Exception: Error: failed to wrap C++ object

Current workaround

To solve the main issue of calling a function taking a reference or pointer as argument, I have a very ugly workaround. When I comment out the traits check in classes::find, then it seems to do what I want. The problem is that this relies on undefined behaviour (I don't think that it's safe to convert an instance to a different type with static_cast). So I would be keen to have a more permanent solution that this:

	template<typename Traits>
	static object_registry<Traits>& find(v8::Isolate* isolate, type_info const& type)
	{
		classes* info = instance(operation::get, isolate);
		type_info const& traits = type_id<Traits>();
		if (info)
		{
			auto it = info->find(type);
			if (it != info->classes_.end())
			{
//                if ((*it)->traits != traits)
//                {
//                    throw std::runtime_error((*it)->class_name()
//                        + " is already registered in isolate "
//                        + pointer_str(isolate) + " before of "
//                        + class_info(type, traits).class_name());
//                }
				return *static_cast<object_registry<Traits>*>(it->get());
			}
		}
		//assert(false && "class not registered");
		throw std::runtime_error(class_info(type, traits).class_name()
			+ " is not registered in isolate " + pointer_str(isolate));
	}
@pmed
Copy link
Owner

pmed commented Jul 18, 2018

Hi Jean-Claude,

Thank you for the detailed issue description.

References to wrapped class as function arguments should be converted with convert_ref in ptr_traits. There is a case in the v8pp::class_ test.

The cause of issue could be due to const qualifiers for the argument references. I will add your code into the test case in order to check it.

This checking of traits types in find() function was added to prevent implicit conversions between different pointer types. But if such conversions seems to be handy, we could try to enable them. Maybe by adding helper functions into the traits classes for raw_ptr_traits to shared_ptr_traits and vice versa, similar to convert_ref templates.

Best regards,
Pavel

@jcmonnin
Copy link
Contributor Author

Hi Pavel,

Thanks for the reply.
You are right, calling a function taking a reference actually does work.
I was taking wrong conclusions for some reason. I clearly tried to add two many things in my test case.

The problem with the call to a function/method taking a raw pointer remains.

Given my initial post is too big and contains wrong things, I propose to close it and I will reopen another one with a more concise example about it. Is that ok or do you want to continue on this issue number?

Thanks,
Jean-Claude

@pmed
Copy link
Owner

pmed commented Jul 19, 2018 via email

@jcmonnin
Copy link
Contributor Author

Hi Pavel,

I'd like to come back to calling a function taking a raw pointer (when the class has been declared with v8pp::shared_ptr_traits). For the example above this would be a function like:

void printTestRawPtr(const Test* test)

This fails because unwrap_object calls classes::find which throws exception because of mismatching traits.

In the API I need to bind these type of functions are pretty common and I need to bind my classes to JavaScript with v8pp::shared_ptr_traits because there is no other clean way to express the ownership if the instances.

As said in the initial comment, commenting out some lines in classes::find make the test case go through, but the implementation is not clean (I think it would be undefined behaviour to cast the instance to a different type than what it has been instantiated):

	template<typename Traits>
	static object_registry<Traits>& find(v8::Isolate* isolate, type_info const& type)
	{
		classes* info = instance(operation::get, isolate);
		type_info const& traits = type_id<Traits>();
		if (info)
		{
			auto it = info->find(type);
			if (it != info->classes_.end())
			{
//                if ((*it)->traits != traits)
//                {
//                    throw std::runtime_error((*it)->class_name()
//                        + " is already registered in isolate "
//                        + pointer_str(isolate) + " before of "
//                        + class_info(type, traits).class_name());
//                }
				return *static_cast<object_registry<Traits>*>(it->get());
			}
		}
		//assert(false && "class not registered");
		throw std::runtime_error(class_info(type, traits).class_name()
			+ " is not registered in isolate " + pointer_str(isolate));
	}

If you see a strait-forward way to fix it, please let me know.

Otherwise maybe the suggestions below are helpful (hopefully it's not too much off topic);

In the old days, I was using luabind to bind C++ classes lua code (it was pre C+11, so this was with boost::shared_ptr). It was handling the case above. Nowadays sol2 is the reference for binding lua code and it has very flexible support for smart pointers. It might be worthwhile to have a look there:

http://luabind.sourceforge.net/docs.html#class_smart_pointers
http://sol2.readthedocs.io/en/latest/api/unique_usertype_traits.html

When a smart pointer is used, the smart pointer is not replacement of the raw pointer as in v8pp, but an additional concept (holder_type/actual_type). The traits are defined independently of the wrapper class. This allows more flexibility. It's not necessary to make the choice to either use shared_ptr or raw pointers everywhere. It's possible to mix things more freely (and also supports other smart pointer types than std::shared_ptr):

http://sol2.readthedocs.io/en/latest/tutorial/all-the-things.html#c-classes-from-c

Possibly this doesn't really translate to v8pp/JavaScript, but maybe you find some of the ideas useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants