SWIG: Casting Revisited
A while back I wrote SWIG and a Miss, which is a post about several of the problems I’ve encountered with SWIG. At that time I didn’t have a solution for dealing with down casting – the process of casting from a base class to a more derived class.
I ended up coming up with a solution that I thought would be good to do a post on, since there doesn’t seem to be much out there on SWIG and these types of problems.
For those unfamiliar with why it’s difficult, let me explain. When your API returns a native pointer to be wrapped, of lets say class Foo, and your function returns Foo* but the instance it returns is actually the more derived version Bar. Sadly SWIG has no way of knowing this, so the object it creates in C# would be a C# Foo class. Therefore, if you tried to cast the C# Foo to the more derived C# Bar you’d be unable to because as far as .Net is concerned the instance of Foo is just a Foo and nothing more.
Alright now that you understand the problem, lets talk about how we can solve it.
How We Can Solve It
The solution to the problem can be solved in essentially 3 ways,
1 // Walk Away
Simply don’t do anything that would require it. Don’t expose classes that users will ever need to downcast to for any reason. If this option is available to you, take it. This however means that your base class needs to support all the functionality a user will ever need, something not always possible or desirable. But if you can, do it, because once you get to script land, down casting becomes a expensive process.
2 // Manual Cast Wrapper
Write or generate C++ functions for every downcast that you would like to perform and SWIG those functions. This would allow you to take object Foo as a parameter, returns Bar, does the dynamic cast in C++ and returns the pointer to Bar.
This option is manpower intensive and is truly brute forcing a solution. It also causes potential holes where nasty aliasing occurs and can become a real problem if the source object is ever deleted. Imagine an API with a factory function that returns object Foo, but Foo is actually Bar due to the way the factory works. You need to access a Bar specific only function on the object so you downcast Foo to Bar.
Now in C++ these objects are the same object, but SWIG doesn’t know this, so two completely different C# objects are created one wrapping the native pointer to Foo, and one wrapping the native pointer to Bar. If the SWIG layer was properly authored, the factory function notes that the object returned was created with new memory and thus SWIG is responsible for deleting the object.
So we call our Bar specific function and we decide to hang onto Bar and we let Foo on the stack pass out of scope, because why hang onto it, it’s the same object as Bar…right? But Foo was responsible for managing the memory for the common native object they both point to. So the underlying native object is destroyed when the garbage collector runs next after Foo is no longer referenced and now our Bar object points to totally bogus memory.
3 // Custom Solution
The solution I ended up coming up with requires you to have a custom RTTI system that allows you to check if an object is a specific type by name.
The first step is to extend the C# wrapper for your base class with RTTI information. If you don’t have a base RTTI object this will be a bit harder. The reason we have a new m_castedSource member variable is to solve the aliasing problem. After the cast we can store the source here so that we never have to worry about the garbage collector accidentally cleaning up memory we’re still using.
The next step is to create a function that can create the C# method needed to factory the C# object. We could use reflection, but that method is slow and we can do a far better job by emitting IL at runtime to specifically initialize one type and just cache those created methods.
After that we need to add a function that can perform the down cast. This is just a matter of checking if the object is actually the desired casting type. If it is we get or create the factory delegate for the type. We construct the new wrapper object and hand it the pointer for the type. (NOTE: This is only viable if the new type isn’t offset in the vtable, as would be the case with multiple inheritance).