[Solved] How to use the SDL3 callback app structure in Python?

Hi. How to use the SDL3 callback app structure in Python? Is it possible?

I can write in C:

#define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */

But how to activate it in Python?

pip install PySDL3

main.py

import sdl3

print(dir(sdl3))

I’ve found SDL_EnterAppMainCallbacks in the output above but I don’t know how to use it. I want to rewrite the official template to Python: SDL/examples/template.c at main · libsdl-org/SDL · GitHub

1 Like

The emccue’s answer on Discord:

Well, you have two options

  1. Reimplement what the callbacks do in python - python already owns the main function so there isn’t much you can do. time to write while True:
  2. Start your program in C and initialize python through its C api

Topic on GitHub

2 Likes

Using Function prototypes, it’s possible to use SDL_EnterAppMainCallbacks in pure Python code.

Example code:

#!/usr/bin/env python3
import ctypes
import dataclasses
import sys
import time

import sdl3


@dataclasses.dataclass
class AppState:
    window: ctypes.POINTER(sdl3.SDL_Window)
    renderer: ctypes.POINTER(sdl3.SDL_Renderer)


def SDL_AppEvent(c_appstate, c_event: ctypes.POINTER(sdl3.SDL_Event)) -> sdl3.SDL_AppResult:  # event: ctypes.POINTER(sdl3.SDL_Event)
    event: sdl3.SDL_Event = c_event.contents
    # appstate: AppState = ctypes.cast(c_appstate, ctypes.POINTER(ctypes.py_object)).contents.value
    if event.type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    return sdl3.SDL_APP_CONTINUE


def SDL_AppIterate(c_appstate) -> sdl3.SDL_AppResult:
    appstate: AppState = ctypes.cast(c_appstate, ctypes.POINTER(ctypes.py_object)).contents.value
    if not sdl3.SDL_SetRenderDrawColor(appstate.renderer, int(time.time() * 255 / 2) % 256, 0, 0, sdl3.SDL_ALPHA_OPAQUE):
        print("SDL_SetRenderDrawColorFloat failed")
        return sdl3.SDL_APP_FAILURE
    if not sdl3.SDL_RenderClear(appstate.renderer):
        print("SDL_RenderClear failed")
        return sdl3.SDL_APP_FAILURE
    if not sdl3.SDL_RenderPresent(appstate.renderer):
        print("SDL_RenderPresent failed")
        return sdl3.SDL_APP_FAILURE
    return sdl3.SDL_APP_CONTINUE


def SDL_AppInit(c_appstate, argc, argv) -> sdl3.SDL_AppResult:  # argc: int, argv: ctypes.POINTER[ctypes.c_char_p]
    p_window = ctypes.POINTER(sdl3.SDL_Window)()
    p_renderer = ctypes.POINTER(sdl3.SDL_Renderer)()
    if not sdl3.SDL_CreateWindowAndRenderer(b"Hello world!", 640, 480, 0, ctypes.pointer(p_window), ctypes.pointer(p_renderer)):
        print("sdl3.SDL_CreateWindowAndRenderer failed")
        return sdl3.SDL_APP_FAILURE
    state = AppState(window=p_window.contents, renderer=p_renderer.contents)
    c_appstate[0] = ctypes.cast(ctypes.pointer(ctypes.py_object(state)), ctypes.c_void_p)
    return sdl3.SDL_APP_CONTINUE


def SDL_AppQuit(c_appstate, result) -> None:  # ctypes.POINTER[sdl3.SDL_AppResult]
    appstate: AppState = ctypes.cast(c_appstate, ctypes.POINTER(ctypes.py_object)).contents.value
    sdl3.SDL_DestroyRenderer(appstate.renderer)
    sdl3.SDL_DestroyWindow(appstate.window)


def main():
    c_SDL_AppInit_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.POINTER(ctypes.c_void_p), ctypes.c_int, ctypes.POINTER(ctypes.c_char_p))
    c_SDL_AppIterate_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.c_void_p)
    c_SDL_AppEvent_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.c_void_p, ctypes.POINTER(sdl3.SDL_Event))
    c_SDL_AppQuit_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, sdl3.SDL_AppResult)

    c_arg_array_t = ctypes.c_char_p * (len(sys.argv) + 0)
    c_args = c_arg_array_t(*[ctypes.c_char_p(a.encode()) for a in sys.argv])

    result = sdl3.SDL_EnterAppMainCallbacks(ctypes.c_int(len(sys.argv)), c_args,
                                            c_SDL_AppInit_cfunctype(SDL_AppInit),
                                            c_SDL_AppIterate_cfunctype(SDL_AppIterate),
                                            c_SDL_AppEvent_cfunctype(SDL_AppEvent),
                                            c_SDL_AppQuit_cfunctype(SDL_AppQuit))
    return result


if __name__ == "__main__":
    raise SystemExit(main())
2 Likes

There was a bug related to the appstate in my previous example.
The AppState/ctypes.py_object object was created and released in SDL_AppInit.
When running in debug mode, the example failed when accessing the appstate argument in the SDL_AppEvent and SDL_AppIterate callback functions.

#!/usr/bin/env python3
import ctypes
import dataclasses
import sys
import time
import typing

import sdl3


@dataclasses.dataclass
class AppState:
    window: ctypes.POINTER(sdl3.SDL_Window)
    renderer: ctypes.POINTER(sdl3.SDL_Renderer)


state: typing.Optional[AppState] = None


def SDL_AppEvent(c_appstate, c_event: ctypes.POINTER(sdl3.SDL_Event)) -> sdl3.SDL_AppResult:
    event: sdl3.SDL_Event = c_event.contents
    if event.type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    return sdl3.SDL_APP_CONTINUE


def SDL_AppIterate(c_appstate) -> sdl3.SDL_AppResult:
    if not sdl3.SDL_SetRenderDrawColor(state.renderer, int(time.time() * 255 / 2) % 256, 0, 0, sdl3.SDL_ALPHA_OPAQUE):
        print("SDL_SetRenderDrawColorFloat failed")
        return sdl3.SDL_APP_FAILURE
    if not sdl3.SDL_RenderClear(state.renderer):
        print("SDL_RenderClear failed")
        return sdl3.SDL_APP_FAILURE
    if not sdl3.SDL_RenderPresent(state.renderer):
        print("SDL_RenderPresent failed")
        return sdl3.SDL_APP_FAILURE
    return sdl3.SDL_APP_CONTINUE


def SDL_AppInit(c_appstate, argc, argv) -> sdl3.SDL_AppResult:
    p_window = ctypes.POINTER(sdl3.SDL_Window)()
    p_renderer = ctypes.POINTER(sdl3.SDL_Renderer)()
    if not sdl3.SDL_CreateWindowAndRenderer(b"Hello world!", 640, 480, 0, ctypes.pointer(p_window), ctypes.pointer(p_renderer)):
        print("sdl3.SDL_CreateWindowAndRenderer failed")
        return sdl3.SDL_APP_FAILURE
    global state
    state = AppState(window=p_window.contents, renderer=p_renderer.contents)
    c_appstate[0] = ctypes.cast(ctypes.pointer(ctypes.py_object(state)), ctypes.c_void_p)
    return sdl3.SDL_APP_CONTINUE


def SDL_AppQuit(c_appstate, result) -> None:  # ctypes.POINTER[sdl3.SDL_AppResult]
    appstate: AppState = ctypes.cast(c_appstate, ctypes.POINTER(ctypes.py_object)).contents.value
    sdl3.SDL_DestroyRenderer(appstate.renderer)
    sdl3.SDL_DestroyWindow(appstate.window)


def main():
    c_SDL_AppInit_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.POINTER(ctypes.c_void_p), ctypes.c_int, ctypes.POINTER(ctypes.c_char_p))
    c_SDL_AppIterate_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.c_void_p)
    c_SDL_AppEvent_cfunctype = ctypes.CFUNCTYPE(sdl3.SDL_AppResult, ctypes.c_void_p, ctypes.POINTER(sdl3.SDL_Event))
    c_SDL_AppQuit_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, sdl3.SDL_AppResult)

    c_arg_array_t = ctypes.c_char_p * (len(sys.argv) + 0)
    c_args = c_arg_array_t(*[ctypes.c_char_p(a.encode()) for a in sys.argv])

    result = sdl3.SDL_EnterAppMainCallbacks(ctypes.c_int(len(sys.argv)), c_args,
                                            c_SDL_AppInit_cfunctype(SDL_AppInit),
                                            c_SDL_AppIterate_cfunctype(SDL_AppIterate),
                                            c_SDL_AppEvent_cfunctype(SDL_AppEvent),
                                            c_SDL_AppQuit_cfunctype(SDL_AppQuit))
    return result


if __name__ == "__main__":
    raise SystemExit(main())
2 Likes

Thanks a lot!

Aermoss answered here:

Thank you for pointing out! I implemented ‘SDL_main_impl.h’ in version 0.8.1b0, you can now use callbacks like this:

import ctypes, os

os.environ["SDL_MAIN_USE_CALLBACKS"] = "1"

import sdl3

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate: sdl3.LP_c_void_p, argc: ctypes.c_int, argv: sdl3.LP_c_char_p) -> sdl3.SDL_AppResult: ...

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate: ctypes.c_void_p) -> sdl3.SDL_AppResult: ...

@sdl3.SDL_AppEvent_func
def SDL_AppEvent(appstate: ctypes.c_void_p, event: sdl3.LP_SDL_Event) -> sdl3.SDL_AppResult: ...

@sdl3.SDL_AppQuit_func
def SDL_AppQuit(appstate: ctypes.c_void_p, result: sdl3.SDL_AppResult) -> None: ...
1 Like

Aermoss published a slightly modified version of madebr’s example here:

import os, ctypes, time

os.environ["SDL_MAIN_USE_CALLBACKS"] = "1"

import sdl3

class AppState(ctypes.Structure):
    _fields_ = [
        ("window", ctypes.POINTER(sdl3.SDL_Window)),
        ("renderer", ctypes.POINTER(sdl3.SDL_Renderer))
    ]

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate: sdl3.LP_c_void_p, argc: ctypes.c_int, argv: sdl3.LP_c_char_p) -> sdl3.SDL_AppResult:
    state = AppState()

    if not sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO):
        sdl3.SDL_Log("Couldn't initialize SDL: %s", sdl3.SDL_GetError())
        return sdl3.SDL_APP_FAILURE

    if not sdl3.SDL_CreateWindowAndRenderer("Hello, World!".encode(), 640, 480, 0, ctypes.byref(state.window), ctypes.byref(state.renderer)):
        sdl3.SDL_Log("Couldn't create window/renderer: %s", sdl3.SDL_GetError())
        return sdl3.SDL_APP_FAILURE

    appstate[0] = ctypes.cast(ctypes.pointer(state), ctypes.c_void_p)
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppEvent_func
def SDL_AppEvent(appstate: ctypes.c_void_p, event: sdl3.LP_SDL_Event) -> sdl3.SDL_AppResult:
    if sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate: ctypes.c_void_p) -> sdl3.SDL_AppResult:
    state: AppState = sdl3.SDL_DEREFERENCE(ctypes.cast(appstate, ctypes.POINTER(AppState)))
    sdl3.SDL_SetRenderDrawColor(state.renderer, int(time.time() * 255 / 2) % 256, 0, 0, sdl3.SDL_ALPHA_OPAQUE)
    sdl3.SDL_RenderClear(state.renderer)
    sdl3.SDL_RenderPresent(state.renderer)
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppQuit_func
def SDL_AppQuit(appstate: ctypes.c_void_p, result: sdl3.SDL_AppResult) -> None:
    ... # SDL will clean up the window/renderer for us.
1 Like