转换普通结构体:C#是否将它们复制到堆上?

7

我有一个原生的DLL,是用Delphi编写的,主动使用回调机制:一个回调函数被“注册”,然后从DLL内部调用:

function RegisterCallback(CallbackProc: TCallbackProc): Integer; stdcall;

大多数回调函数都是通过引用传递简单的结构体,例如以下内容:

TCallbackProc = procedure(Struct: PStructType); stdcall;

其中PStructType被声明为

TStructType = packed record 
  Parameter1: array[0..9] of AnsiChar;
  Parameter2: array[0..19] of AnsiChar;
  Parameter3: array[0..29] of AnsiChar;
end;
PStructType = ^TStructType;

这个动态链接库(DLL)被一个用C#编写的.NET应用程序所使用。C#代码编写得非常粗心,整个应用程序表现不可靠,出现难以识别的异常错误,并且每次运行时都会在不同的位置引发异常。

我没有理由怀疑这个DLL,因为它已经被证明是一个相当坚固的软件部件,已经被用于许多其他应用程序中。我目前担心的是这些结构在C#中的使用方式。

假设上面的记录在C#中重新声明如下:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct TStructType
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
    public string Parameter1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
    public string Parameter2;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 30)]
    public string Parameter3;
}

回调函数声明为:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void CallbackProc(ref TStructType Struct);

现在有些有趣的事情开始了。假设在DLL中,已注册的回调以以下方式被调用:
var
  Struct: TStructType;
begin
  // Struct is initialized and filled with values
  CallbackProc(@Struct);
end;

但是我在C#应用程序中看到的,令我非常不喜欢的是,被驱动的结构体被保存为指针以备将来使用:

private void CallbackProc(ref TStructType Struct)
{
    SomeObjectList.Add(Struct); // !!! WTF?
}

据我了解,结构体变量是在Delphi的堆栈中创建的,存储在DLL深处,并在客户端应用程序中将其指针存储在堆中,这是一种纯粹的冒险行为。
我不是C#的大粉丝/专家,所以请原谅我的天真问题,马歇尔是否在幕后做一些事情,例如将结构体复制到堆中或类似的操作,还是应用程序有时能正常工作纯属偶然?
提前致谢。
1个回答

4

C#中的struct是值类型。这意味着

SomeObjectList.Add(Struct)

这将复制结构体。所以,没有什么需要担心的。

实际上,在CallbackProc中,您并没有操作在Delphi代码中分配的对象。这是因为p/invoke marshaller必须将其收到的原始指针转换为TStructType对象。而TStructType包含C#字符串,这些字符串与那些Delphi字符数组绝对不可匹配。因此,marshaller已经在您的C#代码和Delphi代码之间添加了一层。

由于该函数通过引用接收结构体,因此发生的情况如下:

  1. 在调用CallbackProc之前,marshaller将未序列化的原始非托管指针转换为TStructType对象。
  2. 然后将CallbackProc传递给该TStructType对象的引用。
  3. CallbackProc返回时,p/invoke marshaller将TStructType对象序列化回原始的未处理的非托管指针。

其中一个后果是,您对TStructType对象所做的更改在回调过程返回之前对Delphi代码不可见。相比之下,当您调用将变量作为var参数传递的Delphi过程时,该过程中的任何更改都会立即在该过程之外可见。


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