如何在C#中将异常封送到IntPtr

本文关键字:IntPtr 异常 | 更新日期: 2023-09-27 18:21:35

我想在非托管C程序集中保留一个指向托管Exception对象的指针。

我试过很多方法。这是我发现的唯一一个通过初步测试的。

有更好的方法吗

我真正想做的是处理ExceptionWrapper构造函数和析构函数中的alloc和free方法,但结构不能有构造函数或析构函数。

编辑:回复:为什么我想要这个:

我的C结构有一个函数指针,它是用封送为非托管函数指针的托管委托设置的。被管理的代表使用外部设备进行一些复杂的测量,在这些测量过程中可能会出现异常。我想跟踪最后一个发生的事件及其堆栈跟踪。现在,我只保存异常消息。

我应该指出,托管委托不知道它正在与一个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);
    }
}

如何在C#中将异常封送到IntPtr

这不起作用。您正在非托管内存中隐藏对Exception对象的引用。垃圾收集器在那里看不到它,因此无法更新引用。当C稍后将指针吐回时,在GC压缩堆之后,引用将不再指向对象。

您需要使用GCHandle.Aloc()固定指针,这样垃圾收集器就无法移动对象。并且可以将AddrOfPinnedObject()返回的指针传递给C代码。

如果C代码长时间保持该指针,那就相当痛苦了。下一种方法是给C代码一个句柄。创建一个Dictionary<int, Exception>来存储异常。有一个递增的静态int。这是可以传递给C代码的"handle"值。这并不完美,当程序添加了超过40亿个异常并且计数器溢出时,您会遇到麻烦。希望你永远不会有那么多例外。

您想要序列化

作为旁注,您的声明:

我真正想做的是处理ExceptionWrapper构造函数和析构函数中的alloc和free方法,但结构不能有构造函数或析构函数。

不是真的。C#中的struct可以具有构造函数,而不具有构造函数,只是不允许用户显式声明无参数构造函数。也就是说,例如,您可以声明一个接受Exception的构造函数。对于在托管代码中没有广泛使用的析构函数,如果类将包含一些非托管资源,则应该实现IDisposable

Exception是不可blitable的,您可能不会按照您描述的方式对其进行编组,但它可以序列化为字节arry,从而使互操作成为可能。我读过你的另一个问题:

在非托管回调的委托中引发异常的含义

并从代码中获取一些用法。让我们有两个项目,一个用于托管代码,另一个用于非托管代码。您可以使用所有默认设置创建它们,但请注意,可执行映像的比特度应设置为相同。每个项目只有一个文件需要修改:

  • 托管控制台应用程序-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;
            }
        }
    }
    
  • 未损坏的类库-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;
    }
    

使用这些代码,您可以先序列化异常,然后在以后的任何时候对其进行反序列化,以检索表示原始异常的对象-只是引用与原始异常不同,它引用的对象也不同。

我想添加一些代码注释:

  1. DllImport的dll名称和方法入口点应该根据您的实际构建进行修改,我没有处理损坏的名称。

  2. Unmanaged Helper只是示例非托管方法StoreExceptionRetrieveException的互操作的演示;你不必像我处理它们那样编写代码,你可能想用自己的方式设计。

  3. 如果要在非托管代码中反序列化异常,则可能需要设计自己的格式化程序,而不是BinaryFormatter。我不知道微软规范在非cli C++中的现有实现。


如果您想用C而不是C++构建代码,则必须更改Compile As选项(Visual Studio)并将MyDll.cpp重命名为MyDll.C;还要注意,被篡改的名称将类似于_StoreException@8;使用DLL导出查看器或十六进制编辑器来查找确切的名称。