How to send a string by reference to an unmanaged C library that modifies that string?

Donnie picture Donnie · Jun 29, 2011 · Viewed 7.4k times · Source

I am new to the world of interacting with unmanaged libraries. I have an unmanaged C function that modifies a string by reference within the function. I'm having trouble passing a string from C# and getting it modified by the C function.

Here's the C function:

__declspec(dllexport) void __stdcall Test(char* name)
{
    *name = "Bar";
}

This is the C# DLL import code:

[DllImport(@"C:/blah/mylibrary.dll")]
public extern static string Test(string name);

This is the code I'm using to call the function:

string s = "foo";
Test(s);
//I want s to be "Bar" after the above line

I have tried using "ref" and "out" on the string parameter, and tried Marshalling as an LPStr. Depending on what I try, I either get an error like

"The pointer passed in as a String must not be in the bottom 64K of the process's address space."

or

"Attempted to read or write protected memory. This is often an indication that other memory is corrupt."

I'm sure I'm just doing something stupid with my pointers. Can someone help me determine the appropriate C# code to get "s" to equal "bar"?

Thank you

Answer

Remus Rusanu picture Remus Rusanu · Jun 29, 2011

Your C Test function doesn't do anything like you said it does. All it does it takes a local variable (name) and assigns it to a fixed string. To do what you said it does it would had to do a copy operation into the address pointed to by name:

__declspec(dllexport) void __stdcall Test(char* name)
{
    strcpy(name, "Bar");
}

Of course, such an operation is a disaster in waiting since you have incorrect function signature (buffer lengths are not specified).

Considering that the C function is as above, then you should follow the rules specified at Default Marshaling for Strings:

In some circumstances, a fixed-length character buffer must be passed into unmanaged code to be manipulated. Simply passing a string does not work in this case because the callee cannot modify the contents of the passed buffer. Even if the string is passed by reference, there is no way to initialize the buffer to a given size.

The solution is to pass a StringBuilder buffer as the argument instead of a string. A StringBuilder can be dereferenced and modified by the callee, provided it does not exceed the capacity of the StringBuilder. It can also be initialized to a fixed length. For example, if you initialize a StringBuilder buffer to a capacity of N, the marshaler provides a buffer of size (N+1) characters. The +1 accounts for the fact that the unmanaged string has a null terminator while StringBuilder does not.

So your DLL should be like this:

[DllImport(@"C:/blah/mylibrary.dll")]
public extern static string Test(StringBuilder name);

and call it by passing a properly sized StringBuilder:

StringBuilder foo = new StringBuilder(256);
Test(foo);

Some sanity would be added to the C interface if you add a length parameter.