Skip to content

Conversation

@castholm
Copy link
Contributor

@castholm castholm commented Mar 29, 2025

Motivation

I use SDL from a different language than C, which means that I can't use the macro magic in SDL_main.h and instead have to call SDL_RunApp() manually from the native entry point to get SDL to perform all the necessary platform-specific setup. This language also has its own preferred way of obtaining the command-line arguments as an argv, which means that I need to get them and pass them forward to SDL_RunApp(). On most platforms, this is fine and SDL will pass the provided argv along unmodified to the main function. However, on Windows, SDL ignores the provided argv completely and instead gets and parses the command-line string into a new argv and uses that instead.

This not only means that my native entry point and SDL_RunApp() are both doing the same exact work twice, but it also becomes a problem if the original argv that was passed to SDL_RunApp() did not actually come from the command-line (maybe I use a hard-coded argv for debugging/testing), or if it has been modified beforehand (maybe SDL is only used for part of my program and my intent is for a command-line invocation like app --backend=SDL arg1 arg2 to get passed on to the SDL-specific parts as app arg1 arg2), since SDL will overwrite the user-provided argv on Windows.

I think it would be better and enable more advanced use cases (especially from non-C languages) if SDL_RunApp() only overrode the argv if the user didn't provide one (i.e. passed null).

The changes

This PR changes the Windows and GDK implementations of SDL_RunApp() so that they properly respect the user-provided argv and forward it to the main function, and only fall back to parsing the Windows command-line string if the argv parameter is null. Important to note is that the Windows/GDK entry points in SDL_main_impl.h already always pass null to SDL_RunApp(), so this change is backward-compatible and does not affect any apps that use SDL_main.h.

The only kind of usage this change is backward-incompatible with is if the user explicitly calls SDL_RunApp() with a non-null argv (e.g. an empty argv) and still expects SDL to override it. But the likelihood of there being some existing application that relies on this very specific behavior is not only extremely slim, the behavior also only occurs on Windows and has never been documented as guaranteed by the API, so I personally think it is safe to make this change. (If you're extra charitable you could also consider this a bug/spec fix that defines the behavior of some previously undefined edge cases in the API.)

I also rewrote the way Windows/GDK processes the command-line to require fewer allocations, down from 2 + argc to only 2, and ensured the allocations are properly freed even on failure. You could further reduce it down to just 1 if you parsed the command-line string on your own instead of using CommandLineToArgvW(), but that felt like too much work (especially considering it would probably require lots of unit tests to ensure the behavior is correct).

Outside of the Windows-specific stuff, this PR also ensures that all other platforms use the { "SDL_app", NULL } dummy argv as a fallback if the if the user passes null (previously only some platforms did this, which could result in null dereferences in user code).

The doc comments in SDL_main.h have been updated to document the argv-handling behavior.

Other stuff

This PR adds a new header file. I updated the Visual Studio project files but I don't have convenient access to Xcode at the moment so the Xcode project files might need to be updated by a maintainer.

I also don't have access to any of the NDA'd private implementations so I don't know if any of them need be updated to take any of the changes in this PR into consideration for maximum consistency between all platforms.

* \since This macro is available since SDL 3.2.0.
*/
#define SDL_MAIN_AVAILABLE
#define SDL_MAIN_AVAILABLE 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I changed these because I think the wiki does not currently pick up defines that don't have any values. For example, https://wiki.libsdl.org/SDL3/SDL_MAIN_AVAILABLE doesn't exist.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's definitely a bug in wikiheaders, I'll take a look at that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That bug is fixed in a0fa64a.

Comment on lines -27 to -28
/* Win32-specific SDL_RunApp(), which does most of the SDL_main work,
based on SDL_windows_main.c, placed in the public domain by Sam Lantinga 4/13/98 */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this comment as to not misrepresent the origins of the code since I changed the implementation pretty drastically, but I could restore this if you prefer.

Comment on lines 58 to 60
// Because of how the Windows command-line works, we know for sure that the buffer size required to store all
// argument strings converted to UTF-8 (with null terminators) is guaranteed to be less than or equal to the
// size of the original command-line string converted to UTF-8.
Copy link
Contributor Author

@castholm castholm Mar 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments for details.

The gist is that the arguments in the command-line string are always either bare, "quoted" or "escape \\"quotes\\" using backslashes", which means that when parsed, each argument always results in a string of the same length or shorter than the argument in the command-line string, never longer. In addition, arguments are always separated by at least one space character (and the entire command-line string is null-terminated), so if we use the length of the whole command-line string as the buffer size there's still enough room for null terminators even in the worst case (app arg1 arg2\0 -> app\0arg1\0arg2\0)

@maia-s
Copy link
Contributor

maia-s commented Mar 29, 2025

Would it be possible to also pass the original args on other platforms when you pass NULL, like Windows? This would make it consistent across platforms and would also make my own optional wrapper around it simpler :v (arguments in Rust aren't null-terminated so I have to allocate new ones that are properly terminated for C. currently my wrapper doesn't pass through args at all but i should fix that either way)

@castholm
Copy link
Contributor Author

Would it be possible to also pass the original args on other platforms when you pass NULL, like Windows?

I could be wrong but AFAIK there's not really any convenient or reliable way of getting the argv outside of the native entry point on most platforms. Windows is actually kinda unique in that is has an easily accessible GetCommandLineW() that you can call at any point.

On Linux I've read that you can theoretically get the arguments by reading /proc/self/cmdline, but I have no idea how reliable this is in practice (and I'm not sure if I want to be the one to implement and test it). Either way, if you already get a C specs-compliant char **argv via your native entry point (which Windows doesn't), it's probably best to use that one.

Needing to copy and null-terminate your Rust strings seems slightly annoying, but in the grand scheme of things your app will probably end up doing similar numbers of allocations if you copy it yourself as it would if SDL copied it from somewhere else.

@castholm
Copy link
Contributor Author

@slouken Sorry for tagging you directly, but do you think it would be possible to land a change like this before SDL 3.4?

This PR has bitrotted but I think I can bring it back into shape quickly, I just need to know if there's any interest in the change at all (and if there are any private main implementations that I don't know about that might complicate things).

To re-summarize: this change is in order to support programs (mainly within the context of other languages/ecosystems, but also C) that need/want to process argv themselves and do certain work outside of SDL before initializing it by passing a modified argv to SDL_RunApp(). This doesn't currently work on Windows because SDL clobbers the passed argv.

@slouken slouken added this to the 3.4.0 milestone Oct 24, 2025
@slouken
Copy link
Collaborator

slouken commented Oct 24, 2025

I'll throw this into the milestone and defer to @icculus on this one.

@icculus
Copy link
Collaborator

icculus commented Oct 25, 2025

I'm going to look through this PR more closely before merging, because I suspect some of this should be trimmed out, but the basic idea (don't override argv if passed by the app) is fine with me.

icculus added a commit that referenced this pull request Oct 25, 2025
This new implementation only parses the command line into an argv when
the provided argv is NULL. This lets programs that don't want to/can't
include `SDL_main.h` to do their own custom argument processing before
explicitly calling `SDL_RunApp()` without having SDL clobber the argv.

If the user includes `SDL_main.h` as normal, the behavior remains the
same as before (because `SDL_main_impl.h` passes a NULL argv).

In addition, this new implementation performs fewer allocations and no
longer leaks on failure.
`SDL_SetMainReady()` is not a hard requirement on most platforms, but
calling it sets up the main thread ID and thus has implications for
`SDL_IsMainThread()` for apps that don't use the video subsystem.
@castholm
Copy link
Contributor Author

Went over the whole changeset and made a few changes:

  • Removed the new header file with the "call main with fallback" function, it felt like unneeded complexity. Instead the same few lines of simple fallback handling is copy/pasted into each SDL_RunApp() implementation in a way that makes sense for each platform.
  • The Windows/GDK command line handling is now also similarly duplicated between both implementation, instead of having GDK awkwardly try to use half of the Windows implementation. Feel free to merge/move them to some common shared place if you prefer not having two copies of the same algorithm.
  • I updated/refactored the GDK SDL_RunApp() implementation a bit so that it frees resources properly, most notably it now calls XblCleanupAsync() on the way out per the recommendation of the GDK docs.
  • Made sure all SDL_RunApp() implementations call SDL_SetMainReady(), because not doing so apparently has implications for SDL_IsMainThread() for apps that don't use the video subsystem.


if (GDK_RegisterChangeNotifications()) {
// We are now ready to call the main function.
SDL_SetMainReady();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters but I'll just quickly note that I swapped the order of GDK_RegisterChangeNotifications() and SDL_SetMainReady() around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants