mpen
mpen

Reputation: 282895

How to marshal to ANSI string via attribute?

This works:

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_GetError")]
private static extern IntPtr SDL_GetError();

public static string GetError()
{
    return Marshal.PtrToStringAnsi(SDL_GetError());
}

This crashes:

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_GetError")]
[return: MarshalAs(UnmanagedType.LPStr)]
public static extern string GetError();

This article suggests that the return attribute is essentially like calling Marshal.PtrToStringAnsi, so what's the deal?


As Daniel pointed out, it's probably crashing because the marshaller is attempting to free the memory. The article also states,

N.B. : Note that the unmanaged side must not use the “new” keyword or the “malloc()” C function to allocate memory. The Interop Marshaler will not be able to free the memory in these situations. This is because the “new” keyword is compiler dependent and the “malloc” function is C-library dependent.

I've tried freeing the char pointer with Marshal.FreeHGlobal, Marshal.FreeCoTaskMem and Marshal.FreeBSTR -- they all crash. There aren't any other ways of freeing the memory AFAIK, so I'm guessing the memory was allocated via new or malloc(). So what now, I'm hooped? I have a permanent memory leak in my program?

I checked the source. The string is created via static char errmsg[SDL_ERRBUFIZE]. My C is rusty, but I guess it's declared as static so that it doesn't get freed when it goes out of function scope. I don't remember where static arrays live in memory-land though; is there some way of freeing them?

Edit: Wait... it's static. That means each time there's a new error it will overwrite the old error message, hence why SDL_GetError() only returns the most recent error message. Ergo, I don't have to worry about freeing it.

As such, if all the return: MarshalAs... options attempt to free the memory, then the only solution is my current one. This is optimal after all.

Upvotes: 8

Views: 1673

Answers (3)

ptp
ptp

Reputation: 541

Another alternative using ICustomMarshaler. Usage:

[DllImport(OpenAL.Name, EntryPoint = "alGetString")]
[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(OpenALStringMarshaler))]
public static extern string GetString(int name);

internal class OpenALStringMarshaler : ICustomMarshaler
{
    #region ICustomMarshaler Members

    public void CleanUpManagedData(object ManagedObj) { }

    public void CleanUpNativeData(IntPtr pNativeData) { }

    public int GetNativeDataSize()
        => -1;

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        throw new NotSupportedException();
    }

    public object MarshalNativeToManaged(IntPtr pNativeData)
        => Marshal.PtrToStringAnsi(pNativeData);

    #endregion

    public static ICustomMarshaler GetInstance(string cookie)
    {
        if (cookie == null)
        {
            throw new ArgumentNullException(nameof(cookie));
        }

        var result = new OpenALStringMarshaler();

        return result;
    }
}

Upvotes: 1

mpen
mpen

Reputation: 282895

I did some digging. The source for SDL_GetError is:

const char *
SDL_GetError(void)
{
    static char errmsg[SDL_ERRBUFIZE];

    return SDL_GetErrorMsg(errmsg, SDL_ERRBUFIZE);
}

We can see that the memory for the string is allocated as a static char array. It is overwritten every time SDL_GetError is called. As such we can't and don't need to free it.

Since the [return: MarshalAs.*] methods all attempt to free memory after marshalling the type, they will not work (and further cause the program to crash).

As such, your (my) original solution is optimal.

Upvotes: 4

Daniel Rose
Daniel Rose

Reputation: 17648

As stated in the linked article, when using [return: MarshalAs(UnmanagedType.LPStr)], the memory of the native string is freed by the CLR using FreeCoTaskMem(). If you manually create the managed string object via Marshal.PtrToStringAnsi(), the memory is not freed at all.

If it crashes, then probably the string was not created on the unmanaged side via CoTaskMemAlloc(), but via new() or malloc() (for example). The API of SDL_GetError() should state whose job it is to free the native string and how.

Upvotes: 5

Related Questions