libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
9.71k stars 1.8k forks source link

Make SDL_main a header-only lib #6648

Closed icculus closed 1 year ago

icculus commented 1 year ago

Make SDL_main a header-only lib

Right now using SDL_main can be painful, especially with MinGW: There you have to link -lmingw32 -lSDL2main -lSDL2 in that exact order (you can add other libs in between, but libmingw32 must be before libSDL2main, and libSDL2main must be before libSDL2) - especially getting libmingw32 there isn't trivial, see https://github.com/libsdl-org/SDL/blob/1bfefb5b4d5fbd9899fca460442240dad2bd4510/sdl2-config.cmake.in#L27-L41 I think this happens because main() (or WinMain() or whatever) being in a static lib (instead of a source file that gets compiled as part of the build, or a .o/.obj) seems to confuse MinGW, which otherwise would automagically link libmingw32.

Another (also Windows-specific) problem is that .lib files generated by MinGW don't work with modern MSVC versions ("module unsafe for SAFESEH"). For the SDL2.lib only used for linking the DLL it's not that bad, because it can relatively easily be generated from SDL2.dll (create a .def with mingw's gendef SDL2.dll and then use lib.exe /machine:x86 /def:SDL2.def /out:SDL2.lib in the VS developer console to generate the lib) - but that doesn't help for SDL2main.lib, which is a proper static lib and not just this stub-thing used on Windows to link DLLs. So binary builds of SDL2 for Windows that have been built with MinGW aren't really compatible with MSVC, mostly because of SDL2main.

These problems would vanish if SDL2main was a header-only lib that you just #include in your source file that implements int main(int argc, char** argv) (and only there). It could still do its #define main SDL_main trick, and it could contain the platform-specific SDL_main code (or nothing at all for most of the platforms which don't use it). SDL.h wouldn't #include <SDL_main.h> anymore (maybe it shouldn't do that either way). Like SDL_config.h it would be generated when SDL is built (or probably just copied from some dir in src/ to include/).

As a nice side-effect, it would make it clearer when it is used an when not: Right now if you don't want to use it on e.g. Windows, you need to #define SDL_MAIN_HANDLED before #include <SDL.h> (or in your build process), which is not obvious - with SDL_main as a header-only lib, you'd just do nothing, and do #include <SDL_main.h> if you want to use it. Also I think that right now if you have any variable (or function, enum member, ...) called main - in any file that just includes SDL.h, not just the one with main() in it! - and that #define main SDL_main is active on your platform, that variable will be renamed as well, which could lead to confusion..

Furthermore, it would make it easier to copy the SDL_main implementation into your own source and adjust it to your needs.

Originally posted by @DanielGibson in https://github.com/libsdl-org/SDL/issues/3519#issuecomment-1014960548

slouken commented 1 year ago

Windows is the only platform where you could conceivably do this. Windows RT / Xbox, iOS, PSP, etc. all require you to have a separate file with the main implementation. iOS actually requires you to have the main implementation in an Objective C file included with your application - you can't use a static library at all.

What you're proposing is essentially including SDL_windows_main.c in a header that you include, but only on Windows. At that point you might as well include SDL_windows_main.c as a source file in your project. That file was put in the public domain in 1998 for just that purpose.

I completely agree with your reasoning, and agree with the benefits, but I don't see an elegant way to do that and still support other platforms.

@DanielGibson, feel free to submit a PR which solves this.

DanielGibson commented 1 year ago

Windows RT / Xbox, iOS, PSP, etc. all require you to have a separate file with the main implementation

That sucks, and the only of these platforms I can even develop for and test on is Win RT.

I'll look into it anyway.

A middle ground might be to provide an empty dummy libSDL3main on platforms that can use a header-only solution, and a real libSDL3main only for those that need it (because it needs to be implemented in obj-c or c++ or whatever) and use the header-only solution?

Then building SDL3 apps could work the same everywhere: You #include <SDL3/SDL_main.h> in your source file that implements main() and link against libSDL3main (even though that's a no-op on most platforms) - though in case you have to handle different operating systems differently in your build system anyway (or don't care about the platforms that need it as a lib), you can also skip the linking part of course. (Admittedly, this is pretty much the same as it works now, with the difference that we get the header-only benefits on Windows and similar platforms that need the SDL_main functionality and allow implementing it in C)

DanielGibson commented 1 year ago

Regarding:

Windows RT / Xbox, iOS, PSP, etc. all require you to have a separate file with the main implementation

Why iOS and PSP? They both seem to implement their SDL main stuff in plain C, so it could be in a header-only lib - or am I missing anything?

Of course it seems to be different for Haiku, ngage and WinRT, those three use C++ (though I wonder if any of those platforms would support putting the SDL_main C++ code into the dynamic libSDL and call that from a possibly-C-header-only SDLmain)

slouken commented 1 year ago

Ah, you're right, I was thinking that iOS was Objective C.

iOS is particularly painful in that you have to have the code in your application, you can't link to a library to pick up the main entry point.

I'm willing to entertain a new approach in SDL3, as long as it works well across the different platforms.

DanielGibson commented 1 year ago

Some remarks and questions about the C++ platforms:

WinRT

If I understand https://github.com/libsdl-org/SDL/blob/main/docs/README-winrt.md correctly, there is no static SDLmain lib for WinRT, instead apps need to add SDL_winrt_main_NonXAML.cpp to their build and compile it as part of their app, because

WinRT uses a different signature for each app's main() function. SDL-based apps that use this port must compile in SDL_winrt_main_NonXAML.cpp (in SDL\src\main\winrt) directly in order for their C-style main() functions to be called.

I think this means that moving that code into a header-only SDL_main_winrt.hpp that must be #included from a .cpp file of the project would make usage slightly easier (no need to copy a source file from the SDL sourcetree)? (And if for some reason that's a bad idea, we can still ignore WinRT for SDL_main considerations because there is no libSDLmain for WinRT to link against either way)

Haiku

Disclaimer: Haiku isn't documented in docs/ and I have no experience with Haiku/BeOS development at all so I can only make deductions from the source code

As far as I see it, there is no main()-like function in src/main/haiku/. Apparently the way it works is that you have a "normal" main() function and code, and when you call SDL_Init() and then through SDL_VideoInit() HAIKU_VideoInit() (from src/video/haiku/SDL_bvideo.cc) is called which calls SDL_InitBeApp() (a similar path exists via SDL_InitAudio()). And then there's the counter-part, HAIKU_VideoQuit() calling SDL_QuitBeApp().

Anyway, SDL_InitBeApp() and SDL_QuitBeApp() are implemented in SDL_main. So question for people who know about Haiku: Is that really necessary? As apparently everything that happens at startup, including linking/loading libSDL, main() and calling SDL_Init() works without the code in SDL_main, does that code really have to be in a separate, static library? Couldn't SDL_InitBeApp() and SDL_QuitBeApp() (and the class SDL_BApp that's created/destroyed by those functions) also be implemented in the dynamic libSDL3 itself?

ngage

Another platform I know nothing about :)

I see extern "C" int main(int argc, char *argv[]); (which then is not referenced/used again - does it have a specific purpose?) and TInt E32Main() which seems to contain relevant code and eventually calls SDL_main and uses C++-constructs.

So, question for people who know about ngage development: Couldn't we move the code from E32Main() into int SDL_E32MainImpl() in libSDL itself, and let the SDLmain part be pure C code that just calls that? (Maybe int SDL_E32MainImpl(SDL_main_func mainfunc) to pass the users main function as a function pointer that's called there)

Android

Bonus-question (as this doesn't use C++): src/main/android/SDL_android_main.c is an empty file with a As of SDL 2.0.6 this file is no longer necessary. comment - maybe it should just be removed?

DanielGibson commented 1 year ago

One more thing: SDL_main.h currently serves multiple purposes:

  1. Decide SDL_MAIN_NEEDED and SDL_MAIN_AVAILABLE - also used by SDLs implementation code
  2. #define main SDL_main so the libSDLmain code can call SDL_main() instead of main()
  3. Declare some platform-specific functions like SDL_RegisterApp() and SDL_WinRTRunApp(), SDL_UIKitRunApp() etc that users may wanna call in their own main() code

So when turning SDL_main.h into a header-only lib that (on platforms that use some kind of SDL_main functionality, basically SDL_MAIN_AVAILABLE I guess) implements the main() magic that in SDL2 lives in libSDLmain, we have (at least) two options how to handle this:

  1. Split it in two headers: One with the main-#define and implementation, the other setting SDL_MAIN_NEEDED and SDL_MAIN_AVAILABLE and declaring platform-specific main-related functions, if any. For example SDL_main_impl.h (for the header-only lib with the implementation and the main-#define) and SDL_main.h for the rest
  2. Keep it in one header, but put #ifndef SDL_MAIN_HANDLED around the implementation parts
DanielGibson commented 1 year ago

I started implementing this, so far only for ("classic") Windows. You can see the WIP at https://github.com/libsdl-org/SDL/pull/6750

DanielGibson commented 1 year ago

Now that #6750 is merged, this can probably be closed :)

@madebr: As you seem to be very involved with SDLs CMake build, do you think it would now be feasible to do some magic in SDL3Config.cmake (or whatever is shipped with SDL3 builds) to make it possible to use MinGW builds with MSVC?

As I wrote above:

For the SDL2.lib only used for linking the DLL it's not that bad, because it can relatively easily be generated from SDL2.dll (create a .def with mingw's gendef SDL2.dll and then use lib.exe /machine:x86 /def:SDL2.def /out:SDL2.lib in the VS developer console to generate the lib) - but that doesn't help for SDL2main.lib, which is a proper static lib and not just this stub-thing used on Windows to link DLLs.

As the SDLmain part of the problem is now solved, it should be possible to ship SDL3.def with the cmake scripts, and when they're used with MSVC (and the SDL3.dll was built with MinGW), the required SDL3.lib could be created from SDL3.def with the lib.exe of the users machine

Just a wild idea ;)

madebr commented 1 year ago

That's correct for SDLx.dll, but the MSVC and MinGW sdk's also ship static libraries. The SDL2 MinGW sdk also contain libSDL2.a and libSDL2_test.a. The SDL2 VC sdk also contains SDL2test.lib. The static SDLx libraries depends on the c library it is compiled with. So I am not 100% sure this will work for these.

Last time I tried, I was able to use the VC sdk with a MinGW toolchain (using a shared SDL). So I think generating an import library is not needed in this case. I haven't tested the reverse case (a MinGW SDK with a VC toolchain).

DanielGibson commented 1 year ago

Using an SDL SDK built with MSVC with MinGW works (MinGW can use the .lib), but the other way around doesn't work because MSVC can't use libSDL2.dll.a or however it's called (not the full static lib but the equivalent of the glue .lib for DLLs). And even though MinGW can generate .lib files, those are incompatible with modern versions of MSVC ("module unsafe for SAFESEH").

Yeah, full static libs built with MinGW still wouldn't work with MSVC, but having the dynamic lib work might be nice to have (people aren't supposed to link SDL statically anyway :-p)

slouken commented 1 year ago

Yeah, full static libs built with MinGW still wouldn't work with MSVC, but having the dynamic lib work might be nice to have (people aren't supposed to link SDL statically anyway :-p)

Speaking of which, @madebr, can we default SDL static off in CMake?

madebr commented 1 year ago

Speaking of which, @madebr, can we default SDL static off in CMake?

Sure, I will include it in the next cmake pr.

madebr commented 1 year ago

Using an SDL SDK built with MSVC with MinGW works (MinGW can use the .lib), but the other way around doesn't work because MSVC can't use libSDL2.dll.a or however it's called (not the full static lib but the equivalent of the glue .lib for DLLs). And even though MinGW can generate .lib files, those are incompatible with modern versions of MSVC ("module unsafe for SAFESEH").

I don't see a SAFESEH warning when linking against a libSDL3.dll.a, created by a mingw64 toolchain from msys2 (Using gcc 11.2.0 and cl 19.29.30147 (=MSVC2019)) Perhaps current toolchains have fixed this?

I created this branch on my sdl2 fork that will recreate a sdl2 import library, but I don't see any benefits and don't really like it.

I think that warning happened with older import libraries. For a retro project, I link to ancient directx7 sdk import libraries (downloaded from archive.org) where I remember seeing similar warnings.

madebr commented 1 year ago

Speaking of which, @madebr, can we default SDL static off in CMake?

You can currently do that by configuring with -DBUILD_SHARED_LIBS=ON.

slouken commented 1 year ago

This has been implemented, thanks!