Marshalling a char** in C#

2019-03-19 03:56发布

问题:

I am interfacing with code that takes a char** (that is, a pointer to a string):

int DoSomething(Whatever* handle, char** error);

Basically, it takes a handle to its state, and if something goes wrong, it returns an error code and optionally an error message (the memory is allocated externally and freed with a second function. That part I've figued out :) ).

I, however, am unsure how to handle in in C#. What I have currently:

[DllImport("mydll.dll", CallingConvention = CallingConvention.Cdecl)]
private static unsafe extern int DoSomething(IntPtr handle, byte** error);

public static unsafe int DoSomething(IntPtr handle, out string error) {
    byte* buff;

    int ret = DoSomething(handle, &buff);

    if(buff != 0) {
        // ???
    } else {
        error = "";
    }

    return ret;
}

I've poked around, but I can't figure out how to turn that into a byte[], suitable for feeding to UTF8Encoding.UTF8.GetString()

Am I on the right track?

EDIT: To make more explicit, the library function allocates memory, which must be freed by calling another library function. If a solution does not leave me with a pointer I can free, the solution is unacceptable.

Bonus question: As implied above, this library uses UTF-8 for its strings. Do I need to do anything special in my P/Invokes, or just use string for normal const char* parameters?

回答1:

You should just be able to use a ref string and have the runtime default marshaller take care of this conversion for you. You can hint the char width on the parameter with [MarshalAs(UnmanagedType.LPStr)] to make sure that you are using 8-bit characters.

Since you have a special deallocation method to call, you'll need to keep the pointer, like you've already shown in your question's example.

Here's how I'd write it:

[DllImport("mydll.dll", CallingConvention = CallingConvention.Cdecl)] 
private static unsafe extern int DoSomething(
    MySafeHandle handle, void** error); // byte** should work, too, I'm just lazy

Then you can get a string:

var errorMsg = Marshal.PtrToStringAnsi(new IntPtr(*error));

And cleanup:

[DllImport("mydll.dll", CallingConvention = CallingConvention.Cdecl)] 
private static extern int FreeMyMemory(IntPtr h);

// ...

FreeMyMemory(new IntPtr(error));

And now we have the marshalled error, so just return it.

return errorMsg;

Also note the MySafeHandle type, which would inherit from System.Runtime.InteropServices.SafeHandle. While not strictly needed (you can use IntPtr), it gives you a better handle management when interoping with native code. Read about it here: http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.safehandle.aspx.



回答2:

For reference, here is code that compiles (but, not tested yet, working on that next tested, works 100%) that does what I need. If anyone can do better, that's what I'm after :D

public static unsafe int DoSomething(IntPtr handle, out string error) {
    byte* buff;

    int ret = DoSomething(handle, &buff);

    if(buff != null) {
        int i = 0;

        //count the number of bytes in the error message
        while (buff[++i] != 0) ;

        //allocate a managed array to store the data
        byte[] tmp = new byte[i];

        //(Marshal only works with IntPtrs)
        IntPtr errPtr = new IntPtr(buff);

        //copy the unmanaged array over
        Marshal.Copy(buff, tmp, 0, i);

        //get the string from the managed array
        error = UTF8Encoding.UTF8.GetString(buff);

        //free the unmanaged array
        //omitted, since it's not important

        //take a shot of whiskey
    } else {
        error = "";
    }

    return ret;
}

Edit: fixed the logic in the while loop, it had an off by one error.