Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cocoa: integrate NSApplication into an existing c++ mainloop

I know, that I am not the first one to try to use Cocoa on OSX together with an existing c/c++ main loop, but I am not really liking the solutions I came across so far so I came up with a different idea I'd like to discuss. The most common way I found (in glut, glfw, SDL and also QT I think) is to use polling to replace NSApplications run method and process the events yourself with this:

nextEventMatchingMask:untilDate:inMode:dequeue: 

This has the big disadvantage that the cpu is never really idle since you have to poll the whole time to check if there are any new events, furthermore its not the only thing going on inside NSApplications run function, so it might break some details if you use this replacement.

So what I'd like to do is to keep the cocoa runLoop intact. Imagine you'd have your own timer methods implemented in c++ which would usually be managed and fired inside your main loop (this is just a small part as an example). My idea would be to move all my looping portions to a secondary thread (since NSApplication run needs to be called from the main thread as far as I know), and then post custom Events to my derived version of NSApplication that handles them appropriately inside its sendEvent: method. For instance if my timers measured in my c++ loop fire, I'd post a custom event to NSApplication that in turn runs loopFunc() function of my application (residing in the mainthread as well) which appropriately sends the events down my c++ event chain. So first of all, do you think this would be a good solution? If yes, how would you implement that in cocoa, I only found this method inside the NSEvent Reference to post custom NSApplicationDefined events:

otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:

and then use something like:

[NSApp postEvent:atStart:] 

to notify NSApplication.

I'd rather post an event without any information about the window (in otherEventWithType), can I simply ignore that part?

Then I'd imagine to overwrite NSApplications sendEvent function similar to this:

    - (void)sendEvent:(NSEvent *)event {     //this is my custom event that simply tells NSApplication      //that my app needs an update     if( [event type] == NSApplicationDefined)     {         myCppAppPtr->loopFunc(); //only iterates once     }         //transform cocoa events into my own input events     else if( [event type] == NSLeftMouseDown)     {              ...              myCppAppPtr->loopFunc(); //also run the loopFunc to propagate input events     }         ...      //dont break the cocoa event chain     [super sendEvent:event];  } 

sorry for the long post, but this has been bothering me quite a bit since I am really not happy with what I found about this subject so far. Is this how I'd post and check for a custom event inside NSApplication, and do you think this is a valid approach to integrate cocoa into an existing runloop without polling?

like image 541
moka Avatar asked Jul 18 '11 11:07

moka


1 Answers

okay, after all this took me way more time than I expected, and I'd like to outline the things I tried and tell you what experiences I had with them. This will hopefully save people trying to integrate Cocoa into an existing mainloop alot of time in the future. The first function I found when searching for the discussed matter was the function

nextEventMatchingMask:untilDate:inMode:dequeue: 

but as I said in the question, my main problem with this was that I would have to constantly poll for new events which would waste quite some CPU Time. So I tried the following two methods to simply let my mainloops update function get called from the NSApplications mainloop:

  1. Post a custom Event to NSApplication, overwrite NSApplications sendEvent: function and simply call my mainloops update function from there. Similar to this:

    NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined                                                      location: NSMakePoint(0,0)                                                modifierFlags: 0                                                    timestamp: 0.0                                                 windowNumber: 0                                                      context: nil                                                      subtype: 0                                                        data1: 0                                                        data2: 0];                 [NSApp postEvent: event atStart: YES];   //the send event function of my overwritten NSApplication    - (void)sendEvent:(NSEvent *)event {     //this is my custom event that simply tells NSApplication      //that my app needs an update     if( [event type] == NSApplicationDefined)     {         myCppAppPtr->loopFunc(); //only iterates once     } } 

    This was only a good idea in theory because if my app updated very quickly (for instance due to a timer firing quickly), the whole cocoa event queue became totoally unresponsive because I added so many custom events. So don't use this...

  2. Use performSelectorOnMainThread with a cocoaFunction that in turn calls my update function

    [theAppNotifier performSelectorOnMainThread:@selector(runMyMainLoop) withObject:nil waitUntilDone:NO ]; 

    This was alot better, the app and cocoa EventLoop was very responsive. If you are only trying to achieve something simple I'd recommend going down this route since it is the easiest of the ones proposed here. Anyways I had very little control about the order of things happening with this approach (this is crucial if you have a multithreaded app), i.e when my timers fired and would do a rather long job, often times they would reschedule before any new mouse/keyboard input could be added to my eventQueue and thus would make the whole input sluggish. Turnin on Vertical Sync on a window that was drawn by a repeating timer was enough to let this happen.

  3. After all I had to come back to nextEventMatchingMask:untilDate:inMode:dequeue: and after some trial and error I actually found a way to make it work without constant polling. The structure of my loop is similar to this:

    void MyApp::loopFunc() {     pollEvents();     processEventQueue();     updateWindows();     idle(); } 

    where pollEvents and idle are the important functions, basically I use something similar to this.

    void MyApp::pollEvents() {     NSEvent * event;      do     {         event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES];               //Convert the cocoa events to something useful here and add them to your own event queue          [NSApp sendEvent: event];     }     while(event != nil); } 

    To implement the blocking inside the idle() function I did this (not sure if this is good, but it seems to work great!) :

    void MyApp::idle() {     m_bIsIdle = true;     NSEvent * event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:NO];     m_bIsIdle = false; } 

    this causes cocoa to wait till there is an event, if that happenes idle simply exits and the loopfunc starts again. To wake up the idle function if i.e. one of my timers (i dont use cocoa timers) fires, i once again use a custom event:

    void MyApp::wakeUp() {     m_bIsIdle = false;      //this makes sure we wake up cocoas run loop     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];     NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined                                         location: NSMakePoint(0,0)                                    modifierFlags: 0                                        timestamp: 0.0                                     windowNumber: 0                                          context: nil                                          subtype: 0                                            data1: 0                                            data2: 0];     [NSApp postEvent: event atStart: YES];     [pool release]; } 

    Since I clear the whole cocoa event queue right afterwards i don't have the same problems as described in section 1. However, there are some drawbacks with this approach aswell because I think it does not do everything that [NSApplication run] is doing internally, i.e. application delegate things like this:

    - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication {       return YES; } 

    don't seem to work, anyways I can live with that since you can easily check yourself if the last window just closed.

I know this answer is pretty lengthy, but so was my journey to get there. I hope this helps someone and prevents people from doing the mistakes I did.

like image 186
moka Avatar answered Sep 25 '22 07:09

moka