Marshaling strings to unmanaged memory, passing th

2019-06-22 15:28发布

问题:

I am creating unmanaged memory block in c# and fill it with data from structs.

I iterate through the list of structs and do something like this:

Marshal.StructureToPtr(structTemp, currentMemoryPosition, false);
currentMemPosition = new IntPtr(currentMemPosition.ToInt64() + structSize);      

The struct contains reference type: "string". I've looked into BOL for a StructureToPtr method and there it says:

"All other reference types (for example, strings and arrays) are marshaled to copies"

What exactly does it mean?

Does it mean that the reference to that string will still be in the memory, in spite of the fact the instance of a struct will go out of scope?

The above unmanaged memory block, I pass to c++ method which makes use of it. When the job is done on c++ part I iterate again through structs in memory (in c#) and:

Marshal.DestroyStructure(currentMemPosition, typeof(struct));

The most important question for me is:

Whether I can:

1) Create structs with strings inside
2) Marshal them to unmanaged mamory
3) Make use of them on c++ side
4) **Return them from c++**
5) Read them again in c#
6) Deallocate them in c# by using Marshal.DestroyStructure (EDITED)

The struct layout with string reference type is:

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi, Pack = 1), Serializable]
internal struct TempStruct
{
    [MarshalAs(UnmanagedType.LPStr, SizeConst = 36)]
    private string iuTest;

    public TempStruct(Guid IuTest)
        : this()
    {
        iuTest = IuTest.ToString("D");
    }
}

回答1:

"All other reference types (for example, strings and arrays) are marshaled to copies" What exactly does it mean?

The Marshal.StructureToPtr creates copies of the string. It does it even if you are marshaling to LPWStr. This is different from passing parameters to methods, where sometimes strings/arrays aren't copied around but are passed directly.

So after calling Marshal.StructureToPtr you now have two copies of your iuTest: one in the variable iuTest, that is directly managed by .NET (and so that will be deallocated automatically), and one in the copy created by Marshal.StructureToPtr. This copy must be destroyed manually, for example with Marshal.DestroyStructure.

Note that the SizeConst = 36 is ignored here, because the exact quantity of memory needed will be allocated by Marshal.StructureToPtr.

Full example:

C#

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1), Serializable]
internal struct TempStruct
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string iuTest;

    public TempStruct(Guid IuTest)
        : this()
    {
        iuTest = IuTest.ToString("D");
    }
}

[DllImport("NativeLibrary", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern void TempStructMethod(IntPtr ptr);

and then:

var str = new TempStruct(Guid.NewGuid());

Console.WriteLine("C# side: {0}", str.iuTest);

// It will be 4 or 8, depending if you are running 32 or 64 bits
int size = Marshal.SizeOf(typeof(TempStruct));

IntPtr ptr = Marshal.AllocCoTaskMem(size);

// Marshaling to ptr
Marshal.StructureToPtr(str, ptr, false);

// Call your C++ method
TempStructMethod(ptr);

Console.WriteLine("C# side in original struct: {0}", str.iuTest);

// Marshaling back
str = (TempStruct)Marshal.PtrToStructure(ptr, typeof(TempStruct));

Console.WriteLine("C# side after marshaled back: {0}", str.iuTest);

// Freeing the "content" of the marshaled struct (the marshaled 
// string in this case)
Marshal.DestroyStructure(ptr, typeof(TempStruct));

// Freeing the memory allocated for the struct object (the 
// "container")
Marshal.FreeCoTaskMem(ptr);

and C++ (CoTaskMem* are in #include <Objbase.h>):

extern "C"
{
    __declspec(dllexport) void TempStructMethod(TempStruct *ts)
    {
        printf("C++ side: %s\n", ts->iuTest);

        // If you want to free a C# marshaled string use CoTaskMemFree
        // See in https://github.com/dotnet/coreclr/blob/4cf8a6b082d9bb1789facd996d8265d3908757b2/src/vm/fieldmarshaler.cpp
        // FieldMarshaler_StringAnsi::DestroyNativeImpl and
        // FieldMarshaler_StringUni::DestroyNativeImpl 

        // Here we want to modify the string C-side

        // First we free it
        CoTaskMemFree(ts->iuTest);
        ts->iuTest = NULL;

        char *str = "abcdefab-cdef-abcd-efab-cdefabcdefab";
        int len = strlen(str) + 1;

        // Then we allocate a new string
        // Use CoTaskMemAlloc to allocate memory that can be freed by C#
        ts->iuTest = (char*)CoTaskMemAlloc(len);

        // Then we copy our string in the allocated memory
        strcpy(ts->iuTest, str);

        // Note that you could have reused the "original"
        // memory of ts->iuTest in this case, because it
        // was of the "right" size. I freed and reallocated
        // just to show how to do it!
    }
}

Result:

C# side: d3ccb13c-fdf9-4f3d-9239-8d347c18993c
C++ side: d3ccb13c-fdf9-4f3d-9239-8d347c18993c
C# side in original struct: d3ccb13c-fdf9-4f3d-9239-8d347c18993c
C# side after marshaled back: abcdefab-cdef-abcd-efab-cdefabcdefab

C#-side you can even use the char* pointer of the marshaled struct... You know that it is at offset 0 of the marshaled struct (because it is the first field), so:

IntPtr ptr2 = Marshal.ReadIntPtr(ptr, 0); // will read the char* pointer
string str2 = Marshal.PtrToStringAnsi(ptr2);
Console.WriteLine("Unmarshaling manually: {0}", str2);

(not directly connected to the question, asked in char):

marshalling an array from C# to C++, marshalling back from C++ to C# another array:

C++

struct TempStruct
{
    char* iuTest;
};

extern "C"
{
    __declspec(dllexport) void GetSomeData(TempStruct *inPtr, TempStruct **outPtr, int *numPtr)
    {
        // Number of elements we want to return
        *numPtr = 10;

        // Space for these elements
        *outPtr = (TempStruct*)malloc(*numPtr * sizeof(TempStruct));

        for (int i = 0; i < *numPtr; i++)
        {
            TempStruct *curr = *outPtr + i;

            // Here we allocate some space for the char* iuTest
            curr->iuTest = (char*)malloc(10);

            // And we write something on it (in this case the 'i')
            itoa(i, curr->iuTest, 10);
        }
    }

    __declspec(dllexport) void FreeSomeData(TempStruct *ptr, int num)
    {
        for (int i = 0; i < num; i++)
        {
            TempStruct *curr = ptr + i;

            // First we free the char* iuTest
            free(curr->iuTest);
        }

        // Then we free the ptr
        free(ptr);
    }
}

C#

// Some TempStruct(s) to pass to C++
TempStruct[] someData = new TempStruct[5];

for (int i = 0; i < someData.Length; i++)
{
    someData[i] = new TempStruct(Guid.NewGuid());
}

// C++ will return its TempStruct array in ptr
IntPtr ptr;
int length;

GetSomeData(someData, out ptr, out length);

// This must be equal to C++ sizeof(TempStruct)
int size = Marshal.SizeOf(typeof(TempStruct));

TempStruct[] someData2 = new TempStruct[length];

for (int i = 0; i < length; i++)
{
    // We marshal back an element of TempStruct
    IntPtr curr = (IntPtr)(ptr + (size * i));
    someData2[i] = (TempStruct)Marshal.PtrToStructure(curr, typeof(TempStruct));
}

// Important! We free the TempStruct allocated by C++. We let the
// C++ do it, because it knows how to do it.
FreeSomeData(ptr, length);