SynchronizationContext for Cocoa/Monobjc

Our current project does a heavy use of SynchronizationContext to be sure that all asynchronous notifications are executed in the proper thread. For our UI on Windows this means the main WPF’s thread which is provided by the framework. The problem raised when we started writing our Cocoa UI since we wanted to use the SynchronizationContext pattern we had been using on other platforms.

Monobjc provides a ISynchronizeInvoke interface implementation in NSView and is in fact quite straightforward to use but building your own SynchronizationCotext subclass for Cocoa
around the Monobjc bridge is also not that hard. The idea I had was to encapsulate the .Net callback and object state around an object (SyncObject) which inherits from NSObject.

Then we register our Objective-C callback into the main’s thread runloop by calling performSelectorOnMainThread:withObject:waitUntilDone: The ObjC callback is intercepted by the bridge which is then ran on the Mono side. There we can safely call the original .Net callback which is executed in the main thread.

The SyncObject can be (optionally) registered in a dictionary so that the Mono’s GC doesn’t have the chance to clean the object before the selector is executed. We unregister the SyncObject once the callback has been executed. This step is in fact unnecessary as the Monobjc bridge caches the object until a Release is intercepted from the ObjC side, that reference will keep the GC away from us. I have left the code commented just in case the implementation changes (which seems quite rare to happen…).

The idea can also be extended to any thread with a bit more work. If you extend this I would like to hear from you.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Monobjc.Cocoa;
using Monobjc;

namespace vsn.MacOSXComponents.Threading
{
    [ObjectiveCClass]
    public class SyncObject : NSObject
    {
        private SendOrPostCallback m_Callback;
        private Object m_State;

        public delegate void DeregisterDelegate(SyncObject sync_object);
#if USE_SYNCOBJS_DICTIONARY
        private DeregisterDelegate m_DeregisterDelegate;
#endif

public static readonly Class SyncObjectClass = Class.GetClassFromType(typeof(SyncObject));

        ///
        /// Initializes a new instance of the  class.
        ///
        public SyncObject() { }

        ///
        /// Initializes a new instance of the  class.
        ///
        ///

        public SyncObject(IntPtr nativePointer)
            : base(nativePointer) { }

        public override void  Dispose()
{
// This is here for testing purpouses
//Console.Write("Disposing SyncObject");
 	 base.Dispose();
}

        ///
        /// initializes the synchronization object with the given call back and object state
        ///
        /// callback
        /// state
        /// if true, the current thread waits until the operation is performed
        ///

        public void Initialize(SendOrPostCallback d, Object state, bool wait, DeregisterDelegate dereg_delegate)
        {
            m_Callback = d;
            m_State = state;
#if USE_SYNCOBJS_DICTIONARY
            m_DeregisterDelegate = dereg_delegate;
#endif

            // Call the NSObject's performSelector method from csharp code
        PerformSelectorOnMainThreadWithObjectWaitUntilDone(ObjectiveCRuntime.Selector("bridgedCallback:"), this, wait);
        }

        ///
        /// This is our callback which is invoked by the main thread's runloop
        ///
        [ObjectiveCMessage("bridgedCallback:")]
        public void BridgedCallback(NSObject param)
        {
            // Invoke the true callback
            m_Callback(m_State);         

#if USE_SYNCOBJS_DICTIONARY
    // Un register this object from our cache
            if (m_DeregisterDelegate != null)
                m_DeregisterDelegate(this);
#endif

// This region is unsafe as we are not protected against the GC. Monobjc
// has a cached reference to this object so shouldn't be a problem but
// could be a problem if the implementation changes.
// Consider using a CER
// GC.Collect(); // Testing

// Mark this object for deletion by an AutoreleasePool
this.Autorelease();
        }
    }

    ///
    /// Cocoa's SynchronizationContext implementation
    ///
    /// Only Post and Send methods are implemented
    ///
    public class CocoaRunLoopSynchronizationContext : SynchronizationContext
    {
        private Dictionary m_SyncObjectsDictionary;
        private readonly object m_lock = null;

        ///
        /// Default constructor
        ///
        public CocoaRunLoopSynchronizationContext()
            : base()
        {
            m_SyncObjectsDictionary = new Dictionary();
            m_lock = new object();
        }

        public override void Post(SendOrPostCallback d, Object state)
        {
    SyncObject sync = new SyncObject();
#if USE_SYNCOBJS_DICTIONARY
            lock(m_lock)
               m_SyncObjectsDictionary.Add(sync.GetHashCode(), sync);
#endif
            sync.Initialize(d, state, false, DeregisterSyncObject);

        }

        public override void Send(SendOrPostCallback d, Object state)
        {
            SyncObject sync = new SyncObject();
#if USE_SYNCOBJS_DICTIONARY
            lock (m_lock)
                m_SyncObjectsDictionary.Add(sync.GetHashCode(), sync);
#endif
            sync.Initialize(d, state, true, DeregisterSyncObject);
        }

        ///
        /// Called by SyncObject to remove itself from the dictionary so that no reference is held
        ///
        ///
target object to unregister
        private void DeregisterSyncObject(SyncObject sync_object)
        {
#if USE_SYNCOBJS_DICTIONARY
    lock (m_lock)
m_SyncObjectsDictionary.Remove(sync_object.GetHashCode());
#endif
        }
    }
}
Advertisements