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, this technique will work only if the C++ API you are wrapping with SWIG has something similar to C#
object.GetType()
or Java Object.getClass()
.Maintenance is easy because you need only maintain a single list of concrete classes, and a single list of abstract classes. Further, there is no harm in these lists being supersets of the classes that are actually involved in polymorphism.
The Example C++ API: OpenAccess EDA Database
In lieu of the standard animal or vehicle polymorphism example, I will use the example I have at hand, the OpenAccess chip design database I am wrapping in C# using SWIG. OpenAccess contains hundreds of classes that describe the details of an integrated circuit design, but let’s just concentrate on a subset that everyone can understand.
All classes that we might want to downcast are derived from base class oaObject
. This class has an oaObject::getType()
method that identifies the concrete class of an instance. It does pretty much the same thing as C# object.GetType()
or Java Object.getClass()
.
oaShape
is derived from oaObject
. oaShape
is an abstract base class for the geometric shapes drawn on the surface of an integrated circuit. Descended from oaShape
are concrete classes like oaRect
, oaPolygon
and oaEllipse
. There are actually more shapes, but these three are sufficient to illustrate.
The OpenAccess C++ API contains methods that return a polymorphic oaShape*
. This oaShape*
might be an oaRect*
, oaPolygon*
or oaEllipse*
.
SWIG Proxy Class Returns an Instance of the Abstract Class
The problem is that when SWIG encounters a C++ method that returns oaShape*
, it generates a C# proxy class that returns new oaShape(cPtr)
. This is a concrete instance of an abstract base class–something that makes no sense. This C# instance of oaShape
cannot be downcasted to oaPolygon
, even if getType()
tells you that the underlying C++ object is in fact a polygon.
Instantiate Concrete Classes, not Abstract Classes
The solution is to instantiate the correct concrete class in C#. Add a C# intermediate class method that instantiates the concrete class according to the oaType
enum value returned by oaObject.getType()
:
%pragma(csharp) imclasscode=%{ public static oaObject oasInstantiateConcreteOaObject(IntPtr cPtr, bool owner) { oaObject ret = null; if (cPtr == IntPtr.Zero) { return ret; } oaType type = $modulePINVOKE.oaObject_getType(new HandleRef(null, cPtr)); switch (type) { case oaType.oacEllipseType: ret = new oaEllipse(cPtr, owner); break; case oaType.oacPolygonType: ret = new oaPolygon(cPtr, owner); break; case oaType.oacRectType: ret = new oaRect(cPtr, owner); break; // Repeat for every other concrete type. // Not just oaShapes. All of them. default: System.Diagnostics.Debug.Assert(false, String.Format("Encountered type '{0}' that is not known to be an OpenAccess concrete class", type.ToString())); break; } return ret; } %}
This single method can instantiate in C# every concrete class in the C++ API that is derived from oaObject
. Note that there is no need to restrict the classes appearing in this method to the concrete classes that are actually used polymorphically–go ahead and add them all. They just need to be derived from oaObject
and of course they must actually be concrete classes.
%typemap to Instantiate Concrete Classes
Next SWIG needs to be told to use the above method in C# proxy class code for methods that return an instance of an abstract class. You do this with %typemap(csout)
:
%typemap(csout, excode=SWIGEXCODE) OpenAccess_4::oaShape *, /* Insert here every other abstract type returned in the C++ API */ { IntPtr cPtr = $imcall; $csclassname ret = ($csclassname) $modulePINVOKE.oasInstantiateConcreteOaObject(cPtr, $owner);$excode return ret;}
Once again, there is no harm in listing too many abstract classes here, as long as they really are abstract classes derived from oaObject
.
Generate the C# API
Repeat for any other polymorphic class hierarchies your C++ API may have.
Then, generate the C# wrapper code with SWIG as usual.
Use the C# Cast Operator to Downcast
Now that the C# instances returned are of the correct concrete cast, you can use downcasts in your C# client code.
For example, the OpenAccess oaBlock.getShapes()
C# method returns an ICollection<oaShape>
. I can iterate over the shapes, determine the type using getType()
, and downcast to the respective concrete type:
oaBlock block = // ... foreach (oaShape shape in block.getShapes()) { switch (shape.getType()) { case oaType.oacEllipseType: oaEllipse ellipse = (oaEllipse)shape; // Do something elliptical break; case oaType.oacPolygonType: oaPolygon polygon = (oaPolygon)shape; // Do something polygonal break; case oaType.oacRectType: oaRect rect = (oaRect)shape; // Do something rectangular break; default: Debug.Assert(false, "Encountered unknown oaShape type '{0}'.", shape.getType()); break; } }
Alternative Approaches
David Piepgrass has a similar method for polymorphic C# using SWIG, but he marshals a structure containing all the information required using a single P/Invoke call, while my method requires two P/Invoke calls.
We also generate Perl, Python, Ruby and Tcl scripting language bindings for OpenAccess. In it we use a similar approach for polymorphic return values in scripting languages with SWIG.
Photo Credit: “Tin Peaks” by my aunt, Norma McGehee Woodward. This photo is used by permission and copyright 2012 Norma McGehee Woodward.
Comments
Post a Comment