Search code examples
c#c++returndllimport

Passing a return char* from C++ to C# with DllImport


I am a newbie on both C# WPF and C++.

Recently, I got an external .dll, which returns a char*, and I want to receive the return value in C# by using DllImport. Then, using str.Split(';') to separate the characters. To the purpose, I created a button to show the first character in the string I split on a label when I click the button.

Therefore, I use an IntPtr to receive a char*, which is from C++ .dll, and call Marshal.PtrToStringAnsi() to turn it into a string. However, when I execute the code, it sometimes works but sometimes crushes. Then error code always shows

Unhandled exception at 0x00007FFC06839269 (ntdll.dll) in UITest.exe: 0xC0000374: heap corruption  (parameters: 0x00007FFC068A27F0).

I thought my code is reasonable and I couldn't find root causes. Can anyone help me? Thanks!

The below shows the content in .dll and also the code I used in C#.

  • C++ code in Dlltest.h:

    #define DLL_EXPORT extern "C" __declspec(dllexport)
    char* getRbtData = nullptr;
    DLL_EXPORT char* func_getRbtData();
    
  • C++ code in Dlltest.cpp:

    char* func_getRbtData()
    {
       getRbtData = new char(128);
       memset(getRbtData, 0, strlen(getRbtData));
       char* _getRbtData = "1.0;23.0;55.0;91.0;594.0;";
       memcpy(getRbtData, _getRbtData, strlen(_getRbtData));
       return getRbtData;
    };
    
  • C# code in UITest.xaml.cs:

    [DllImport("DllTest.dll",EntryPoint = "func_getRbtData", CharSet = CharSet.Ansi)]
    
    public static extern IntPtr func_getRbtData();
    
    string[] words;
    private void btn_test_Click(object sender, RoutedEventArgs e)
    {
        IntPtr intptr = func_getRbtData();
        string str = Marshal.PtrToStringAnsi(intptr);
        words = str.Split(';');
        lb_content.Content = words[1];
    }
    

Solution

  • There are several problems with your code.

    On the C++ side, your DLL function is implemented all wrong:

    • getRbtData = new char(128);

      You are allocating a single char whose value is 128, not an array of 128 chars. You need to use new char[128] instead for that.

    • memset(getRbtData, 0, strlen(getRbtData));

      getRbtData is not a pointer to a null-terminated string, so strlen(getRbtData) is undefined behavior. It reads into surrounding memory while calculating the length until it finds a random 0x00 byte in memory.

      And then the subsequent memset() into getRbtData will overwrite that surrounding memory. If it doesn't just crash outright.

    • char* _getRbtData = "1.0;23.0;55.0;91.0;594.0;";

      Prior to C++11, this assignment is OK but discouraged. In C++11 and later, this assignment is actually illegal and won't compile.

      String literals are read-only data, so you need to use const char instead of char in your pointer type. You should do that even in older compilers.

    • memcpy(getRbtData, _getRbtData, strlen(_getRbtData));

      strlen(_getRbtData) is OK since _getRbtData is a pointer to a null-terminated string.

      However, since getRbtData is not allocated with enough memory to receive all of the copied chars, memcpy() into getRbtData is also undefined behavior and will trash memory, if not crash outright.

    • return getRbtData;

      This is OK to pass the pointer to C#.

      However, since the memory is being allocated with new (better, new[]), it needs to be freed with delete (delete[]), which you are not doing. So you are leaking the memory.

      Marshal.PtrToStringAnsi() on the C# side will not (and cannot) free new'ed memory for you. So your C# code will need to pass the pointer back to the DLL so it can delete the memory properly.

      Otherwise, you will need to allocate the memory using the Win32 API LocalAlloc() or CoTaskMemAlloc() function so that the Marshal class can be used on the C# side to free the memory directly without passing it back to the DLL at all.

    On the C# side, you are using the wrong calling convention on your DllImport statement. The default is StdCall for compatibility with most Win32 API functions. But your DLL function is not specifying any calling convention at all. Most C/C++ compilers will default to __cdecl unless configured differently.

    With that said, try this instead:

    Dlltest.h

    #define DLL_EXPORT extern "C" __declspec(dllexport)
    DLL_EXPORT char* func_getRbtData();
    DLL_EXPORT void func_freeRbtData(char*);
    

    Dlltest.cpp

    char* func_getRbtData()
    {
        const char* _getRbtData = "1.0;23.0;55.0;91.0;594.0;";
        int len = strlen(_getRbtData);
        char *getRbtData = new char[len+1];
        // alternatively:
        /*
        char *getRbtData = (char*) LocalAlloc(LMEM_FIXED, len+1);
        if (!getRbtData) return NULL;
        */
        memcpy(getRbtData, _getRbtData, len+1);
        return getRbtData;
    }
    
    void func_freeRbtData(char *p)
    {
        delete[] p;
        // alternatively:
        // LocalFree((HLOCAL)p);
    }
    

    UITest.xaml.cs

    [DllImport("DllTest.dll", EntryPoint = "func_getRbtData", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr func_getRbtData();
    
    [DllImport("DllTest.dll", EntryPoint = "func_freeRbtData", CallingConvention = CallingConvention.Cdecl)]
    public static extern void func_freeRbtData(IntPtr p);
    
    string[] words;
    private void btn_test_Click(object sender, RoutedEventArgs e)
    {
        IntPtr intptr = func_getRbtData();
        string str = Marshal.PtrToStringAnsi(intptr);
        func_freeRbtData(intptr);
        // alternatively:
        // Marshal.FreeHGlobal(intptr);
        words = str.Split(';');
        lb_content.Content = words[1];
    }