none
Passing a SAFEARRAY of structs from .NET C# to VS6.0 C

    Question

  • I am trying to call a C routine written in VS 6.0 C with the following signature:

     

    short WINAPI DoInsert (SAFEARRAY **psa)

     

    I am using the following code in C#:

     

    [DllImport("APApply.dll",EntryPoint="DoInsert")]

    private static extern short DoInsert{

        [MarshalAs(UnmanagedType.SafeArray,SafeArraySubType=VarEnum.VT_USERDEFINED)]

        ref WID[] recordArray};

     

    ...

     

    int recordCount;

     

    (compute recordCount)

     

    WID[] recordArray = new WID[recordCount];

     

    (build recordArray)

     

    short res = DoInsert(ref recordArray);

     

    When I try to execute this code, I get an ArgumentException with the following text:

    "The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))"

     

    I have tried several variations of this idea with no success.  Can someone explain what I am doing wrong?

    Wednesday, July 18, 2007 11:01 PM

Answers

  • I have given up trying to do this using MarshalAs.

     

    Instead, I created the SAFEARRY in C#, used Marshal.AllocCoTaskMem and Marshal.Copy to copy it to unmanaged memory and passed an IntPtr pointing to the SAFEARRY in unmanaged memory.  I do not know if this is the only way to do it, but it works for me.

    Sunday, July 22, 2007 1:25 PM

All replies

  • VT_USERDEFINED strikes me as a wild guess.  What does the 'C' code actually expect?  Does it use IRecordInfo to access the structure members?
    Thursday, July 19, 2007 6:25 PM
    Moderator
  • I have already tried VT_USERDEFINED with no success.  The C code does not use IRecordInfo to access the structure members.  It uses SafeArrayAccessData to retrieve the address of the array data and store it in a structure pointer.  It then steps through the array by incrementing the structure pointer.  However, execution is not even reaching the C code.  It throws an InvalidArgumentException when trying to marshal the data for the call.
    Friday, July 20, 2007 12:15 AM
  • Just remove SafeArraySubType to let .NET marshal the WID structures.
    Friday, July 20, 2007 12:18 AM
    Moderator
  • I tried that also.  It had no effect on the problem.
    Friday, July 20, 2007 1:39 AM
  • I have given up trying to do this using MarshalAs.

     

    Instead, I created the SAFEARRY in C#, used Marshal.AllocCoTaskMem and Marshal.Copy to copy it to unmanaged memory and passed an IntPtr pointing to the SAFEARRY in unmanaged memory.  I do not know if this is the only way to do it, but it works for me.

    Sunday, July 22, 2007 1:25 PM
  • Hey Jim,

    Do you have any sample code for this?  I'm trying to do something quite similar.

    Many thanks,
    Mike
    Thursday, October 11, 2007 10:47 PM
  •  

    I can post excerpts from the code that I used but it is pretty specific to my application.  I am trying to write a Visual Studio 2005 C# program that interfaces with a Visual Studio 6.0 C library (.dll).  In particular, I want to call a function that has the following signature:

     

    short WINAPI DoInsert(
        HANDLEDATA      *HandleRec,
        SAFEARRAY       **psa,
        char            *err_msg,
        unsigned short  *relrecnum,
        long            custom_options)

     

    This is the only use I have for a SAFEARRAY type.  The SAFEARRY is a single dimension array of structures.  The lower bound of the single dimension is always 0.  The upper bound of the single dimension varies for each call.  Each element of the array represents a variable length record.  However, the record is represented by a fixed length structure that contains a pointer to a variable length string and some other fixed length members.

    I am using Platform Invoke to call the library function.  Following is the definition of the SAFEARRAY header structure and the declaration of the function I want to call:

     

    [StructLayout(LayoutKind.Sequential)]
    struct SafeArray
    {
        public ushort   dimensions;     // Count of dimensions in the SAFEARRAY
        public ushort   features;       // Flags to describe SAFEARRAY usage
        public uint     elementSize;    // Size of an array element
        public uint     locks;          // Number of times locked without unlocking
        public IntPtr   dataPtr;        // Pointer to the array data
        public uint     elementCount;   // Element count for first (only) dimension
        public int      lowerBound;     // Lower bound for first (only) dimension
    }


    [DllImport("WPApply.dll". EntryPoint="DoOnsert")]
    static extern short DoInsert(
        ref HandleData  databaseHandles,
        ref IntPtr      safeArrayPtr,
        StringBuilder   message;
        ref short       relativeRecordNumber,
        int             customOptionBits)

     

    At the time of the call to DoInsert, the COM Task memory contains the following memory allocations:
        1.  A block containing the SAFEARRAY header, which is described by the SafeArray structure.
        2.  A block containing an array of fixed length structures, which represent the array elements.
        3.  A separate block of memory for each variable length string that is pointed to from one of the fixed length structures.

    Since the first block never changes in size, I allocate once at the beginning of the program and it remains allocated until the end of the program.  It is allocated by a statement similar to the following:

     

        IntPtr  safeArrayPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(SafeArray)));

     

    The size of the second block is dependent on the number of elements in the array.  This block is originally allocated based on the number of elements in the array for the first call to DoInsert.  The block remains allocated after the call to DoInsert.  For subsequent calls, if an array with a greater number of elements is required, the block is deallocated and then reallocated to the newer, larger size; otherwise, the block is just reused.  This logic is contained in a separate function as follows:

     

    public void AllocateRecordBuffer(
        int recordCount)
    {
        // Check if required number of records exceeds current capacity of data buffer
        if (recordCount > recordBufferCount)
        {
            // Current record data buffer capacity exceeded -- free existing data buffer
            Marshal.FreeCoTaskMem(recordBufferPtr);

     

            // Allocate new record data buffer with new capacity
            int recordSize =

                WpInfoxData.FIXED_LENGTH_DATA_LENGTH * Marshal.SizeOf(typeof(byte)) +

                Marshal.SizeOf(typeof(IntPtr));
            recordBufferPtr = Marshal.AllocCoTaskMem(recordCount * recordSize);

     

            // Indicate current maximum number of records that can fit in data buffer
            recordBufferCount = recordCount;
        }
    }

     

    In this function, recordCount indicated the required capacity of this block measured in records.  The recordBufferCount value indicates the specified current capacity of the memeory block also specified in records.  This value is initialized to 0 before the first call to this function.  The class that holds the record data is called WpInfoxData and the member called FIXED_LENGTH_DATA_LENGTH is the length of the fixed data not including the pointer to th variable length.

    Now it is time to call DoInsert.  The array records is an array of WpInfoxData elements.  The array is passed as a SAFEARRAY to DoInsert as follows:

     

        // Ensure record buffer is large enough
        AllocateRecordBuffer(records.Count);

     

        // Build SAFEARRAY header for array of records
        SafeArray safeArray;
        safeArray.dimensions = 1;
        safeArray.features = 0;
        safeArray.elementSize =

            (uint)((WpInfoxData.FIXED_LENGTH_DATA_LENGTH * Marshal.SizeOf(typeof(byte))) +

            Marshal.SizeOf(typeof(IntPtr)));
        safeArray.locks = 0;
        safeArray.dataPtr = recordBufferPtr;
        safeArray.elementCount = (uint)records.Count;
        safeArray.lowerBound = 0;

     

        // Build SAFEARRAY array data
        int offset = 0;
        for (int i = 0; i < records.Count; i += 1)
        {
            // Add record to array buffer
            offset = recordsIdea.AddToRecordBuffer(recordBufferPtr, offset);
        }

     

        // Copy SAFEARRAY header for use by DoInsert
        Marshal.StructureToPtr(safeArray, safeArrayPtr, false);

     

    The AddToRecordBuffer method is a very repetive function that packs a large number of data elements into the record buffer.  The following provides a sample of how a few such elements are packed into the record buffer.

     

    public int AddToRecordBuffer(
        IntPtr bufferPtr,
        int offset)
    {
        // Check if a variable length data buffer already exists
        if (varDataPtr != IntPtr.Zero)
        {
            // Variable length data buffer already exists -- free the buffer
            Marshal.FreeCoTaskMem(varDataPtr);
        }

     

        // Allocate variable length data buffer and copy variable length byte array into it
        varDataPtr = Marshal.AllocCoTaskMem(bytes.Length * Marshal.SizeOf(typeof(byte)));
        Marshal.Copy(bytes, 0, varDataPtr, bytes.Length);

     

        // Store record number in data buffer and update offset
        Marshal.WriteInt32(bufferPtr, offset, recordNumber);
        offset += Marshal.SizeOf(typeof(int));

     

        // Store relative record number in data buffer and update offset
        Marshal.WriteInt16(bufferPtr, offset, relativeRecordNumber);
        offset += Marshal.SizeOf(typeof(short));

     

        // Store transaction type in data buffer and update offset
        Marshal.WriteByte(bufferPtr, offset, transactionType);
        offset += Marshal.SizeOf(typeof(byte));

     

        // Store state array in data buffer and update offset
        for (int i = 0; i < state.Length; i += 1)
        {
            // Store byte of state array in data buffer and update offset
            Marshal.WriteByte(bufferPtr, offset, stateIdea);
            offset += Marshal.SizeOf(typeof(byte));
        }

     

        // Store filler byte in data buffer and update offset
        Marshal.WriteByte(bufferPtr, offset, 0);
        offset += Marshal.SizeOf(typeof(byte));

     

        // Store flags in data buffer and update offset
        uint temp = flags;
        for (int i = 0; i < sizeof(uint); i += 1)
        {
            // Store byte of flags in data buffer and update offset
            Marshal.WriteByte(bufferPtr, offset, (byte)(temp & 0xff));
            offset += Marshal.SizeOf(typeof(byte));

     

            // Shift next byte into place
            temp >>= 8;
        }

     

        // Store variable length data length in data buffer and update offset
        Marshal.WriteInt32(bufferPtr, offset, bytes.Length);
        offset += Marshal.SizeOf(typeof(int));

     

        // Store variable length data buffer pointer in data buffer and update offset
        Marshal.WriteIntPtr(bufferPtr, offset, varDataPtr);
        offset += Marshal.SizeOf(typeof(IntPtr));

     

        // Return updated offset
        return offset;
    }

     

    The call to DoInsert is made as follows:

     

        short status = DoInsert(ref lsdbHandles2, ref safeArrayPtr, sbMessage,

            ref relativeRecordNumber, customOptionBits);

     

    Following the call, the memory for the SAFEARRAY header and the array buffer remains but the memory for each variable length data buffer is freed as follows:

     

        // Free memory allocated for each record
        foreach (WpInfoxData record in records)
        {
            // Free memory allocated for variable length data for record
            record.FreeVarDataMemory();
        }

     

    The FreeVarDataMemory function is as follows:

     

    public void FreeVarDataMemory()
    {
        // Check if variable length data buffer is allocated
        if (varDataPtr != IntPtr.Zero)
        {
            // Variable length data buffer exists -- free the buffer
            Marshal.FreeCoTaskMem(varDataPtr);
            varDataPtr = IntPtr.Zero;
        }
    }

     

    I realize this is sketchy and fairly explicit to my application, but I hope it helps.

    Saturday, October 13, 2007 12:08 AM