Search code examples
dllimport

How to properly wrap an SDL function using LibraryImport?


I have the following C function from SDL:

/**
 * Get a list of currently connected displays.
 *
 * \param count a pointer filled in with the number of displays returned, may
 *              be NULL.
 * \returns a 0-terminated array of display instance IDs or NULL on failure;
 *          call SDL_GetError() for more information. This should be freed
 *          with SDL_free() when it is no longer needed.
 *
 * \since This function is available since SDL 3.1.3.
 */
extern SDL_DECLSPEC SDL_DisplayID * SDLCALL SDL_GetDisplays(int *count);

I'm trying to write a proper wrapper for this method in C#. The question is how to define the method signature correctly.

Here are the two approaches I've tried:

Option 1:

[LibraryImport(SDLLibrary, EntryPoint = "SDL_GetDisplays"), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial IntPtr GetDisplays(out IntPtr count);

This works. It's not very convenient to use count as an IntPtr, but it gets the job done.

Option 2:

[LibraryImport(SDLLibrary, EntryPoint = "SDL_GetDisplays"), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial IntPtr GetDisplays(out int count);

This also works. However, the original method allows count to be NULL, which isn't possible with this signature. This could lead to undefined behavior. Despite that, it's more convenient to use.

My Question: Which of these options is preferable to use for wrapping this function in C#, considering both safety and usability? Or is there a better way to handle the nullable count parameter in the C# wrapper?


Solution

  • I tested this case in detail, and experimentally found out the following:

    If you marshal out a parameter that.

    1. has a meaningful type that does not allow null without an explicit Nullable wrapper that is not supported by the marshalizer (e.g. float).
    2. Declared in the marshalizable library as float* (a pointer to a memory region that may be empty)

    Thus, if the marshalizable function writes null there, then in C# the value of such a variable will be 0 (since out will declare it as 0 when initializing the variable, and null will simply not be written).

    As a result, we will have out float = 0 without any error on the part of dotnet, so, answering your own question, in this case you can disregard the compliance with the original documentation and use the more convenient variant with out float instead of out IntPtr, but keep in mind that in case of an error, in the place where null should be, there will be 0.

    I hope I was able to explain it in a clear way, and that people who have the same question will understand me.

    I would be glad if people who are more knowledgeable in the subject would complement me if I have made a mistake.