SDL: main: Add an optional `appstate` param to main callback entry points.

From 38e3c6a4aa338d062ca2eba80728bfdf319f7104 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Wed, 27 Mar 2024 17:22:08 -0400
Subject: [PATCH] main: Add an optional `appstate` param to main callback entry
 points.

This allows apps to maintain state data without using global variables.

Fixes #9377.
---
 include/SDL3/SDL_main.h       | 39 ++++++++++++++++++++++++++++-------
 src/main/SDL_main_callbacks.c | 10 +++++----
 test/loopwave.c               |  8 +++----
 test/testaudio.c              |  8 +++----
 test/testaudiocapture.c       |  8 +++----
 test/testcamera.c             |  8 +++----
 test/testsprite.c             |  8 +++----
 7 files changed, 57 insertions(+), 32 deletions(-)

diff --git a/include/SDL3/SDL_main.h b/include/SDL3/SDL_main.h
index b7705d6605ad7..0659ad61d4562 100644
--- a/include/SDL3/SDL_main.h
+++ b/include/SDL3/SDL_main.h
@@ -158,10 +158,10 @@
 extern "C" {
 #endif
 
-typedef int (SDLCALL *SDL_AppInit_func)(int argc, char *argv[]);
-typedef int (SDLCALL *SDL_AppIterate_func)(void);
-typedef int (SDLCALL *SDL_AppEvent_func)(const SDL_Event *event);
-typedef void (SDLCALL *SDL_AppQuit_func)(void);
+typedef int (SDLCALL *SDL_AppInit_func)(void **appstate, int argc, char *argv[]);
+typedef int (SDLCALL *SDL_AppIterate_func)(void *appstate);
+typedef int (SDLCALL *SDL_AppEvent_func)(void *appstate, const SDL_Event *event);
+typedef void (SDLCALL *SDL_AppQuit_func)(void *appstate);
 
 /**
  * You can (optionally!) define SDL_MAIN_USE_CALLBACKS before including
@@ -203,6 +203,12 @@ typedef void (SDLCALL *SDL_AppQuit_func)(void);
  * This function should not go into an infinite mainloop; it should do any
  * one-time setup it requires and then return.
  *
+ * The app may optionally assign a pointer to `*appstate`. This pointer will
+ * be provided on every future call to the other entry points, to allow
+ * application state to be preserved between functions without the app
+ * needing to use a global variable. If this isn't set, the pointer will
+ * be NULL in future entry points.
+ *
  * If this function returns 0, the app will proceed to normal operation,
  * and will begin receiving repeated calls to SDL_AppIterate and SDL_AppEvent
  * for the life of the program. If this function returns < 0, SDL will
@@ -210,6 +216,7 @@ typedef void (SDLCALL *SDL_AppQuit_func)(void);
  * an error to the platform. If it returns > 0, the SDL calls SDL_AppQuit
  * and terminates with an exit code that reports success to the platform.
  *
+ * \param appstate a place where the app can optionally store a pointer for future use.
  * \param argc The standard ANSI C main's argc; number of elements in `argv`
  * \param argv The standard ANSI C main's argv; array of command line arguments.
  * \returns -1 to terminate with an error, 1 to terminate with success, 0 to continue.
@@ -222,7 +229,7 @@ typedef void (SDLCALL *SDL_AppQuit_func)(void);
  * \sa SDL_AppEvent
  * \sa SDL_AppQuit
  */
-extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppInit(int argc, char *argv[]);
+extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppInit(void **appstate, int argc, char *argv[]);
 
 /**
  * App-implemented iteration entry point for SDL_MAIN_USE_CALLBACKS apps.
@@ -248,6 +255,9 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppInit(int argc, char *argv[]);
  * This function should not go into an infinite mainloop; it should do one
  * iteration of whatever the program does and return.
  *
+ * The `appstate` parameter is an optional pointer provided by the app during
+ * SDL_AppInit(). If the app never provided a pointer, this will be NULL.
+ *
  * If this function returns 0, the app will continue normal operation,
  * receiving repeated calls to SDL_AppIterate and SDL_AppEvent for the life
  * of the program. If this function returns < 0, SDL will call SDL_AppQuit
@@ -255,6 +265,7 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppInit(int argc, char *argv[]);
  * platform. If it returns > 0, the SDL calls SDL_AppQuit and terminates with
  * an exit code that reports success to the platform.
  *
+ * \param appstate an optional pointer, provided by the app in SDL_AppInit.
  * \returns -1 to terminate with an error, 1 to terminate with success, 0 to continue.
  *
  * \threadsafety This function is not thread safe.
@@ -264,7 +275,7 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppInit(int argc, char *argv[]);
  * \sa SDL_AppInit
  * \sa SDL_AppEvent
  */
-extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppIterate(void);
+extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppIterate(void *appstate);
 
 /**
  * App-implemented event entry point for SDL_MAIN_USE_CALLBACKS apps.
@@ -293,6 +304,9 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppIterate(void);
  * This function should not go into an infinite mainloop; it should handle
  * the provided event appropriately and return.
  *
+ * The `appstate` parameter is an optional pointer provided by the app during
+ * SDL_AppInit(). If the app never provided a pointer, this will be NULL.
+ *
  * If this function returns 0, the app will continue normal operation,
  * receiving repeated calls to SDL_AppIterate and SDL_AppEvent for the life
  * of the program. If this function returns < 0, SDL will call SDL_AppQuit
@@ -300,6 +314,8 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppIterate(void);
  * platform. If it returns > 0, the SDL calls SDL_AppQuit and terminates with
  * an exit code that reports success to the platform.
  *
+ * \param appstate an optional pointer, provided by the app in SDL_AppInit.
+ * \param event the new event for the app to examine.
  * \returns -1 to terminate with an error, 1 to terminate with success, 0 to continue.
  *
  * \threadsafety This function is not thread safe.
@@ -309,7 +325,7 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppIterate(void);
  * \sa SDL_AppInit
  * \sa SDL_AppIterate
  */
-extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppEvent(const SDL_Event *event);
+extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppEvent(void *appstate, const SDL_Event *event);
 
 /**
  * App-implemented deinit entry point for SDL_MAIN_USE_CALLBACKS apps.
@@ -330,13 +346,20 @@ extern SDLMAIN_DECLSPEC int SDLCALL SDL_AppEvent(const SDL_Event *event);
  * it after this function returns and before the process terminates, but
  * it is safe to do so.
  *
+ * The `appstate` parameter is an optional pointer provided by the app during
+ * SDL_AppInit(). If the app never provided a pointer, this will be NULL.
+ * This function call is the last time this pointer will be provided, so
+ * any resources to it should be cleaned up here.
+ *
+ * \param appstate an optional pointer, provided by the app in SDL_AppInit.
+ *
  * \threadsafety This function is not thread safe.
  *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_AppInit
  */
-extern SDLMAIN_DECLSPEC void SDLCALL SDL_AppQuit(void);
+extern SDLMAIN_DECLSPEC void SDLCALL SDL_AppQuit(void *appstate);
 
 #endif  /* SDL_MAIN_USE_CALLBACKS */
 
diff --git a/src/main/SDL_main_callbacks.c b/src/main/SDL_main_callbacks.c
index 37657a86669cd..a87a56b2fcff4 100644
--- a/src/main/SDL_main_callbacks.c
+++ b/src/main/SDL_main_callbacks.c
@@ -26,6 +26,7 @@ static SDL_AppEvent_func SDL_main_event_callback;
 static SDL_AppIterate_func SDL_main_iteration_callback;
 static SDL_AppQuit_func SDL_main_quit_callback;
 static SDL_AtomicInt apprc;  // use an atomic, since events might land from any thread and we don't want to wrap this all in a mutex. A CAS makes sure we only move from zero once.
+static void *SDL_main_appstate = NULL;
 
 // Return true if this event needs to be processed before returning from the event watcher
 static SDL_bool ShouldDispatchImmediately(SDL_Event *event)
@@ -46,7 +47,7 @@ static SDL_bool ShouldDispatchImmediately(SDL_Event *event)
 static void SDL_DispatchMainCallbackEvent(SDL_Event *event)
 {
     if (SDL_AtomicGet(&apprc) == 0) { // if already quitting, don't send the event to the app.
-        SDL_AtomicCompareAndSwap(&apprc, 0, SDL_main_event_callback(event));
+        SDL_AtomicCompareAndSwap(&apprc, 0, SDL_main_event_callback(SDL_main_appstate, event));
     }
 }
 
@@ -95,7 +96,7 @@ int SDL_InitMainCallbacks(int argc, char* argv[], SDL_AppInit_func appinit, SDL_
     SDL_main_quit_callback = appquit;
     SDL_AtomicSet(&apprc, 0);
 
-    const int rc = appinit(argc, argv);
+    const int rc = appinit(&SDL_main_appstate, argc, argv);
     if (SDL_AtomicCompareAndSwap(&apprc, 0, rc) && (rc == 0)) {  // bounce if SDL_AppInit already said abort, otherwise...
         // make sure we definitely have events initialized, even if the app didn't do it.
         if (SDL_InitSubSystem(SDL_INIT_EVENTS) == -1) {
@@ -121,7 +122,7 @@ int SDL_IterateMainCallbacks(SDL_bool pump_events)
 
     int rc = SDL_AtomicGet(&apprc);
     if (rc == 0) {
-        rc = SDL_main_iteration_callback();
+        rc = SDL_main_iteration_callback(SDL_main_appstate);
         if (!SDL_AtomicCompareAndSwap(&apprc, 0, rc)) {
             rc = SDL_AtomicGet(&apprc); // something else already set a quit result, keep that.
         }
@@ -132,7 +133,8 @@ int SDL_IterateMainCallbacks(SDL_bool pump_events)
 void SDL_QuitMainCallbacks(void)
 {
     SDL_DelEventWatch(SDL_MainCallbackEventWatcher, NULL);
-    SDL_main_quit_callback();
+    SDL_main_quit_callback(SDL_main_appstate);
+    SDL_main_appstate = NULL;  // just in case.
 
     // for symmetry, you should explicitly Quit what you Init, but we might come through here uninitialized and SDL_Quit() will clear everything anyhow.
     //SDL_QuitSubSystem(SDL_INIT_EVENTS);
diff --git a/test/loopwave.c b/test/loopwave.c
index 1aa627061d983..5ff01511948f2 100644
--- a/test/loopwave.c
+++ b/test/loopwave.c
@@ -42,7 +42,7 @@ static int fillerup(void)
     return 0;
 }
 
-int SDL_AppInit(int argc, char *argv[])
+int SDL_AppInit(void **appstate, int argc, char *argv[])
 {
     int i;
     char *filename = NULL;
@@ -119,17 +119,17 @@ int SDL_AppInit(int argc, char *argv[])
     return 0;
 }
 
-int SDL_AppEvent(const SDL_Event *event)
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
 {
     return (event->type == SDL_EVENT_QUIT) ? 1 : 0;
 }
 
-int SDL_AppIterate(void)
+int SDL_AppIterate(void *appstate)
 {
     return fillerup();
 }
 
-void SDL_AppQuit(void)
+void SDL_AppQuit(void *appstate)
 {
     SDL_DestroyAudioStream(stream);
     SDL_free(wave.sound);
diff --git a/test/testaudio.c b/test/testaudio.c
index 59da630657e9b..c4e60ef20a5f1 100644
--- a/test/testaudio.c
+++ b/test/testaudio.c
@@ -1036,7 +1036,7 @@ static void WindowResized(const int newwinw, const int newwinh)
     state->window_h = newwinh;
 }
 
-int SDL_AppInit(int argc, char *argv[])
+int SDL_AppInit(void **appstate, int argc, char *argv[])
 {
     int i;
 
@@ -1094,7 +1094,7 @@ int SDL_AppInit(int argc, char *argv[])
 
 static SDL_bool saw_event = SDL_FALSE;
 
-int SDL_AppEvent(const SDL_Event *event)
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
 {
     Thing *thing = NULL;
 
@@ -1214,7 +1214,7 @@ int SDL_AppEvent(const SDL_Event *event)
     return SDLTest_CommonEventMainCallbacks(state, event);
 }
 
-int SDL_AppIterate(void)
+int SDL_AppIterate(void *appstate)
 {
     if (app_ready_ticks == 0) {
         app_ready_ticks = SDL_GetTicks();
@@ -1232,7 +1232,7 @@ int SDL_AppIterate(void)
     return 0;  /* keep going. */
 }
 
-void SDL_AppQuit(void)
+void SDL_AppQuit(void *appstate)
 {
     while (things) {
         DestroyThing(things);  /* make sure all the audio devices are closed, etc. */
diff --git a/test/testaudiocapture.c b/test/testaudiocapture.c
index 04ab2f0832baf..82027019e9070 100644
--- a/test/testaudiocapture.c
+++ b/test/testaudiocapture.c
@@ -21,7 +21,7 @@ static SDL_AudioStream *stream_in = NULL;
 static SDL_AudioStream *stream_out = NULL;
 static SDLTest_CommonState *state = NULL;
 
-int SDL_AppInit(int argc, char **argv)
+int SDL_AppInit(void **appstate, int argc, char **argv)
 {
     SDL_AudioDeviceID *devices;
     SDL_AudioSpec outspec;
@@ -145,7 +145,7 @@ int SDL_AppInit(int argc, char **argv)
     return 0;
 }
 
-int SDL_AppEvent(const SDL_Event *event)
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
 {
     if (event->type == SDL_EVENT_QUIT) {
         return 1;  /* terminate as success. */
@@ -169,7 +169,7 @@ int SDL_AppEvent(const SDL_Event *event)
     return 0;  /* keep going. */
 }
 
-int SDL_AppIterate(void)
+int SDL_AppIterate(void *appstate)
 {
     if (!SDL_AudioDevicePaused(SDL_GetAudioStreamDevice(stream_in))) {
         SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
@@ -195,7 +195,7 @@ int SDL_AppIterate(void)
     return 0;  /* keep app going. */
 }
 
-void SDL_AppQuit(void)
+void SDL_AppQuit(void *appstate)
 {
     SDL_Log("Shutting down.\n");
     const SDL_AudioDeviceID devid_in = SDL_GetAudioStreamDevice(stream_in);
diff --git a/test/testcamera.c b/test/testcamera.c
index 51ba1d2aa509b..c0d9ec4763678 100644
--- a/test/testcamera.c
+++ b/test/testcamera.c
@@ -26,7 +26,7 @@ static SDL_Surface *frame_current = NULL;
 static SDL_CameraDeviceID front_camera = 0;
 static SDL_CameraDeviceID back_camera = 0;
 
-int SDL_AppInit(int argc, char *argv[])
+int SDL_AppInit(void **appstate, int argc, char *argv[])
 {
     int devcount = 0;
     int i;
@@ -159,7 +159,7 @@ static int FlipCamera(void)
     return 0;
 }
 
-int SDL_AppEvent(const SDL_Event *event)
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
 {
     switch (event->type) {
         case SDL_EVENT_KEY_DOWN: {
@@ -209,7 +209,7 @@ int SDL_AppEvent(const SDL_Event *event)
     return SDLTest_CommonEventMainCallbacks(state, event);
 }
 
-int SDL_AppIterate(void)
+int SDL_AppIterate(void *appstate)
 {
     SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 255);
     SDL_RenderClear(renderer);
@@ -262,7 +262,7 @@ int SDL_AppIterate(void)
     return 0;  /* keep iterating. */
 }
 
-void SDL_AppQuit(void)
+void SDL_AppQuit(void *appstate)
 {
     SDL_ReleaseCameraFrame(camera, frame_current);
     SDL_CloseCamera(camera);
diff --git a/test/testsprite.c b/test/testsprite.c
index 7db994c9d7985..8bd6e16bb78e2 100644
--- a/test/testsprite.c
+++ b/test/testsprite.c
@@ -45,7 +45,7 @@ static SDL_bool suspend_when_occluded;
 /* -1: infinite random moves (default); >=0: enables N deterministic moves */
 static int iterations = -1;
 
-void SDL_AppQuit(void)
+void SDL_AppQuit(void *appstate)
 {
     SDL_free(sprites);
     SDL_free(positions);
@@ -386,12 +386,12 @@ static void MoveSprites(SDL_Renderer *renderer, SDL_Texture *sprite)
     SDL_RenderPresent(renderer);
 }
 
-int SDL_AppEvent(const SDL_Event *event)
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
 {
     return SDLTest_CommonEventMainCallbacks(state, event);
 }
 
-int SDL_AppIterate(void)
+int SDL_AppIterate(void *appstate)
 {
     Uint64 now;
     int i;
@@ -425,7 +425,7 @@ int SDL_AppIterate(void)
     return 0;  /* keep going */
 }
 
-int SDL_AppInit(int argc, char *argv[])
+int SDL_AppInit(void **appstate, int argc, char *argv[])
 {
     int i;
     Uint64 seed;