Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Illegal glfwDestroyWindow in window close handler #46

Open
Radbuglet opened this issue Dec 27, 2021 · 2 comments
Open

Illegal glfwDestroyWindow in window close handler #46

Radbuglet opened this issue Dec 27, 2021 · 2 comments
Assignees
Labels
bug Something isn't working

Comments

@Radbuglet
Copy link

On MacOS, the following code will cause a segfault when the user quits the application using command+Q:

using Glfw;

// ...

public static void Main()
{
	using var window = new NativeWindow(1920, 1080, "My App");
	while (!window.IsClosed)
	{
		Glfw.PollEvents();
	}
	Console.WriteLine("Goodbye!");
}

The access violation happens after the NativeWindow.Closed event is finished dispatching, where the window close request callback returns control back to GLFW. Looking into the implementation of the protected NativeWindow.OnClosing handler, this seems to happen because underlying window is destroyed in the middle of the callback (see this line, which is responsible for destroying the window handle). This is illegal, with the GLFW reference documentation for glfwDestroyWindow stating that:

Reentrancy
This function must not be called from a callback.

This is a logical error, not an error in C#'s FFI. Here is the equivalent C code which exhibits the same faulty behavior:

#include <stdio.h>
#include <GLFW/glfw3.h>

static void onClosing(GLFWwindow* window)
{
	printf("Destroying %p\n", window);
	// ...
	glfwDestroyWindow(window);
}

int main(void)
{
	// Setup GLFW
	if (!glfwInit())
		return -1;

	// Setup window
	GLFWwindow* window = glfwCreateWindow(1920, 1080, "My App", NULL, NULL);

	if (!window)
		goto stop;

	glfwSetWindowCloseCallback(window, onClosing);

	// Main loop
	while (!glfwWindowShouldClose(window))
	{
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	// Cleanup
	puts("Goodbye!");
stop:
	glfwTerminate();
	return 0;
}

There's no easy way to fix this without changing the API. A non-breaking (but potentially confusing) fix could be to invisibly handle window destruction at the end of the Glfw.PollEvents method call by destroying all windows that still have the shouldClose flag, but that breaks the user expectation that the Glfw class is just raw FFI. A better (but breaking) approach would be to introduce a dedicated wrapper around Glfw.PollEvents that "handles higher-level behavior for the GLFW-NET-specific wrapper objects" i.e. implements the behavior of the previous proposal but in a different method.

@ForeverZer0
Copy link
Owner

Looking at this right now, perhaps I am missing something, but I do not even see why I chose to call base.Close() or what purpose it is even serving, other than propagate the call down to the parent class. The issue with that is I don't even believe the SafeHandle parent class even utilizes the method other than calling Dispose.

Shortly after the new year, I will have some proper time to invest back into this project. As you stated, I wish to keep priority on keeping the raw FFI class as close to being a 1:1 analog as I can. I already had some plans on redoing that portion to take advantage of unmanaged delegates over the DllImport paradigm, which would also allow for me to create my own loader and get rid of the whole library renaming debacle (hindsight 20/20).

I will definitely take a look at this oversight of mine at that time. I have a vague memory of something like this occurring at some point when testing, but I was unable to reproduce it or figure out what the issue was at the time, so it got forgotten about.

@ForeverZer0 ForeverZer0 self-assigned this Dec 27, 2021
@ForeverZer0 ForeverZer0 added the bug Something isn't working label Dec 27, 2021
@Radbuglet
Copy link
Author

but I do not even see why I chose to call base.Close() or what purpose it is even serving, other than propagate the call down to the parent class

base.Close() calls SafeHandle.Close, which is an alias to SafeHandle.Dispose, which—after some use count shenanigans to make multi-threaded finalization with PInvoke safe—calls NativeWindow.ReleaseHandle and destroys the window.

So the base.Close() call was presumably to destroy the window, which is necessary because the window won't be properly closed in multi-window applications otherwise. Most people either don't write multi-window applications or handle window destruction after glfwPollEvents so they don't typically run into this issue.


Unrelated:

I already had some plans on redoing that portion to take advantage of unmanaged delegates over the DllImport paradigm, which would also allow for me to create my own loader and get rid of the whole library renaming debacle (hindsight 20/20).

Yeah, from what I can tell, the compiler doesn't even choose the proper library on OSX and defaults to "glfw", which only matches libglfw without the .dylib extension. Not sure why DllImport properly prefixes the file with lib but then omits the extension but O.K.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants