Description
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!