Swig downcasting from Base* to Derived*

2020-03-06 06:01发布

问题:

I have the following c++ classes (simplified) which I am exposing to Python using SWIG:

struct Component
{
    virtual void update();
}

struct DerivedComponent : public Component
{
    void update() { cout << "DerivedComponent::update()" << endl; }
    void speak() { cout << "DerivedComponent::speak()" << endl; }
}

class Entity
{
public:
    Component* component(const std::string& class_name)
    {
        return m_components[class_name];
    }

private:
    std::unordered_map<std::string, Component*> m_components;
}

Now, in Python I can successfully call component("DerivedComponent").update() on an Entity instance. However, I cannot call component("DerivedComponent").speak() since the type returned by component("DerivedComponent") is reported as <class 'module.Component'>.

I obviously need to downcast the result of the component() function in order to call methods defined in DerivedComponent. I had hoped that Swig would perform automatic downcasting like I believe that Boost.Python does.

Short of defining a whole bunch of typecasting functions in c++ and exposing them to Python, is there any better solution for downcasting using either Swig or Python? What are my options?

回答1:

You can do exactly what you want in Python, with a little work. It works as you hope because in Python downcasting is kind of meaningless as the return types of functions (or types in general) aren't strongly typed, so we can modify your Entity::component function to always return the most derived type no matter what it is.

To make that work with your C++/Python binding you need to write an 'out' typemap for Entity::component. I've written an example of how it might work. In this case we have to bodge it slightly because the only way to know what to downcast it to comes from the argument to the function. (If for example your base class had a method that returned this as a string/enum you could simplify this further and not depend on the input arguments).

%module test

%{
#include "test.hh"
%}

%include <std_string.i>

%typemap(out) Component * Entity::component {
    const std::string lookup_typename = *arg2 + " *";
    swig_type_info * const outtype = SWIG_TypeQuery(lookup_typename.c_str());
    $result = SWIG_NewPointerObj(SWIG_as_voidptr($1), outtype, $owner);
}

%include "test.hh"

This uses the SWIG_TypeQuery function to ask the Python runtime to lookup the type based on arg2 (which for your example is the string).

I had to make some changes to your example header (named test.hh in my example) to fix a few issues before I could make this into a fully working demo, it ended up looking like:

#include <iostream>
#include <map>
#include <string>

struct Component
{
    virtual void update() = 0;
    virtual ~Component() {}
};

struct DerivedComponent : public Component
{
    void update() { std::cout << "DerivedComponent::update()" << std::endl; }
    void speak() { std::cout << "DerivedComponent::speak()" << std::endl; }
};

class Entity
{
public:
    Entity() {
       m_components["DerivedComponent"] = new DerivedComponent;
    }

    Component* component(const std::string& class_name)
    {
        return m_components[class_name];
    }

private:
    std::map<std::string, Component*> m_components;
};

I then built it with:

swig -py3 -c++ -python -Wall test.i
g++ -Wall -Wextra test_wrap.cxx -I/usr/include/python3.4/ -lpython3.4m -shared -o _test.so

With this in place I could then run the following Python:

from test import *

e=Entity()
print(e)

c=e.component("DerivedComponent")
print(c)
print(type(c))

c.update()
c.speak()

This works as you'd hope:

<test.Entity; proxy of <Swig Object of type 'Entity *' at 0xb7230458> >
Name is: DerivedComponent *, type is: 0xb77661d8
<test.DerivedComponent; proxy of <Swig Object of type 'DerivedComponent *' at 0xb72575d8> >
<class 'test.DerivedComponent'>
DerivedComponent::update()
DerivedComponent::speak()


回答2:

I was looking to do something similar and came up with a similar but different solution based on this question.

If you know the possible types ahead of time and don't mind the extra overhead, you can have the 'out' typemap loop through and dynamic_cast to each to automatically return the object with its real type. SWIG already has this implemented for pointers with the %factory feature:

%factory(Component* /* or add method name. this is just the typemap filter */,
     DerivedComponent1,
     DerivedComponent2);

Looking at factory.swg and boost_shared_ptr.i I got this working for shared_ptr and dynamic_pointer_cast as well:

%define %_shared_factory_dispatch(Type)
if (!dcast) {
  SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<Type> dobj
          = SWIG_SHARED_PTR_QNAMESPACE::dynamic_pointer_cast<Type>($1);
  if (dobj) {
    dcast = 1;
    SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<Type> *smartresult
            = dobj ? new SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<Type>(dobj) : 0;
    %set_output(SWIG_NewPointerObj(%as_voidptr(smartresult),
                                   $descriptor(SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<Type> *),
                                   SWIG_POINTER_OWN));
  }
}%enddef

%define %shared_factory(BaseType,Types...)
%typemap(out) SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<BaseType> {
  int dcast = 0;
  %formacro(%_shared_factory_dispatch, Types)
  if (!dcast) {
      SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<BaseType> *smartresult
              = $1 ? new SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<BaseType>($1) : 0;
      %set_output(SWIG_NewPointerObj(%as_voidptr(smartresult),
                                     $descriptor(SWIG_SHARED_PTR_QNAMESPACE::shared_ptr<BaseType> *),
                                     SWIG_POINTER_OWN));
  }
}%enddef

// Apply dynamic_pointer cast to all returned shared_ptrs of this type
%factory(Component /* must be a type for shared_ptr */,
     DerivedComponent1,
     DerivedComponent2);