How to properly downcast in C# with a SWIG generat

2019-01-27 00:40发布

问题:

I've got a very large and mature C++ code base that I'm trying to use SWIG on to generate a C# interface for. I cannot change the actual C++ code itself but we can use whatever SWIG offers in the way of extending/updating it. I'm facing an issue where a C++ function that is written as below is causing issues in C#.

A* SomeClass::next(A*)

The caller might do something like:

A* acurr = 0;
while( (acurr = sc->next(acurr)) != 0 ){
    if( acurr isoftype B ){
        B* b = (B*)a;
        ...do some stuff with b..
    }
    elseif( acurr isoftype C )
    ...
}

Essentially, iterating through a container of elements that, depending on their true type, does something different. The SWIG generated C# layer for the "next" function unfortunately does the following:

return new A();

So the calling code in C# cannot determine if the returned object is actually a derived class or not, it actually appears to always be the base class (which does make sense). I've come across several solutions:

  1. Use the %extend SWIG keyword to add a method on an object and ultimately call dynamic_cast. The downside to this approach, as I see it, is that this requires you to know the inheritance hierarchy. In my case it is rather huge and I see this is as a maintenance issue.
  2. Use the %factory keyword to supply the method and the derived types and have SWIG automatically generate the dynamic_cast code. This appears to be a better solution that the first, however upon a deeper look it still requires you to hunt down all the methods and all the possible derived types it could return. Again, a huge maintenance issue. I wish I had a doc link for this but I can't find one. I found out about this functionality by looking through the example code that comes with SWIG.
  3. Create a C# method to create an instance of the derived object and transfer the cPtr to the new instance. While I consider this clumsy, it does work. See an example below.
    public static object castTo(object fromObj, Type toType)
    {
        object retval = null;

        BaseClass fromObj2 = fromObj as BaseClass;
        HandleRef hr = BaseClass.getCPtr(fromObj2);
        IntPtr cPtr = hr.Handle;
        object toObj = Activator.CreateInstance(toType, cPtr, false);

        // make sure it actually is what we think it is
        if (fromObj.GetType().IsInstanceOfType(toObj))
        {
            return toObj;
        }

        return retval;
    }

Are these really the options? And if I'm not willing to dig through all the existing functions and class derivations, then I'm left with #3? Any help would be appreciated.

回答1:

By default SWIG generates C# and Java code that does not support downcast for polymorphic return types. I found a straightforward way to solve this, provided your C++ code has a way to identify the concrete class of the C++ instances that are returned. That is, my technique will work only if the C++ API you are wrapping with SWIG has something similar to C# object.GetType() or Java Object.getClass().

The solution is to add a C# intermediate class method to instantiate the concrete class that the C++ says it is. Then, use %typemap(out) to tell SWIG to use this intermediate class method when returning the abstract classes.

That's an awfully terse explanation, so refer to my blog article that shows how to generate polymorphic C# and Java that you can downcast. It has all the details.



回答2:

The third solution in the original post does not work in Swig 2.0 where constructors are private (thus the Activator cannot find the constructor). Therefore different BindingFlags have to be used. Here is a generic variant of the third solution mentioned in the original post:

public class SwigHelper
{
    public static T CastTo<T>(object from, bool cMemoryOwn)
    {
        System.Reflection.MethodInfo CPtrGetter = from.GetType().GetMethod("getCPtr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
        return CPtrGetter == null ? default(T) : (T) System.Activator.CreateInstance
        (
            typeof(T),
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
            null,
            new object[] { ((HandleRef) CPtrGetter.Invoke(null, new object[] { from })).Handle, cMemoryOwn },
            null
        );
    }
}

Given two SWIG wrappers Foo and Bar where Bar : Foo, you can now try to downcast Foo to Bar like in the following example:

Foo foo = new Bar();
Bar bar = SwigHelper.CastTo<Bar>(foo, false);


回答3:

We added functions to the interface to get the specific type needed:

// .cpp
class foo : public bar {
}

///////////// part of swig
// .i (swig)
%extend foo {
    static foo* GetFoo( bar* iObj ) {
         return (foo*)iObj;
   }
}

It is a bit tedious, beause it had to be done for every class, but then again, it could be turned into a SWIG macro.