Turns out this is really hard to do with C# and SWIG. The best I can offer is a crude workaround.
I made the following header for us to wrap to demonstrate this:
#include <iostream>
class Foo {
public:
virtual ~Foo() {}
virtual void Bar() = 0;
};
inline void test_catch(Foo& f) {
try {
f.Bar();
}
catch (const std::exception& e) {
std::cerr << "Caught: " << e.what() << "\n";
}
}
And this C# implementation of Foo:
public class CSharpDerived : Foo
{
public override void Bar()
{
System.Console.WriteLine("In director method");
throw new System.Exception("This is a special message");
}
}
If you were targeting Python with SWIG you'd use %feature("director:except")
, as in my example. The C# SWIG language module doesn't seem to support this though.
The strategy we need to employ to solve this is to catch the managed exception and then re-throw it as something that inherits from std::exception
. There are two problems we need to solve to emulate this for C#:
- How do we catch the exceptions from managed code?
- How do we inject our code to re-throw into the right bits of the SWIG generated output?
Catch exceptions from managed code:
It looks like there are two approaches to this.
Firstly if you're not using mono on Linux I think vectored exception handlers would be able to catch this, but probably not get much information out of the exception.
The second approach to this is to catch the exception inside managed code. This is solvable, but something of a hack. We can insert our code to do this using the csdirectorout typemap:
%typemap(csdirectorout) void %{
try {
$cscall;
}
catch(System.Exception e) {
// pass e.ToString() somewhere now
}
%}
The bodge here is that we have to specify the type of the director method - my example above will only work for C++ functions that don't return anything. You can obviously write more of these typemaps (SWIGTYPE would be a good one), but there's a lot of repetition in that.
Inject code to re-throw as a C++ native exception
This gets even uglier. I didn't find any reliable way to inject code into the C++ side of the generated director call. I expected that directorout would work for void as a return type as with the csdirectorout typemap, but with SWIG 3.0.2 I could not get this to happen (and confirmed it with `-debug-tmsearch when running SWIG). There doesn't even seem to be anywhere we could play a trick with the pre-processor and make a call into a macro that gave us a chance to add code.
The next thing I tried was to have my csdirectorout
call a C++ function again that did the re-throwing, before the call to the C# implementation had returned completely. For reference my attempt at this looked like:
%module(directors="1") test
%{
#include "test.hh"
%}
%include <std_string.i>
%{
#include <exception>
struct wrapped_exception : std::exception {
wrapped_exception(const std::string& msg) : msg(msg) {}
private:
virtual const char * what () const noexcept {
return msg.c_str();
}
std::string msg;
};
%}
%inline %{
void throw_native(const std::string& msg) {
throw wrapped_exception(msg);
}
%}
%typemap(csdirectorout) void %{
try {
$cscall;
}
catch(System.Exception e) {
test.throw_native(e.ToString());
}
%}
%feature("director") Foo;
%include "test.hh"
I tested it with:
public class runme {
static void Main()
{
System.Console.WriteLine("Running");
using (Foo myFoo = new CSharpDerived())
{
test.test_catch(myFoo);
}
}
}
So when we catch the exception inside C# we pass the string to another C++ function, and end up at the point where we throw it as a C++ exception with a stack like:
---------------------------
| throw_native() | C++ |
| SwigDirectorBar() | C# |
| Foo::Bar() | C++ |
| test_catch() | C++ |
| Main() | C# |
| pre-main() | ??? |
---------------------------
But the runtime bombs out when the exception gets thrown - it's caught by the C# implementation I used Mono and blocked from propagating any further as a C++ exception.
So at the moment the best solution I've got is a workaround: have the C# code set a global variable and check for it manually within C++ where you might expect to find it has been set, i.e. in the SWIG interface:
%inline %{
char *exception_pending
%}
%typemap(csdirectorout) void %{
try {
$cscall;
}
catch(System.Exception e) {
test.exception_pending = e.ToString();
}
%}
and an invasive change to test_catch()
:
inline void test_catch(Foo& f) {
try {
f.Bar();
check_for_csharp_exception_and_raise();
}
catch (const std::exception& e) {
std::cerr << "Caught: " << e.what() << "\n";
}
}
Where check_for_csharp_exception_and_raise
is something like:
void check_for_csharp_excception_and_raise() {
if (exception_pending) {
std::string msg = exception_pending;
delete[] exception_pending;
exception_pending = NULL;
throw wrapped_exception(msg);
}
}
Which I really dislike as a solution, but seems to be the best thing on offer right now, at least without breaking mono compatibility and patching SWIG a little.