//
you're reading...
Interop Marshaling

Returning a C++ Class from an API in C#

1. Introduction.

1.1 Just today someone from the MSDN Common Language Runtime Forum asked a question about writing an API that can be called in C# to return a C++ class.

1.2 First of all, it must be noted that it is not possible to use an unmanaged class (e.g. a C++ class) in managed code (e.g. C#).

1.3 It is only possible to do so if the unmanaged class is exposed as a COM coclass and an instance of it is wrapped in a Runtime-Callable Wrapper.

1.4 However, it is possible to define a managed class or struct that represents the C++ class. This is the subject of this blog.

2. Representing a C++ class as a Managed Class or Struct.

2.1 A C# version of an unmanaged class is essentially a managed representation of the unmanaged class. It must contain equivalent, managed members. These managed members are usually marked with MarshalAsAttributes. Note well that there may be limitations (see section 3 below).

2.2 As an example, let’s say we have the following C++ class :

class CTestClass
{
public:
  CTestClass(int i, char* str)
  {
    m_int = i;
    strcpy(m_szString, str);
    m_pStr = "Fixed string.";
  }

private:
  int m_int;
  char m_szString[256];
  char* m_pStr;
};

2.3 It is possible to create an API that returns a pointer to such a C++ class, e.g. :

CTestClass* __stdcall TestAPI01(int i, char* str);

2.4 Such an API may be declared in C# code, for example, as :

[DllImport("TestCPPDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern CTestClass TestAPI01(int i, [MarshalAs(UnmanagedType.LPStr)] string str);

2.5 At runtime, the interop marshaler will be able to marshal the members of such an unmanaged class to its managed equivalent. The marshaling process is done with the help of various MarshalAsAttributes and the StructLayoutAttribute.

3. Limitations.

3.1 Of course, the methods of the unmanaged class cannot be “marshaled” to managed code. The managed class is essentially a copy of the unmanaged class’s structure. Methods of the unmanaged class must be re-created in managed code.

3.2 Not all unmanaged types may be represented properly in managed code. It all depends on whether there are UnmanagedType enums that can be used to represent them in managed code.

3.3 When you allocate a class instance in TestAPI01(), you must use either GlobalAlloc() (called with the GMEM_FIXED flag) or CoTaskMemAlloc(). You must not use the new operator. Doing so will cause a memory leak. This will be demonstrated in the example codes which will be expounded below. It will also be explained in greater detail in section 6 below.

3.5 If the C++ class contain any virtual methods (either declared on its own or inherited from an interface or base class), calculations must be made to return a pointer to the basic structure of the class itself (i.e. without the v-table pointers).

3.5 Note also that if the class is derived from base classes, then the member variables of the base classes must be included in the definition of the equivalent managed class. If this is not desireable, calculations must be made to return a pointer to the basic structure of the class itself (i.e. without the base class members).

4. Sample CTestClass Declaration in C++ and C#.

4.1 The example code presented in this section will use the following CTestClass C++ class which is an embellishment of the same class presented in section 2 above :

#pragma pack(1)
class CTestClass
{
public:
  CTestClass(int i, char* str)
  {
    m_int = i;
    strcpy(m_szString, str);
    m_pStr = "Fixed string.";
  }

  CTestClass& operator = (const CTestClass& rhs)
  {
    m_int = rhs.m_int;
    strcpy(m_szString, rhs.m_szString);
    m_pStr = rhs.m_pStr;
    return *this;
  }

private:
  int m_int;
  char m_szString[256];
  char* m_pStr;
};

I have enhanced CTestClass to contain the = operator. This will come in handy as we shall see later on.

4.2 A sample C# representation of the class :

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi, Pack=1)]
class CTestClass
{
  private int  m_int;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
  private string m_szString;
  [MarshalAs(UnmanagedType.LPStr)]
  private string m_pStr;
};

Note that the use of the various MarshalAsAttributes and the StructLayoutAttribute is very important. It determines how the interop marshaler views the equivalent unmanaged version of the class.

Here is an analysis of the declaration of the C# CTestClass :

4.2.1 The StructLayoutAttribute as used for the CTestClass :

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi, Pack=1)]

indicates that the unmanaged version of the CTestClass class is laid out in memory with its members appearing sequentially. The character used for string members are ANSI strings. The struct member alignment is 1 byte.

This matches the C++ CTestClass class as listed in section 4.1.

4.2.2 The first member “m_int” is an int type which is a blittable type. Attributes are not needed to help the interop marshaler blit the managed member to its unmanaged counterpart.

4.2.3 The “m_szString” member is declared as follows :

[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
 private string m_szString;

This indicates that “m_szString”, as a managed object, is of string type. Its unmanaged counterpart, however, is a fixed length (due to UnmanagedType.ByValTStr) ANSI charactered (due to CharSet=CharSet.Ansi) string. This string has 256 characters (due to SizeConst=256).

4.2.4 The “m_pStr” member is declared as follows :

[MarshalAs(UnmanagedType.LPStr)]
 private string m_pStr;

This indicates that “m_pStr”, as a managed object, is of string type. Its unmanaged counterpart, however, is a single byte, null-terminated ANSI character string.

4.3 As you can clearly see, the CTestClass has been declared such that its unmanaged counterpart matches the layout of the C++ CTestClass.

5. Sample Implementation for TestAPI01().

5.1 Let us now turn our attention to the TestAPI() function. The following is a sample implementation :

extern "C" __declspec(dllexport) CTestClass* __stdcall TestAPI01(int i, char* str)
{
  // Get the size of the class.
  size_t  stSize = sizeof(CTestClass);
  // Allocate in the global heap a block of memory
  // large enough for an instance of the class.
  CTestClass* pTestObject = (CTestClass*)(GlobalAlloc(GMEM_FIXED, stSize));
  // Create a temporary instance of the class and assign its values
  // to the instance in the global heap.
  *pTestObject = CTestClass(i, str);
  // Return the global heap class instance.
  return pTestObject;
}

Please refer to the comments in the above code for a clear explanation of the synopsis of the function.

5.2 The following is a sample C# code that calls TestAPI01() :

CTestClass test_obj = TestAPI01(100, "My String");

5.3 According to point 5.1, this API is to return a pointer to an unmanaged CTestClass instance. Now, if it returns a pointer to an unmanaged CTestClass instance, how will the C# code be able to use it ?

5.4 As it turns out, this is where the interop marshaler comes into play. The interop marshaler in fact, expects any unmanaged API that returns an unmanaged class or struct via a pointer. See section 6 below for a full analysis of the marshaling process.

6. How the Interop Marshaler marshals CTestClass from Unmanaged to Managed Code.

6.1 As stated in section 3.3, when an unmanaged function or COM method returns an unmanaged class or structure, it must be returned as a pointer to a memory block that is allocated by either by GlobalAlloc() or CoTaskMemAlloc().

6.2 Given the memory pointer, the interop marshaler will be able to identify the API that was used to allocate it.

6.3 The interop marshaler first internally allocate a new managed CTestStruct structure. It will then copy field values from the class/struct to their managed equivalents in the C# class or struct. This is done with the help of specifications of each managed member.

6.4 For example, the “m_int” member is an int type and so its unmanaged value will be directly blitted to the managed “m_int” menber by the interop marshaler.

6.5 The “m_szString” is a string type. It is not blittable. Hence the interop marshaler will use the member’s attribute information to determine the memory format of the member’s unmanaged counterpart. The member’s attribute has been designated as follows :

[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
private string m_szString;

This indicates that the unmanaged counterpart is a fixed-length character array. The character type is determined by the CharSet argument of the StructLayoutAttribute applied to the containing structure (which is CharSet.Ansi). Furthermore the length of the character array is determined by the SizeConst field (which is 256).

Hence the interop marshaler will proceed to treat the unmanaged counterpart as an array of 256 ANSI characters. We know that this specification is indeed correct. The interop marshaler will internally allocate a managed string for m_szString and use the ANSI character array as the character data for the string. This is likely performed by using one of the following string class’ constructors :

[CLSCompliantAttribute(false)]
public String(
sbyte* value,
int startIndex,
int length
);

or

[CLSCompliantAttribute(false)]
public String(
sbyte* value,
int startIndex,
int length,
Encoding enc
);

6.6 The “m_pStr” member is also a string but its attribute is specified as follows :

[MarshalAs(UnmanagedType.LPStr)]
private string m_pStr;

This indicates that its unmanaged counterpart is a single-byte and NULL-terminated ANSI character string. We know that this is indeed true. The interop marshaler will internally allocate a new string and to treat the unmanaged member’s address as a NULL-terminated string and use it to set the character values for the managed string.

This is likely performed by using the following string class constructor :

[CLSCompliantAttribute(false)]
public String(
sbyte* value
);

6.7 Now when the whole managed structure has been constructed this way, the unmanaged memory will be de-allocated by the interop marshaler. If the unmanaged memory was allocated previously by GlobalAlloc(), it will be de-allocated by the interop marshaler via the Marshal.FreeHGlobal() method. If the unmanaged memory was allocated using CoTaskMemAlloc(), it will be freed by Marshal.FreeCoTaskMem().

Note that it is correct that the interop marshaler should be the entity that frees the unmanaged memory because the API has returned it to its caller. This makes the interop marshaler the owner of the unmanaged memory and so it is at liberty to free it.

6.8 Now if the unmanaged memory was allocated by some method other than by GlobalAlloc() or CoTaskMemAlloc() (e.g. by the new operator), the interop marshaler will not know how to free this memory. It will be left with no choice but to leave it alone and assume that there exists program logic to free the memory. This can result in memory leak.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: