Passing a SAFEARRAY of structs from .NET C# to VS6.0 C
-
Wednesday, July 18, 2007 11:01 PM
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?
All Replies
-
Thursday, July 19, 2007 6:25 PMModeratorVT_USERDEFINED strikes me as a wild guess. What does the 'C' code actually expect? Does it use IRecordInfo to access the structure members?
-
Friday, July 20, 2007 12:15 AMI 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:18 AMModeratorJust remove SafeArraySubType to let .NET marshal the WID structures.
-
Friday, July 20, 2007 1:39 AMI tried that also. It had no effect on the problem.
-
Sunday, July 22, 2007 1:25 PM
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.
-
Thursday, October 11, 2007 10:47 PMHey Jim,
Do you have any sample code for this? I'm trying to do something quite similar.
Many thanks,
Mike -
Saturday, October 13, 2007 12:08 AM
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 = records
.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, state
);
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.

