Returning a string from PInvoke? [duplicate]
Asked Answered
M

4

12

I am using PInvoke for interoperability between Native Code (C++) and Managed Code (C#). I just write a simple function which gets a string from C++ code. My code looks like

C# Code:

[DllImport("MyDll.dll")]
private static extern string GetSomeText();
public static string GetAllValidProjects() {
    string s = GetSomeText();
    return s;
}

C++ Code

char* GetSomeText() {
    std::string stri= "Some Text Here";
    char * pchr = (char *)stri.c_str();
    return pchr;
}

All works fine at C++ end, i.e the variable pchr contains "Some Text Here" but at C# the string s contains noting in it. I don't know what I am doing wrong. Any help would be appreciated

Mayor answered 14/3, 2011 at 12:1 Comment(2)
Possible dublicate https://mcmap.net/q/529612/-marshal-quot-char-quot-in-cFancywork
Totally understandable why you didn't find the original, with no overlap in the titles. This shouldn't be deleted, so others can find it (and the original it now links to) in future searches.Episcopalian
G
23

First of all, as others have pointed out, your C++ is broken even before trying interop. You are returning a pointer to stri's buffer. But because stri is destroyed as soon as the function returns, the return value is not valid.

What's more, even if you fixed this, you need to do more. It won't work allocating memory in your C++ code which you would need the C# code to deallocate.

There are a few options to do it right.

Your C# code can ask the C++ code how long the string is. Then a C# StringBuilder is created and allocated to the appropriate size. Next the StringBuilder object is passed to the C++ code and its default marshalling is as a LPWSTR. In this approach the C# code allocates the string and your C++ code receives a C string to which it must copy the buffer.

Alternatively you can return a BSTR from the C++ which allows allocation in the native C++ code and deallocation in the C# code.

The BSTR approach is probably how I would do it. It looks like this:

C++

#include <comutil.h>
BSTR GetSomeText()
{
    return ::SysAllocString(L"Greetings from the native world!");
}

C#

[DllImport(@"test.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.BStr)]
private static extern string GetSomeText();

Update

Hans Passant added a couple of useful observations in the comments. First of all, most P/Invoke interop is done against an existing interface which cannot be changed and you do not have the luxury of picking your preferred interop interfacing approach. It would appear that is not the case here, so which approach should be chosen?

Option 1 is to allocate the buffer in the managed code, after having first asked the native code how much space is needed. Perhaps it is enough to use a fixed size buffer that both parties agree on.

Where option 1 falls down is when assembling the string is expensive and you don't want to do it twice (e.g. once to return its length, and once again for the contents). This is where option 2, the BSTR comes into play.

Hans pointed out one drawback of the BSTR, namely that it carries a UTF-16 payload but your source data may well char*, which is a "bit of a hassle".

To overcome the hassle you can wrap up the conversion from char* to BSTR like this:

BSTR ANSItoBSTR(char* input)
{
    BSTR result = NULL;
    int lenA = lstrlenA(input);
    int lenW = ::MultiByteToWideChar(CP_ACP, 0, input, lenA, NULL, 0);
    if (lenW > 0)
    {
        result = ::SysAllocStringLen(0, lenW);
        ::MultiByteToWideChar(CP_ACP, 0, input, lenA, result, lenW);
    } 
    return result;
}

That's the hardest one out of the way, and now it's easy to add other wrappers to convert to BSTR from LPWSTR, std::string, std::wrstring etc.

Gailey answered 14/3, 2011 at 12:7 Comment(8)
Hey, you didn't get downvoted. You might want to mention that this is undefined behavior in C++ as well.Exophthalmos
@Hans Thanks. I've updated. Not only did I not get downvoted, one of the other answers got up-voted!Gailey
@Hans Out of interest, since you know a lot more about this than I do, how would you return a string from native to managed? Is the BSTR approach sensible in your opinion, or is there something better?Gailey
You don't usually have a choice since you normally have to interop with existing code. But the sensible C way (C++ does not apply) is to pass a buffer as an argument to the function. And the buffer length of course. There's never a memory management problem that way. And it is efficient in C since you simply pass a local variable. The buffer overflow risk is not so nice. BSTR is not wrong in .NET interop but a bit of a hassle with char*.Exophthalmos
@Hans Thanks. This is no doubt not the normal case because the code presented could never work, even pure native. The point about BSTR and C++ is that char* is ANSI and BSTR is wide char?Gailey
Yes, BSTR is a utf-16 encoded string.Exophthalmos
@Hans for what it's worth, your promptings spurred me on to provide a recipe for overcoming the BSTR "hassle"!Gailey
If you get linker errors from this, try #include <comdef.h> instead.Edgewise
B
3

Another way to get a string from C++. In case the you cannot modify your c++ dll. You can declare the DllImport with IntPtr instead of string. When the function is invoked, you can marshal the Ptr back to String.

[DllImport("MyDll.dll")]
private static extern IntPtr GetSomeText();
public static string GetAllValidProjects()
{
    string s = Marshal.PtrToStringAnsi(GetSomeText());
    return s;
}

Note : as mention in the previous post. "Some Text Here" is allocated on the stack so as soon as the function returns, the stack will unwire. Therefore the data is potential be overridden. Hence you shall use Marshal.PtrToStringAnsi right after the call. Don't hold to the IntPtr.

char* GetSomeText()    
{
    std::string stri= "Some Text Here"; 
    char * pchr = (char *)stri.c_str();
    return pchr;
} 
Biaxial answered 11/5, 2015 at 16:33 Comment(1)
Please, please, please don't advocate using memory after it's been freed. You're in a managed language, and as soon as you return from this function, the GC can decide to collect memory, move things around, etc, and if this happens before your Marshal call, you will read garbage in and there's nothing you can do about it.Tripodic
A
-1

It is because

std::string stri= "Some Text Here";

is a stack object and it is getting destroyed after GetSomeText() call. After the call pchr pointer which you are returning is invalid.

You may need to allocate space for the text dynamically to be able to access it latter.
Change your C++ function definition to:

char* GetSomeText()    
{
   std::string stri = "Some Text Here";
   return strcpy(new char[stri.size()], stri.c_str());
}

Something like above. You got the idea...

Aragon answered 14/3, 2011 at 12:12 Comment(8)
That would be OK if we weren't crossing the native/managed boundary and if the same allocator/deallocator was used on both side. But this is not the case here.Gailey
@David thanks for -1. Returning result of c_str() of local std::string is not valid thing to do. cplusplus.com/reference/string/string/c_strAragon
No, but your way doesn't work either. It would be fine within a single C++ module, but can't work with .net interop.Gailey
I'm not .net expert. But it should work, you will definitely have memory management issues, and it is not a "nice" solution, but I expect you will get the text in C# code! Hopefully @Mayor will confirm it soon.Aragon
It doesn't work. In order to answer this question you need to understand the .net marshalling. I don't think you do.Gailey
It doesn't work, it is an undiagnosable memory leak in XP, a crash in Vista and up. Hopefully the OP doesn't use XP or he'll confirm it "works". The pinvoke marshaller cannot release the string, it doesn't use the same CRT. It will try anyway with CoTaskMemFree().Exophthalmos
That is fine Guys. +1 for the information. I have never happened to use C# I just answered from C++ point of view, for author to not return invalid pointer. You better know what are C# side issues here. So it is a memory leak but it works for XP? :)Aragon
No, it doesn't work anywhere. I just might appear to work on XP.Gailey
D
-4

GetSomeText() is returning pointer of char is wont work if u declare string variable to store it.

try

[DllImport("MyDll.dll")]
private static extern string GetSomeText();
public static string GetAllValidProjects()
{
    string s = new string(GetSomeText());
    return s;
}

there's a constructor that takes char*

Diplegia answered 14/3, 2011 at 12:44 Comment(2)
The pinvoke marshaller cannot deal with C++ objects.Exophthalmos
Leaks memory (and doesn't work).Verso

© 2022 - 2024 — McMap. All rights reserved.