Search code examples
c#c++pinvokemarshalling

P/Invoke C++ DLL for filling a managed buffer


Basically I have a DLL and a header file (C++) for a data acquisition device that I use for my research. The device has 8 sensors and each sensor has x,y,z,etc data points. I'd like to create a UI program in WPF for this device. So far I've successfully been able to communicate with the device in .NET via an unsafe code implementation and read data into an array of structs (where each struct corresponds to one of the sensors). However I'd like to see if I can get it working with a strictly safe code implementation, but I've just been hitting brick walls for a while now. This is my first time working with unmanaged vs. managed code so please bear with me. I've searched countless threads online and the rabbit hole just keeps getting deeper so I'd like some advice from someone with experience. Basically the API header file has a function with the following definition:

int GetSynchronousRecord(USHORT sensorID, void *pRecord, int recordSize);

Essentially we pass the function a buffer by reference, and it fills it up. I have the option of getting either a single sensor's data, or all sensors at once depending on the sensorID argument I pass. What HAS worked for me so far (in the managed world) is if I do the following:

[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));

So this implementation works fine, I can get all the coordinate data and at a really good speed. However, I'd like to get ALL the sensors at once. In the C++ API code samples, they pass the GetSynchronousRecord function an ARRAY of STRUCTS, one struct for each sensor. I tried to do the same in C# as follows:

[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);
...
}

But this straight up crashes my program with an System.ExecutionEngineException error. I've read that since my function is expecting a void* pointer, that I should use an IntPtr argument, but this approach seemed quite confusing to be honest. Another thing I tried is to actually loop over each sensor and call the function for the sensor, but this dropped the speed INSANELY almost to 1 record/second (instead of 100 records/second). Many other similar threads on stackexchange say to use out parameter, or to use [In, Out] attribute on the function definition, but none of these suggestions worked.

TL;DR: If I understand my situation correctly, I have a MANAGED array of structs that I need to correctly pass to a C++ function as an argument (pass by reference), and then the function will fill my structs with data from a data acquisition device.

I apologize for the wall of text/code, any information for me from someone with experience would be much appreciated.

EDIT: Just to clarify, the GetSynchronousRecord function is INSIDE a while loop where on each iteration I'm getting new data points for each struct.


Solution

  • Your second p/invoke declaration is wrong. You had

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

    The problem is the array parameter. Because you pass that array by ref that actually makes it a double pointer. Instead you want to simply remove the ref and declare the import like so:

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

    The [Out] attribute tells the marshaler that the data is flowing out of the function. Without it the default assumption is that the data flows in.

    When you call the function do so like this:

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