docs: Added first draft of README-main-functions.md

Reference PR #8247.
This commit is contained in:
Ryan C. Gordon 2023-11-03 11:39:53 -04:00
parent 70d75b4a23
commit 853c28e624
No known key found for this signature in database
GPG Key ID: FA148B892AB48044
1 changed files with 213 additions and 0 deletions

View File

@ -0,0 +1,213 @@
# Where an SDL program starts running.
## History
SDL has a long, complicated history with starting a program.
In most of the civilized world, an application starts in a C-callable
function named "main". You probably learned it a long time ago:
```c
int main(int argc, char **argv)
{
printf("Hello world!\n");
return 0;
}
```
But not all platforms work like this. Windows apps might want a different
function named "WinMain", for example, so SDL set out to paper over this
difference.
Generally how this would work is: your app would always use the "standard"
`main(argc, argv)` function as its entry point, and `#include` the proper
SDL header before that, which did some macro magic. On platforms that used
a standard `main`, it would do nothing and what you saw was what you got.
But those other platforms! If they needed something that _wasn't_ `main`,
SDL's macro magic would quietly rename your function to `SDL_main`, and
provide its own entry point that called it. Your app was none the wiser and
your code worked everywhere without changes.
In SDL1, you linked with a static library that had startup code that _had_ to
run before you hit SDL_main(). For example, on macOS it would do various
magic to the process to make sure it was in the right state. Windows would
register win32 window classes and such. Things would break if you tried to
circumvent this, and you were in for a lot of trouble if you tried to use
SDL on a platform that needed this when you didn't control the entry point
(for example, as a plugin, or an SDL binding in a scripting language).
In SDL2, the necessary support code moved into the main library, and the tiny
static library _only_ handled the basics of getting from the platform's real
entry point (like WinMain) to SDL_main; if the real entry was _already_
standard main, the static library and macro magic was unnecessary. The goal
was to make it so you didn't have to change _your_ code to work on multiple
platforms and remove the original limitations.
In SDL3, we've taken this much, much further.
## The main entry point in SDL3
SDL3 still has the same macro tricks, but the static library is gone. Now it's
supplied by a "single-header library," which is to say you
`#include <SDL3/SDL_main.h>` and that header will insert a small amount of
code into the source file that included it, so you no longer have to worry
about linking against an extra library that you might need on some platforms.
You just build your app and it works.
You should _only_ include SDL_main.h from one file (the umbrella header,
SDL.h, does _not_ include it), and know that it will `#define main` to
something else, so if you use this symbol elsewhere as a variable name, etc,
it can cause you unexpected problems.
SDL_main.h will also include platform-specific code (WinMain or whatnot) that
calls your _actual_ main function. This is compiled directly into your
program.
If for some reason you need to include SDL_main.h in a file but also _don't_
want it to generate this platform-specific code, you should define a special
macro before includin the header:
```c
#define SDL_MAIN_NOIMPL
```
If you are moving from SDL2, remove any references to the SDLmain static
library from your build system, and you should be done. Things should work as
they always have.
If you have never controlled your process's entry point (you are using SDL
as a module from a general-purpose scripting language interpreter, or you're
SDL in a plugin for some otherwise-unrelated app), then there is nothing
required of you here; there is no startup code in SDL's entry point code that
is required, so using SDL_main.h is completely optional. Just start using
the SDL API when you are ready.
## Main callbacks in SDL3
There is a second option in SDL3 for how to structure your program. This is
completly optional and you can ignore it if you're happy using a standard
"main" function.
Some platforms would rather your program operate in chunks. Most of the time,
games tend to look like this at the highest level:
```c
int main(int argc, char **argv)
{
initialize();
while (keep_running()) {
handle_new_events();
do_one_frame_of_stuff();
}
deinitialize();
}
```
There are platforms that would rather be in charge of that `while` loop:
iOS would rather you return from main() immediately and then it will let you
know that it's time to update and draw the next frame of video. Emscripten
(programs that run on a web page) absolutely requires this to function at all.
Video targets like Wayland can notify the app when to draw a new frame, to
save battery life and cooperate with the compositor more closely.
In most cases, you can add special-case code to your program to deal with this
on different platforms, but SDL3 offers a system to handle transparently on
the app's behalf.
To use this, you have to redesign the highest level of your app a little. Once
you do, it'll work on all supported SDL platforms without problems and
`#ifdef`s in your code.
Instead of providing a "main" function, under this system, you would provide
several functions that SDL will call as appropriate.
Using the callback entry points works on every platform, because on platforms
that don't require them, we can fake them with a simple loop in an internal
implementation of the usual SDL_main.
The primary way we expect people to write SDL apps is still with SDL_main, and
this is not intended to replace it. If the app chooses to use this, it just
removes some platform-specific details they might have to otherwise manage,
and maybe removes a barrier to entry on some future platform. And you might
find you enjoy structuring your program like this more!
## How to use main callbacks in SDL3
To enable the callback entry points, you include SDL_main with an extra define
from a single source file in your project:
```c
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
```
Once you do this, you do not include a "main" function at all (and if you do,
the app will likely fail to link). Instead, you provide the following
functions:
First:
```c
int SDL_AppInit(int argc, char **argv);
```
This will be called _once_ before anything else. argc/argv work like they
always do. If this returns 0, the app runs. If it returns < 0, the app calls
SDL_AppQuit and terminates with an exit code that reports an error to the
platform. If it returns > 0, the app calls SDL_AppQuit and terminates with
an exit code that reports success to the platform. This function should not
go into an infinite mainloop; it should do any one-time startup it requires
and then return.
Then:
```c
int SDL_AppIterate(void);
```
This is called over and over, possibly at the refresh rate of the display or
some other metric that the platform dictates. This is where the heart of your
app runs. It should return as quickly as reasonably possible, but it's not a
"run one memcpy and that's all the time you have" sort of thing. The app
should do any game updates, and render a frame of video. If it returns < 0,
SDL will call SDL_AppQuit and terminate the process with an exit code that
reports an error to the platform. If it returns > 0, the app calls
SDL_AppQuit and terminates with an exit code that reports success to the
platform. If it returns 0, then SDL_AppIterate will be called again at some
regular frequency. The platform may choose to run this more or less (perhaps
less in the background, etc), or it might just call this function in a loop
as fast as possible. You do not check the event queue in this function
(SDL_AppEvent exists for that).
Next:
```c
int SDL_AppEvent(const SDL_Event *event);
```
This will be called once for each event pushed into the SDL queue. This may be
called from any thread, and possibly in parallel to SDL_AppIterate. The fields
in `event` do not need to be free'd (as you would normally need to do for
SDL_EVENT_DROP_FILE, etc), and your app should not call SDL_PollEvent,
SDL_PumpEvent, etc, as SDL will manage this for you. Return values are the
same as from SDL_AppIterate(), so you can terminate in response to
SDL_EVENT_QUIT, etc.
Finally:
```c
void SDL_AppQuit(void);
```
This is called once before terminating the app--assuming the app isn't being
forcibly killed or crashed--as a last chance to clean up. After this returns,
SDL will call SDL_Quit so the app doesn't have to (but it's safe for the app
to call it, too). Process termination proceeds as if the app returned normally
from main(), so atexit handles will run, if your platform supports that.