0
\$\begingroup\$

I'm trying to implement an input listener for FNA (modern version of XNA). Since FNA does not supply any sort of way to listen to native input this has to be done through the windows API and native calls. This is how I set this up:

public KeyboardListener(GameWindow window) {
    var procedurePointer = Marshal.GetFunctionPointerForDelegate<WindowProcedure>(Procedure);
    procedure = new IntPtr(SetWindowLongPtr(window.Handle, GWL_WNDPROC, procedurePointer.ToInt64()));
    if (procedure == IntPtr.Zero)
    {
        var error = GetLastError();
        throw error switch
        {
            ERROR_INVALID_WINDOW_HANDLE => new InvalidOperationException("Invalid window handle"),
            _ => new InvalidOperationException($"Unknown error message '0x{error:X4}'")
        };
    }
}

// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptra#parameters
private const int GWL_WNDPROC = -4;

// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlonga
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int SetWindowLongPtr(IntPtr hWnd, int nIndex, long dwNewLong);

[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
private static extern uint GetLastError();

I construct the keyboard listener during the Initialize method of Game:

class MyGame : Game {
    protected override void Initialize() {
        new KeyboardListener(Window);
    }
}

This is the method call that reports the error: SetWindowLongPtr(window.Handle, GWL_WNDPROC, procedurePointer.ToInt64()) and it throws "Invalid window handle". It's worth noting that the example code I saw converted the pointers to Int32 instead of Int64, but I am on a 64 bit machine so the pointers can not fit in 32 bits.


What I've tried & figured out so far...

  • Debugging tells me that both window.Handle and procedurePointer are non-null and at least point to something.
  • I have tried to initialize it earlier or later, but that doesn't seem to make a difference.
  • Other native methods such as SDL2.SDL.SDL_MinimizeWindow(Window.Handle) work perfectly fine and don't complain about the window handle.
\$\endgroup\$
1
\$\begingroup\$

So after like 5 hours of debugging of course I solve it 3 minutes after I write up this answer (as always Stack Exchange is the best rubber duck).

The clue is that SDL2.SDL.SDL_MinimizeWindow(Window.Handle) and all other SDL calls work perfectly fine. Turns out FNA is slightly different from XNA in that Window.Handle points to an SDL window, not a Win32 window.

The fix for this is easy, fortunately:

// This is required, otherwise this procedure will be garbage collected. Note that this field must be static.
private static WindowPointer nonGcProcedure;

public KeyboardListener(GameWindow window) {
    nonGcProcedure ??= Procedure;

    // Convert the SDL handle to a win handle
    var info = new SDL.SDL_SysWMinfo();
    SDL.SDL_VERSION(out info.version); // This *might* not be required
    if (SDL.SDL_GetWindowWMInfo(window.Handle, ref info) == SDL.SDL_bool.SDL_FALSE)
        throw new InvalidOperationException();

    var winPointer = info.info.win.window;
    var procedurePointer = Marshal.GetFunctionPointerForDelegate(nonGcProcedure);
    procedure = new IntPtr(SetWindowLongPtr(winPointer, GWL_WNDPROC, procedurePointer.ToInt64()));
    if (procedure == IntPtr.Zero)
    {
        var error = GetLastError();
        throw error switch
        {
            ERROR_INVALID_WINDOW_HANDLE => new InvalidOperationException("Invalid window handle"),
            _ => new InvalidOperationException($"Unknown error message '0x{error:X4}'")
        };
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.