Skip to content

SDL2: SDL_CreateWindow(..., SDL_WINDOW_OPENGL) fails in a side module when using pyodide #24106

Closed
@pthom

Description

@pthom

Hi,

When using pyodide, a python module built with emscripten (as a side module) will fail when
calling SDL_CreateWindow(..., flags=SDL_WINDOW_OPENGL): this will ultimately lead to an "indirect call to null".

This issue was initially diagnosed (with a potential fix) at pyodide/pyodide#5584

Root Cause: JS-defined EGL functions can't be called via function pointers

  • The "indirect call to null" crash occurs in SDL when it attempts to call eglGetDisplay and other EGL functions through a function pointer.
  • These functions are implemented in JavaScript (see egl.js). Asking for the C address of these functions from a side module result in getting a NULL address.
  • However those functions can be called, even from a side module. A limitation only occurs when trying to store these functions addresses in a function pointers (those pointer addresses will be 0)

Here's an example from SDL’s source:
https://github.com/libsdl-org/SDL/blob/2359383fc187386204c3bb22de89655a494cd128/src/video/emscripten/SDL_emscriptenopengles.c#L32-L67

This code can be summarized as:

#define LOAD_FUNC(NAME) _this->egl_data->NAME = NAME;  // a macro that stores a function pointer
...
LOAD_FUNC(eglGetDisplay); // _this->egl_data->eglGetDisplay is a pointer to eglGetDisplay() (will be NULL)
...
_this->egl_data->egl_display = _this->egl_data->eglGetDisplay(EGL_DEFAULT_DISPLAY);  // Call the function pointer => fail

Potential Solution

I have a working patch for SDL2 (Note: in SDL3, the handling of EGL/emscripten was rewritten).

This patch will trigger when EMSCRIPTEN_DYNAMIC_LINKING is defined (which is the case for Pyodide). It simply adds C trampolines (my_eglXXX functions) that call the JS-defined EGL functions directly. These trampoline functions live in C, have valid addresses, and can safely be used in function pointers.

Here’s what the patch looks like (inside SDL/src/video/emscripten/SDL_emscriptenopengles.cSDL_emscriptenopengles.c):

#ifndef EMSCRIPTEN_DYNAMIC_LINKING
#define LOAD_FUNC(NAME) _this->egl_data->NAME = NAME;
#else
/* In dynamic linking mode (e.g. when building a Pyodide side module), there’s a caveat:
- Most egl* functions are implemented in JavaScript and do not have stable function pointers.
  That is, their address in C is 0, and indirect calls will crash.
- We define C trampolines (my_eglXXX) that wrap these JS functions, and use them instead.
- This gives us valid function pointers and allows the SDL dynamic backend to work correctly.
- Exception: eglGetProcAddress is implemented natively and does have a valid address.
*/
#define LOAD_FUNC(NAME) _this->egl_data->NAME = my_##NAME;

static EGLDisplay my_eglGetDisplay(NativeDisplayType d) { return eglGetDisplay(d); }
static EGLBoolean my_eglInitialize(EGLDisplay d, EGLint *m, EGLint *n) { return eglInitialize(d, m, n); }
static EGLBoolean my_eglTerminate(EGLDisplay d) { return eglTerminate(d); }
// ... and so on
#endif

This patch makes no changes to SDL behavior in normal (non-side-module) builds, and (based on my tests) resolves the issue in Pyodide side modules.

I would be interested in getting feedback: does this patch seem legit and should it be upstreamed to SDL? More generally, how could this issue be fixed, and how could I help if needed.

Reproduction code

I can reproduce this issue when using pyodide. This comment provides a link to a minimal code which reproduces the issue withing pyodide.

The C++ code reproducer code is:

#include <SDL.h>
#include <stdio.h>

#include <SDL_video.h>
#include <emscripten/emscripten.h>

void dummy_sdl_call()    // a function that will be called by python (from a side module)
{
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
        fprintf(stderr, "SDL_Init Error: %s\n", SDL_GetError());
        return;
    }

    SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");

    printf("About to call SDL_CreateWindow\n");
    int window_flags = SDL_WINDOW_OPENGL;
    SDL_Window* window = SDL_CreateWindow(
        "Dummy Window",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        640, 480,
        window_flags
    );
    printf("SDL_CreateWindow called\n");

    if (!window) {
        fprintf(stderr, "SDL_CreateWindow Error: %s\n", SDL_GetError());
    } else {
        printf("SDL_CreateWindow succeeded\n");
        SDL_DestroyWindow(window);
    }

    SDL_Quit();
}

#ifndef BUILD_EMS_APP
#include <nanobind/nanobind.h>
NB_MODULE(daft_lib, m) {
    m.def("dummy_sdl_call", &dummy_sdl_call);
}
#else

int main(int argc, char* argv[])
{
    dummy_sdl_call();
    return 0;
}

#endif

This code can also be compiled as a standalone emscripten html app with the command line below

 emcc daft_lib.cpp -o daft_lib.html \
  -sUSE_SDL=2 \
  -sMAX_WEBGL_VERSION=2  \
  -D BUILD_EMS_APP \
  --shell-file ${EMSDK}/upstream/emscripten/src/shell_minimal.html

However, this will not trigger the issue as the app will not be compiled as a side module in this case. I'd be interested if someone finds a way to adapt it so that the code is compiled as a side module.

Version of emscripten/emsdk:

This can be reproduced using the emsdk provided by the latest version of pyodide.

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.6 (1ddaae4d2d6dfbb678ecc193bc988820d1fc4633)
clang version 21.0.0git (https:/github.com/llvm/llvm-project 4775e6d9099467df9363e1a3cd5950cc3d2fde05)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /home/pascal/dvp/_Bundle/imgui_bundle_online/imgui_bundle_pyodide_tooling/repositories/pyodide/emsdk/emsdk/upstream/bin

Thanks to all creators and maintainers for the huge work on emscripten!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions