Create: 2025-07-25, Update: 2025-08-01
Today, I accidentally discover a new thing in SDL, the main callback, which is useful for abstracting the main function in different platforms, which in my use case, wasm.
But since I don't like to use C or C++ to write game anymore, I recently have been trying out a lot of different programming languages, e.g. Zig, C3, Odin. Rust is out of the question because of the RAII and lifetime stupidity.
I was settled with Odin, as a currently available Jai alternative. When Jai is out publicly, all of my Odin code will be ported to Jai.
I would like to use Odin to try this new SDL feature.
We have to define four functions, so this would be the bare minimum.
app.odin
package app
import "base:runtime"
import "core:c"
import "core:log"
import sdl "vendor:sdl3"
@(export)
SDL_AppInit :: proc "c" (
    appstate: ^rawptr,
    argc: c.int,
    argv: [^]cstring,
) -> sdl.AppResult {
    context = runtime.default_context()
    context.logger = log.create_console_logger()
    ok := sdl.Init({.VIDEO})
    if !ok {
        log.errorf("%s", sdl.GetError())
        return .FAILURE
    }
    log.infof("successfully init SDL")
    return .SUCCESS
}
@(export)
SDL_AppEvent :: proc "c" (
    appstate: rawptr,
    event: ^sdl.Event,
) -> sdl.AppResult {
    return .SUCCESS
}
@(export)
SDL_AppIterate :: proc "c" (appstate: rawptr) -> sdl.AppResult {
    return .SUCCESS
}
@(export)
SDL_AppQuit :: proc "c" (appstate: rawptr, result: sdl.AppResult) {
    sdl.Quit()
    context = runtime.default_context()
    log.destroy_console_logger(context.logger)
}
In order to use the main callback provided by SDL, we need a #define. In Odin, there is no define. Fuck.
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL_main.h>
I have tried defining it while compiling, does not work.
odin build . -define:SDL_MAIN_USE_CALLBACKS=true
Some C code need to be written. But there is also a pure Odin hack, skip to the end.
sdl_config.c
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL_main.h>
I would like to use Odin as static library, just to realize that it has not been implemented.
odin build . -build-mode:static
Internal Compiler Error: TODO(bill): -build-mode:static on non-windows targets
illegal instruction
Really?
Dynamic then.
odin build . -build-mode:dynamic -out:libapp.so
Compile the c code with this library.
cc sdl_config.c -o app -L . -lapp -lSDL3
Run
export LD_LIBRARY_PATH=.:LD_LIBRARY_PATH
./app
[INFO ] --- [2025-07-25 14:16:11] [app.odin:23:SDL_AppInit()] successfully init SDL
Done.
Another method is to compile Odin as object and directly compile it with the C code. A few things to be noticed is that since we don't have a main in the Odin code, we have to specify it with --no-entry-point. Also, we have to use position independent code, so set it into pic.
odin build . -build-mode:obj -reloc-mode:pic --no-entry-point
After that, we will have a lot of free floating object files, which is ugly. Compile it with a C compiler. In this way, the whole app is self contained. No more LDmagic. And in theory much more portable.
cc sdl_config.c *.o -o app -lSDL3
Initially when I published this blog, I do not know about this method. This is more like a hack then a real solution. But it works.
I found this in a Reddit post. A solution was posted by the OP in the comment section.
It makes use of the SDL_EnterAppMainCallbacks function, which in the
original SDL header has this comment.
/**
 * An entry point for SDL's use in SDL_MAIN_USE_CALLBACKS.
 *
 * Generally, you should not call this function directly. This only exists to
 * hand off work into SDL as soon as possible, where it has a lot more control
 * and functionality available, and make the inline code in SDL_main.h as
 * small as possible.
 *
 * Not all platforms use this, it's actual use is hidden in a magic
 * header-only library, and you should not call this directly unless you
 * _really_ know what you're doing.
 *
 * \param argc standard Unix main argc.
 * \param argv standard Unix main argv.
 * \param appinit the application's SDL_AppInit function.
 * \param appiter the application's SDL_AppIterate function.
 * \param appevent the application's SDL_AppEvent function.
 * \param appquit the application's SDL_AppQuit function.
 * \returns standard Unix main return value.
 *
 * \threadsafety It is not safe to call this anywhere except as the only
 *               function call in SDL_main.
 *
 * \since This function is available since SDL 3.2.0.
 */
extern SDL_DECLSPEC int SDLCALL SDL_EnterAppMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit);
This is a function that we are not suppose to call.
So a pure Odin solution is to directly call this function in a ordinary Odin main. We can still make use of our functions defined earlier. Just add this main in the end.
main :: proc() {
    c_args := make([]cstring, len(os.args))
    defer delete(c_args)
    for arg, i in os.args {
        cstr, _ := strings.clone_to_cstring(arg)
        c_args[i] = cstr
    }
    defer for arg in c_args {
        delete(arg)
    }
    sdl.EnterAppMainCallbacks(
        cast(c.int)len(os.args),
        raw_data(c_args),
        SDL_AppInit,
        SDL_AppIterate,
        SDL_AppEvent,
        SDL_AppQuit,
    )
}
This is a much sophisticated solution than the Reddit comment one as it handles argc and argv gracefully.
We can easily run our program by.
odin run .
All the SDL callback functions do not need to be exported.
When using SDL main callbacks, the code can be easily run on various different platforms. However, I mostly concern about wasm only. If you have any experience in using emscripten and SDL, you may notice a lot of weird work around to make our code run in the browser. Theoretically, using SDL main callbacks will solve this issue. I just do not know how it does in Odin.