我在SO周围搜索并发现了各种相关问题,其中一些基本上回答“不要这样做。”
我想调用一些访问各种现有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);
}
}
}
更新:更正了测试和输出,显示单声道和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!
答案 0 :(得分:4)
是的,只要在Windows上运行代码,就可以完成这项工作。 C ++异常和.NET异常都是基于Windows提供的本机SEH支持构建的。但是,在使用Mono时,您将无法在Linux或Apple操作系统上获得此类保证。
使用正确的设置构建C ++代码非常重要,MSVC ++编译器使用优化来避免在可以看到代码永远不会抛出C ++异常时注册异常过滤器。这在你的情况下不起作用,你的RaiseException委托目标将抛出一个,编译器没有机会猜测它。 必须使用/ EHa进行编译,以确保在堆栈展开时调用C ++析构函数。您可以在this answer中找到更多详细信息。
答案 1 :(得分:3)
如果您计划在Mono上运行,答案很简单:
不要这样做
在异常将展开的本机方法中不再执行任何代码。没有清理,没有C ++析构函数,没有。
另一方面,这意味着如果您确定堆栈中的任何本机帧都没有任何清理要做(如果您正在编写C ++代码,这可能比它看起来更难),那么您就是免费的随意抛出托管异常。
我坚决反对这样做的原因是因为我曾经花了两天时间追踪内存泄漏,因为异常处理通过本机帧解除。追踪非常困难,我有一段时间感到困惑(断点没有被击中,printfs不打印......但是with the right tools可能需要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的析构函数。
答案 2 :(得分:0)
您可能遇到无法正确释放本机资源的问题。
当抛出异常时,堆栈将被解除,直到找到匹配的try-catch块为止。
这一切都很好,但是在本地和托管中间存在一些副作用。
在常规C#中,在异常的路上在块中创建的所有对象最终将被垃圾收集器释放。但是除非你在使用块中,否则不会调用Dispose()。
另一方面,在C ++中,如果你有一个本机异常,用new()创建的所有对象都可能保持悬空状态,你会有内存泄漏,堆栈上的对象会被正确销毁。堆栈被解开了。
但是如果您没有设置/ EHa,并且您有托管异常,它将只展开托管代码。因此,可能不会调用在堆栈上创建的本机对象的本机析构函数,并且您可能有内存泄漏,甚至更糟糕 - 锁未被解锁...