[转]Passing Managed Structures With Strings To Unmanaged Code Part 3

[转]Passing Managed Structures With Strings To Unmanaged Code Part 3

1. Introduction.

1.1 In part 1 of this series of blogs we studied how to pass a managed structure (which contains strings) to unmanaged code. The structure was passed as an “in” (by-value) parameter, i.e. the structure was passed to the unmanaged code as a read-only parameter.

1.2 Then in part 2, we studied the techniques for receiving such a structure from unmanaged code as an “out” parameter.

1.3 Here in part 3, I shall expound on passing a structure (which contain strings) to and from unmanaged code. That is, as a parameter passed in both “in” and “out” directions (or a reference parameter).

2. Sample Structure, Unmanaged String Representations and Marshaling Direction.

2.1 We shall continue to use the test structures that we have bult up from part 1.

2.2 No matter the direction of parameter passing, the ways that a string structure member can be represented in unmanaged code remain the same :

  • As a fixed length inline NULL-terminated character array (aka C-style string).
  • As a pointer to a NULL-terminated character array (better known as LPCSTR in C/C++ lingo).
  • As a BSTR.

2.3 Note that this time, the marshaling direction will be 2-ways. That is, from the managed side to the unmanaged side and reverse. Hence the string allocated on the managed side will be visible on the unmanaged side. Furthermore, the string allocated on the unmanaged side and returned “outwardly” to the managed side will also be marshaled.

2.4 Throughout the various examples below, attention will be given to the effects of marshaling an “in” and “out” parameter.

3. Passing a Structure with a Fixed-Length Inline String.

3.1 For the demonstrative codes presented in this section, we shall use the C# structure TestStruct01 :

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
public struct TestStruct01
{
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)]
  public string m_strString;
};

3.2 And the C++ equivalent is as usual :

#pragma pack (1)

struct TestStruct01
{
  char m_szString[21];
};

3.3 An example C++-based unmanaged API that takes such a structure as an “in” and “out” parameter is listed below :

void __stdcall ReferenceTestStruct01
(
  /*[in, out]*/ TestStruct01* ptest_struct_01
)
{
  printf
  (
    "Value of ptest_struct_01 -> m_szString before change : [%s].
",
    ptest_struct_01 -> m_szString
  );
  strcpy (ptest_struct_01 -> m_szString, "From unmanaged code.");
}

3.4 The C# declaration for such an API is listed below :

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void ReferenceTestStruct01([In][Out] ref TestStruct01 test_struct_01);

3.5 And a sample C# code for invoking the API :

static void ReferenceTestStruct01()
{
  TestStruct01 test_struct_01 = new TestStruct01();

  test_struct_01.m_strString = "From managed code.";

  ReferenceTestStruct01(ref test_struct_01);

  DisplayTestStruct01(test_struct_01);
}

This time, the test_struct_01 structure is both instantiated and initialized before the call to ReferenceTestStruct01(). This is a necessary action because “test_struct_01″ is to be passed as a “ref” parameter which means that its value is both passed to and set by the ReferenceTestStruct01() API.

We then used DisplayTestStruct01() (an API we have already met in part 1) to display the value set for “test_struct_01.m_strString”.

3.6 Under the covers, the following is how the interop marshaler performs the marshaling process :

  • When the ReferenceTestStruct01() API was called, the interop marshaler will allocate a buffer with space large enough to hold the structure which is listed in point 3.2.
  • Note that this temporary buffer need not always be freshly allocated using Marshal.AllocHGlobal() or Marshal.AllocCoTaskMem(). This buffer may be from a cached and re-usable memory area managed by the CLR.
  • But wherever it originated, this memory buffer serves to store a temporary unmanaged version of the TestStruct01 structure.
  • The interop marshaler will then pass a pointer to this structure to the ReferenceTestStruct01() API.
  • This time, being passed as both “in” and “out”, the unmamaged test_struct_01 structure will contain its initialized field values. Hence the ptest_struct_01 -> m_szString will contain the string “From managed code.”
  • The API will then re-assign the character values of ptest_struct_01 -> m_szString.
  • When the API returns, the interop marshaler will assign the string value in the unmanaged “m_szString” field to the counterpart “m_strString” field of the managed “test_struct_01″ structure. This is the effect of being an “out” parameter.
  • The managed “test_struct_01″ structure will now be completely re-set by the ReferenceTestStruct01() API.
  • At this time, the interop marshaler is free to re-use (for other purposes) the original buffer space which was used to hold the temporary structure.

3.7 The following expected output will be displayed on the console :

Value of ptest_struct_01 -> m_szString before change : [From managed code.].
test_struct_01.m_szString : [From unmanaged code.].

3.8 Whenever we deal with a structure with a string field typed as an inline embedded character array, things are pretty simple. Most of the marshaling work is done by the interop marshaler.

3.9 It is the interop marshaler that allocates memory space for the unmanaged structure and performs the marshaling of field values to the unmanaged structure. When the unmanaged API returned, the interop marshaler transcribes the new character values of the inline character array field to the corresponding string field of the managed structure. The interop marshaler then either destroys the unmanaged structure or re-uses the memory occuppied by it.

4. Passing a Structure with a Pointer to a NULL-terminated Character Array.

4.1 In this section, we use the TestStruct02 structure that we first defined in part 1 :

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TestStruct02
{
  [MarshalAs(UnmanagedType.LPStr)]
  public string m_strString;
};

4.2 The corresponding C++ equivalent of such a structure is :

#pragma pack(1)

struct TestStruct02
{
  LPCSTR m_pszString;
};

4.3 An example C++-based unmanaged API that takes such a structure as an “in” and “out” parameter is listed below :

void __stdcall ReferenceTestStruct02(/*[in, out]*/ TestStruct02* ptest_struct_02)
{
  printf ("Value of ptest_struct_02 -> m_pszString before change : [%s].
", ptest_struct_02 -> m_pszString);

  if (ptest_struct_02 -> m_pszString)
  {
    ::CoTaskMemFree((LPVOID)(ptest_struct_02 -> m_pszString));
    ptest_struct_02 -> m_pszString = NULL;
  }
  char szTemp[] = "From unmanaged code.";
  ptest_struct_02 -> m_pszString = (LPSTR)::CoTaskMemAlloc(sizeof(szTemp));
  strcpy ((LPSTR)(ptest_struct_02 -> m_pszString), szTemp);
}

4.4 The C# declaration for such an API is listed below :

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void ReferenceTestStruct02([In][Out] ref TestStruct02 test_struct_02);

4.5 And a sample C# code for invoking the API :

static void ReferenceTestStruct02()
{
  TestStruct02 test_struct_02 = new TestStruct02();

  test_struct_02.m_strString = "From managed code.";

  ReferenceTestStruct02(ref test_struct_02);

  DisplayTestStruct02(test_struct_02);
}

Just like the example we met in section 3, the test_struct_02 structure is both instantiated and initialized before the call to ReferenceTestStruct02(). This is a necessary action because test_struct_02 is to be passed as a “ref” parameter which means that its value is both passed to and set by the ReferenceTestStruct02() API.

We then used DisplayTestStruct02() (an API we have already met in part 1) to display the value set for test_struct_02.m_strString.

4.6 Under the covers, the following are the processes that take place throughout the ReferenceTestStruct02() API call :

  • When the ReferenceTestStruct02() API was called, the interop marshaler will allocate a buffer with space large enough to hold the structure which is listed in point 4.2.
  • Note that this temporary buffer need not always be freshly allocated using Marshal.AllocHGlobal() or Marshal.AllocCoTaskMem(). This buffer may be from a cached and re-usable memory area managed by the CLR.
  • But wherever it originated, this memory buffer serves to store a temporary unmanaged version of the TestStruct02 structure.
  • The interop marshaler will then pass a pointer to this structure to the ReferenceTestStruct02() API.
  • This time, being passed as both “in” and “out”, the unmamaged test_struct_02 structure must contain the initialized field values of the managed test_struct_02 structure.
  • Using the current value in test_struct_02.m_strString (i.e. “From managed code.”), the interop marshaler will allocate (using Marshal.StringToCoTaskMemAnsi()) a sufficiently-sized character buffer and then copy the managed string to it. A pointer to this buffer will be assigned to the m_pszString field of the unmanaged structure.
  • This is done before ReferenceTestStruct02() is invoked.
  • Hence when code control reaches ReferenceTestStruct02(), and ptest_struct_02 -> m_pszString is to be printed, “From managed code.” will be displayed on the console.
  • The API will then re-assign ptest_struct_02 -> m_pszString by first freeing the buffer that it points to (using ::CoTaskMemFree()).
  • Note that ::CoTaskMemFree() must be used because it matches Marshal.AllocCoTaskMem().
  • The API will then re-allocate a new buffer using ::CoTaskMemAlloc() and make ptest_struct_02 -> m_pszString point to it.
  • A new string will be assigned to the new buffer.
  • Then when the API returns, the interop marshaler will assign the string contained in the buffer pointed to by ptest_struct_02 -> m_pszString to the counterpart m_strString field of the managed test_struct_02 structure. This is the effect of being an “out” parameter.
  • The managed test_struct_02 structure will now be completely re-set by the ReferenceTestStruct02() API.
  • At this time, the interop marshaler is free to re-use (for other purposes) the original buffer space which was used to hold the temporary structure.

4.7 The following expected output will be displayed on the console screen :

Value of ptest_struct_02 -> m_pszString before change : [From managed code.].
test_struct_02.m_pszString : [From unmanaged code.].

4.8 When we pass by reference a structure with a string field typed as a pointer to a NULL-terminated character array, the unmanaged code shares some responsibility in the re-allocation of memory for the unmanaged string field. The memory allocation and deallocation must be done using ::CoTaskMemAlloc() and ::CoTaskMemFree(). This is the required protocol.

4.9 Note well that the managed client code need not always pre-assign value to test_struct_02.m_strString before calling ReferenceTestStruct02(). If a NULL pointer was set for ptest_struct_02 -> m_pszString, then ReferenceTestStruct02() need not call ::CoTaskMemFree().

4.10 In the same way, if, on return from ReferenceTestStruct02(), ptest_struct_02 -> m_pszString is NULL, the interop marshaler will not free it. However, this will effective set the managed test_struct_02.m_strString field to an empty string.

4.11 If ReferenceTestStruct02() simply leaves ptest_struct_02 -> m_pszString alone and does not free it nor re-assign it, then on return, the interop marshaler will use whatever buffer that ptest_struct_02 -> m_pszString points to (as if it was freshly allocated by ReferenceTestStruct02()) to assign to test_struct_02.m_strString. It will then free the buffer pointed to by ptest_struct_02 -> m_pszString.

5. Passing a Structure with a BSTR.

5.1 For this section, we use the TestStruct03 structure that we first defined in part 1 :

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TestStruct03
{
  [MarshalAs(UnmanagedType.BStr)]
  public string m_strString;
};

5.2 The corresponding C++ equivalent of such a structure is :

#pragma pack(1)

struct TestStruct03
{
  BSTR m_bstr;
};

5.3 An example C++-based unmanaged API that takes such a structure as an “in” and “out” parameter is listed below :

void __stdcall ReferenceTestStruct03(/*[in, out]*/ TestStruct03* ptest_struct_03)
{
  printf ("Value of ptest_struct_03 -> m_bstr before change : [%S].
", ptest_struct_03 -> m_bstr);

  if (ptest_struct_03 -> m_bstr)
  {
    ::SysFreeString(ptest_struct_03 -> m_bstr);
    ptest_struct_03 -> m_bstr = NULL;
  }  

  ptest_struct_03 -> m_bstr = ::SysAllocString(L"BSTR from unmanaged code.");
}

5.4 The C# declaration for such an API is listed below :

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void ReferenceTestStruct03([In][Out] ref TestStruct03 test_struct_03);

5.5 And a sample C# code for invoking the API :

static void ReferenceTestStruct03()
{
  TestStruct03 test_struct_03 = new TestStruct03();

  test_struct_03.m_strString = "From managed code.";

  ReferenceTestStruct03(ref test_struct_03);

  DisplayTestStruct03(test_struct_03);
}

Just like the example we met in section 3 and 4, the test_struct_03 structure is both instantiated and initialized before the call to ReferenceTestStruct03(). This is a necessary action because test_struct_03 is to be passed as a “ref” parameter which means that its value is both passed to and set by the ReferenceTestStruct03() API.

We then used DisplayTestStruct03() (an API we have already met in part 1) to display the value set for test_struct_03.m_strString.

5.6 Under the covers, the following are the processes that take place throughout the ReferenceTestStruct03() API call :

  • When the ReferenceTestStruct03() API was called, the interop marshaler will allocate a buffer with space large enough to hold the structure which is listed in point 5.2.
  • Note that this temporary buffer need not always be freshly allocated using Marshal.AllocHGlobal() or Marshal.AllocCoTaskMem(). This buffer may be from a cached and re-usable memory area managed by the CLR.
  • But wherever it originated, this memory buffer serves to store a temporary unmanaged version of the TestStruct03 structure.
  • The interop marshaler will then pass a pointer to this structure to the ReferenceTestStruct03() API.
  • This time, being passed as both “in” and “out”, the unmamaged test_struct_03 structure must contain the initialized field values of the managed test_struct_03 structure.
  • Using the current value in test_struct_03.m_strString (i.e. “From managed code.”), the interop marshaler will allocate (using Marshal.StringToBSTR()) a BSTR with matching characters. A pointer to this BSTR will be assigned to the m_bstr field of the unmanaged structure.
  • This is done before ReferenceTestStruct03() is invoked.
  • Hence when code control reaches ReferenceTestStruct03(), and ptest_struct_03 -> m_bstr is to be printed, “From managed code.” will be displayed on the console.
  • The API will then re-assign ptest_struct_03 -> m_bstr by first freeing the BSTR that it points to (using ::SysFreeString()).
  • Note that ::SysFreeString() must be used because it matches Marshal.StringToBSTR().
  • The API will then re-allocate a new BSTR (“BSTR from unmanaged code.”) using ::SysAllocString() and make ptest_struct_03 -> m_bstr point to it.
  • Then when the API returns, the interop marshaler will assign the BSTR pointed to by ptest_struct_03 -> m_bstr to the counterpart m_strString field of the managed test_struct_03 structure. This is the effect of being an “out” parameter.
  • The managed test_struct_03 structure will now be completely re-set by the ReferenceTestStruct03() API.
  • At this time, the interop marshaler is free to re-use (for other purposes) the original buffer space which was used to hold the temporary structure.

5.7 The following expected output will be displayed on the console screen :

Value of ptest_struct_03 -> m_bstr before change : [From managed code.].
test_struct_03.m_bstr : [BSTR from unmanaged code.].

5.8 Just like the case in section 4 where we used a LPCSTR, when we pass by reference a structure with a string field typed as a BSTR, the unmanaged code shares some responsibility in the re-allocation of memory for the BSTR. The memory allocation and deallocation must be done using ::SysAllocString() and ::SysFreeString(). This is the required protocol.

5.9 Again, just like the case in section 4, the managed client code need not always pre-assign value to test_struct_03.m_strString before calling ReferenceTestStruct03(). If a NULL pointer was set for ptest_struct_03 -> m_bstr, then ReferenceTestStruct03() need not call ::SysFreeString().

5.10 In the same way, if, on return from ReferenceTestStruct03(), ptest_struct_03 -> m_bstr is NULL, the interop marshaler will not free it. However, this will effective set the managed test_struct_03.m_strString field to an empty string.

5.11 If ReferenceTestStruct03() simply leaves ptest_struct_03 -> m_bstr alone and does not free it nor re-assign it, then on return, the interop marshaler will use whatever buffer that ptest_struct_03 -> m_bstr points to (as if it was freshly allocated by ReferenceTestStruct03()) to assign to test_struct_03.m_strString. It will then free the BSTR pointed to by ptest_struct_03 -> m_bstr.

6. Special Handling for a Structure Passed by Pointer.

6.1 If the arrangement is such that a managed structure (which contains a string field) is to be passed as a pointer to an unmanaged API, and then get its string field assigned by the unmanaged API, special processing will need to be done.

6.2 As usual an unmanaged representation of the structure must be constructed somewhere in memory. And then a pointer to this unmanaged structure is to be passed to the API.

6.3 Furthermore, because the spirit and intent of what we are accomplishing now is to have the structure passed to the API in 2 directions “in” and “out” (in other words, as a reference “ref” parameter), the unmanaged structure must be initialized (to the exact values of the managed version) before being passed to the API. This point will be emphasized in the example codes which will be presented later on.

6.4 Let’s revisit the ReferenceTestStruct01() API :

void __stdcall ReferenceTestStruct01(/*[in, out]*/ TestStruct01* ptest_struct_01)
{
  printf ("Value of ptest_struct_01 -> m_szString before change : [%s].
", ptest_struct_01 -> m_szString);
  strcpy (ptest_struct_01 -> m_szString, "From unmanaged code.");
}

6.5 The original C# declaration for ReferenceTestStruct01() is as follows (as per point 3.4) :

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void ReferenceTestStruct01([In][Out] ref TestStruct01 test_struct_01);

6.6 Now, because the unmanaged API really does take a pointer to an unmanaged TestStruct01 structure, we can import the same API (i.e. ReferenceTestStruct01()) using a different declaration :

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ReferenceTestStruct01")]
public static extern void ReferenceTestStruct01_ByPointer(IntPtr ptest_struct_01);

6.7 This time, instead of passing in a managed TestStruct01 structure to the API, we need to pass an IntPtr which points to an unmanaged representation of a TestStruct01 structure. The following is a sample code :

static void ReferenceTestStruct01_ByPointer()
{
  // Create TestStruct01 and assign value to its field as usual.
  TestStruct01 test_struct_01 = new TestStruct01();

  test_struct_01.m_strString = "From managed code.";

  // Determine the size of the TestStruct01 for marshaling.
  int iSizeOfTestStruct01 = Marshal.SizeOf(typeof(TestStruct01));
  // Allocate in unmanaged memory a block of memory the size
  // of an unmanaged TestStruct01 structure.
  IntPtr ptest_struct_01 = Marshal.AllocHGlobal(iSizeOfTestStruct01);
  // Transfer the contents of the managed TestStruct01
  // (i.e. test_struct_01) to the unmanaged memory
  // which now serves as the unmanaged representation
  // of test_struct_01.
  Marshal.StructureToPtr(test_struct_01, ptest_struct_01, false);
  // Call the API using a pointer to the unmanaged test_struct_01.
  ReferenceTestStruct01_ByPointer(ptest_struct_01);
  // We now transfer the contents of the unmanaged TestStruct01
  // (pointed to by ptest_struct_01) to the managed memory
  // of test_struct_01.
  //
  // Remember that the spirit and intent of the
  // ReferenceTestStruct01_ByPointer() API is to
  // pass a TestStruct01 structure 2 ways : "in" and "out".
  // Hence whatever changes done to the structure
  // inside the API must be reflected on return.
  test_struct_01 = (TestStruct01)Marshal.PtrToStructure(ptest_struct_01, typeof(TestStruct01));
  // We now display the contents of the managed test_struct_01.
  DisplayTestStruct01(test_struct_01);
  // We must remember to destroy the unmanaged test_struct_01 structure.
  // Doing this will free any fields which are references
  // to memory.
  Marshal.DestroyStructure(ptest_struct_01, typeof(TestStruct01));
  // Finally, the block of memory allocated for the
  // unmanaged test_struct_01 must itself be freed.
  Marshal.FreeHGlobal(ptest_struct_01);
  ptest_struct_01 = IntPtr.Zero;
}

This code will produce the following output on the console :

Value of ptest_struct_01 -> m_szString before change : [From managed code.].
test_struct_01.m_szString : [From unmanaged code.].

6.8 Basically, the marshaling must be done completely manually. The following are pertinent points concerning the code above :

  • A managed TestStruct01 structure is to be allocated and its string field assigned.
  • An unmanaged representation of this structure will need to be allocated and initialized properly with field values from the managed version.
  • We first need to calculate the size of the unmanaged representation. This is done by using Marshal.SizeOf() which uses the StructLayoutAttribute of the structure and various MarshalAsAttributes assigned to the fields to perform the calculation.
  • With this size calculated, we can proceed to create the unmanaged representation by allocating in memory such a structure. This is done by using Marshal.AllocHGlobal(). Note that Marshal.AllocCoTaskMem() may be used as well. An IntPtr will be returned. This structure will have a memory format as depicted in the structure of point 3.2.
  • Next, we need to ensure that the unmanaged TestStruct01 structure is a replica of its managed counterpart. That is, all field values of the managed structure must be copied to the same values in the corresponding unmanaged fields. After all, remember that the structure is to be passed as an “in” parameter.
  • We do this by using the Marshal.StructureToPtr() method.
  • After this is done, the unmanaged representation of the TestStruct01 structure is complete. The IntPtr may be passed to the ReferenceTestStruct01_ByPointer() API.
  • The ReferenceTestStruct01_ByPointer() API will use the passed in value of ptest_struct_01 -> m_szString to display the contained string. After that it will assign character values into the same string buffer.
  • Now, remember that the spirit and intent of calling ReferenceTestStruct01_ByPointer() is to have this API display and then set the field values for the structure. Hence when the API returns, we need to transfer the contents of the unmanaged structure (which may have changed) to that of the managed version. This is done by using Marshal.PtrToStructure().
  • When Marshal.PtrToStructure() is done, the managed TestStruct01 structure will contain the same string value as that of the unmanaged representation.
  • Next comes the important task of memory deallocation : that of the fields of the unmanaged structure (which are not inline and embedded as part of the structure itself), and that of the unmanaged structure itself.
  • Fields of a structure which are not embedded as part of the structure are usually references to memory elsewhere. These must be destroyed with the help of the Marshal.DestroyStructure() method. This method clears the non-embedded fields with help from the MarshalAsAttributes assigned to the fields.
  • Now, with a field like TestStruct01.m_szString which is an inline embedded array of 21 ANSI characters, nothing needs to be done. The memory occuppied by this field is automatically recovered when the structure itself is recovered.
  • If we were dealing with TestStruct02, then with a field like TestStruct02.m_pszString (LPCSTR), the memory pointed to by this field, which will be allocated when Marshal.StructureToPtr() is called, Marshal.DestroyStructure() will recover the memory occuppied by the buffer pointed to by TestStruct02.m_pszString.
  • If we were dealing with TestStruct03, then with a field like TestStruct03.m_bstr (BSTR), the BSTR pointed to by this field, which will be allocated when Marshal.StructureToPtr() is called, Marshal.DestroyStructure() will free the BSTR pointed to by TestStruct03.m_bstr.

6.9 To help to test cases where we pass TestStruct02 and TestStruct03 by pointers, I have prepared the following code constructs for testing purposes :

static void InitializeStruct(ref TestStruct01 test_struct_01, string strInitial)
{
  test_struct_01.m_strString = strInitial;
}

static void InitializeStruct(ref TestStruct02 test_struct_02, string strInitial)
{
  test_struct_02.m_strString = strInitial;
}

static void InitializeStruct(ref TestStruct03 test_struct_03, string strInitial)
{
  test_struct_03.m_strString = strInitial;
}

delegate void Delegate_InitializeStruct<T>(ref T t, string strInitial);

delegate void Delegate_DisplayTestStruct0x<T>(T t);

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ReferenceTestStruct01")]
public static extern void ReferenceTestStruct01_ByPointer(IntPtr ptest_struct_01);

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ReferenceTestStruct02")]
public static extern void ReferenceTestStruct02_ByPointer(IntPtr ptest_struct_02);

[DllImport("TestDLL01.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ReferenceTestStruct03")]
public static extern void ReferenceTestStruct03_ByPointer(IntPtr ptest_struct_03);

delegate void Delegate_ReferenceTestStruct0x_ByPointer(IntPtr ptest_struct_0x);

static void ReferenceTestStruct0x_ByPointer<T>
(
  Delegate_InitializeStruct<T> pInitializer,
  Delegate_ReferenceTestStruct0x_ByPointer pReferenceFunction,
  Delegate_DisplayTestStruct0x<T> pDisplayFunction
) where T : new()
{
  // Create an instance of T and assign value to its field as usual.
  T test_struct_0x = new T();

  pInitializer(ref test_struct_0x, "From managed code.");

  // Determine the size of T for marshaling.
  int iSizeOfTestStruct0x = Marshal.SizeOf(typeof(T));
  // Allocate in unmanaged memory a block of memory the size
  // of an unmanaged T structure.
  IntPtr ptest_struct_0x = Marshal.AllocHGlobal(iSizeOfTestStruct0x);
  // Transfer the contents of the managed T
  // (i.e. test_struct_0x) to the unmanaged memory
  // which now serves as the unmanaged representation
  // of T.
  Marshal.StructureToPtr(test_struct_0x, ptest_struct_0x, false);
  // Call the API using a pointer to the unmanaged test_struct_0x.
  pReferenceFunction(ptest_struct_0x);
  // We now transfer the contents of the unmanaged T
  // (pointed to by ptest_struct_0x) to the managed memory
  // of test_struct_0x.
  //
  // Remember that the spirit and intent of the
  // ReferenceTestStruct0x_ByPointer() API is to
  // pass a T structure 2 ways : "in" and "out".
  // Hence whatever changes done to the structure
  // inside the API must be reflected on return.
  test_struct_0x = (T)Marshal.PtrToStructure(ptest_struct_0x, typeof(T));
  // We now display the contents of the managed test_struct_01.
  pDisplayFunction(test_struct_0x);
  // We must remember to destroy the unmanaged test_struct_0x structure.
  // Doing this will free any fields which are references
  // to memory.
  Marshal.DestroyStructure(ptest_struct_0x, typeof(T));
  // Finally, the block of memory allocated for the
  // unmanaged test_struct_0x must itself be freed.
  Marshal.FreeHGlobal(ptest_struct_0x);
  ptest_struct_0x = IntPtr.Zero;
}

The above code provides the following :

  • 3 overloaded initializer functions (InitializeStruct()), one each for the structs TestStruct01, TestStruct02 and TestStruct03. We have met these 3 functions previously in part 1.
  • Delegate Delegate_InitializeStruct<T> which is meant to generically point to one of the initializer functions.
  • Delegate Delegate_DisplayTestStruct0x<T> which is meant to point to one of the versions of the DisplayTestStructXX() APIs.
  • C# declarations for the ReferenceTestStruct01_ByPointer(), ReferenceTestStruct02_ByPointer() and ReferenceTestStruct03_ByPointer() APIs.
  • Delegate Delegate_ReferenceTestStruct0x_ByPointer which is meant to point to one of the ReferenceTestStructXX_ByPointer() APIs (i.e. ReferenceTestStruct01_ByPointer(), ReferenceTestStruct02_ByPointer() or ReferenceTestStruct02_ByPointer()).
  • ReferenceTestStruct0x_ByPointer<T>() which generically performs the equivalent of the ReferenceTestStruct01_ByPointer() function (listed in point 6.7) but for all the test structs.

6.10 For more details on passing a pointer to a structure from managed to unmanaged code, please refer to :

Passing a Pointer to a Structure from C# to C++ Part 1.

Passing a Pointer to a Structure from C# to C++ Part 2.

Passing a Pointer to a Structure from C# to C++ Part 3.

7. In Conclusion.

7.1 Here in part 3, we have examined how to pass a structure (which contains a string field) from managed to unmanaged code as an “in” and “out” (ref) parameter.

7.2 We have essentially combined the code actions done in part 1 and part 2. The dual directionality of marshaling has been duely emphasized.

7.3 We have learnt the 3 possible ways to express a string in unmanage code : as an inline character array, as a pointer to a character array, as a BSTR. We have also seen how each of these 3 types of strings may be passed to and returned from unmanaged code.

7.4 We also touched a little on the special handling required where the structures are passed by pointer.

7.5 Throughout this series of blogs, I hope to have demonstrated to the reader the basic principles behind marshaling : that managed data structures must first be transformed into unmanaged equivalents before they can be passed to unmanaged code. And that data to be received from unmanaged code must be transformed to its managed equivalent before it can be used in managed code.

7.6 It may take a while for the newbie to sink this in. But it is certainly consistent and repeatable.