C# P/C++;用于填充托管缓冲区的DLL

C# P/C++;用于填充托管缓冲区的DLL,c#,c++,pinvoke,marshalling,C#,C++,Pinvoke,Marshalling,基本上,我有一个DLL和一个头文件(C++),用于数据采集设备,用于我的研究。该设备有8个传感器,每个传感器有x、y、z等数据点。我想用WPF为这个设备创建一个UI程序。到目前为止,我已经成功地通过一个不安全的代码实现与.NET中的设备通信,并将数据读入一个结构数组(其中每个结构对应一个传感器)。然而,我想看看我是否能让它与一个严格安全的代码实现一起工作,但我已经碰壁有一段时间了。这是我第一次使用非托管代码和托管代码,请耐心听我说。我在网上搜索了无数的帖子,兔子洞越来越深,所以我想从有经验的人那

基本上,我有一个DLL和一个头文件(C++),用于数据采集设备,用于我的研究。该设备有8个传感器,每个传感器有x、y、z等数据点。我想用WPF为这个设备创建一个UI程序。到目前为止,我已经成功地通过一个不安全的代码实现与.NET中的设备通信,并将数据读入一个结构数组(其中每个结构对应一个传感器)。然而,我想看看我是否能让它与一个严格安全的代码实现一起工作,但我已经碰壁有一段时间了。这是我第一次使用非托管代码和托管代码,请耐心听我说。我在网上搜索了无数的帖子,兔子洞越来越深,所以我想从有经验的人那里得到一些建议。基本上,API头文件具有以下定义的函数:

int GetSynchronousRecord(USHORT sensorID, void *pRecord, int recordSize);
本质上,我们通过引用向函数传递一个缓冲区,然后它填充它。我可以选择获取单个传感器的数据,或者根据我传递的sensorID参数一次获取所有传感器的数据。到目前为止(在管理世界中)对我有效的是,如果我做了以下事情:

[StructLayout(LayoutKind.Sequential)]
public struct DOUBLE_POSITION_ANGLES_TIME_Q_RECORD
{
    public double x;     
    public double y; 
    public double z;  
    public double a; 
    public double e;    
    public double r;     
    public double time;
    public ushort quality;
};  

...

[DllImport("ATC3DG64.DLL", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetSynchronousRecord(ushort sensorID, ref DOUBLE_POSITION_ANGLES_TIME_Q_RECORD record, int recordSize);

...

DOUBLE_POSITION_ANGLES_TIME_Q_RECORD record = new DOUBLE_POSITION_ANGLES_TIME_Q_RECORD();
// Get the data from SENSOR_1 only
errorCode = GetSynchronousRecord(1, ref record, Marshal.SizeOf(record));
所以这个实现工作得很好,我可以以非常好的速度获得所有坐标数据。然而,我想一次得到所有的传感器。在C++ API代码示例中,它们传递了 GETSCORMUBIONGROUND> <强>函数:一个结构数组,每个传感器都有一个结构。我试着在C#中做同样的事情,如下所示:

[DllImport("ATC3DG64.DLL", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetSynchronousRecord(ushort sensorID, ref DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[] record, int recordSize);

// Define Array of Struct for each sensor
DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[] record = new DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[8];
while(recording) {
...
// Get data from ALL sensors (0xffff)
errorCode = GetSynchronousRecord(0xffff, ref record, Marshal.SizeOf(record)*8);
...
}
但这直接导致我的程序崩溃,出现System.ExecutionEngineeException错误。我已经读到,因为我的函数需要一个void*指针,所以我应该使用一个IntPtr参数,但老实说,这种方法看起来相当混乱。我尝试的另一件事是实际循环每个传感器并调用传感器的函数,但这将速度疯狂地降低到几乎1条记录/秒(而不是100条记录/秒)。stackexchange上的许多其他类似线程表示使用
out
参数,或在函数定义上使用[In,out]属性,但这些建议都不起作用

TL;Dr:<强>如果我正确地理解我的情况,我有一组托管的结构,我需要正确地传递给C++函数作为参数(通过引用),然后函数将用数据采集设备的数据填充我的结构。 我为文字/代码墙道歉,如果有经验的人能给我提供任何信息,我将不胜感激


编辑:只是澄清一下,GetSynchronousRecord函数位于while循环中,在每次迭代中,我都会为每个结构获取新的数据点

最好的方法可能是使用
IntPtr
而不是
ref DOUBLE\u POSITION\u ANGLES\u TIME\u Q\u RECORD

我将p/Invoke签名更改为:

[DllImport("ATC3DG64.DLL", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetSynchronousRecord(ushort sensorID, IntPtr record, int recordSize);
创建指向所需内存空间的IntPtr:

IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf<DOUBLE_POSITION_ANGLES_TIME_Q_RECORD>() * 8);
IntPtr ptr=Marshal.AllocHGlobal(Marshal.SizeOf()*8);
调用p/Invoke函数(该函数应使用结构填充内存):

errorCode=GetSynchronousRecord(0xffff,ptr,Marshal.SizeOf()*8);
从内存块获取结构:

DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[] records = new DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[8];
for (i = 0; i < 8; i++) {
     records[i] = Marshal.PtrToStructure<DOUBLE_POSITION_ANGLES_TIME_Q_RECORD>(IntPtr.Add(ptr, i * Marshal.SizeOf<DOUBLE_POSITION_ANGLES_TIME_Q_RECORD>()));
}
Marshal.FreeHGlobal(ptr);
DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[]records=新的DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[8];
对于(i=0;i<8;i++){
记录[i]=Marshal.PtrToStructure(IntPtr.Add(ptr,i*Marshal.SizeOf());
}
弗里赫全球元帅(ptr);

您的第二个p/invoke声明是错误的。你有

[DllImport("ATC3DG64.DLL", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetSynchronousRecord(
    ushort sensorID, 
    ref DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[] record, 
    int recordSize
);
问题是数组参数。因为通过
ref
传递该数组,它实际上是一个双指针。相反,您只需删除
ref
,并按如下方式声明导入:

[DllImport("ATC3DG64.DLL", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetSynchronousRecord(
    ushort sensorID, 
    [Out] DOUBLE_POSITION_ANGLES_TIME_Q_RECORD[] record, 
    int recordSize
);
[Out]
属性告诉封送拆收器数据正在从函数中流出。如果没有它,默认假设是数据流入

调用函数时,请执行以下操作:

errorCode = GetSynchronousRecord(0xffff, record, Marshal.SizeOf(record)*record.Length);

任何托管的“指针”都必须是固定的,以便GC在执行本机部分时不会移动它,谢谢Sasha,但我不确定在我的代码中,哪些地方是相关的,
fixed
是否仅在不安全的上下文中使用?我认为我不需要不安全的代码。您没有显示将第二个参数更改为引用传递的数组的pinvoke版本。这是错误的。很容易修复,但在问题完成之前我不会回答。@David Heffernan很抱歉,为了简洁起见,我没有包含它,但我已经更新了代码,在使用数组的情况下显示了P/Invoke参数,我希望这就是您的意思。谢谢。在将分配的数据传输到托管内存后,即在记录数组被填充后,还应该调用
Marshal.FreeHGlobal(ptr)
(理想情况下在
finally
块中,以避免发生异常时内存泄漏)来释放分配的内存。谢谢,这很有意义。我不确定我是否可以在这里问一个后续问题?因此,
GetSynchronousRecord
函数实际上位于一个循环中,用于连续获取数据。我是否必须在每次循环迭代中释放
ptr
内存,或者在每次迭代中旧数据将被新数据覆盖(然后我可以在采集结束后释放
ptr
)@LittleFinger了解您如何描述
GetSynchronousRecord
函数,不,它不会在for循环中。只有在调用了
GetSynchronousRecord
函数之后,您才能循环获取每个结构,然后在循环完成后释放它(我在回答中更新了该循环)@ub3rst4r抱歉,我的意思是,所有代码都在while循环中,因为我需要连续获取所有8个传感器数据。在每个
循环迭代过程中,我知道我需要
循环遍历每个结构。但我的问题是,我是否必须分配和免费
errorCode = GetSynchronousRecord(0xffff, record, Marshal.SizeOf(record)*record.Length);