Skip to content

Cant embed winit as a dylib into cocoa due to NSApp delegate limitations in winit > 0.29 #4322

@salluzziluca

Description

@salluzziluca

Description

Hi! I've been embedding winit in Linux and Windows without much trouble.
But in macOS I'm having some issues.

I was previously able to make it work using the old run_return, which is no longer available in the latest versions.
When trying to replace it with pump_events, I started getting errors that I didn’t face before — for example:

tried to get a delegate that was not the one Winit has registered

Context

I'm trying to embed winit inside a Cocoa application.
To do this, I load a dynamic library that initializes winit and manually call pump_winit_once on a separate thread.


About NSApp ownership

I’m aware of the requirement that winit must be the one to initialize the NSApp, and not the host Cocoa app.
That’s why I deliberately initialize winit before creating the Cocoa app and its delegate.
This pattern allowed me to avoid the typical panic:

winit must be the one to initialize NSApp

Here’s how I set it up:

fn main() {
    println!("🎯 Starting Cocoa app with winit integration...");

    // Initialize winit BEFORE creating the Cocoa App
    println!("🔧 Initializing winit BEFORE Cocoa...");
    let lib = unsafe {
        Library::new("../target/debug/libwinit_embed.dylib")
            .expect("Failed to load winit_embed library")
    };

    let init_winit: Symbol<unsafe extern "C" fn() -> bool> = unsafe {
        lib.get(b"init_winit").expect("Failed to get init_winit function")
    };

    let init_result = unsafe { init_winit() };
    if init_result {
        println!("✅ Winit initialized successfully BEFORE Cocoa");
    } else {
        println!("❌ Failed to initialize winit");
        return;
    }

    // NOW create the Cocoa app
    let window = Window::new(Default::default());
    let app_delegate = CocoaAppDelegate { window };
    App::new("com.example.cacaotest", app_delegate).run();
}

🔁 Pumping the event loop manually

let lib_clone = lib;
std::thread::spawn(move || {
    let pump_winit_once: Symbol<PumpWinitOnceFn> = unsafe {
        lib_clone.get(b"pump_winit_once").expect("Failed to get pump_winit_once function")
    };

    loop {
        let exit = unsafe { pump_winit_once() };
        if exit != 0 {
            break;
        }
        sleep(Duration::from_millis(16));
    }
});

Internal state management with thread_local!

thread_local! {
    static EVENT_LOOP: RefCell<Option<EventLoop<()>>> = RefCell::new(None);
    static APP: RefCell<PumpDemo> = RefCell::new(PumpDemo::default());
}

pub unsafe extern "C" fn pump_winit_once() -> i32 {
    let mut result = 0;

    EVENT_LOOP.with(|event_loop_cell| {
        APP.with(|app_cell| {
            let mut event_loop_opt = event_loop_cell.borrow_mut();
            let mut app = app_cell.borrow_mut();

            let Some(ref mut event_loop) = *event_loop_opt else {
                result = 1;
                return;
            };

            let status = event_loop.pump_app_events(Some(Duration::ZERO), &mut *app);

            if let PumpStatus::Exit(code) = status {
                *event_loop_opt = None;
                result = code as i32;
            }
        });
    });

    result
}

Related issues

This is related to #4015 in which the solution is to use the master version of winit at that time (I think its fc6cf89)

Final thoughts

This approach was working fine with run_return, but with pump_events I started facing issues related to the internal delegate tracking logic. I assume some state is being checked that may not play well with dynamic library usage or Cocoa-managed event loops.

Any help or insight would be appreciated! Thanks for maintaining such an awesome crate. 🙌

macOS version

ProductName:            macOS
ProductVersion:         15.5
BuildVersion:           24F74

Winit version

0.30.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    B - bugDang, that shouldn't have happenedDS - macos

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions