C# 是否可以使用委托函数从非托管代码引发.Net异常?

C# 是否可以使用委托函数从非托管代码引发.Net异常?,c#,.net,exception,mono,unmanaged,C#,.net,Exception,Mono,Unmanaged,我四处搜索,发现了各种相关的问题,其中一些回答基本上是“不要那样做” 我想调用一些非托管C++代码,它访问各种现有的C++代码。现有代码可能有各种错误条件,我想将它们映射到C#异常中。通过在Java和JNI中执行类似的操作,似乎可以使用委托函数来引发已定义的异常,然后可以直接从非托管代码调用该异常。然后调用看起来像(csharp)->(非托管)->(csharp委托,抛出/设置挂起异常),然后返回 下面的代码似乎工作正常(vs2010,mono)。我的问题是,这种方法是否有任何问题?例如,规范说

我四处搜索,发现了各种相关的问题,其中一些回答基本上是“不要那样做”

我想调用一些非托管C++代码,它访问各种现有的C++代码。现有代码可能有各种错误条件,我想将它们映射到C#异常中。通过在Java和JNI中执行类似的操作,似乎可以使用委托函数来引发已定义的异常,然后可以直接从非托管代码调用该异常。然后调用看起来像(csharp)->(非托管)->(csharp委托,抛出/设置挂起异常),然后返回

下面的代码似乎工作正常(vs2010,mono)。我的问题是,这种方法是否有任何问题?例如,规范说,在调用非托管代码后,异常不能保证仍然“挂起”,或者线程问题等等

// 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);
    } 
  }
}

您可能会遇到无法正确释放本机资源的问题

当抛出异常时,堆栈将展开,直到找到匹配的try-catch块

这一切都很好,很好,但是在本地和管理的中间会有一些副作用。 在常规C#中,在异常过程中在块中创建的所有对象最终都会被垃圾收集器释放。但是,除非处于using块中,否则不会调用Dispose()

另一方面,在C++中,如果你有一个本地异常,用New()创建的所有对象可能会悬空,并且会出现内存泄漏,并且堆栈中的对象在堆栈解开时会被正确地销毁。
但是如果您没有/EHa集,并且您有一个托管异常,那么它只会释放托管代码。因此,在堆栈上创建的本机对象的本机析构函数可能不会被调用,并且可能会出现内存泄漏,甚至更糟糕的情况-锁未解锁…

是的,只要在Windows上运行代码,就可以实现这一点。C++异常和.NET异常都是在Windows提供的本机SEH支持之上构建的。在Linux或Apple操作系统上,您将无法获得这样的保证,然而,当您使用Mono时,这是一个问题


<> P>重要的是,用正确的设置来构建C++代码,MSVC++编译器使用优化来避免注册异常过滤器,当它可以看到代码不能抛出C++异常时。这在您的情况下不起作用,您的RaiseException委托目标将抛出一个,编译器没有机会猜到这一点。您必须编译/EHA以确保在解压缩堆栈时调用C++析构函数。您将在中找到更多详细信息。

如果您计划在Mono上运行,答案很简单:

不要这样做

本机方法中将不再执行任何代码,异常将解除。没有清理,没有C++析构函数,没有。

另一方面,这意味着如果你确信栈上的任何一个本地帧都没有任何清理的话(如果你写C++代码,这比看起来更困难),那么你可以随意抛出托管异常。 我之所以如此坚决地建议不要这样做,是因为我曾经花了两天时间跟踪内存泄漏,因为异常处理通过本机框架展开。这很难追踪,我有一段时间很困惑(断点没有命中,printfs没有打印…但可能需要5分钟)

如果您仍然决定从本机代码抛出托管异常,我会在返回托管代码之前这样做:

void native_function_called_by_managed_code ()
{
    bool result;

    /* your code */

    if (!result)
        throw_managed_exception ();
}
<>我将把自己限制在C方法中,因为它太容易进入C++中的自动内存管理,否则会泄漏:

void native_function_called_by_managed_code ()
{
    bool result;
    MyCustomObject obj;

    /* your code */

    if (!result)
        throw_managed_exception ();
}

这可能会泄漏,因为MyCustomObject的析构函数没有被调用。

这个问题似乎离题了,因为它实际上要求进行代码审查。更合适。我不认为这是离题的。这并不要求进行代码审查,除非作为附加注释。从问题中删除代码,并将“下面的代码似乎工作正常”更改为“该方法似乎工作正常”,您仍然有一个完整的问题。包含代码只会使您更容易理解所询问的内容。我添加到代码中,以测试非托管代码中的资源清理,并在底部显示观察到的结果(在OSX上的mono下使用/EHa、/EHsc和)。上面显示的输出值是visual studio下的调试版本。当更改为通过优化发布时,/EHsc确实未能按照@Hans Passant所述运行析构函数。两篇关于某些问题的有用参考文献是,并且.Net异常是使用实现的,因此我相信只要非托管代码正确处理异常,本机资源就会正确释放。我用一些资源分配更新了代码。看起来堆栈上的对象被清理得很好。谷歌似乎同意你在Windows上的前提,即除非使用/EHa,否则不会调用析构函数,但我没有注意到这一点。mono似乎也很好用。@Marvin,我相信你的测试用例是错误的。如果将堆栈变量声明为
Allocated a0(0)
分配的a1(1),它们没有被释放。还要注意,在输出中,对象是在抛出异常之前释放的,而不是在抛出异常之后释放的。对不起,我应该更加注意您编写的内容。您是正确的,mono的行为正如您所描述的。有趣的是,在关于mono异常处理未来的讨论中,有人提到使用JNI方法简单地设置一个挂起的异常并在返回托管代码时处理它可能会更简单更好。您的详细讨论(链接)非常有用。但是我很困惑。我更新了测试(上面)来进行一些堆栈分配,它们似乎被/EHa、EHsc和mono破坏了。我
void native_function_called_by_managed_code ()
{
    bool result;
    MyCustomObject obj;

    /* your code */

    if (!result)
        throw_managed_exception ();
}