home *** CD-ROM | disk | FTP | other *** search
- static char rcsid[] = "$Id: ThreadedApp.m,v 1.2 1997/04/03 20:09:26 croehrig Exp $";
- /*
- * ThreadedApp: adds support for a multi-threaded application.
- * (c) 1997 Chris Roehrig <croehrig@House.ORG>
- */
- #import "ThreadedApp.h"
- #import <objc/objc-runtime.h>
-
-
- @implementation ThreadedApp : Application
-
- // export global to identify the main thread...
- cthread_t mainThread;
-
-
- /*****************************************************************
- * Basic Thread support
- */
-
- // simple "function control block" to invoke a method
- struct _fcb {
- IMP function;
- id target;
- SEL selector;
- any_t arg;
- mutex_t lock;
- condition_t cond;
- volatile BOOL done;
- };
-
-
- static void method_dispatcher( struct _fcb *fcb )
- {
- struct _fcb myfcb;
-
- // lock the fcb...
- mutex_lock( fcb->lock );
-
- // copy the args from parent's stack-based struct...
- // (ignore the lock and cond; they are only copied by reference)
- myfcb = *fcb;
-
- fcb->done =YES;
- // let parent know it's ok to exit and destroy my args
- condition_signal( fcb->cond );
- mutex_unlock( fcb->lock );
-
- // call the method...
- myfcb.function( myfcb.target, myfcb.selector, myfcb.arg );
- }
-
-
- - (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget
- withObject:(id)anArgument
- {
- // allocate all data off stack...
- struct _fcb fcb;
- struct mutex m;
- struct condition c;
-
- fcb.function = [aTarget methodFor:aSelector];
- if( fcb.function ){
- fcb.target = aTarget;
- fcb.selector = aSelector;
- fcb.arg = (any_t) anArgument;
- mutex_init( &m );
- fcb.lock = &m;
- condition_init( &c );
- fcb.cond = &c;
- fcb.done = NO;
- mutex_lock( &m );
- cthread_detach(
- cthread_fork((any_t(*)())method_dispatcher,(any_t)&fcb) );
-
- // wait for thread to copy its args before exiting and
- // destroying them!
- while( !fcb.done )
- condition_wait( &c, &m );
-
- mutex_unlock( &m );
- mutex_clear( &m );
- condition_clear( &c );
- }
- }
-
-
- - (void)exitCurrentThread
- {
- cthread_exit(0);
- }
-
-
- - (void)sleepCurrentThread:(int)msec
- {
- thread_switch(THREAD_NULL, SWITCH_OPTION_WAIT, msec);
- }
-
-
-
-
- /*****************************************************************
- * Callback support
- *
- * Callbacks are done by sending the main AppKit thread a Mach message
- * requesting it to invoke a method.
- *
- * The callback Mach message is faked as a simple message
- * (i.e. no out-of-line data), since the message never crosses task/memory
- * spaces, and so memory references are still valid.
- *
- * The Mach message handler is invoked by the DPSGetEvent routine
- * called by the Application's event loop. It monitors the port and
- * calls the handler when messages arrive on it. If DPSGetEvent
- * (or DPSPeekEvent) is NOT being called, the messages will not be
- * handled. The AppKit thread MUST remain responsive to events in order
- * for messages to be handled.
- *
- * For fast callbacks, the Mach message handler invokes the callback method
- * directly. The DPSGetEvent routine is not reentrant and so the callback
- * method cannot consume events. In particular, this means it cannot
- * use Locks since they use event loops (see Locks, below). The callback
- * method can be blocking (wait) or non-blocking.
- *
- * For safe and slow callbacks, the Mach message handler does not invoke the
- * callback method directly. Instead, it posts an (NX_APPDEFINED)
- * EV_CALLBACK event to the Application's event queue which gets handled
- * by the Application's applicationDefined: method. This is to allow the
- * called-back method to run modal event loops itself. (In particular, this
- * is necessary if the called-back method uses locks to communicate with
- * other threads (see Locks, below).
- * The DPSGetEvent routine is not reentrant, so it is not possible for
- * the message handler to eventually call DPSGetEvent again. By posting
- * an event to the event queue, the message handler returns, DPSGetEvent
- * returns, and the EV_CALLBACK event is processed by the Application's
- * event loop, as if it is any other event. This allows the called-back
- * method to have full access to the AppKit's features and to run its
- * own event loop if it desires.
- *
- * Locks
- * It is possible for the main (AppKit) thread to use Locks and
- * ConditionLocks to synchronize with its other threads. However, it
- * is forbidden for these Locks to block since that would prevent messages
- * from being received and may cause other threads to block waiting to
- * send messages to the main thread, potentially resulting in deadlock.
- * The AppKit thread MUST remain responsive to events!
- *
- * The CJRLock and CJRConditionLock objects handle the case where
- * the main AppKit thread acquires a lock. They do this by
- * testing to see if they can acquire the lock (mutex_try_lock). If
- * they can, they've succeeded. If not, they start a modal loop getting
- * NX_APPDEFINED events, and looking for EV_UNLOCK events. When one is
- * received, this indicates that another thread has unlocked a mutex.
- * and we try to acquire the lock again. This loops until success.
- * This is a sort of non-blocking "block" since other events aren't
- * processed during the block (they remain on the queue, however).
- * The CJRLock's unlock methods send the required EV_UNLOCK messages.
- *
- * In addition, EV_CALLBACK events are continued to be processed during
- * this "blocking" modal loop. This has some bizarre consequences:
- * Another callback method can be dispatched and preempt a callback method
- * that is currently blocked waiting for lock to come free. This
- * preemptive callback method can now also "block" waiting for a lock.
- * The ThreadedApp's lock methods are re-entrant, so this doesn't pose
- * a problem. As in all threaded programming, you should not rely
- * on the order that callbacks might occur.
- *
- */
-
- // data for wait callbacks
- struct waitdata {
- struct mutex lock; // statically allocated
- struct condition cond;
- volatile id retval;
- volatile BOOL done;
- };
-
- // thresholds for messages
- #define FAST_THRESHOLD (NX_MODALRESPTHRESHOLD+1)
- #define BASE_THRESHOLD NX_BASETHRESHOLD
-
- // types and options for our Mach messages and events...
- // message/event type (0-255)...
- #define EV_UNLOCK 1 // threadDidUnlock event
- #define EV_CALLBACK 2 // callback event
- // message/event options (bit set)...
- #define EV_CALLBACK_ARG 0x0100 // callback method takes an arg
- #define EV_CALLBACK_WAIT 0x0200 // block and wait for method to return
- #define EV_CALLBACK_POST 0x0400 // post the callback as an event
-
- // callback message...
- #define CALLBACK_DATA 4
- typedef struct {
- msg_header_t h;
- msg_type_t t;
- id target;
- SEL sel;
- id arg;
- struct waitdata *data;
- } callbackMsg;
-
- // unlock message...
- #define UNLOCK_DATA 1
- typedef struct {
- msg_header_t h;
- msg_type_t t;
- id lock;
- } unlockMsg;
-
-
- // global variables...
- static BOOL blocked; // YES if the app is "blocked" in an event loop
- // waiting for an appropriate EV_UNLOCK event
- static int block_count; // number of nested blocks (diagnostic)
- static BOOL fastPending; // YES if a fast callback is in progress
-
- // callback ports
- static port_t fastPort, basePort;
-
-
- static void post_unlock_event( unlockMsg *msg )
- {
- NXEvent ev;
-
-
- ev.type = NX_APPDEFINED;
- ev.location.x = 0;
- ev.location.y = 0;
- ev.time = 0L;
- ev.flags = 0;
- ev.window = 0;
- ev.data.compound.subtype = msg->h.msg_id;
- ev.data.compound.misc.L[0] = (long) msg->lock; /* the lock object */
- ev.data.compound.misc.L[1] = 0; /* unused */
- ev.ctxt = [NXApp context];
-
- if( DPSPostEvent( &ev, FALSE ) != 0 ){
- printf( "handle_unlock: DPSPostEvent cannot post EV_UNLOCK\n" );
- }
- }
-
-
- static void post_callback_event( callbackMsg *msg )
- {
- NXEvent ev;
-
- /*
- * ok, uck. I've got 4 int-sized parms to send and here they go:
- * target goes in L[0]
- * data goes in L[1]
- * sel goes in flags
- * arg goes in window
- *
- * Doesn't matter what they are, since the window-manager didn't make
- * the message anyways.
- */
- ev.type = NX_APPDEFINED;
- ev.location.x = 0;
- ev.location.y = 0;
- ev.time = 0L;
- ev.flags = (int) msg->sel;
- ev.window = (int) msg->arg;
- ev.data.compound.subtype = msg->h.msg_id;
- ev.data.compound.misc.L[0] = (long) msg->target;
- ev.data.compound.misc.L[1] = (long) msg->data;
-
- ev.ctxt = [NXApp context];
-
- if( DPSPostEvent( &ev, FALSE ) != 0 ){
- printf( "handle_callback: DPSPostEvent cannot post EV_CALLBACK\n" );
- }
- }
-
-
- static void do_callback( callbackMsg *msg )
- {
- int options;
- id ret;
- struct waitdata *d;
-
- options = msg->h.msg_id & 0xff00;
-
- if( options & EV_CALLBACK_WAIT ){
- d = msg->data;
- mutex_lock( &d->lock );
- }
-
- fastPending = YES;
- if( options & EV_CALLBACK_ARG ){
- ret = [msg->target perform:msg->sel with:msg->arg];
- } else {
- ret = [msg->target perform:msg->sel];
- }
- fastPending = NO;
-
- if( options & EV_CALLBACK_WAIT ){
- d->retval = ret;
- d->done = YES;
- condition_signal( &d->cond );
- mutex_unlock( &d->lock );
- }
- }
-
-
- static void callbackHandler( msg_header_t *msg )
- /*
- * This is called from DPSGetEvent or DPSPeekEvent if there
- * is a message on the queue.
- * This handler converts the Mach message to the appropriate
- * NXEvent and posts it to the event queue (similar to what the
- * NXTrackingTimer does).
- * For fast callbacks, it invokes the method directly.
- */
- {
- int msg_type, msg_options;
-
-
- #if 0
- putchar('*');
- fflush(stdout);
- #endif
-
- msg_type = msg->msg_id & 0x00ff;
- msg_options = msg->msg_id & 0xff00;
-
- switch( msg_type ){
- case EV_UNLOCK:
- // no point sending UNLOCK events if no one cares...
- if( blocked ){
- post_unlock_event( (unlockMsg *)msg );
- }
- break;
- case EV_CALLBACK:
- if( msg_options & EV_CALLBACK_POST ){
- // callbacks posted as events...
- post_callback_event( (callbackMsg *)msg );
- } else {
- // fast callbacks; do them here...
- do_callback( (callbackMsg *)msg );
- }
- break;
- default:
- printf( "unknown message: 0x04%x\n", msg->msg_id );
- break;
- }
- }
-
-
- - (NXEvent*)getNextEvent:(int)mask waitFor:(double)timeout threshold:(int)level
- /*
- * Override the master NXApp event getter to get and dispatch NX_APPDEFINED
- * events if they aren't already included in the mask. This allows
- * callbacks and EV_UNLOCK events to get through unsuspecting modal loops
- * such as for handling mouse-down events.
- */
- {
- NXEvent *ev;
-
- if( mask & NX_APPDEFINEDMASK ){
- // caller must know what they're doing...
- return [super getNextEvent:mask waitFor:timeout threshold:level];
- }
- mask |= NX_APPDEFINEDMASK;
- for(;;){
- ev = [super getNextEvent:mask waitFor:timeout threshold:level];
- if( !ev || (ev->type != NX_APPDEFINED) ) break;
- [self applicationDefined:ev];
- }
-
- return ev;
- }
-
-
- - (NXEvent*)getNextEvent:(int)mask
- {
- NXEvent *ev;
-
- if( mask & NX_APPDEFINEDMASK ){
- // caller must know what they're doing...
- return [super getNextEvent:mask ];
- }
- mask |= NX_APPDEFINEDMASK;
- for(;;){
- ev = [super getNextEvent:mask];
- if( !ev || (ev->type != NX_APPDEFINED) ) break;
- [self applicationDefined:ev];
- }
-
- return ev;
- }
-
-
- - applicationDefined:(NXEvent *)ev
- /*
- * This method receives the NX_APPDEFINED events dispatched by the
- * Application's run: and sendEvent: methods (but not by other
- * AppKit modal loops: Button, Slider, etc.)
- * This is where we perform the slow callbacks.
- */
- {
- id ret;
- int ev_type, ev_options;
- id target, arg;
- SEL sel;
- struct waitdata *d;
-
- ev_type = ev->data.compound.subtype & 0x00ff;
- ev_options = ev->data.compound.subtype & 0xff00;
-
- switch( ev_type ){
- case EV_UNLOCK:
- // Don't need this now; must be left on the queue from a
- // previous lock: modal loop below...
- // printf( "applicationDefined: Received a rogue EV_UNLOCK!\n" );
- break;
- case EV_CALLBACK:
- /* uck. See handle_callback above */
- target = (id) ev->data.compound.misc.L[0];
- sel = (SEL) ev->flags;
- arg = (id) ev->window;
-
- if( ev_options & EV_CALLBACK_WAIT ){
- d = (struct waitdata *)ev->data.compound.misc.L[1];
- mutex_lock( &d->lock );
- }
-
- // The event data in ev can be invalidated by this method
- // invocation if it recursively calls DPSGetEvent.
- // That's ok; we've now got a local copy of everything we need.
- if( ev_options & EV_CALLBACK_ARG ){
- ret = [target perform:sel with:arg];
- } else {
- ret = [target perform:sel];
- }
-
- if( ev_options & EV_CALLBACK_WAIT ){
- d->retval = ret;
- d->done = YES;
- condition_signal( &d->cond );
- mutex_unlock( &d->lock );
- }
- break;
- default:
- printf( "applicationDefined: Unknown event: %d\n",
- ev->data.compound.subtype );
- break;
- }
- return self;
- }
-
-
- /*
- * The "designated" callback function.
- */
-
- // option bits for the master callback method...
- #define WAIT 0x02
- #define ARG 0x08
- #define HIGH 0x01
- #define POST 0x04
- #define FAST HIGH
- #define SAFE (HIGH|POST)
- #define SLOW POST
-
- - (id)callback:target perform:(SEL)aSel with:anObject opt:(int)option
- {
- // allocate all variables off stack so it's reentrant and thread-safe
- callbackMsg msg;
- struct waitdata d;
-
-
- // if this is called from the main thread, just do a perform:
- // otherwise 'wait' may deadlock
- if( cthread_self() == mainThread ){
- if( option & ARG ){
- return [target perform:aSel with:anObject];
- } else {
- return [target perform:aSel];
- }
- }
-
- // setup header
- msg.h.msg_simple = TRUE;
- msg.h.msg_size = sizeof(callbackMsg);
- msg.h.msg_local_port = PORT_NULL;
- msg.h.msg_remote_port = (option&HIGH) ? fastPort : basePort;
- msg.h.msg_type = MSG_TYPE_NORMAL;
-
- msg.t.msg_type_name = MSG_TYPE_INTEGER_32;
- msg.t.msg_type_size = 32;
- msg.t.msg_type_number = CALLBACK_DATA;
- msg.t.msg_type_inline = TRUE;
- msg.t.msg_type_longform = FALSE;
- msg.t.msg_type_deallocate = FALSE;
-
-
- // set up message data
- msg.h.msg_id = EV_CALLBACK;
- msg.target = target;
- msg.sel = aSel;
-
- // set options...
- if( option & POST ){
- msg.h.msg_id |= EV_CALLBACK_POST;
- }
- if( option & ARG ){
- msg.h.msg_id |= EV_CALLBACK_ARG;
- msg.arg = anObject;
- }
- if( option & WAIT ){
- msg.h.msg_id |= EV_CALLBACK_WAIT;
- msg.data = &d;
- d.done = NO;
- mutex_init( &d.lock );
- condition_init( &d.cond );
- mutex_lock( &d.lock );
- }
-
- msg_send((msg_header_t *)&msg, MSG_OPTION_NONE,0);
-
- if( option & WAIT ){
- // wait for method to return
- while( !d.done ){
- condition_wait( &d.cond, &d.lock );
- }
- mutex_unlock( &d.lock );
- mutex_clear( &d.lock );
- condition_clear( &d.cond );
- return d.retval;
- } else {
- return nil;
- }
- }
-
-
-
- // Exported variations on the theme
-
-
- - (id)callbackAndWaitTarget:target perform:(SEL)aSel
- {
- return [self callback:target perform:aSel with:nil opt:FAST|WAIT];
- }
-
- - (id)callbackAndWaitTarget:target perform:(SEL)aSel with:anObject
- {
- return [self callback:target perform:aSel with:anObject opt:FAST|WAIT|ARG];
- }
-
- - (void)callbackTarget:target perform:(SEL)aSel
- {
- [self callback:target perform:aSel with:nil opt:FAST];
- }
-
- - (void)callbackTarget:target perform:(SEL)aSel with:anObject
- {
- [self callback:target perform:aSel with:anObject opt:FAST|ARG];
- }
-
- - (id)safeCallbackAndWaitTarget:target perform:(SEL)aSel
- {
- return [self callback:target perform:aSel with:nil opt:SAFE|WAIT];
- }
-
- - (id)safeCallbackAndWaitTarget:(id)target perform:(SEL)aSel with:(id)anObject
- {
- return [self callback:target perform:aSel with:anObject opt:SAFE|WAIT|ARG];
- }
-
- - (void)safeCallbackTarget:target perform:(SEL)aSel
- {
- [self callback:target perform:aSel with:nil opt:SAFE];
- }
-
- - (void)safeCallbackTarget:(id)target perform:(SEL)aSel with:(id)anObject
- {
- [self callback:target perform:aSel with:anObject opt:SAFE|ARG];
- }
-
- - (id)slowCallbackAndWaitTarget:target perform:(SEL)aSel
- {
- return [self callback:target perform:aSel with:nil opt:SLOW|WAIT];
- }
-
- - (id)slowCallbackAndWaitTarget:(id)target perform:(SEL)aSel with:(id)anObject
- {
- return [self callback:target perform:aSel with:anObject opt:SLOW|WAIT|ARG];
- }
-
- - (void)slowCallbackTarget:target perform:(SEL)aSel
- {
- [self callback:target perform:aSel with:nil opt:SLOW];
- }
-
- - (void)slowCallbackTarget:(id)target perform:(SEL)aSel with:(id)anObject
- {
- [self callback:target perform:aSel with:anObject opt:SLOW|ARG];
- }
-
-
-
- + new
- {
- [super new];
-
- // register ports for receiving callback messages
- port_allocate( task_self(), &fastPort );
- DPSAddPort( fastPort, (DPSPortProc)callbackHandler,
- sizeof(callbackMsg), NULL, FAST_THRESHOLD );
-
- port_allocate( task_self(), &basePort );
- DPSAddPort( basePort, (DPSPortProc)callbackHandler,
- sizeof(callbackMsg), NULL, BASE_THRESHOLD );
-
- // identify the main Thread
- mainThread = cthread_self();
-
- // set runtime to be thread-safe...
- objc_setMultithreaded(YES);
-
- return NXApp;
- }
-
-
- - free
- {
- DPSRemovePort(fastPort);
- DPSRemovePort(basePort);
- port_deallocate( task_self(), fastPort );
- port_deallocate( task_self(), basePort );
-
- return [super free];
- }
-
-
-
- @end
-
-
-
- /*****************************************************************
- * Support for CJRLocks...
- */
-
-
- @implementation ThreadedApp (CJRLock)
-
- - (void)threadDidUnlock:sender
- // construct a EV_UNLOCK Mach message to send to fastPort...
- {
- unlockMsg msg;
-
- if( !blocked ){
- // no point in sending UNLOCK events if no one cares...
- return;
- }
- // setup header
- msg.h.msg_simple = TRUE;
- msg.h.msg_size = sizeof(unlockMsg);
- msg.h.msg_local_port = PORT_NULL;
- msg.h.msg_remote_port = fastPort;
- msg.h.msg_type = MSG_TYPE_NORMAL;
-
- // type field for zero data...
- msg.t.msg_type_name = MSG_TYPE_INTEGER_32;
- msg.t.msg_type_size = 32;
- msg.t.msg_type_number = UNLOCK_DATA;
- msg.t.msg_type_inline = TRUE;
- msg.t.msg_type_longform = FALSE;
- msg.t.msg_type_deallocate = FALSE;
-
- // message type...
- msg.h.msg_id = EV_UNLOCK;
- msg.lock = sender; // the lock object that unlocked
-
- msg_send((msg_header_t *)&msg, MSG_OPTION_NONE,0);
- }
-
-
- - (void)lock: (mutex_t)mutex
- /*
- * This method is called from CJRLock and CJRConditionLock methods
- * and should only be invoked from the main thread.
- * It is possible for this routine to be called recursively in response
- * to servicing applicationDefined:
- */
- {
- NXEvent *ev;
- int saved_block;
-
- if( fastPending ){
- // this doesn't return
- [self error: "Cannot use locks from a fast callback method\n"];
- }
-
- if( !mutex_try_lock(mutex) ){
- saved_block = blocked;
- block_count++;
- blocked = YES;
- for(;;){
- ev = [self getNextEvent:NX_APPDEFINEDMASK
- waitFor:NX_FOREVER
- threshold:FAST_THRESHOLD];
- if( ev->data.compound.subtype == EV_UNLOCK ){
- if( mutex_try_lock(mutex) ) break;
- } else {
- // handle the event...
- [self applicationDefined:ev ];
- }
- }
- blocked = saved_block;
- block_count--;
- }
- }
-
-
- - (void) lock:(mutex_t)mutex whenCondition:(condition_t)cond
- withData:(volatile int *)data is:(int)value
- /*
- * This method is called from CJRConditionLock methods
- * and should only be invoked from the main thread.
- * It is possible for this routine to be called recursively in response
- * to servicing applicationDefined:
- */
- {
- NXEvent *ev;
- int saved_block;
-
- if( fastPending ){
- // this doesn't return
- [self error: "Cannot use locks from a fast callback method\n"];
- }
-
- if( mutex_try_lock(mutex) ){
- if( *data == value ) {
- return;
- }
- mutex_unlock(mutex);
- }
- saved_block = blocked;
- block_count++;
- blocked = YES;
- for(;;){
- ev = [self getNextEvent:NX_APPDEFINEDMASK
- waitFor:NX_FOREVER
- threshold:FAST_THRESHOLD];
- if( ev->data.compound.subtype == EV_UNLOCK ){
- if( mutex_try_lock(mutex) ){
- if( *data == value ) break;
- mutex_unlock(mutex);
- }
- } else {
- // handle the event...
- [self applicationDefined:ev ];
- }
- }
-
- blocked = saved_block;
- block_count--;
- }
-
- @end
-