Application API #10

Closed
opened 2026-05-26 03:34:39 +02:00 by lee · 0 comments

Context

The current <banjo/main.h> mixes two concepts that should be separate:

  • Entry-point abstraction — the portable substitute for main / WinMain / UIApplicationMain / Emscripten's no-main story. Where the OS hands control to your code.
  • Application lifecycle — setup → loop → teardown, plus the redraw model. How a banjo program is structured at runtime.

Hot reloading (future) only blurs the entry-point side; the lifecycle stays clean. Splitting now means hot reload doesn't have to re-think the whole header later.

Outcome

Split <banjo/main.h> into two headers; the lifecycle side becomes a runtime API rather than a compile-time macro mode.

<banjo/main.h> (narrowed, macro-free, include-only opt-in)

Optional. Include it on platforms where the OS calls something other than main() (Windows GUI subsystem, Emscripten, future iOS). The include itself is the opt-in — no BJ_AUTOMAIN macro. On Linux / macOS / Win32-console you just write a normal main() and don't include this header at all.

Banjo handles the platform handshake internally (e.g. #define main bj_main so banjo can provide the real main / WinMain / emscripten_set_main_loop stub that calls into the user's function). The user always writes int main(int, char*[]).

<banjo/app.h> (new)

Application lifecycle. Pass-by-struct config, no setters, no macros.

typedef void (*bj_app_setup_fn)   (void* user_data);
typedef void (*bj_app_step_fn)    (void* user_data);
typedef void (*bj_app_teardown_fn)(void* user_data);

struct bj_app {
    bj_app_setup_fn    setup;
    bj_app_step_fn     step;
    bj_app_teardown_fn teardown;
    void*              user_data;
};

BANJO_EXPORT int  bj_app_run(const struct bj_app* app);
BANJO_EXPORT void bj_app_quit(int exit_code);

User writes (Linux / macOS / Win32 console):

#include <banjo/app.h>

int main(int argc, char* argv[]) {
    (void)argc; (void)argv;
    struct bj_app app = {
        .setup    = my_setup,
        .step     = my_step,
        .teardown = my_teardown,
    };
    return bj_app_run(&app);
}

Or with the entry-point abstraction (Windows GUI / Emscripten / iOS):

#include <banjo/main.h>    // include = opt in to the platform stub
#include <banjo/app.h>

int main(int argc, char* argv[]) {   // banjo's internal rename makes this work
    struct bj_app app = { ... };
    return bj_app_run(&app);
}

Same source body either way. Only the include of main.h differs. Some programs will include it unconditionally on every platform (no-op where it isn't needed) for portability.

BJ_AUTOMAIN and BJ_AUTOMAIN_CALLBACKS retired

Both macros are gone. The callback mode that BJ_AUTOMAIN_CALLBACKS enabled becomes the only mode — every banjo program registers callbacks via struct bj_app. The opt-in for the platform stub is the <banjo/main.h> include itself.

Window creation lives in setup; window-level callbacks (redraw, key, button, cursor, enter) live in <banjo/window.h> and are registered on each window after creation.

Design notes

  • Pass-by-struct, not setters. Config is data, expressed as data. No hidden mutation of a singleton. Designated initializers + struct is idiomatic C99 (Vulkan, SDL3, etc.). Lifetime is sound: the struct lives on the stack of main and bj_app_run doesn't return until the loop ends.
  • No app handle. Implicit singleton. State in module-static inside src/app.c. Same as GLFW.
  • No macros. The include is the opt-in for main.h. The struct fields are the opt-in for individual callbacks. Less ceremony, fewer things to remember.
  • Symmetry with audio. Banjo's audio API already uses "open with callback." App adopts the same shape. Three subsystems (app, audio, window), one mental model: banjo calls you, not the other way around.

Per-platform bj_app_run

Platform Implementation
Linux / Win32 / macOS while (!quit) bj_app_step_internal(); then teardown
Emscripten emscripten_set_main_loop(internal_step, 0, 1) and return; JS runtime drives the loop
iOS (future) Hand control to UIApplicationMain; the run loop calls our step internally
DLL hot-reload (future) Host calls our step in its loop

Per-platform <banjo/main.h> content

Platform What main.h emits
Linux / macOS console Nothing (header is effectively a no-op include)
Win32 console Nothing
Win32 GUI (/SUBSYSTEM:WINDOWS) WinMain stub that calls bj_main (renamed user main via #define)
Emscripten JS-loop integration; user's main is reachable via emscripten_set_main_loop_arg
iOS (future) UIApplicationMain stub

Banjo can provide both main and WinMain stubs on Windows unconditionally — the linker picks whichever the chosen subsystem expects. No user-side discrimination needed.

Out of scope

  • Window-level redraw callback and dirty-rect invalidation — separate card.
  • Public bj_app_step for users running their own outer loop — can come later if asked.
  • Multi-app or multi-thread variants.

Migration

  • BJ_AUTOMAIN and BJ_AUTOMAIN_CALLBACKS deleted. Examples using either are rewritten to use the struct.
  • examples/start.c tutorial rewritten as the canonical "create struct, call run" shape.
  • The Doxygen \file block at the top of <banjo/main.h> (currently describing bj_main as the user-facing function) is rewritten to reflect that the user writes main() and banjo's machinery handles the platform handshake.

Depends on / blocks

Lands together with the redraw-callback card since setup is where users register the redraw callback on the window they just created.

## Context The current `<banjo/main.h>` mixes two concepts that should be separate: - **Entry-point abstraction** — the portable substitute for `main` / `WinMain` / `UIApplicationMain` / Emscripten's no-`main` story. *Where the OS hands control to your code.* - **Application lifecycle** — setup → loop → teardown, plus the redraw model. *How a banjo program is structured at runtime.* Hot reloading (future) only blurs the entry-point side; the lifecycle stays clean. Splitting now means hot reload doesn't have to re-think the whole header later. ## Outcome Split `<banjo/main.h>` into two headers; the lifecycle side becomes a runtime API rather than a compile-time macro mode. ### `<banjo/main.h>` (narrowed, macro-free, include-only opt-in) Optional. Include it on platforms where the OS calls something other than `main()` (Windows GUI subsystem, Emscripten, future iOS). **The include itself is the opt-in** — no `BJ_AUTOMAIN` macro. On Linux / macOS / Win32-console you just write a normal `main()` and don't include this header at all. Banjo handles the platform handshake internally (e.g. `#define main bj_main` so banjo can provide the real `main` / `WinMain` / `emscripten_set_main_loop` stub that calls into the user's function). The user always writes `int main(int, char*[])`. ### `<banjo/app.h>` (new) Application lifecycle. Pass-by-struct config, no setters, no macros. ```c typedef void (*bj_app_setup_fn) (void* user_data); typedef void (*bj_app_step_fn) (void* user_data); typedef void (*bj_app_teardown_fn)(void* user_data); struct bj_app { bj_app_setup_fn setup; bj_app_step_fn step; bj_app_teardown_fn teardown; void* user_data; }; BANJO_EXPORT int bj_app_run(const struct bj_app* app); BANJO_EXPORT void bj_app_quit(int exit_code); ``` User writes (Linux / macOS / Win32 console): ```c #include <banjo/app.h> int main(int argc, char* argv[]) { (void)argc; (void)argv; struct bj_app app = { .setup = my_setup, .step = my_step, .teardown = my_teardown, }; return bj_app_run(&app); } ``` Or with the entry-point abstraction (Windows GUI / Emscripten / iOS): ```c #include <banjo/main.h> // include = opt in to the platform stub #include <banjo/app.h> int main(int argc, char* argv[]) { // banjo's internal rename makes this work struct bj_app app = { ... }; return bj_app_run(&app); } ``` Same source body either way. Only the include of main.h differs. Some programs will include it unconditionally on every platform (no-op where it isn't needed) for portability. ### `BJ_AUTOMAIN` and `BJ_AUTOMAIN_CALLBACKS` retired Both macros are gone. The callback mode that `BJ_AUTOMAIN_CALLBACKS` enabled becomes the only mode — every banjo program registers callbacks via `struct bj_app`. The opt-in for the platform stub is the `<banjo/main.h>` include itself. Window creation lives in `setup`; window-level callbacks (redraw, key, button, cursor, enter) live in `<banjo/window.h>` and are registered on each window after creation. ## Design notes - **Pass-by-struct, not setters.** Config is data, expressed as data. No hidden mutation of a singleton. Designated initializers + struct is idiomatic C99 (Vulkan, SDL3, etc.). Lifetime is sound: the struct lives on the stack of `main` and `bj_app_run` doesn't return until the loop ends. - **No app handle.** Implicit singleton. State in module-static inside `src/app.c`. Same as GLFW. - **No macros.** The include is the opt-in for main.h. The struct fields are the opt-in for individual callbacks. Less ceremony, fewer things to remember. - **Symmetry with audio.** Banjo's audio API already uses "open with callback." App adopts the same shape. Three subsystems (app, audio, window), one mental model: *banjo calls you, not the other way around.* ## Per-platform `bj_app_run` | Platform | Implementation | |---|---| | Linux / Win32 / macOS | `while (!quit) bj_app_step_internal();` then teardown | | Emscripten | `emscripten_set_main_loop(internal_step, 0, 1)` and return; JS runtime drives the loop | | iOS (future) | Hand control to `UIApplicationMain`; the run loop calls our step internally | | DLL hot-reload (future) | Host calls our step in its loop | ## Per-platform `<banjo/main.h>` content | Platform | What main.h emits | |---|---| | Linux / macOS console | Nothing (header is effectively a no-op include) | | Win32 console | Nothing | | Win32 GUI (`/SUBSYSTEM:WINDOWS`) | `WinMain` stub that calls `bj_main` (renamed user `main` via `#define`) | | Emscripten | JS-loop integration; user's `main` is reachable via `emscripten_set_main_loop_arg` | | iOS (future) | `UIApplicationMain` stub | Banjo can provide both `main` and `WinMain` stubs on Windows unconditionally — the linker picks whichever the chosen subsystem expects. No user-side discrimination needed. ## Out of scope - Window-level redraw callback and dirty-rect invalidation — separate card. - Public `bj_app_step` for users running their own outer loop — can come later if asked. - Multi-app or multi-thread variants. ## Migration - `BJ_AUTOMAIN` and `BJ_AUTOMAIN_CALLBACKS` deleted. Examples using either are rewritten to use the struct. - `examples/start.c` tutorial rewritten as the canonical "create struct, call run" shape. - The Doxygen `\file` block at the top of `<banjo/main.h>` (currently describing `bj_main` as the user-facing function) is rewritten to reflect that the user writes `main()` and banjo's machinery handles the platform handshake. ## Depends on / blocks Lands together with the redraw-callback card since `setup` is where users register the redraw callback on the window they just created.
lee closed this issue 2026-05-26 04:26:31 +02:00
OragonEfreet changed title from The animation example doesn't seem to compile to Application API 2026-05-26 09:47:14 +02:00
OragonEfreet added this to the 1.0 milestone 2026-05-26 09:47:44 +02:00
OragonEfreet added this to the (deleted) project 2026-05-26 09:47:47 +02:00
OragonEfreet removed this from the (deleted) project 2026-05-26 10:29:28 +02:00
OragonEfreet removed this from the 1.0 milestone 2026-05-26 10:30:29 +02:00
OragonEfreet added this to the (deleted) project 2026-05-26 10:31:38 +02:00
OragonEfreet removed this from the (deleted) project 2026-05-26 12:53:48 +02:00
OragonEfreet added this to the 1.0 milestone 2026-05-26 12:53:53 +02:00
OragonEfreet removed this from the 1.0 milestone 2026-05-26 12:53:57 +02:00
OragonEfreet added this to the (deleted) project 2026-05-26 12:58:33 +02:00
OragonEfreet added this to the 1.0 milestone 2026-05-26 13:03:07 +02:00
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
OragonEfreet/banjo#10
No description provided.