Proper way to marshal char* to string using C# interoperability. Get "a heap has been corrupted" exception

59 views Asked by At

not a lot of experience in C++. Attempting to Marshal char* in C to string in C#, using [MarshalAs(UnmanagedType.LPStr)], but I get a "a heap has been corrupted" exception. It does however work in reverse (string in C# to char* in C). What am I doing wrong here?

More details: on C side, this is the struct:

struct STRUCTV
{
    char* _pretext;
    char* _result;
};

this is C function signature: void add_all_numbers(STRUCTV *structV);

This is C# side

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCTV
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string _pretext; 
    [MarshalAs(UnmanagedType.LPStr)]
    public string _result;  //<----THIS ISNT WORKING

}

internal partial class NativeMethods
{
    [DllImport(
    "Library.dll",
    EntryPoint = "function1",
    CallingConvention = CallingConvention.Cdecl,
    CharSet = CharSet.Ansi)]
    public static extern void function1(ref STRUCTV structV);
}

this is how i use it:

STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = null;
NativeMethods.function1(ref myStruct); //<---- ntdll.dll : A heap has been corrupted"
Console.WriteLine(myStruct._result);

In case this is important, this is what happens inside C code if that's important:

string resultstring = GetResult();
char* result = new char[resultstring.length() + 1];
strcpy_s(result, resultstring.length() + 1, resultstring.c_str());
structV->_result = result;

Some additional information. it works if I don't marshal the char* as string in _result, and i want to understand why.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCTV
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string _pretext; 
    public IntPtr _result;
}

STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = IntPtr.Zero;
NativeMethods.function1(ref myStruct);
string resultString = Marshal.PtrToStringAnsi(myStruct._result);
Console.WriteLine(resultString);    
1

There are 1 answers

0
Remy Lebeau On

The C# code is assuming that it's responsible for passing the _result string into the C++ code as a preallocated char* pointer. But, the C++ code is actually allocating its own memory for the _result string and returning the pointer back to the C# code. So, you shouldn't marshal the _result string using LPStr the way you are trying to do, since it's output data not input data.

Using IntPtr is one viable way to handle this. You can then use Marshal.PtrToStringAnsi() to convert the IntPtr into a C# string. However, in this case, do note that the C++ code is new[]'ing the _result string, and C# cannot free that memory. So, you will have to export another function from the C++ code which the C# code can then call to pass the returned IntPtr back to C++ so it can properly delete[] the memory, eg:

struct STRUCTV
{
    char* _pretext;
    char* _result;
};

void add_all_numbers(STRUCTV *structV)
{
    ...
    string resultstring = GetResult();
    char* result = new char[resultstring.length() + 1];
    strcpy_s(result, resultstring.length() + 1, resultstring.c_str());
    structV->_result = result;
    ...
}

void free_result(char *ptr)
{
    delete[] ptr;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCTV
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string _pretext; 
    public IntPtr _result;
}

internal partial class NativeMethods
{
    [DllImport(
    "Library.dll",
    EntryPoint = "add_all_numbers",
    CallingConvention = CallingConvention.Cdecl,
    CharSet = CharSet.Ansi)]
    public static extern void add_all_numbers(ref STRUCTV structV);

    [DllImport(
    "Library.dll",
    EntryPoint = "free_result",
    CallingConvention = CallingConvention.Cdecl)]
    public static extern void free_result(IntPtr result);
}

STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = IntPtr.Zero;
NativeMethods.add_all_numbers(ref myStruct);
string resultString = Marshal.PtrToStringAnsi(myStruct._result);
Console.WriteLine(resultString);    
NativeMethods.free_result(myStruct._result);

The alternative is to allocate the memory on the C# side instead, and then have the C++ code just fill the memory, eg:

struct STRUCTV
{
    char* _pretext;
    char* _result;
    int _result_max_size;
};

void add_all_numbers(STRUCTV *structV)
{
    ...
    string resultstring = GetResult();
    strcpy_s(structV->_result, structV->_result_max_size, resultstring.c_str());
    ...
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCTV
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string _pretext; 
    public char[] _result;
    int _result_max_size;
}

internal partial class NativeMethods
{
    [DllImport(
    "Library.dll",
    EntryPoint = "add_all_numbers",
    CallingConvention = CallingConvention.Cdecl,
    CharSet = CharSet.Ansi)]
    public static extern void add_all_numbers(ref STRUCTV structV);
}

char[] buffer = ArrayPool<char>.Shared.Rent(256);
STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = buffer;
myStruct._result_max_size = buffer.Length;
NativeMethods.add_all_numbers(ref myStruct);
string resultString = new string(buffer);
Console.WriteLine(resultString);    
ArrayPool<char>.Shared.Return(buffer);

// or

IntPtr buffer = Marshal.AllocHGlobal(256);
STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = buffer;
myStruct._result_max_size = 256;
NativeMethods.add_all_numbers(ref myStruct);
string resultString = Marshal.PtrToStringAni(buffer);
Console.WriteLine(resultString);    
Marshal.FreeHGlobal(buffer);

Alternatively:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCTV
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string _pretext; 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public string _result;
    int _result_max_size;
}

internal partial class NativeMethods
{
    [DllImport(
    "Library.dll",
    EntryPoint = "add_all_numbers",
    CallingConvention = CallingConvention.Cdecl,
    CharSet = CharSet.Ansi)]
    public static extern void add_all_numbers(ref STRUCTV structV);
}

STRUCTV myStruct = new STRUCTV();
myStruct._pretext = "Pretext";
myStruct._result = "";
myStruct._result_max_size = 256;
NativeMethods.add_all_numbers(ref myStruct);
Console.WriteLine(myStruct._result);

Read Default Marshalling for Strings on MSDN for more details.