SiegeLord / DAllegro5

D binding to the Allegro5 game development library
Other
42 stars 15 forks source link

`argv` pointer casting in `al_run_allegro`/`main_runner` causes crash when running from a macOS .app bundle #56

Open Rhys-T opened 6 days ago

Rhys-T commented 6 days ago

I was trying to get my macOS build of Lix packaged up as a proper double-clickable .app bundle, but for some reason, even though the executable ran by itself, as soon as I tried it as part of the bundle it would get a SIGSEGV in _D8allegro56system14al_run_allegroFMDFZiZ11main_runnerUiPPaZi (which demangles to int extern(C)main_runner(int, char**) allegro5.system.al_run_allegro(scope int() delegate)). I did some digging, and I think I see what's going on.

If I'm reading this library's system.d correctly, it's giving the underlying al_run_main call a fake argv, with the address of the game's callback function stored where the characters of argv[0] would normally be. main_runner then casts the pointer back the other way to find and invoke the callback function. The problem is that on macOS, if Allegro detects that the program is running from a .app bundle, it ignores the argc/argv it was passed and instead creates its own fake one containing the path to the app bundle[^notexe] and the path to the document dropped on the app, if any:

https://github.com/liballeg/allegro5/blob/5114ee60410c9aac6a0f23402570eb41a4cc9095/src/macosx/osx_app_delegate.m#L158-L192

So instead of getting the callback pointer, main_runner ends up using the first 8 bytes of the bundle path as an address, and crashes.

[^notexe]: Just the path to the bundle, not all the way to the executable, for some reason.

SiegeLord commented 6 days ago

Nice job finding the mechanism (I knew of the crash, but not why it happened). That's some weird logic in Allegro.

Here's the alternative (based on global variables) that I used in a pinch last time I hit this:

int al_run_allegro(scope int delegate() user_main)
{
    __gshared int delegate() user_main2;
    user_main2 = user_main;
    extern(C) static int main_runner(int argc, char** argv)
    {   
        version(OSX)
        {   
            thread_attachThis();
            rt_moduleTlsCtor();
        }   

        auto main_ret = user_main2();

        version(OSX)
        {   
            thread_detachThis();
            rt_moduleTlsDtor();
        }   

        return main_ret;
    }   

    return al_run_main(0, null, &main_runner);
}
Rhys-T commented 6 days ago

I tried patching that version into the copy of the library I was building Lix with, and so far it seems to work perfectly. (Other than the game suddenly not being Retina-capable, but it's not the library's fault that I haven't fully set up the Info.plist file.) Thanks!

I think the reason Allegro ignores the original argv is that on macOS, dropped or double-clicked documents don't get passed via argv - and sometimes the app gets an extra argument that looks like -psn0123456, which seems to just be used to set its 'process serial number' for some mostly-deprecated Mac APIs - so changing it to just the app and the document helps make things look as consistent across different platforms as possible. (Still seems weird that argv[0] ends up as the entire .app bundle and not the executable, though…)

Is using a global like this going to work as the official fix? I'm not that familiar with D, or Allegro for that matter, but I figured the fake argv trick was there for a reason. Are there any thread-safety concerns with using the global? Is there any way that a (non-broken) program could have multiple al_run_allegros going at once, fighting over the variable?