c# - 与不安全代码一起使用时 ref 的安全性如何?

标签 c# unsafe

使用 Microsoft Visual C# 2010,我最近注意到您可以通过 ref 将对象传递给非托管代码。因此,我给自己下了任务,尝试编写一些非托管代码,使用托管代码的回调将 C++ char* 转换为 C# 字符串。我做了两次尝试。

尝试 1: 调用存储 ref 参数的非托管函数。然后,一旦该函数返回托管代码,调用另一个非托管函数,该函数调用将 char* 转换为托管字符串的回调函数。

C++
typedef void (_stdcall* CallbackFunc)(void* ManagedString, char* UnmanagedString);

CallbackFunc UnmanagedToManaged = 0;
void* ManagedString = 0;

extern "C" __declspec(dllexport) void __stdcall StoreCallback(CallbackFunc X) {
    UnmanagedToManaged = X;
}
extern "C" __declspec(dllexport) void __stdcall StoreManagedStringRef(void* X) {
    ManagedString = X;
}
extern "C" __declspec(dllexport) void __stdcall CallCallback() {
    UnmanagedToManaged(ManagedString, "This is an unmanaged string produced by unmanaged code");
}

C#
[DllImport("Name.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void StoreCallback(CallbackFunc X);
[DllImport("Name.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void StoreManagedStringRef(ref string X);
[DllImport("Name.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void CallCallback();

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void CallbackFunc(ref string Managed, IntPtr Native);

static void Main(string[] args) {
    string a = "This string should be replaced";

    StoreCallback(UnmanagedToManaged);
    StoreManagedStringRef(ref a);
    CallCallback();
}

static void UnmanagedToManaged(ref string Managed, IntPtr Unmanaged) {
    Managed = Marshal.PtrToStringAnsi(Unmanaged);
}

尝试 2:将字符串引用传递给非托管函数,该函数将字符串引用传递给托管回调。

C++
typedef void (_stdcall* CallbackFunc)(void* ManagedString, char* UnmanagedString);

CallbackFunc UnmanagedToManaged = 0;

extern "C" __declspec(dllexport) void __stdcall StoreCallback(CallbackFunc X) {
    UnmanagedToManaged = X;
}
extern "C" __declspec(dllexport) void __stdcall DoEverything(void* X) {
    UnmanagedToManaged(X, "This is an unmanaged string produced by unmanaged code");
}

C#
[DllImport("Name.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void StoreCallback(CallbackFunc X);
[DllImport("Name.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void DoEverything(ref string X);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void CallbackFunc(ref string Managed, IntPtr Unmanaged);

static void Main(string[] args) {
    string a = "This string should be replaced";

    StoreCallback(UnmanagedToManaged);
    DoEverything(ref a);
}

static void UnmanagedToManaged(ref string Managed, IntPtr Unmanaged) {
    Managed = Marshal.PtrToStringAnsi(Unmanaged);
}

尝试 1 无效,但尝试 2 有效。在尝试 1 中,似乎一旦非托管代码在存储 ref 后返回,ref 就会变得无效。为什么会这样?

鉴于尝试 1 的结果,我怀疑尝试 2 能否可靠地工作。那么,当与非托管代码一起使用时,代码非托管端的 ref 有多安全?或者换句话说,在使用 ref 时什么在非托管代码中不起作用?

我想知道的是:

当使用 ref 将对象传递给非托管代码时究竟会发生什么?

它是否保证在非托管代码中使用 ref 时对象将停留在它们在内存中的当前位置?

在非托管代码中 ref 有什么限制(我不能用 ref 做什么)?

最佳答案

关于 p/invoke 工作原理的完整讨论超出了 Stack Overflow 问答的适当范围。但简而言之:

在您的两个示例中,您都没有真正将托管变量的地址传递给非托管代码。 p/invoke 层包括将托管数据转换为非托管代码可用的数据的编码(marshal)处理逻辑,然后在非托管代码返回时转换回来。

在这两个示例中,p/invoke 层都必须创建一个中间对象以用于编码(marshal)处理。在第一个示例中,当您再次调用非托管代码时,该对象已经消失。当然,在第二个示例中,情况并非如此,因为所有工作都是同时发生的。

我相信您的第二个示例应该可以安全使用。也就是说,p/invoke 层足够智能,可以在这种情况下正确处理 ref。第一个示例不可靠,因为 p/invoke 被滥用,而不是因为 ref 参数的任何基本限制。


补充几点:

  • 我不会在这里使用“不安全”这个词。是的,调用非托管代码在某些方面是不安全的,但在 C# 中,“不安全”具有非常具体的含义,与 unsafe 关键字的使用有关。我在您的代码示例中没有看到任何实际使用 unsafe 的内容。
  • 在这两个示例中,您都存在与使用传递给非托管代码的委托(delegate)相关的错误。特别是,虽然 p/invoke 层可以将托管委托(delegate)引用转换为非托管代码可以使用的函数指针,但它对委托(delegate)对象的生命周期一无所知。它将使对象存活足够长的时间,以便 p/invoked 方法调用完成,但如果您需要它存活的时间更长(就像这里的情况),您需要自己做。例如,对存储引用的变量使用 GC.KeepAlive()。 (您可能可以通过在调用 StoreCallback() 和稍后调用函数指针的非托管代码之间插入对 GC.Collect() 的调用来重现崩溃被使用)。

关于c# - 与不安全代码一起使用时 ref 的安全性如何?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34803294/

相关文章:

c# - Azure 事件中心 - 同一事件中心中的多种事件类型

c# - 通过反射确定方法是否不安全

c# - 字节数组中的字符递增

pointers - 使用原始指针访问 RefCell<HashMap<T>> 的 &T 是否安全?

c# - 您可以限制 .NET 进程对象的 CPU 使用率吗?

c# - 在 C# 中使不安全代码变得安全

c# - 检查数组是否包含无效值并返回 false?

javascript - 如何在文本区域显示来自服务器的数据

c# - 如何隐藏列(GridView)但仍然访问它的值?

c# - 模拟和委派