Binding a Cocoa framework for Xamarin.Mac

(13 Sep 2015) This article is fairly old. See https://developer.xamarin.com/samples/mac/XMBindingExample/ for a more up-to-date sample that works with the Unified API. And see http://developer.xamarin.com/guides/ios/advanced_topics/binding_objective-c/objective_sharpie/ for the current Objective Sharpie documentation.

Create a simple Cocoa framework in Xcode

Let's start by making our very own Objective-C framework project in Xcode. That way we can look at exactly how the Xcode side lines up with the Xamarin side.

  1. Select the Cocoa Framework template project.

    The "Cocoa Framework" template project in Xcode

  2. Follow the prompts to finish setting up the library. I'll call the framework Simple for this example.

  3. Add a method to the default class (in Simple.m). For example something like this will be fine:

    @implementation Simple
    - (NSString *)run:(BOOL *)successful
    {
        *successful = YES;
        return @"OK";
    }
    @end
  4. Add a corresponding method declaration in the header file (Simple.h):

    @interface Simple : NSObject
    - (NSString *)run:(BOOL *)successful;
    @end
  5. Add the header file to the Public section of the Copy Headers build phase. This will copy Simple.h into the Simple.framework/Headers directory, so that end users will have access to header. This is the conventional setup for pre-built frameworks that you download.

    The "Cocoa Framework" template project in Xcode

  6. Set Build Settings -> Architectures -> Architectures to 32-bit Intel (i386). This is required because Xamarin.Mac does not yet support 64-bit libraries.

    Xcode: Build Settings -> Architectures -> Architectures -> 32-bit Intel (i386)

  7. Again under Build Settings, disable Automatic Reference Counting.

    Xcode: Build Settings -> Apple LLVM 5.1 - Language - Objective C -> Objective-C Automatic Reference Counting -> No

    This is necessary because the 32-bit legacy Objective-C runtime does not support ARC. Without this change, Xcode will hit an error during compilation:

    error: -fobjc-arc is not supported on platforms using the legacy runtime

  8. Build!

    You can now build the project in Xcode via Product -> Build. If you like, you can first set Xcode to use the Release configuration under Product -> Scheme -> Edit Scheme -> Run -> Info [tab] -> Build Configuration -> Release. If you'd rather build from the command line, that's fine too:

    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -project Simple.xcodeproj -target Simple -sdk macosx10.9 -configuration Release clean build

  9. The compiled framework will now be present in:

    Built using Xcode GUI: ~/Library/Developer/Xcode/DerivedData/Simple-${UUID}/Build/Products/Release/Simple.framework
    Built using example commandline: build/Release/Simple.framework

Create a Xamarin.Mac app that will use the framework

  1. Create a new Xamarin.Mac application project.

    Xamarin Studio: New Solution -> C# -> Mac -> Xamarin.Mac Project

  2. For convenience, copy the Simple.framework directory into the project folder, for example into a lib/ subdirectory.

  3. Confirm that Mono can load the dynamic library from the compiled framework. To do this, add a few lines like these at the beginning of the Main() method:

    if (Dlfcn.dlopen("/Users/username/Projects/MacApp1/MacApp1/lib/Simple.framework/Simple", 0) == IntPtr.Zero) {
        Console.Error.WriteLine("Unable to load the dynamic library.");
    }

    Then try running the app. If you hit the error, double-check that the binary for the library is 32-bit. For example, you can use lipo on the command line:

    lipo -i Simple.framework/Simple
    Non-fat file: Simple.framework/Simple is architecture: i386

Create a first draft of the bindings definition file using Objective Sharpie

(13 Sep 2015) This section is out-of-date for both Unified and Classic API bindings. See http://developer.xamarin.com/guides/ios/advanced_topics/binding_objective-c/objective_sharpie/ for the current Objective Sharpie documentation.

Given that our example framework only contains one class and one method, we could easily write the binding definition by hand, but let's use Objective Sharpie so that we can get an idea of how that tool works too.

Using the GUI

  1. Download and unzip Objective Sharpie.

  2. Create a prefix header that matches Simple/Simple/Simple/Simple-Prefix.pch. For example, create a file named prefix.pch with the following contents:

    #import <Cocoa/Cocoa.h>
  3. On the first window of the Objective Sharpie wizard, select a target OS X framework, and add the prefix header under extra clang command line arguments.

    -include /Users/username/Projects/MacApp1/MacApp1/lib/prefix.pch

    Objective Sharpie: Target SDK 'OS X 10.9', extra clang command line arguments '-include /Users/username/Projects/MacApp1/MacApp1/lib/prefix.pch'

  4. On the next window, add the Simple.h header file from the library, and leave Follow #include and #import directives ON.

    Objective Sharpie: Select header files, Advanced 'follow #include and #import directives'

  5. Complete the Objective Sharpie wizard by entering a namespace, selecting Generate, and saving the resulting .cs file.

Using the command line

Create the prefix header file (prefix.pch) as described for the GUI, and then run sharpie from the command line:

LIB_DIR=/Users/username/Projects/MacApp1/MacApp1/lib
sharpie -sdk:macosx10.9 -follow-includes -scope:"$LIB_DIR/Simple.framework" "$LIB_DIR/Simple.Framework/Headers/Simple.h" -n Simple -clang -F "$LIB_DIR" -include "$LIB_DIR/prefix.pch" > "$LIB_DIR/SimpleBindingDefinition.cs"

Errors if the prefix header is not included

The prefix header tells clang to import Cocoa/Cocoa.h implicitly in every source file. In our sample based on the default Xcode framework template project, this isn't strictly necessary because Simple.h already explicitly imports Foundation/Foundation.h. But some pre-compiled frameworks depend on the prefix header. We can test this problem by deleting the Foundation/Foundation.h import from Simple.h. If we then run sharpie again, without the -import argument, clang produces a few errors:

A/Headers/Simple.h:9:21: error: cannot find interface declaration for 'NSObject', superclass of 'Simple'
@interface Simple : NSObject
~~~~~~~~~~~~~~~~~   ^
/Users/username/Projects/MacApp1/MacApp1/lib/Simple.framework/Versions/A/Headers/Simple.h:10:4: error: expected a type
- (NSString *)run:(BOOL *)successful;
   ^
/Users/username/Projects/MacApp1/MacApp1/lib/Simple.framework/Versions/A/Headers/Simple.h:10:20: error: expected a type
- (NSString *)run:(BOOL *)successful;

Fix-up the binding definition, and compile it into a .dll with bmac

Now that we have a binding definition file, we can try running bmac on it to create the binding library .dll. Since Objective Sharpie only provides a rough draft of the definition, some hand-adjustments are usually necessary at this step.

Build attempt 1: error due to Unmapped pointer

/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/bin/bmac -oSimple.dll --tmpdir=/tmp -baselib=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/lib/mono/XamMac.dll -r=System.Drawing SimpleBindingDefinition.cs

SimpleBindingDefinition.cs(9,16): warning CS0658: `unmapped' is invalid attribute target. All attributes in this attribute section will be ignored
SimpleBindingDefinition.cs(9,33): warning CS0658: `:' is invalid attribute target. All attributes in this attribute section will be ignored
SimpleBindingDefinition.cs(9,41): error CS1525: Unexpected symbol `]', expecting `(', `,', `.', or `]'

These warning and errors are caused by line 9 in the binding definition:

string Run ([unmapped: pointer: Pointer] successful);

The problem is that the BOOL* trips up Objective Sharpie. Let's change this to a ref bool:

string Run (ref bool successful);

Build attempt 2: missing using MonoMac.Foundation;

SimpleBindingDefinition.cs(5,21): error CS0246: The type or namespace name `NSObject' could not be found. Are you missing `MonoMac.Foundation' using directive?
SimpleBindingDefinition.cs(8,4): error CS0246: The type or namespace name `Export' could not be found. Are you missing an assembly reference?

Simply add the required using line:

using MonoMac.Foundation;

Build attempt 3: success

After these two small changes, the binding builds successfully.

We can take a look at the result by opening Simple.dll in Xamarin Studio's Assembly Browser:

Hierarchical tree view of the namespaces, classes, and methods within Simple.dll

One interesting thing to note here is the SimpleBindingDefinition namespace. This namespace contains the helper classes for the binding. By default, bmac uses the name of the binding definition file for this namespace, but this can sometimes cause trouble.

Possible additional complication: helper classes namespace

As an experiment, let's see what would have happened if we had named the binding definition Simple.cs:

/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/bin/bmac -oSimple.dll --tmpdir=/tmp -baselib=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/lib/mono/XamMac.dll -r=System.Drawing Simple.cs

/tmp/Simple/Simple.g.cs(79,40): error CS0117: `Simple.Simple' does not contain a definition for `Messaging'
/tmp/Simple/Simple.g.cs(29,30): (Location of the symbol related to previous error)
/tmp/Simple/Simple.g.cs(81,40): error CS0117: `Simple.Simple' does not contain a definition for `Messaging'
/tmp/Simple/Simple.g.cs(29,30): (Location of the symbol related to previous error)

What's happening here is that at two places in the generated Simple.g.cs file, Messaging is referred to as Simple.Messaging rather than global::Simple.Messaging. Since these references occur within a class named Simple, the compiler tries to look them up within that class rather than within the enclosing namespace.

Workaround: change the helper classes namespace

The easiest workaround is to change the namespace for the helper classes. Renaming the file is one option, but bmac also has a command line argument for this:

      --ns=VALUE             Sets the namespace for storing helper classes

For example, this will work:

/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/bin/bmac -oSimple.dll --tmpdir=/tmp -baselib=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/lib/mono/XamMac.dll -r=System.Drawing Simple.cs --ns=SimpleHelpers

Alternative workaround: hand-edit the generated binding

A different, trickier workaround is to fix-up the generated Simple.g.cs file by hand. This wouldn't be much fun for a large binding, but it's easy to do for this small example binding, and gives us a little more insight into the binding process.

  1. Run bmac with additional verbose output:

    /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/bin/bmac -oSimple.dll --tmpdir=/tmp -baselib=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/lib/mono/XamMac.dll -r=System.Drawing Simple.cs -v

  2. Copy the final compiler invocation from the verbose output:

    mcs -unsafe -target:library -out:'Simple.dll' /tmp/ObjCRuntime/Messaging.g.cs /tmp/Simple/Simple.g.cs -r:'System.Drawing' -r:'/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/usr/lib/mono/XamMac.dll'

  3. Edit /tmp/Simple/Simple.g.cs, and replace the two occurrences of (Simple.Messaging with (global::Simple.Messaging.

  4. Re-run the compiler command from step (2).

Call Simple.Simple.Run(ref bool) in the Xamarin.Mac app

  1. Add a reference to the newly created Simple.dll.

  2. Keep the Dlfcn.dlopen() method from earlier.

  3. Add a few lines to test out the Simple.Simple.Run(ref bool) method.

All together, it might look something like this:

using System;
using MonoMac.ObjCRuntime;
using MonoMac.AppKit;

namespace MacApp1
{
    class MainClass
    {
        static void Main(string[] args)
        {
            if (Dlfcn.dlopen("/Users/username/Projects/MacApp1/MacApp1/lib/Simple.framework/Simple", 0) == IntPtr.Zero)
            {
                Console.Error.WriteLine("Unable to load the dynamic library.");
            }

            bool success = false;
            string result = (new Simple.Simple()).Run(ref success);

            Console.WriteLine("Ran the Objective-C method with return value: {0} and success state: {1}", result, success);

            NSApplication.Init();
            NSApplication.Main(args);
        }
    }
}

Now run the app! With a little luck, the console output will show that the method ran successfully:

Ran the Objective-C method with return value: OK and success state: True

Found a mistake?

Submit a comment or correction

License

Copyright (c) 2014 Xamarin Inc http://www.xamarin.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

Updates

13 Sep 2015 Added notes about up-to-date sample and new Objective Sharpie docs.
01 May 2014 Posted