다음을 통해 공유


.NET Interop and exception handling in mixed applications

I thought it would be useful to write a blog post about .NET Interop and exception handling, as I have found that, although there is available information on this, it is spread on the net and not easy to be put together - so here we go.

With the advent of .NET, one of the key features that has enabled developer productivity is without doubt code-level interoperability between the native and managed world . This allows developers to bring a large variety of code components under the same application hood:

  • .NET assemblies
  • .NET Winforms controls
  • WPF components (via WPF/Winforms interoperability)
  • native Dlls
  • MFC
  • COM / ActiveX
  • and so on

To accomplish interoperability at code level, several mechanisms to go from native to managed and the other way round are available:

  • P/Invoke : allows direct calls from .NET into native Dlls via DllImports. Marshalling of data types can be controlled by DllImport attributes. A sort of a native-to -managed counterpart of this is the Marshal::GetFunctionPointerForDelegate() function in System.Runtime.InteropServices, which allows converting a managed delegate into a function pointer that can be called from unmanaged code. For example:

namespace ClassLibrary1

{

    public class Class1

    {

        [DllImport("NativeDll.dll", EntryPoint = "#1")]

        static extern UInt32 MyFunction(UInt32 a);

        public int Calculate(int a, int b)

        {

                     int r = (int)MyFunction(200);

  • COM Interop : allows bi-directional interop via COM. Marshalling to/from .NET is taken care of transparentty by the CLR using wrappers, so this can be expensive in terms of performance.

Usually you would use Interop Assemblies (usually generated with tlbimp ) to call into COM from .NET. For the other way round, you can register a .NET assembly for COM Interop by using regasm or use the integrated Visual Studio support (Register for COM interop).

  • CLR hosting API : allosw native application to host and fine-grain control the CLR and call into .NET components. However, having to handle all CLR hosting aspects manually is a significant overhead.
  • C++/CLI : also known as It Just Works(IJW),  achieves interoperability at compiler level (C++, /clr compiler flag) by mixing native and managed code in a transparent way. It provides a lot of control on where the native/managed transition occurs and also provide high performance (basically, by transparently handling P/Invoke calls)

//Calling into managed code from C++ native MFC app:

using namespace ClassLibrary1;

BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)

END_MESSAGE_MAP()

void CallManagedFromMFC()

{

                                                Class1^ c = gcnew Class1();

                                                int result = c->Calculate(3,4);

                                                CString str = String::Format("Result: {0}", result);

                                                AfxMessageBox(str);

}

 

Clearly, things don't always work seamlessly together when integrating native and managed code and one major aspect in a mixed-mode scenario is error and exception handling - simply because there are significant differences in the exception generation and handling mechanisms in different types of components, especially when crossing the native/managed border.

One important thing to know is that the Common Language Runtime represents its managed exceptions as native SEH(Structured Exception Handling) exceptions and installs its own SEH filter for that - but the managed exception information is not carried in the SEH exception but rather managed internally in the CLR. This allows the CLR to fit quite well in a mixed application which is using also SEH.

While you can get CLR exceptions as native SEH in a native UnhandledExceptionFilter(), you will not be able to access managed exception information - simply because only the CLR itself can access that information.If you are chaining a SEH  UnhandledExceptionFilter() in your mixed application - which you should only do for good reason - there are two thighs to take into consideration:

  • propelry chain your filter as a "good Win32 citizen" and call the previously set filter before doing your processing
  • it is imperative not to interfere with the CLR SEH exceptions and always return EXCEPTION_CONTINUE_SEARCH for such exceptions - as for any exceptions you are not handling yourself: otherwise you can easily put the CLR in an undefined state.

Here's a code snippet, again from a MFC Application which sets a trivial UnhandledExceptionFilter that should behave well with the CLR:

#define CLR_EXCEPTION_CODE 0xE0434F4D

LPTOP_LEVEL_EXCEPTION_FILTER topLevelEF;

LONG WINAPI UEFHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)

{

                        // call previous handler first

                        (*topLevelEF)(ExceptionInfo);

                        _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;

                        if (ExceptionRecord->ExceptionCode==CLR_EXCEPTION_CODE)

                        {

                                                // CLR Exception in native handler

                                                return EXCEPTION_CONTINUE_SEARCH;

                        }

                       

                        return EXCEPTION_CONTINUE_SEARCH;

}

BOOL CMfcTestApp::InitInstance()

{

                        topLevelEF= SetUnhandledExceptionFilter(UEFHandler);

As said, you will not be able to get managed exception information in a native handler - you must use try/catch in C++/CLI - that is, in a managed context - if you really need that information.

Generally speaking, it is easier to deal with native errors/exceptions in managed code that the other way round, the main reason being that native code is unaware of managed concepts like managed exceptions, managed threads, application domains and so on. Let's see some common scenarios:

  1. P/Invoke is fairly straightforward: it is common practice to wrap your P/Invoke call in a try/catch block to get the exceptions generated by the call. You will get both exceptions generated by the P/Invoke service itself (like not finding the Dll or the entry point) and exceptions generated by the call itself. For Win32 functions, you can also retrieve the Win32 error code. See here for more details.
  2. In COM Interop, the runtime is converting COM HRESULT values to managed exceptions and the other way round: there is a layer of separation at the COM interfaces level. See here for further reading: https://msdn.microsoft.com/en-us/library/awy7adbx.aspx
  3. Ther CLR Hosting APIs allow extensive control of how CLR faults and exceptions are handled - but this is a fairly complex topic and most people prefer the C++/CLI approach
  4. C++/CLI provides a seamless exception handling approach for the programmer: you can catch both managed and native exceptions in /clr compiled code.  

Because CLR is using SEH, you will also get exceptions appropriately  when calling managed code from say a native MFC app which, in turn, calls again into native code and so on - it is very useful to understand how this happens, which is very nicely explained  in this article by Gaurav Khanna. There are a few things, that work differently when you host .NET code in a native application:

  • because you are running on a thread that has not been created by the CLR, the CLR unhandled exception mechanism may not be triggered in some situations - actually, it will only be triggered if the native thread is calling into .NET outside of any SEH handler protection
  • one side effect of this that I have encountered is that you will not get a AppDomain.UnhandledException Event in your application.This actually makes sense, as AppDomain is a purely managed concept.

If you rely on AppDomain.UnhandledException to log errors in your managed code, this will not work when running managed code on a native thread. If you absolutely need to get detailed managed exception information in a central handler - which is NOT something recommended (catch-all is not a good practice) - you will have to try/catch managed exception on a native frame that will get them. From a troubleshooting point of view, it should be better to just let your app crash and thus get exception info (dump) post-mortem rather than unwinding the stack in a catch-all handler. For reference, however, here's how you can get managed exceptions by overriding OnCmdMsg()  in a MFC app:

BOOL CMfcTestApp::OnCmdMsg(

   UINT nID,

   int nCode,

   void* pExtra,

   AFX_CMDHANDLERINFO* pHandlerInfo

)

{

    try

    {

        return CWinApp::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);

    }

    catch (System::Exception ^ex)

    {

                                                System::Windows::Forms::MessageBox::Show("Whoops! Maneged exception.");

        return 0;

    }

}

 

  • Speaking of AppDomains: since this is a pure managed concept, the question arises: in which AppDomain will a native thread calling into .NET be running ? The CLR must pick an AppDomain and it will the AppDomain the thread was the last time it called into managed code - usually the default AppDomain. You can have some control over the AppDomain by using Delegates and Marshal::GetFunctionPointerForDelegate() , as Delegates keep a reference to their AppDomain.

So next time you are planning an Interop scenario, you might find the above information useful for a check of the available options.

 

References:

BorisJ's Interop 101s: https://blogs.msdn.com/borisj/archive/2006/07/29/683061.aspx

https://msdn.microsoft.com/en-us/magazine/cc163567.aspx

https://msdn.microsoft.com/en-us/magazine/cc163971.aspx

https://msdn.microsoft.com/en-us/magazine/cc793966.aspx