一个委托函数能否从非托管代码中引发.Net异常?

7

我在SO上搜索了很多相关的问题,有些答案基本上是“不要这样做”。

我想调用一些访问现有C++代码的非托管C++代码。现有代码可能存在各种错误条件,我希望将其映射到C#异常中。从在Java和JNI中进行类似操作的经验来看,似乎可以有一个委托函数来引发定义的异常,然后直接从非托管代码调用它们。然后调用看起来像(csharp)->(unmanaged)->(csharp delegate,throw/set pending exception),然后返回。

下面的代码似乎可以正常工作(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);
    } 
  }
}

更新:修正了测试和输出,显示了在使用mono和Windows(带有/EHsc)时的泄漏情况。
// Observed output // with Release builds /EHa, VS2010, .Net 3.5 target
//cstest.exe
// --- Deleted allocated stack '0' ---
// --- Deleted allocated stack '1' ---
// 1 --- 'Hello World!' ---
// 3 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!

// Observed LEAKING output // with Release builds /EHsc, VS2010, .Net 3.5 target
// cstest.exe
// 1 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!

// LEAKING output DYLD_LIBRARY_PATH=`pwd` mono program.exe 
// 1 --- 'Hello World!' ---
// Exception: System.ApplicationException:Something bad happened!

1
这个问题似乎不太适合,因为它实际上是在寻求代码审查。http://codereview.stackexchange.com/可能更合适。 - spender
2
我认为这不是离题的。除了作为附加评论外,这并不是要求代码审查。从问题中删除代码,并将“下面的代码似乎工作正常”更改为“该方法似乎工作正常”,您仍然拥有一个完整的问题。包含代码只是使理解所询问的内容更容易。 - user743382
我在代码中添加了一些内容,以测试未托管代码中的资源清理,并在底部展示了使用/EHa、/EHsc和在OSX上使用Mono时观察到的结果。 - Marvin
上面显示的输出值是在Visual Studio下进行调试构建的。当更改为启用优化的Release版本时,/EHsc未能像@Hans Passant所描述的那样运行析构函数。 - Marvin
两个有用的参考资料,讨论了一些问题,分别是http://www.mono-project.com/Mono:Runtime:Documentation:ExceptionHandling和http://msdn.microsoft.com/en-us/library/vstudio/1deeycx5.aspx。 - Marvin
3个回答

4
是的,只要在Windows上运行代码,您就可以使其正常工作。C++异常和.NET异常都是基于Windows提供的本机SEH支持构建的。但是,在Linux或Apple操作系统上使用Mono时,您将无法保证这样的情况。

重要的是,您需要以正确的设置构建C++代码,MSVC++编译器使用优化来避免在它能够看到代码永远不会引发C++异常时注册异常过滤器。但在您的情况下,RaiseException委托目标即将引发一个异常,编译器没有猜测的机会。 您必须使用/EHa编译,以确保在堆栈展开时调用C++析构函数。 您可以在这个答案中找到更多细节。


你的详细讨论(链接)非常有用。但是我感到困惑。我更新了上面的测试以进行一些堆栈分配,但它们似乎在使用 /EHa、EHsc 和 mono 时被销毁了。我还尝试了一个自定义异常(未显示),并验证了 C# 异常被适当地销毁。 - Marvin
就像我说的那样,只要你在 Windows 上运行这个程序,就没有问题。使用 /EHsc 并不能保证你会遇到问题,特别是在测试代码中,当 C++ 编译器能够看到你正在抛出异常时。 - Hans Passant
现在,当我能够定义事情正常工作的情况时,我可能会自己回答。 目前看来,使用 /EHa 确实有效。 - Marvin

3
如果你计划在Mono上运行,答案很简单: 不要这样做 异常将不会在本地方法中执行任何清理工作,也没有C++析构函数等。但这也意味着,如果你确定堆栈上的原生帧没有任何清理工作(如果你编写C ++代码,则可能比看起来更难),那么你可以自由地随意抛出托管异常。
我如此坚决地反对这样做的原因是,因为曾经有一次我花费了两天时间来跟踪由于异常处理在本地框架中展开而导致的内存泄漏,它很难追踪,我感到非常困惑(断点不会被命中,printf不会打印...但用正确的工具只需要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的析构函数没有被调用。

我更新了代码并进行了一些资源分配。看起来栈上的对象清理得很好。通过谷歌搜索,似乎支持你在Windows上的前提,即除非使用/EHa,否则析构函数不会被调用,但我没有观察到这种情况。而且mono也运行良好。 - Marvin
@Marvin,我认为你的测试用例是不正确的。如果你将堆栈变量声明为Allocated a0(0);Allocated a1(1);,它们是没有被释放的。此外,请注意在你的输出中,对象在异常抛出之前而不是之后被释放。 - Rolf Bjarne Kvinge
抱歉,我应该更加注意你所写的内容。你是正确的,对于mono的行为就像你所描述的那样。有趣的是,在讨论mono异常处理的未来时,提到了使用JNI方法来设置挂起异常并在返回托管代码时处理它可能会更简单和更好。 - Marvin

0

你可能会遇到本地资源没有正确释放的问题。

当抛出异常时,堆栈会一直展开,直到找到匹配的try-catch块为止。

这很好,但是在本地和托管之间存在一些副作用。

在常规的C#中,在到达异常的块上创建的所有对象最终都将由垃圾收集器释放。但是,除非您在使用块中,否则不会调用Dispose()。

另一方面,在C++中,如果您有一个本地异常,所有使用new()创建的对象可能会保持悬空状态,您将有一个内存泄漏,并且堆栈上的对象将在堆栈展开时被正确销毁。

但是,如果您没有设置/EHa,并且您有一个托管异常,它只会展开托管代码。因此,可能不会调用堆栈上创建的本地对象的本地析构函数,您可能会有内存泄漏,甚至更糟糕-锁定未被解锁...


.Net 异常是使用 SEH 实现的,因此我认为只要非托管代码正确处理异常,本地资源就会被正确释放。 - Justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接