wlav / cppyy

Other
400 stars 41 forks source link

No automatic downcast? #87

Closed torokati44 closed 1 year ago

torokati44 commented 2 years ago

In a simple 2-level class hierarchy, I'm trying to call a method defined on the subclass, through a superclass pointer to an instance of the subclass

Consider this snippet:


import cppyy

cppyy.cppdef(
"""
#include <iostream>

class A {
        public:
        void helloA() { std::cout << "A" << std::endl; }
        virtual ~A() {}  // <-- needed for the dynamic_cast in C++, but not enough for Cppyy - at least it causes a different error message
};

class B : public A {
        public:
        void helloB() { std::cout << "B" << std::endl; }
};

A *a = new B();

void callHelloB() {
        dynamic_cast<B*>(a)->helloB();
}
""")

cppyy.gbl.a.helloB() # This does not work, but I wish it did.

I thought any wrapped object automatically acts as the most specialized class it is, regardless of the type of the pointer that was used to access this. How can I dynamic_cast the object to its most specialized type in Python?

wlav commented 2 years ago

That's an issue unique to global variable pointers: the wrapper proxy is a to an A** (that is, &a) to ensure that updates to the global pointer on the C++ side are visible on the Python side, and an A** isn't related to a B** through inheritance (offsets may change if a different derived type is set on the base pointer).

Probably can be improved by checking on access for possible auto downcast, rather than on bind as is currently the case (more efficient otherwise).

As for dynamic_cast, it does exist:

import cppyy.ll
# ...
cppyy.ll.dynamic_cast[cppyy.gbl.B](cppyy.gbl.a).helloB()

which is functional, but not particularly efficient.

torokati44 commented 2 years ago

That's an issue unique to global variable pointers:

Ah, I did not know that. And indeed, switching to this now works fine:

import cppyy

cppyy.cppdef(
"""

// [...]

A *makeAB() { return new B(); }
""")

cppyy.gbl.makeAB().helloB()

Probably can be improved by checking on access for possible auto downcast, rather than on bind as is currently the case

Yes, that is what I would have expected. Although it looks like having this might not actually be important for me, because I only used a global variable in this example snippet, in my actual code I get the wrapper to it like this: cppyy.bind_object(objectAddress, "ns::type::name").

As for dynamic_cast, it does exist

Oh, I wasn't aware, thank you! I tried looking for something like this in the documentation: https://cppyy.readthedocs.io/en/latest/search.html?q=dynamic_cast&check_keywords=yes&area=default But the only result for it was something like: _... There should never be a reason for a dynamic_cast, since that only applies to objects, for which auto-casting ..._

Fortunately I already have access to the actual runtime type name of the objects I'm interested in downcasting right now (via demangling std::type_info::name), so this will probably work for me.

torokati44 commented 2 years ago

I still don't know how to call cppyy.ll.dynamic_cast properly.

The problem is, that I'm actually getting the object to downcast via its pointer from C++, passed across the language boundary as a simple long/int.

So, trying this:

import cppyy
import cppyy.ll

cppyy.cppdef(
"""
// [...]

intptr_t makeAB() {
    A *a = new B();
    return (intptr_t)reinterpret_cast<void*>(a);
}
""")

a = cppyy.bind_object(cppyy.gbl.makeAB(), "A")
cppyy.ll.dynamic_cast["B"](a).helloB()

I'm getting this error:

TypeError: Could not find "cppyy_dynamic_cast<B>" (set cppyy.set_debug() for C++ errors):
  Failed to instantiate "cppyy_dynamic_cast<B>(A&)"

And a really similar one if I'm trying to cast to "B *".

It seems that the wrapper automatically removes the pointer's layer of indirection right when I call bind_object, so dynamic_cast can't work, as a is now a reference, not a pointer. And I also can't use ll.addressof to add back the indirection, as that returns a regular int, not a wrapped pointer.

Could you perhaps guide me further on how to downcast in this case?

wlav commented 1 year ago

Sorry, didn't see the update. That said, cppyy.ll.dynamic_cast["B"](cppyy.gbl.a).helloB() (the original problem) works fine for me. So the issue is only if return as an intptr_t and use of bind_object? Yes, bind_object does not do an auto-cast, but it will do offset calculations if requested and actual differ, as the intend is taken to bind to the type given.

Rather, do:

a = cppyy.ll.reinterpret_cast["A*"](cppyy.gbl.makeAB())

which (perhaps counter-intuitively) will do the auto-downcast on return.

torokati44 commented 1 year ago

The line above gave me this error:

TypeError: Could not find "cppyy_reinterpret_cast<A*>" (set cppyy.set_debug() for C++ errors):
Failed to instantiate "cppyy_reinterpret_cast<A*>(B&)"

I found no way to refer to this object from Python as it's pointer, except for this:

b = cppyy.gbl.makeAB()
a = cppyy.ll.reinterpret_cast["A*"](cppyy.ll.addressof(b))

And even after this, helloB is only in 'a' if the definition of B is presented as source to cppyy before the binding. This is completely understandable, however, fairy unfortunate.

wlav commented 1 year ago

I don't understand ... makeAB() in the code above returns intptr_t:

intptr_t makeAB() {
    A *a = new B();
    return (intptr_t)reinterpret_cast<void*>(a);
}

so it should not be possible for cppyy.ll.reinterpret_cast["A*"](cppyy.gbl.makeAB()) to try to instantiate cppyy_reinterpret_cast<A*>(B&) since there is no B involved anywhere?

Code below is a full example that works for me (TM):

import cppyy
import cppyy.ll

cppyy.cppdef(
"""
#include <iostream>

class A {
        public:
        void helloA() { std::cout << "A" << std::endl; }
        virtual ~A() {}  // <-- needed for the dynamic_cast in C++, but not enough for Cppyy - at least it causes a different error message
};

class B : public A {
        public:
        void helloB() { std::cout << "B" << std::endl; }
};

intptr_t makeAB() {
    A *a = new B();
    return (intptr_t)reinterpret_cast<void*>(a);
}
""")

a = cppyy.ll.reinterpret_cast["A*"](cppyy.gbl.makeAB())
print(a)
a.helloB()

And yes, for an auto-downcast to work, the type of B needs to be known a priori. It's not possible to work in any other way and still be useful (opaque pointers are supported, but won't allow calling methods such as helloB()).

torokati44 commented 1 year ago

Ah, yes, the key is this: intptr_t makeAB(). When I already have a wrapped A object (because I return a typed pointer from makeAB instead of an integer address from C++), I have to use addressof.

wlav commented 1 year ago

Closing as presumed clarified.