当在不安全的代码中使用 ref 关键字时,这是否安全?

4
使用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做什么)?

不安全代码被称为“不安全”,有理由的...它是不安全的。 :) - Almo
1个回答

2
p/invoke是如何工作的完整讨论超出了Stack Overflow Q&A的适当范围。但简单来说:
在你的两个示例中,你实际上都没有将托管变量的地址传递给非托管代码。p/invoke层包括编组逻辑,它将你的托管数据转换为可用于非托管代码的内容,然后在非托管代码返回时再次转换。
在这两个示例中,p/invoke层必须创建一个中间对象以进行编组。在第一个示例中,该对象在您再次调用非托管代码时已消失。当然,在第二个示例中,由于所有工作都同时进行,因此不存在这种情况。
我认为你的第二个示例应该是安全的使用。也就是说,p/invoke层足够聪明,可以正确处理ref的情况。第一个示例不可靠是因为p/invoke被误用,而不是任何fundamental limitation of ref参数。
几个额外的要点:
• 我不会在这里使用“unsafe”这个词。是的,调用非托管代码在某些方面上是不安全的,但在C#中,“不安全”的含义与使用unsafe关键字有关,我没有看到你的代码示例中实际使用了unsafe。
• 在这两个示例中,你都有一个关于你使用传递给非托管代码的委托的错误。特别是,虽然p/invoke层可以将你的托管委托引用转换为非托管代码可以使用的函数指针,但它不知道委托对象的生命周期。它将使该对象保持活动状态,直到p/invoke方法调用完成,但如果您需要它比那更长时间(如在这种情况下),则需要自己处理。例如,在存储引用的变量上使用GC.KeepAlive()。(您可能会通过在调用StoreCallback()和稍后调用非托管代码之间插入对GC.Collect()的调用来重现崩溃)。

你关于第二个额外的点是正确的。插入 GC.Collect() 导致了崩溃。我认为保持委托的静态引用也可以起作用。 - Jerry Hundric

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