.NET Interop: How to get returned string (not null terminated) from unmanaged DLL in C#

1.1k views Asked by At

I defined a function in C DLL library.

__declspec(dllexport) void* GetText();

It will return a string which is dynamically allocated from heap memory (And GlobalAlloc is used here for allocating memory). Note that the returned string is not null-terminated.


Then at C# side I tried two methods to declare the function

[DllImport("D:\\ca\\TextAccessLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
static extern String GetText();

When calling above method, the application will crash without any exception thrown.

[DllImport("D:\\ca\\TextAccessLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr GetText();

ptr = GetText();
string text = Marshal.PtrToStringAuto(ptr, 1000);

And calling this method will return incorrect string. Checked the real bytes by using Marshal.Copy, I found the bytes value is not same as the value in DLL library. (I think it's caused by Virtual Memory, C# process cannot access memory space of the DLL directly)

(Don't mind the string length, I hard coded it to 1000 for ease)


This is the C++ code and the memory value of the string when debugging (It's a Console Application but not the original DLL, because Console Application is easy to debug. But the DLL code is same as this one except the logging part). enter image description here Following is the original DLL code

__declspec(dllexport) char* GetText(){
    VTHDOC hDoc = NULL;
    VTHTEXT hText = VTHDOC_INVALID;
    DAERR da_err = NULL;
    DAERR ta_err = NULL;
    DAERR read_err = NULL;
    char *buf = (char*)GlobalAlloc(GMEM_FIXED, 1000);
    DWORD real_size;


    DAInitEx(SCCOPT_INIT_NOTHREADS, OI_INIT_DEFAULT);
    da_err = DAOpenDocument(&hDoc, 2, "D:\\1TB.doc", 0);
    ta_err = TAOpenText(hDoc, &hText);
    read_err = TAReadFirst(hText, (VTLPBYTE)buf, 1000, &real_size);

    return buf;
}

But at C# side the bytes are not same as C++ side enter image description here

You can see the first byte in C++ is 0, but it's 200 for C# (decimal)


Another thing to note: if I return a const string(e.g. "AASSDD") directly in DLL code, C# side will get the correct string

2

There are 2 answers

5
josh On

As already stated, it works for null-terminated strings only, in the following way:

C# part, declaration:

[DllImport("myDll.dll", EntryPoint = "myString", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
extern private static string myString(out int size);

C# part, usage:

        int size;
        string s = myString(out size);

C++ part:

char* myString(int* size)
{
    *size = 20;
    char* strg = (char*)::GlobalAlloc(GMEM_FIXED, *size);
    memset(strg, 0x3f, *size); //preset with a questionmark
    for (int i=0; i < 9; i++)
        strg[i] = 0x40 + i;
    strg[*size -1] = 0; //limit the maximum string length
    return strg;
}

And the obtained C# string: "@ABCDEFGH??????????", value of size: 20

A treatment of the issue may be found here

4
xanatos On

You can't do it that way. Marshaling of string works only for null-terminated strings (or for BSTR, if you specify some options). You can:

[DllImport("D:\\ca\\TextAccessLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr GetText();

But from there, it isn't clear how the C# program should know the length of the string.

The various Marshal methods of C# handle BSTR (that have internally their length) or NUL terminated strings.