Question

I searched around SO and found various related questions, some answered with essentially "don't do that."

I want to call some unmanaged C++ code that accesses various existing C++ code. The existing code may have a variety of error conditions that I want to map into C# exceptions. From doing something similar in Java and JNI, it seemed that it might be possible to have a delegate function to raise defined exceptions, which could then be called directly from unmanaged code. The calls then look like (csharp)->(unmanaged)->(csharp delegate,throw/set pending exception) and then return back up.

The code below seems to work fine (vs2010, mono). My question is is there any problem with this approach - e.g. the spec says that the exception is not guaranteed to still be "pending" after unmanaged code is called, or threading issues, etc...

// unmanaged.cpp 
#include <cstdio>
#define EXPORT __declspec(dllexport)
#define STDCALL __stdcall

typedef void (STDCALL* raiseExcpFn_t)(const char *);
extern "C" {
  // STRUCT ADDED TO TEST CLEANUP
  struct Allocated {
     int x;
     Allocated(int a): x(a) {}
     ~Allocated() {
    printf("--- Deleted allocated stack '%d' ---\n", x);
    fflush(stdout);
    }
  };

  static raiseExcpFn_t exceptionRaiser = 0;
  EXPORT void STDCALL registerRaiseExcpFn(raiseExcpFn_t fun) {
      exceptionRaiser = fun;
  }
  EXPORT void STDCALL hello(const char * x) {
    Allocated a0(0); 
    try {
      Allocated a1(1);
      printf("1 --- '%s' ---\n", x); fflush(stdout);
      (*exceptionRaiser)("Something bad happened!");
      printf("2 --- '%s' ---\n", x); fflush(stdout);
    } catch (...) {
      printf("3 --- '%s' ---\n", x); fflush(stdout);
      throw;
    }
    printf("4 --- '%s' ---\n", x); fflush(stdout);
  }
}

// Program.cs
using System;
using System.Runtime.InteropServices;

class Program {
  [DllImport("unmanaged.dll")]
  public static extern void registerRaiseExcpFn(RaiseException method);

  [DllImport("unmanaged.dll")]
  public static extern void hello([MarshalAs(UnmanagedType.LPStr)] string m);
  public delegate void RaiseException(string s);
  public static RaiseException excpfnDelegate = 
    new RaiseException(RaiseExceptionMessage);

  // Static constructor (initializer)
  static Program() { 
    registerRaiseExcpFn(excpfnDelegate);
  }

  static void RaiseExceptionMessage(String msg) {
    throw new ApplicationException(msg);
  }

  public static void Main(string[] args) {
    try {   
      hello("Hello World!");
    } catch (Exception e) {
      Console.WriteLine("Exception: " + e.GetType() + ":" + e.Message);
    } 
  }
}

Updated: Corrected test and output, showing mono and Windows (with /EHsc) leaks

// Observed output // with Release builds /EHa, VS2010, .Net 3.5 target
//cstest.exe
// --- Deleted allocated stack '0' ---
// --- Deleted allocated stack '1' ---
// 1 --- 'Hello World!' ---
// 3 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!

// Observed LEAKING output // with Release builds /EHsc, VS2010, .Net 3.5 target
// cstest.exe
// 1 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!

// LEAKING output DYLD_LIBRARY_PATH=`pwd` mono program.exe 
// 1 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!
Was it helpful?

Solution

Yes, you can make this work as long as you run the code on Windows. Both C++ exceptions and .NET exceptions are built on top of the native SEH support provided by Windows. You will not have such guarantee on Linux or Apple operating systems however, a concern when you use Mono.

It is important that you build your C++ code with the correct settings, the MSVC++ compiler uses an optimization to avoid registering exception filters when it can see that code can never throw a C++ exception. That cannot work in your case, your RaiseException delegate target is going to throw one and the compiler has no chance at guessing at that. You must compile with /EHa to ensure your C++ destructors will be called when the stack is unwound. You'll find more details in this answer.

OTHER TIPS

If you're ever planning on running on Mono, the answer is simple:

Don't do it

No more code will be executed in the native methods the exception would unwind over. No cleanup, no C++ destructors, nothing.

On the other hand this means that if you're sure none of the native frames on the stack have any cleanup to do (if you're writing C++ code this can be harder than it looks), then you're free to throw managed exceptions at will.

The reason I so adamantly advice against doing this is because I once spent two days tracking down a memory leak because of exception handling unwinding though native frames. It is quite hard to track down and I was quite boggled for a while (breakpoints aren't hit, printfs don't print... but with the right tools it could have taken 5 minutes).

If you're still decided on throwing managed exceptions from native code, I'd do it just before returning to managed code:

void native_function_called_by_managed_code ()
{
    bool result;

    /* your code */

    if (!result)
        throw_managed_exception ();
}

And I would limit myself to C in those methods, since it's too easy to get into automatic memory management in C++ that would still leak:

void native_function_called_by_managed_code ()
{
    bool result;
    MyCustomObject obj;

    /* your code */

    if (!result)
        throw_managed_exception ();
}

This could leak because MyCustomObject's destructor isn't called.

You might have a problem with native resources not being freed correctly.

When an exception is thrown the stack gets unwind until it finds a matching try-catch block.

This is all good and nice, but there are some side effects of being in the middle of native and managed.

In regular C# all the objects created in the blocks on the way to the exception would eventually be freed by the garbage collector. But Dispose() isn't called unless you're in a using block.

On the other hand, in C++ if you'd have a native exception, all object created with new() would probably stay dangling and you'll have a memory leak, and the objects on the stack would be destroyed properly when the stack got unwound.

BUT if you don't have /EHa set, and you have a managed exception, it will only unwind the managed code. So, the native destructors of native objects created on the stack might not be called, and you might have memory leaks, or even worse - locks not being unlocked...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top