Share via


Character Array Marshaling from C to C#

Question

Monday, June 13, 2011 11:21 PM

Hi,

My unmanaged code is in C, and returns a char const * from the function. More specifically, the function signature is:

__declspec(dllexport) char const *fromDLL()
 {

char const *hyp;

hyp = <initialization>

return hyp;

}

I have to call this function from C#, and print the value of "hyp" (that is returned from the above function) in the C# function. I've tried a couple of options, but the final output does match the value assigned in the native, unmanaged code. The three options/strategies that I have tried are:

Option 1: Using IntPtr in C#, and then using Marshal.PtrToStringAnsi to convert to String

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi)]
    public static extern IntPtr fromDLL();

string hypothesis = Marshal.PtrToStringAnsi(fromDLL());
Console.WriteLine("Hypothesis:" + hypothesis);

Output: Garbage value for hypothesis

 

Option 2: Using String in C#

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi)]
    public static extern string fromDLL();

Console.WriteLine("Hypothesis:" + fromDLL());

Output: No output, throws a System.AccessViolation exception (I suspect that this is for freeing the memory when you use string).

 

Option 3: Using ByteArray in C#

//DLL Import of the function call fromDLL which returns a const char *

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi)]

 public static extern IntPtr fromDLL();

 

//Converting the IntPtr to a Byte Array, and then to a UTF-8 Character Array

IntPtr ptr = fromDLL();

byte[] _byteTemp = new byte[Marshal.SizeOf(ptr)];

Marshal.Copy(p, _byteTemp, 0, Marshal.SizeOf(ptr));

Console.WriteLine("Hypothesis:" + System.Text.Encoding.UTF8.GetChars(_byteTemp));

 

Output: "Hypothesis: System.Char[]" --> (Not the hypothesis in the original, unmanaged C code)

 

Where am I going wrong in either of the three options? Although there are many prior threads that attempt to solve this, but I think, that most of them have suggested either of the three solutions as above. Thanks.

All replies (9)

Tuesday, June 14, 2011 4:16 AM âś…Answered

Hello Anuj K9,

 

Concerning Option 1: Using IntPtr in C#, and then using Marshal.PtrToStringAnsi to convert to String.

1. Note that this technique (original code below) should have worked well :

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi)]
public static extern IntPtr fromDLL();

string hypothesis = Marshal.PtrToStringAnsi(fromDLL());
Console.WriteLine("Hypothesis:" + hypothesis);

2. This is unless inside the fromDLL() function, "hyp" is made to point to some invalid or to a temporary location which will be wiped out by the time fromDLL() returns.

3. The following is an example :

extern "C" __declspec(dllexport) char const * __cdecl fromDLLOriginal()
{
  char szHypothesis[] = "hypothesis";
  char const *hyp;
**  // hyp now points to szHypothesis which is on the stack.**
**  hyp = szHypothesis;**

  return hyp;
}

Here "hyp" is made to point to "szHypothesis" which is a memory location on the stack. By the time, fromDLL() returns, this memory location will be wiped out.

4. If "hyp" points to a global memory location, the Marshal.PtrToStringAnsi() method will work correctly. The global memory where "hyp" points to will also remain.

 

- Bio.

 


Monday, June 13, 2011 11:34 PM

Try settings the calling convention:

 

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
public static extern string fromDLL();

 

Reed Copsey, Jr. - http://reedcopsey.com
If a post answers your question, please click "Mark As Answer" on that post and "Mark as Helpful".


Monday, June 13, 2011 11:39 PM

It's the same error in all three cases.


Monday, June 13, 2011 11:45 PM

It's the same error in all three cases.

With the Cdecl added?  You definitely need that, since you're not declaring the export in C++ as stdcall.

 

Reed Copsey, Jr. - http://reedcopsey.com
If a post answers your question, please click "Mark As Answer" on that post and "Mark as Helpful".


Monday, June 13, 2011 11:45 PM

More specifically, in the case when I use String as a return type (i.e. option 2), the exception is:

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that the memory is corrupt.

at Microsoft.Win32.Win32Native.CoTaskMemFree (IntPtr ptr)

at System.StubHelpers.CSTRMarshaler.ClearNative(IntPtr pNative)

at PlatformInvokeTest.fromDLL()


Monday, June 13, 2011 11:48 PM

With the Cdecl added?  You definitely need that, since you're not declaring the export in C++ as stdcall.

Yes, it is the same error with Cdecl added.


Tuesday, June 14, 2011 3:28 AM

Hello Anuj K9,

 

1. Solution.

1.1 There are several ways to achieve the same objective : returning a string from C/C++ to C#. I will present here a typical method. In section 2 below I shall provide greater explanation on the low-level activities that goes on behind the scenes.

1.2 Described below is the possible solution. Inside fromDLL(), code as follows :

1.2.1 Change the return type from "char const *" to "char*". Similarly, change the type for "hyp" to "char*".

1.2.2 Use the ::CoTaskMemAlloc() API to allocate memory for the character array to be returned to C#. Assign the pointer to this allocated memory to "hyp".

1.2.3 Copy the required string into "hyp".

1.2.4 Return "hyp".

1.3 The following is a sample listing :

const char* pg_hyp = "Hello World";

extern "C" __declspec(dllexport) char*  __cdecl fromDLL()
{
   char *hyp;
   ULONG ulSize = strlen(pg_hyp) + sizeof(char);

   //hyp = <initialization>
   hyp = (char*)::CoTaskMemAlloc(ulSize);
   // Copy the global string pointed to by "pg_hyp"
   // to the memory pointed to by "hyp".
   strcpy(hyp, pg_hyp);
   // Return "hyp".
   return hyp;
}

In the above example listing I assumed that a copy of a global character string is to be returned.

1.4 The C# code should also be modified, albeit only slightly. Declare the API as follows :

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
[return : MarshalAs(UnmanagedType.LPStr)]
public static extern string fromDLL();

static void CallUsingStringAsReturnValue()
{
            Console.WriteLine("Hypothesis:" + fromDLL());
}

The [return : ...] declaration is not absolutely necessary. I have added it for use in later explanations in another post.

I have also added the CallingConvention argument to the DllImportAttribute just to be sure that the calling convention was set correctly as advised by Reed Copsey.

 

2. Explanation.

2.1 First note that according to the DLLImport declaration for fromDLL() in point 1.4 above, the fromDLL() API will return a string which is a managed object.

2.2 Hence the returned characater array from the fromDLL() API must be processed by the CLR and converted into a managed strng object. It will use whatever data is inside the returned character array and transform it into a string object that represents it.

2.3 Now, by receiving a character array, it is the calling code (the CLR) that owns the memory returned from the fromDLL() API.

2.4 Because it owns this memory, the onus is on the CLR to eventually free it. This is where the call to CoTaskMemAlloc() inside fromDLL() comes into play. The CLR can only free unmanaged memory using Marshal.FreeCoTaskMem() (which internally calls CoTaskMemFree()) (or the Marshal.FreeHGlobal() method, if the unmanaged side used GlobalAlloc()).

2.5 Note that the unmanaged side must not use the "new" keyword or the "malloc()" C function. The CLR will not be able to free the memory in these situations. This is because the "new" keyword is compiler dependent and the "malloc" function is C-library dependent. CoTaskMemAlloc() and GlobalAlloc() on the other hand, are Windows APIs which is standard.

 

3. Other Techniques to Return Strings from Unmanaged Code to Managed Code.

3.1 I shall return with more posts to illustrate other ways to return strings from unmanage code to managed code.

3.2 I shall then explain why it is useful to use the [return] declaration as shown in point 1.4 above.

3.3 Note also that the technique that you used as described in "Option 1" [returning an IntPtr from fromDLL()] in the OP should have worked correctly unless "hyp" was made to point to a temporary location. I shall expound on this in my next post.

 

- Bio.

 


Tuesday, June 14, 2011 5:54 AM

Hello Anuj K9,

 

Another Technique : Use BSTR.

1. Yet another way to return a string from unmanaged code to managed code is via a BSTR.

 

2. To do this, the fromDLL() API should be coded as follows :

extern "C" __declspec(dllexport) BSTR  __cdecl fromDLL()
{
 BSTR hyp;

 hyp = ::SysAllocString(L"Hello BSTR");
 
 return hyp;
}

 

3. The C# side code should be as follows :

[DllImport("pocketsphinx.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.BStr)]
public static extern string fromDLL();

static void CallUsingBSTRAsReturnValue()
{
  string strRet = fromDLL();
  Console.WriteLine("Hypothesis:" + strRet);
}

 

4. This time, it is the return declaration that indicates that the returned value is to be treated as an unmanaged BSTR :

[return: MarshalAs(UnmanagedType.BStr)]

The CLR will use the returned BSTR and create a managed string object from the contents of the BSTR. This is done by the Marshal.PtrToStringBSTR() method.

The CLR will then proceed to free the returned unmanaged BSTR using Marshal.FreeBSTR().

 

5. Recall that in my first post I had used the following declaration for fromDLL() :

[DllImport("pocketsphinx.dll", CharSet = CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
[return : MarshalAs(UnmanagedType.LPStr)]
public static extern string fromDLL();

This had indicated to the CLR that the return value from fromDLL() is a pointer to a NULL-terminated ANSI string.

However, this is already the default marshaling specification, even without the CharSet = CharSet.Ansi argument. But using it (together with CharSet = CharSet.Ansi) does clarify things especially for documentation purposes.

 

6. For more information on interop memory allocation issues, refer to :
 
"Marshaling between Managed and Unmanaged Code" by Yi Zhang and Xiaoying Guo
 
http://msdn.microsoft.com/en-us/magazine/cc164193.aspx#S6
 
Refer to the section "Memory Ownership".

 

 

- Bio.

 


Tuesday, June 14, 2011 5:19 PM

Thanks, Lim Bio Liong. Those tips were helpful. I managed to resolve the issue by allocating the memory in the native, unmanaged C code on the heap, and putting the string there, before returning it to C#.