7

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

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

下面的代码似乎工作正常(vs2010,单声道)。我的问题是这种方法有什么问题 - 例如,规范说,在调用非托管代码或线程问题等之后,不能保证异常仍然是“未决”的......

// 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!
4

3 回答 3

4

是的,只要您在 Windows 上运行代码,您就可以完成这项工作。C++ 异常和 .NET 异常都建立在 Windows 提供的本机 SEH 支持之上。但是,您在 Linux 或 Apple 操作系统上不会有这样的保证,这是您使用 Mono 时的一个问题。

使用正确的设置构建 C++ 代码很重要,MSVC++ 编译器使用优化来避免注册异常过滤器,因为它可以看到代码永远不会引发 C++ 异常。这在您的情况下不起作用,您的 RaiseException 委托目标将抛出一个并且编译器没有机会猜测。您必须使用 /EHa 进行编译,以确保在展开堆栈时调用您的 C++ 析构函数。您将在此答案中找到更多详细信息。

于 2013-10-28T16:31:23.393 回答
3

如果您打算在 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 的析构函数。

于 2013-10-28T17:14:38.200 回答
0

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

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

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

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

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

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

于 2013-10-28T15:47:56.640 回答