Destructors not called when native (C++) exception propagates to CLR component
Asked Answered
R

4

9

We have a large body of native C++ code, compliled into DLLs.

Then we have a couple of dlls containing C++/CLI proxy code to wrap the C++ interfaces.

On top of that we have C# code calling into the C++/CLI wrappers.

Standard stuff, so far.

But we have a lot of cases where native C++ exceptions are allowed to propagate to the .Net world and we rely on .Net's ability to wrap these as System.Exception objects and for the most part this works fine.

However we have been finding that destructors of objects in scope at the point of the throw are not being invoked when the exception propagates!

After some research we found that this is a fairly well known issue. However the solutions/ workarounds seem less consistent. We did find that if the native code is compiled with /EHa instead of /EHsc the issue disappears (at least in our test case it did). However we would much prefer to use /EHsc as we translate SEH exceptions to C++ exceptions ourselves and we would rather allow the compiler more scope for optimisation.

Are there any other workarounds for this issue - other than wrapping every call across the native-managed boundary in a (native) try-catch-throw (in addition to the C++/CLI layer)?

Riddle answered 23/3, 2010 at 18:27 Comment(2)
did you find the right answer for thisBoudreaux
I don't recall now, unfortunately.Riddle
M
3

Unfortunately no I do not believe there are any good workarounds. The C++ exception implementation on MS platforms are implemented using SEH exceptions (IIRC). The CLR hooks into SEH handling to catch native exceptions and process them into CLR exceptions. Since it catches them at an SEH level the exception looks like an SEH exception to C++ and destructors are run or not run accordingly.

So as you've noted the best two options are

  • Compile with /EHa
  • Add a try/catch at the entry and exit point of your functions

Ideally you should be doing the second one anyways. In my experience it's considered bad practice to allow C++ exceptions to cross component boundaries.

There is also likely a hacky solution you could achieve using _set_seh_translator (Documentation). However I highly recommend avoiding that function as it can inadventently subvert CLR exception handling and cause a lot of unwanted problems.

Marika answered 23/3, 2010 at 18:35 Comment(1)
We're actually using SetUnhandledExceptionFilter() (see my response to @nobugz) and we're taking account of CLR exceptions in that handling. As for C++ exceptions crossing component boundaries: (1) The promise was that this would be automatically handled for us by the runtime - and, other than this issue, it is. (2) If we were to do it the logical place would be in our C++/CLI proxy code. But because that's already CLI code it's too late to catch this problem. So to go down that route we'd effectively have to have two layers of wrapping (one native, one managed)!Riddle
S
3

You are not doing this right I think. Using _set_se_translator() already requires you to compile with /EHa. From the MSDN Library page:

You must use /EHa when using _set_se_translator.

More seriously, you'll break managed code when you use it. The CLR relies on SEH exceptions to detect various mishaps. It uses SetUnhandledExceptionFilter to trap them. Particularly NullReferenceException and OverflowException (x86) are raised this way. When you inject your own __try block, you'll prevent this exception from flowing into the CLR. Although you'll "handle" it, you won't have any trace of the exact reason for the exception. And managed try/catch blocks can't detect it.

A cure for /EHa efficiency (if it is actually a problem) is 64-bit code. Its function table based stack unwinding is very efficient with zero overhead for a try block.

Sanious answered 23/3, 2010 at 19:38 Comment(1)
We're actually use SetUnhandledExceptionFilter() ourselves. We do chain onto peviously registered filters, so if there is a handler on the .Net side of the fence that still gets a look in (which is what is happening in the case the prompted the question). Using 64-bit code is not an option for us as this is in a framework that must be able to run on desktops (where our current standard is XP).Riddle
B
3

The MSDN page for the /E compiler switch does state this behaviour:-

http://msdn.microsoft.com/en-us/library/1deeycx5(VS.80).aspx

Here is the relevant quote:-

If you use /EHs, then your catch clause will not catch asynchronous exceptions. Also, in Visual C++ 2005, all objects in scope when the asynchronous exception is generated will not be destroyed even if the asynchronous exception is handled.

Basically /EHsc is the optimistic view - it assumes that the only exceptions are true C++ style ones and will optimise accordingly. /EHa on the other hand takes the pessimistic view and assumes that any line of code could cause an exception to be generated.

If you can guarentee that you'll never cause an Access Violation, or In-Page Error or other SEH then use /EHsc. But If you're writing a service and/or want to provide a "best effort" then /EHa is going to be necessary.

I also agree with @JaredPar's sentiments about not allowing exceptions to cross module boundaries. What @nobugz says about the way the CLR handles exceptions may be true, but I think there is a difference between .Net code calling out directly to native code using P/Invoke and calling into a C++/CLI interop DLL. In the former case the CLR has to handle the situation on your behalf, whereas in the latter you are in control and can translate accordingly.

Beverleebeverley answered 23/3, 2010 at 21:51 Comment(2)
Thanks Chris. However the case in my question is with C++ exceptions. As for crossing module boundaries, I'll comment on @JaredPar's response.Riddle
Sorry I focused on the /EHa bit. Are you saying that you see it even with a normal throw? e.g. throw std::runtime_error();Beverleebeverley
E
1

There is a bug in the 2.0 version of the CLR causing this issue. Running your managed executable on the 4.0 CLR will allow the destructors to be called as expected.

See Boost shared mutex not released after exception thrown for details.

Eulaheulalee answered 19/10, 2011 at 20:0 Comment(1)
See also connect.microsoft.com/VisualStudio/feedback/details/382860/…Simons

© 2022 - 2024 — McMap. All rights reserved.