1

I want to keep a pointer to a managed Exception object in an unmanaged C assembly.

I've tried a bunch of ways. This is the only one I've found that passes my very preliminary tests.

Is there a better way?

What I'd really like to do is handle the alloc and free methods in the ExceptionWrapper constructor and destructor, but structs can't have constructors or destructors.

EDIT: Re: Why I would like this:

My C structure has a function pointer that is set with a managed delegate marshaled as an unmanaged function pointer. The managed delegate performs some complicated measurements using external equipment and an exceptions could occur during those measurements. I'd like to keep track of the last one that occurred and its stack trace. Right now, I'm only saving the exception message.

I should point out that the managed delegate has no idea it's interacting with a C DLL.

public class MyClass {
    private IntPtr _LastErrorPtr;

    private struct ExceptionWrapper
    {
        public Exception Exception { get; set; }
    }

    public Exception LastError
    {
        get
        {
            if (_LastErrorPtr == IntPtr.Zero) return null;
            var wrapper = (ExceptionWrapper)Marshal.PtrToStructure(_LastErrorPtr, typeof(ExceptionWrapper));
            return wrapper.Exception;
        }
        set
        {
            if (_LastErrorPtr == IntPtr.Zero)
            {
                _LastErrorPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ExceptionWrapper)));
                if (_LastErrorPtr == IntPtr.Zero) throw new Exception();
            }

            var wrapper = new ExceptionWrapper();
            wrapper.Exception = value;
            Marshal.StructureToPtr(wrapper, _LastErrorPtr, true);
        }
    }

    ~MyClass()
    {
        if (_LastErrorPtr != IntPtr.Zero) Marshal.FreeHGlobal(_LastErrorPtr);
    }
}
Ken Kin
  • 4,503
  • 3
  • 38
  • 76
qxn
  • 17,162
  • 3
  • 49
  • 72
  • 2
    Just curiosity, Why do you need this? `I want to keep a pointer to a managed Exception object in an unmanaged C assembly.` – L.B Jan 27 '12 at 22:03
  • What is the goal of doing this? You could copy the content (e.g. message) to an unmanaged object/exception. – Beachwalker Jan 27 '12 at 22:12
  • I updated my question with my reasoning. I already copy the message. I'd like to save the stack trace and any inner exceptions that occurred. – qxn Jan 27 '12 at 22:23
  • 1
    Passing the stacktrace & inner exceptions as string isn't enough? – L.B Jan 27 '12 at 22:30
  • I suppose it would be. I guess I didn't know this was such a bad idea. You learn something new everyday. – qxn Jan 27 '12 at 22:34

2 Answers2

5

This doesn't work. You are hiding a reference to the Exception object in unmanaged memory. The garbage collector cannot see it there so it cannot update the reference. When the C spits the pointer back out later, the reference won't point the object anymore after the GC has compacted the heap.

You'll need to pin the pointer with GCHandle.Alloc() so the garbage collector cannot move the object. And can pass the pointer returned by AddrOfPinnedObject() to the C code.

That's fairly painful if the C code holds on that pointer for a long time. The next approach is to give the C code a handle. Create a Dictionary<int, Exception> to store the exception. Have a static int that you increment. That's the 'handle' value you can pass to the C code. It is not perfect, you'll run into trouble when the program has added more than 4 billion exceptions and the counter overflows. Hopefully you'll never actually have that many exceptions.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • That makes sense, RE: the point that the GC can't see the Exception object. Thanks. – qxn Jan 27 '12 at 22:29
0

You want serialization.

As a side note, your statement of:

What I'd really like to do is handle the alloc and free methods in the ExceptionWrapper constructor and destructor, but structs can't have constructors or destructors.

is not true. structs in C# can have and do have constructor(s), just not allowing user to declare parameterless constructor explicitly. That is, for example, you can declare a constructor which accepts an Exception. For destructors, which is not widely used in managed code, you should implement IDisposable if your class will hold some unmanaged resources.

Exception is non-blittable, you may not marshalling it the way you described, but it can be serialized as byte arry thus makes interop possible. I've read your another question:

Implications of throwing exception in delegate of unmanaged callback

and take the some of the usage from your code. Lets's to have two projects, one for managed and the other for the unmanaged code. You can create them with all the default settings but note the bitness of the executable images should be set the same. There are just one file per project need to be modified:

  • Managed console application - Program.cs:

    using System.Diagnostics;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Runtime.InteropServices;
    using System.IO;
    using System;
    
    namespace ConsoleApp1 {
        class Program {
            [DllImport(@"C:\Projects\ConsoleApp1\Debug\MyDll.dll", EntryPoint = "?return_callback_val@@YGHP6AHXZ@Z")]
            static extern int return_callback_val(IntPtr callback);
    
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
            delegate int CallbackDelegate();
    
            static int Callback() {
                try {
                    throw new Exception("something went wrong");
                }
                catch(Exception e) {
                    UnmanagedHelper.SetLastException(e);
                }
    
                return 0;
            }
    
            static void Main() {
                CallbackDelegate @delegate = new CallbackDelegate(Callback);
                IntPtr callback = Marshal.GetFunctionPointerForDelegate(@delegate);
                int returnedVal = return_callback_val(callback);
                var e = UnmanagedHelper.GetLastException();
                Console.WriteLine("exception: {0}", e);
            }
        }
    }
    

    namespace ConsoleApp1 {
        public static class ExceptionSerializer {
            public static byte[] Serialize(Exception x) {
                using(var ms = new MemoryStream { }) {
                    m_formatter.Serialize(ms, x);
                    return ms.ToArray();
                }
            }
    
            public static Exception Deserialize(byte[] bytes) {
                using(var ms = new MemoryStream(bytes)) {
                    return (Exception)m_formatter.Deserialize(ms);
                }
            }
    
            static readonly BinaryFormatter m_formatter = new BinaryFormatter { };
        }
    }
    

    namespace ConsoleApp1 {
        public static class UnmanagedHelper {
            [DllImport(@"C:\Projects\ConsoleApp1\Debug\MyDll.dll", EntryPoint = "?StoreException@@YGHHQAE@Z")]
            static extern int StoreException(int length, byte[] bytes);
    
            [DllImport(@"C:\Projects\ConsoleApp1\Debug\MyDll.dll", EntryPoint = "?RetrieveException@@YGHHQAE@Z")]
            static extern int RetrieveException(int length, byte[] bytes);
    
            public static void SetLastException(Exception x) {
                var bytes = ExceptionSerializer.Serialize(x);
    
                var ret = StoreException(bytes.Length, bytes);
    
                if(0!=ret) {
                    Console.WriteLine("bytes too long; max available size is {0}", ret);
                }
            }
    
            public static Exception GetLastException() {
                var bytes = new byte[1024];
    
                var ret = RetrieveException(bytes.Length, bytes);
    
                if(0==ret) {
                    return ExceptionSerializer.Deserialize(bytes);
                }
                else if(~0!=ret) {
                    Console.WriteLine("buffer too small; total {0} bytes are needed", ret);
                }
    
                return null;
            }
        }
    }
    
  • Unnamaged class library - MyDll.cpp:

    // MyDll.cpp : Defines the exported functions for the DLL application.
    //
    
    #include "stdafx.h"
    #define DLLEXPORT __declspec(dllexport)
    #define MAX_BUFFER_LENGTH 4096
    
    BYTE buffer[MAX_BUFFER_LENGTH];
    int buffer_length;
    
    DLLEXPORT
    int WINAPI return_callback_val(int(*callback)(void)) {
        return callback();
    }
    
    DLLEXPORT
    int WINAPI StoreException(int length, BYTE bytes[]) {
        if (length<MAX_BUFFER_LENGTH) {
            buffer_length=length;
            memcpy(buffer, bytes, buffer_length);
            return 0;
        }
    
        return MAX_BUFFER_LENGTH;
    }
    
    DLLEXPORT
    int WINAPI RetrieveException(int length, BYTE bytes[]) {
        if (buffer_length<1) {
            return ~0;
        }
    
        if (buffer_length<length) {
            memcpy(bytes, buffer, buffer_length);
            return 0;
        }
    
        return buffer_length;
    }
    

With these code you can serialize the exception first and then deserialize it at any later time for retrieving the object that represents the original exception - just the reference would not be as same as the original, so are the objects it referenced.

I'd add some note of the code:

  1. The dll name and method entry point of DllImport should be modified as your real build, I didn't manipulate the mangled names.

  2. Unmanaged Helper is just a demonstration of the interop with the example unmanaged methods StoreException and RetrieveException; you don't have to write the code like how I deal with them, you might want to design in your own way.

  3. If you want to deserialize the exception within the unmanaged code, you might want to design your own formatter rather than BinaryFormatter. I don't know an existing implementation of Microsoft's specification in non-cli C++.


In case you want to build the code in C instead of C++, you'll have to change the Compile As option(Visual Studio) and rename MyDll.cpp to MyDll.c; also note the mangled names would be like _StoreException@8; use DLL Export Viewer or hex editors to find the exact name.

Ken Kin
  • 4,503
  • 3
  • 38
  • 76