drshapeless


SDL main callback with Odin

Tags: sdl | programming | c | odin

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.

Bare minimum

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)
}

Challenge

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>

Odin as library

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.

Odin as object

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

Pure Odin

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 to use

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.