Why can't I return a char* string from C++ to C# in a Release build?
Asked Answered
E

5

24

I'm attempting to call the following trivial C function from C#:

SIMPLEDLL_API const char* ReturnString()
{
    return "Returning a static string!";
}

With the following P/Invoke declaration (with or without the return attribute, it makes no difference):

[DllImport("SimpleDll")]
[return: MarshalAs(UnmanagedType.LPStr)]
public static extern string ReturnString();

It works if the DLL is a Debug build but crashes in a Release build (AccessViolationException).

I am calling over a dozen other simple functions and this is the only one that fails (here are the others:)

[DllImport("SimpleDll")] public static extern int NextInt();
[DllImport("SimpleDll")] public static extern void SetNextInt(int x);
[DllImport("SimpleDll")] public static extern int AddInts(int a, int b);
[DllImport("SimpleDll")] public static extern int AddFourInts(int a, int b, int c, int d);
[DllImport("SimpleDll")] public static extern double AddDoubles(double x, double y);
[DllImport("SimpleDll")] public static extern IntPtr AddDoublesIndirect(ref double x, ref double y);
[DllImport("SimpleDll")] [return: MarshalAs(UnmanagedType.U1)]
public static extern char CharStringArgument([MarshalAs(UnmanagedType.LPStr)]string s);
[DllImport("SimpleDll")] [return: MarshalAs(UnmanagedType.U2)]
public static extern char WCharStringArgument([MarshalAs(UnmanagedType.LPWStr)]string s);
[DllImport("SimpleDll")] [return: MarshalAs(UnmanagedType.LPWStr)]
public static extern string ReturnWString();
[DllImport("SimpleDll")] [return: MarshalAs(UnmanagedType.BStr)]
public static extern string ReturnBSTR();
[DllImport("SimpleDll")] public static extern System.Drawing.Point MakePoint(int x, int y);
[DllImport("SimpleDll")] public static extern IntPtr MakePointIndirect(int x, int y);
[DllImport("SimpleDll")] public static extern int GetPointY(System.Drawing.Point p);
[DllImport("SimpleDll")] public static extern int GetPointYIndirect(ref System.Drawing.Point pp);
[DllImport("SimpleDll")] public static extern int SumIntegers(ref int firstElem, int size);
Evvy answered 9/6, 2011 at 22:33 Comment(4)
how are you expecting the .net code to deallocate memory allocated by the C++ runtime?Shermanshermie
see this #5309084Shermanshermie
No memory is allocated by the C++ runtime in this case.Evvy
Does this answer your question? PInvoke for C function that returns char *Foramen
P
34

Or maybe try to use

[DllImport("SimpleDll")]
public static extern IntPtr ReturnString();

and in your calling code, use the Marshal Class

string ret = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(PInvoke.ReturnString());
Phago answered 9/6, 2011 at 22:46 Comment(5)
Problem solved, thanks! I'm guessing this points to some weird bug in the .NET Framework.Evvy
@Qwertie, it isn't a bug in the .NET framework. string marshalling, by default, will create a managed copy of the string and try to de-allocate the C memory used by the char*. Using IntPtr will not automatically create a managed copy of any object and therefore will not automatically try to de-allocate memory. As I'm sure you'll agree, trying to de-allocate memory from a string literal is not a good idea :) As a matter of interest, you might want to see this post for more information: https://mcmap.net/q/337530/-pinvoke-for-c-function-that-returns-charAmphipod
@Pooven, your comment should be an answer :)... calling CoTaskMemFree seems very strange default behavior to me, as I've programmed on Windows my whole career and never called it. CoTaskMemFree is documented as a COM API but if I wanted to pass strings the COM way, I'd use SysAllocString and SysFreeString and marshal as BSTR.Evvy
Is "PInvoke.ReturnString()" correct? I searched and searched for a PInvoke class, but I haven't found one yet. I think maybe it should just be "ReturnString()". When I tried it that way, I just got an empty string though.Mala
PInvoke is a class you define that holds the calls to the native dll. ReturnString is a method within the dll you want to call! The name of the class is just a convention.Phago
B
15

It's also possible to do that with a custom Marshaler:

class ConstCharPtrMarshaler : ICustomMarshaler
{
    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        return Marshal.PtrToStringAnsi(pNativeData);
    }

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        return IntPtr.Zero;
    }

    public void CleanUpNativeData(IntPtr pNativeData)
    {
    }

    public void CleanUpManagedData(object ManagedObj)
    {
    }

    public int GetNativeDataSize()
    {
        return IntPtr.Size;
    }

    static readonly ConstCharPtrMarshaler instance = new ConstCharPtrMarshaler();

    public static ICustomMarshaler GetInstance(string cookie)
    {
        return instance;
    }
}

And use it like this:

[DllImport("SimpleDll")]
[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ConstCharPtrMarshaler))]
public static extern string ReturnString();
Botany answered 30/7, 2016 at 19:16 Comment(3)
This is the way to go :)Adar
Should be marked as answer because it is the most correct way.Avocet
If performance is critical then you should avoid using ICustomMarshalers. Even with basic operations like this, the overhead of the custom marshaler makes your code run about five times slower than calling the same methods directly.Rondelle
C
3

The C++ compiler in Release mode is putting the constant into the data page, which is protected; attempting to hand this off to C# is causing problems. In Debug mode, the compiler isn't optimizing the constant into the data page, and so there's no protection problem.

Calendar answered 9/6, 2011 at 22:46 Comment(1)
The data page is "protected" how? Surely it isn't protected from reading, and the .NET Marshaler shouldn't be writing to the string, right?Evvy
O
2

In my P/Invoke experience, you usually have 2 parameters: 1. a pre-allocated buffer in, and 2, the length of the buffer. The Return value is the length of data returned (not to exceed the original length).

Usually the DEBUG releases won't move memory around as much.

BTW, You can pass in a pre-allocated StringBuilder, then set sb.Lenght = the return value of the C function, then you won't have a bunch of \0 nulls at the end of your string.

Origen answered 9/6, 2011 at 22:43 Comment(0)
O
0

I ran into a similar problem and specifying the "CharSet" seemed to fix it.

[DllImport("SimpleDll", CharSet = CharSet.Ansi)]

Not sure why this would be different in debug and release.

Osmunda answered 9/6, 2011 at 22:43 Comment(1)
I tried your suggestion but it didn't work. Most likely, the MarshalAs attribute has the same effect.Evvy

© 2022 - 2024 — McMap. All rights reserved.