Approach for avoiding main() macro in sdl2-config

I’m writing an API in which you can choose at runtime the window-management backend to use (for example, you can choose either to start the application with SDL2, with GLFW, or with GLUT). When cross-compiling the API lib from Linux to Windows, I realized about the SDL feature of defining main as a macro.

I’ve read about it in the docs, saying that you can avoid that macro definition by defining SDL_MAIN_HANDLED before including the SDL header, and then calling SDL_SetMainReady(); before SDL_Init();

However, that’s not accurate, because the result I get for sdl2-config --cflags when cross compiling to Windows is: -I/my-path-to/include/SDL2 -Dmain=SDL_main …so main is defined as a macro no matter if I define SDL_MAIN_HANDLED.

Now, my questions:

  1. Is there any configure option that I can use when building SDL2, so that sdl2-config won’t define main() as a macro?
  2. The docs don’t explain in detail what can happen when you define SDL_MAIN_HANDLED and call SDL_SetMainReady(). A search with Google returns assorted opinions, such as that you disable error handling by doing that, and that some systems like iOS won’t get the application created if you do that. Is there any failsafe way of avoiding the main() macro definition, while at the same time don’t running into trouble in any system? Why can’t I just call a proper SDL internal function passing to it argc and argv as the first thing my main() does?

Thanks a lot for any guidance.

No, it’s always defined for Windows there https://github.com/libsdl-org/SDL/blob/31849369506d54aac7fa799c8c2851ded18c87f5/configure#L29077

The main from SDL_main does exactly the same thing (currently for SDL2 on Windows, may change later) with some additional code to parse argv: https://github.com/libsdl-org/SDL/blob/31849369506d54aac7fa799c8c2851ded18c87f5/src/main/windows/SDL_windows_main.c#L77

You can use your own main, just call SDL_SetMainReady.
If you have the main macro from sdl2-config, you should undefine it in the file where your main is (and if you call your main from other files (yuck!), there too):

#ifdef main
#undef main
#endif

#define SDL_MAIN_HANDLED

#include <SDL.h>

int main(void) {
  SDL_SetMainReady();
  // ...
}

(You can also use the -U option with gcc)

Thanks. In include/SDL_main.h, there’s a comment related to SDL_SetMainReady(), saying

 *  Calling this yourself without knowing what you're doing can cause
 *  crashes and hard to diagnose problems with your application.
 */

The documentation doesn’t give such a warning, and provides no details on what you are supposed to know when you call SDL_SetMainReady(). Looking at the SDL2 source code, it seems that what is done before calling SDL_main() is very platform-dependent.

Right now, for my initial goal (apps choosing at runtime the backend to use: SDL, GLFW, GLUT, etc), it seems like my only hope is to create my own custom version of SDL so that it doesn’t kidnap main(), and the app calls instead the SDL pre-main initialization from the first line in its main(). But that’s a problem, because I want my API to use a standard SDL2 installation, not a custom one… Bummer.

Nothing to contribute but only mentioning that I had tons of problems with SDL_main, it’s absolutely annoying and gives issues with the worst error messages that makes no sense. The biggest issue is when using some Test framework like Googletest and trying to build tests that needs to be run with Ctest but have dependencies on SDL2, it’s a world of pain that is different on each platform and compiler - biggest offender being MinGW.

In SDL3 it’s header only so maybe it will be easier then. But yeah, can’t offer a hand because I usually have to randomly fiddle with it until it works.

It is. But SDL_MAIN_HANDLED is provided by SDL API (which aims to be stable). It should be stable enough to call
SDL_SetMainReady in your main and follow necessary real main implementation’s points to get the things work. SDL_SetMainReady only flips single boolean flag in SDL2:

void SDL_SetMainReady(void)
{
    SDL_MainIsReady = SDL_TRUE;
}

This is just a way of getting SDL know that you have passed platform-dependent initialization. Only some platforms require this initialization to be performed. You can explore these by looking at the SDL_*_main.c' files. None of the desktop platforms are there (at this moment), but there is a Windows main with a Windows-specific entry point. You can now use crt main in Windows without any problems.

If the platforms you support do not require special initialization, you can get by with just SDL_SetMainReady at any time before calling SDL_Init. It’s an unsatisfactory way to late initialize SDL (exploring platform files to be sure everything is fine), but it sounds much better than forking and maintaining your own SDL.

Yes, I feel that all the stuff done internally by SDL when it kidnaps main() should be done by SDL_Init() instead, passing argc and argv as arguments to it. I guess the current design was chosen for avoiding potential crashes or undefined behaviour if a program does any tasks before calling SDL_Init(), but then it’s much better to just say " SDL_Init() must be the first thing your main() function does" than kidnapping main(). And, if you are really, really, really sure that you want to do that, then at least don’t define main() as a macro, just make the linker crash hard with a duplicate definition of main() if a program uses its own main().

Thank you very much, @yataro . Yes, I agree, much better not to maintain a custom fork myself. One question, though: Am I right assuming that all the work currently done by main_getcmdline() in SDL_windows_main.c is useful only for windowed programs? I mean, for console applications, can’t you just use argc and argv directly without all that work? I’m confused because console_ansi_main() also calls main_getcmdline(), and I’d say that’s unnecessary…

Thanks!

Not really. The code from SDL_windows_main.c is essentially the same code from your crt0.o, implemented using WinAPI with Windows-specific entry point. As I said earlier, you can use the standard int main(int, char **) without Windows-specific code. Any type of application on Windows can read argv. Whether an application is console or graphical is just the value of the subsystem parameter in the executable, and is controlled by the linker.
console_ansi_main and console_wmain are simply two types of main() - the former uses ANSI strings and the latter uses Unicode strings, hence wmain is a wide character main :grin:.

The fact that SDL has both entry points, main/wmain and WinMain is just a compatibility trick.
You should be fine with the default main() if your environment is not too old.
The biggest problem is that you can get characters in 8-bit locale-dependent encoding from int main(int, char**) on Windows, but it depends on the environment. If the program involves working with the command line, you should use wmain on Windows and then recode the arguments to UTF8 for better compatibility. Let me know if you need an example.

1 Like

The need for SDL_main is not only early initialization and, sometimes, late deinitialization, but also a provider of stable main for different platforms. For example, on Windows, there are many pitfalls with main, while SDL_main is stable and provides us with UTF8 argv.

Thank you very much, @yataro