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.