Search code examples
c#objective-ccocoamonomonomac

How to implement Cocoa copyWithZone on derived object in MonoMac C#?


I'm currently porting a small Winforms-based .NET application to use a native Mac front-end with MonoMac. The application has a TreeControl with icons and text, which does not exist out of the box in Cocoa.

So far, I've ported almost all of the ImageAndTextCell code in Apple's DragNDrop example: https://developer.apple.com/library/mac/#samplecode/DragNDropOutlineView/Listings/ImageAndTextCell_m.html#//apple_ref/doc/uid/DTS40008831-ImageAndTextCell_m-DontLinkElementID_6, which is assigned to an NSOutlineView as a custom cell.

It seems to be working almost perfectly, except that I have not figured out how to properly port the copyWithZone method. Unfortunately, this means the internal copies that NSOutlineView is making do not have the image field, and it leads to the images briefly vanishing during expand and collapse operations. The objective-c code in question is:

- (id)copyWithZone:(NSZone *)zone {
    ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone];
    // The image ivar will be directly copied; we need to retain or copy it.
    cell->image = [image retain];
    return cell;
}

The first line is what's tripping me up, as MonoMac does not expose a copyWithZone method, and I don't know how to otherwise call it.

Update

Based on current answers and additional research and testing, I've come up with a variety of models for copying an object.

static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>();

// Method 1

static IntPtr selRetain = Selector.GetHandle ("retain");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

// Method 2

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    _refPool.Add(cell);

    return cell;
}

[Export("dealloc")]
public void Dealloc ()
{
    _refPool.Remove(this);
    this.Dispose();
}

// Method 3

static IntPtr selRetain = Selector.GetHandle ("retain");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    _refPool.Add(cell);
    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

// Method 4

static IntPtr selRetain = Selector.GetHandle ("retain");
static IntPtr selRetainCount = Selector.GetHandle("retainCount");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone (IntPtr zone)
{
    ImageAndTextCell cell = new ImageAndTextCell () {
        Title = Title,
        Image = Image,
    };

    _refPool.Add (cell);
    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

public void PeriodicCleanup ()
{
    List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> ();

    foreach (ImageAndTextCell cell in _refPool) {
        uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount);
        if (count == 1)
            markedForDelete.Add (cell);
    }

    foreach (ImageAndTextCell cell in markedForDelete) {
        _refPool.Remove (cell);
        cell.Dispose ();
    }
}

// Method 5

static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone);
    ImageAndTextCell cell = new ImageAndTextCell(copyHandle) {
        Image = Image,
    };

    _refPool.Add(cell);

    return cell;
}

Method 1: Increases the retain count of the unmanaged object. The unmanaged object will persist persist forever (I think? dealloc never called), and the managed object will be harvested early. Seems to be lose-lose all-around, but runs in practice.

Method 2: Saves a reference of the managed object. The unmanaged object is left alone, and dealloc appears to be invoked at a reasonable time by the caller. At this point the managed object is released and disposed. This seems reasonable, but on the downside the base type's dealloc won't be run (I think?)

Method 3: Increases the retain count and saves a reference. Unmanaged and managed objects leak forever.

Method 4: Extends Method 3 by adding a cleanup function that is run periodically (e.g. during Init of each new ImageAndTextCell object). The cleanup function checks the retain counts of the stored objects. A retain count of 1 means the caller has released it, so we should as well. Should eliminate leaking in theory.

Method 5: Attempt to invoke the copyWithZone method on the base type, and then construct a new ImageAndTextView object with the resulting handle. Seems to do the right thing (the base data is cloned). Internally, NSObject bumps the retain count on objects constructed like this, so we also use the PeriodicCleanup function to release these objects when they're no longer used.

Based on the above, I believe Method 5 is the best approach since it should be the only one that results in a truly correct copy of the base type data, but I don't know if the approach is inherently dangerous (I am also making some assumptions about the underlying implementation of NSObject). So far nothing bad has happened "yet", but if anyone is able to vet my analysis then I would be more confident going forward.


Solution

  • So far I have not found any evidence of trouble, so I'm comfortable adopting "Method 5" that I outlined in my question update, which I'll duplicate here with some additional explanation:

    // An additional constructor
    public ImageAndTextCell (IntPtr handle)
        : base(handle)
    {
    }
    
    // Cocoa Selectors
    static IntPtr selRetainCount = Selector.GetHandle("retainCount");
    static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:");
    
    static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>();
    
    // Helper method to be called at some future point in managed code to release
    // managed instances that are no longer needed.
    public void PeriodicCleanup ()
    {
        List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> ();
    
        foreach (ImageAndTextCell cell in _refPool) {
            uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount);
            if (count == 1)
                markedForDelete.Add (cell);
        }
    
        foreach (ImageAndTextCell cell in markedForDelete) {
            _refPool.Remove (cell);
            cell.Dispose ();
        }
    }
    
    // Overriding the copy method
    [Export("copyWithZone:")]
    public virtual NSObject CopyWithZone(IntPtr zone) {
        IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone);
        ImageAndTextCell cell = new ImageAndTextCell(copyHandle) {
            Image = Image,
        };
    
        _refPool.Add(cell);
    
        return cell;
    }
    

    By invoking the copyWithZone: selector on the base object (via SuperHandle), the underlying Cocoa subsystem will clone the unmanaged object and give back the handle to it, with its retain count already set to 1 (standard obj-c copy convention). It's then possible to construct the derived C# object with the cloned object handle, so the cloned instance becomes the backing object. Then it's a simple matter of cloning any managed C# goodies that belong to the derived type.

    As ta.speot.is pointed out, it's also necessary to hold onto a reference of the managed type somewhere. Without the reference, the object is a candidate for garbage collection at the end of the method. The unmanaged portion of the object is safe on return because it has a positive retain count from the call to the copy selector. I've chosen to store references in a static List, and then periodically call a cleanup method from other parts of code which will traverse the list, check if the corresponding unmanaged objects have any other owners, and dispose the objects if not. Note that I'm checking for a count of 1 instead of 0, because our copied object was actually retained twice: once by the copy selector, and once by the NSObject constructor. The Monomac runtime system will take care of disposing the unmanaged object when the managed side is disposed/collected.