4

We are creating interfaces to a C++ library in Python, Java, and C# using SWIG 3.0.3.

We also provide a Stream interface in C++ and, using the SWIG Director feature, we allow the users to implement this interface in whichever of those supported languages they choose.

The problem is that when an exception is thrown in the user's C# stream implementation the actual error message from C# is lost and C++ cannot retrieve it.

I am wondering if there is any solution to that problem.

Additional info:
1. The documentation for SWIG Java says that this feature (passing exception data in directors) is new to SWIG 3.
2. I had to modify the SWIG Python code slightly to achieve the desired result.

// C++ Stream Interface
class Stream 
{
    virtual uint Seek(long offset, int origin);
    // ...
};


// C# Implementation of a Stream
class CustomStream : Stream 
{
    public override uint Seek(long offset, int origin)
    {
        throw new Exception("This message should be seen in C++ caller");
        return 0;
    }
}


// C++ calling code
try 
{
    pStream->Seek(0, 0);
}
catch(std::exception e)
{
    // Here's where I want to see the exception text.
    std::cout << e.what();
}
bluedog
  • 935
  • 1
  • 8
  • 24
  • 1
    It would be helpful if you showed some of the structure of your code. For example, show the C# portion of an example user stream that raises an exception, and show the corresponding C++ "interface" class code. Show where the exception message is desired to be used but actually empty. – Oliver Jan 09 '15 at 03:36
  • In addition to my workaround you could also manually write a managed class that implements the native interface and then proxies it to a purely managed abstract class, using something like: http://stackoverflow.com/questions/16540267/inheriting-from-native-c-in-c-sharp-and-passing-the-inherit-class-backward-to. You'd need to map the exceptions at the point of proxying and there's basically no help from SWIG for it – Flexo Jan 10 '15 at 06:16
  • Thanks @Flexo. These are good ideas. Absorbing it all... – bluedog Jan 11 '15 at 21:39
  • I wasn't expecting you to accept my answer since its just a workaround and a crude one at that. – Flexo Jan 12 '15 at 07:49

1 Answers1

1

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#:

  1. How do we catch the exceptions from managed code?
  2. 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.

Community
  • 1
  • 1
Flexo
  • 87,323
  • 22
  • 191
  • 272