Journal for 2010

Building Mac Applications in MonoDevelop with MonoMac

Recently Miguel announced MonoMac, a new Mono binding for Cocoa & other Objective-C libraries, based on the MonoTouch binding generator and Objective-C bridge.

This is exciting for many people because it allows writing native Mac GUIs using C# and other .NET languages. I have put together a MonoDevelop addin that simplifies the process of creating, developing and debugging a MonoMac application.

I have created a brief walkthrough to show how easy it is to create a simple Mac application with MonoMac, MonoDevelop and Interface Builder.

First, create a new MonoMac C# project using the New Project dialog.

MonoDevelop New Project dialog showing MonoMac Project types

The new project has an entry point, a definition of the main menu and app delegate, and a window with a controller. The xib files are interface definitions that can be edited with Apple's Interface Builder GUI designer tool. The xib.designer.cs files contain autogenerated partial classes for any classes, actions and outlets defined in the xib files. The Info.plist file is an application manifest, and MonoDevelop will automatically merge in some required values when building the app bundle.

A newly-created MonoMac project in MonoDevelop

The MainWindow.xib file can be opened in Interface Builder.

Interface Builder with the MonoMac project's main window

After adding an NSButton and NSTextField to the window, add an outlet of type NSTextField to the controller definition in the Library.

Adding an outlet to the MainWindowController

Connect this outlet to the text field on the window by dragging the outlet from the Inspector.

Connecting the text field to the outlet on the controller

Similarly, create an action of type NSButton, and connect it to the button on the window.

Connecting the button to an action on the controller

MonoDevelop automatically updates the designer code when it regains focus. The designer partial class contains the new action and outlet.

The autogenerated designer code for the controller's outlets and actions

Implement the action in the controller class. MonoDevelop will automatically complete the partial method signature.

Implementation of the action in the controller

The application is compiled to an app bundle, which can be run or debugged like any other Mono application.

Debugging the MonoMac application

Note that this is an experimental preview, the result of a few days' work, and MonoMac itself is also in an immature state. I make no guarantee about stability of the MonoMac APIs, the MonoDevelop CodeBehind generator, the project format, or functionality of any of the usual MonoDevelop features. With these caveats, you can download a MonoDevelop build with MonoMac included.

UPDATE: The addin can now be installed into the latest version of MonoDevelop 2.4 by following these instructions.

This is a community effort, not a commercial product, so we need your help improving both MonoMac and the MonoDevelop addin!

  • Binding and auditing more Framework APIs.
  • Writing samples and templates to set the patterns for MonoMac applications.
  • Writing tutorials and documentation.
  • Porting Cocoa samples to exercise the bindings and tooling, and prioritize API binding.
  • Enabling the MonoDevelop addin to expose existing C# classes and project resources to Interface Builder.

Email the mono-osx mailing list to join in!

T4 Templates in MonoDevelop

A few week ago, I travelled to GDC and MIX. While in planes, airports and in spare moments in the conference, I implemented a feature I've wanted for some time - integrated T4 templating. This takes the T4 engine that I wrote for ASP.NET MVC templates, and exposed it within the IDE as a "custom tool", like Visual Studio does.

This also meant I had to implement a simple version of VS-style custom tools, or generators. Set the file's "custom tool" property to "TextTemplatingFileGenerator" using the property pad, then whenever you save it, MD will run the generator on the file.

Unlike VS, there is also a file template for the T4 file:
MonoDevelop New File dialog with T4 template

The T4 syntax is very like ASP.NET's render blocks, expressions and directives. Here you can see a simple example that generates a countdown:
T4 template in MonoDevelop generating a countdown

When this is saved, it generates the output file:
T4 output file in MonoDevelop

It's also integrated with MonoDevelop 2.4's new error bubbles, so any errors in T4 code show up in the editor as soon as the file is saved:
Error bubbles in T4 template in MonoDevelop

This is very much a side project for me, so contributions in this area would be very helpful. It would be great to have support for viewing generators' message output and cancelling long-running generators, support for T4 custom directives, debugging T4, code completion in T4 files, and generating template class files that can be compiled into apps.

This is part of the Catchup 2010 series of posts.

Iterator-based Microthreading

Back in May, I was wrapping PhyreEngine and porting the samples to C#. To extend one of them and demonstrate some of the capabilities of C#, Miguel and I decided to use simple iterator-based microthreading, which simulates multithreading but with many microthreads within a single real thread. Unity does something like it in their game engine too. It enables you to use a very imperative style of coding, as if using a single dedicated thread for each, but without anywhere near the overhead of real threads.

Here's the usage example we came up initially with that drove my design of the microthread scheduler:

public class Enemy1
{
	public void Init (Scheduler scheduler)
	{
		scheduler.AddTask (Patrol ());
	}
 
	IEnumerable Patrol ()
	{
		while (alive){
			if (CanSeeTarget ()) {
				yield return Attack ();
			} else if (InReloadStation){
				Signal signal = AnimateReload ();
				yield return signal;
			} else {
				MoveTowardsNextWayPoint ();
				yield return TimeSpan.FromSeconds (1);
			};
		}
		yield break;
	}
 
	IEnumerable Attack ()
	{
		while (TargetAlive && CanSeeTarget ()){
			AimAtTarget ();
			Fire ();
			yield return TimeSpan.FromSeconds (2);
		}
	}
}

The concept is fairly simple. The C# compiler magically transforms this code into an IEnumerator state machine (returned by IEnumerable.GetEnumerator()). Each time IEnumerator.MoveNext() is called, your method "iterates": it runs to the next yield statement, sets the Current property to the value yielded, and keeps enough state that next time it iterates, it can effectively resume where it left off. We can yield nulls to give other microthreads a chance to run, or yield objects to tell the scheduler other things. For example, yielding a TimeSpan could cause the microthread to sleep for that long.

interface IEnumerator
{
	bool MoveNext ();
	object Current;
}

As you can see, although C# iterators are primarily intended for iterating through collections, the yield keyword can also become effectively something like a microthread cooperatively yielding. Your method runs until it yields, then it later resumes from this point, runs to the next yield, and so on.

The class that enumerates your iterator is the scheduler. Before we get to that, we'll cover the boring bits to set some groundwork. First, we need a class to encapsulate the task. This holds hold the IEnumerator and which Scheduler it belongs to, is a singly linked list node, and has a field for arbitrary data we'll use later.

//tasks may move between lists but they may only be in one list at a time
internal class TaskItem
{
	public readonly IEnumerator Task;
	public TaskItem Next;
	public Scheduler Scheduler;
	public long Data;
 
	public TaskItem (IEnumerator task, Scheduler scheduler)
	{
		this.Task = task;
		this.Scheduler = scheduler;
	}
}

Next we need a simple linked list for keeping lists of TaskItem. We're not using LinkedList<T>; this is much simpler, does only what we need, and makes it easy to move tasks between lists and remove them via the enumerator.

internal sealed class TaskList
{
	public readonly Scheduler Scheduler;
 
	public TaskItem First { get; private set; }
	public TaskItem Last { get; private set; }
 
	public TaskList (Scheduler scheduler)
	{
		this.Scheduler = scheduler;
	}
 
	public void Append (TaskItem task)
	{
		Debug.Assert (task.Next == null);
		if (First == null) {
			Debug.Assert (Last == null);
			First = Last = task;
		} else {
			Debug.Assert (Last.Next == null);
			Last.Next = task;
			Last = task;
		}
	}
 
	public void Remove (TaskItem task, TaskItem previous)
	{
		if (previous == null) {
			Debug.Assert (task == First);
			First = task.Next;
		} else {
			Debug.Assert (previous.Next == task);
			previous.Next = task.Next;
		}
 
		if (task.Next == null) {
			Debug.Assert (Last == task);
			Last = previous;
		}
		task.Next = null;
	}
 
	public TaskEnumerator GetEnumerator ()
	{
		return new TaskEnumerator (this);
	}
 
	public sealed class TaskEnumerator
	{
		TaskList list;
		TaskItem current, previous;
 
		public TaskEnumerator (TaskList list)
		{
			this.list = list;
			previous = current = null;
		}
 
		public TaskItem Current { get { return current; } }
 
		public bool MoveNext ()
		{
			TaskItem next;
			if (current == null) {
				if (previous == null)
					next = list.First;
				else
					next = previous.Next;
			} else {
				next = current.Next;
			}
 
			if (next != null) {
				if (current != null)
					previous = Current;
				current = next;
				return true;
			}
			return false;
		}
 
		public void MoveCurrentToList (TaskList otherList)
		{
			otherList.Append (RemoveCurrent ());
		}
 
		public TaskItem RemoveCurrent ()
		{
			Debug.Assert (current != null);
			TaskItem ret = current;
			list.Remove (current, previous);
			current = null;
			return ret;
		}
	}
}

Now we can implement the scheduler. Using the scheduler is very simple. You register tasks with RegisterTask(IEnumerable), then call Run() to run all the active tasks for one yield iteration each. It handles sleeping and waking up tasks and removing tasks once they're finished.

public sealed class Scheduler
{
	TaskList active, sleeping;
 
	public Scheduler ()
	{
		active = new TaskList (this);
		sleeping = new TaskList (this);
	}
 
	public void AddTask (IEnumerator task)
	{
		active.Append (new TaskItem (task, this));
	}
 
	public void Run ()
	{
		//cache this, it's expensive to access DateTime.Now
		long nowTicks = DateTime.Now.Ticks;
 
		//move woken tasks back into the active list
		var en =  sleeping.GetEnumerator ();
		while (en.MoveNext ())
			if (en.Current.Data < nowTicks)
				en.MoveCurrentToList (active);
 
		//run all the active tasks
		en = active.GetEnumerator ();
		while (en.MoveNext ()) {
			//run each task's enumerator for one yield iteration
			IEnumerator t = en.Current.Task;
			if (!t.MoveNext ()) {
				//it finished, so remove it
				en.RemoveCurrent ();
				continue;
			}
 
			//check the current state
			object state = t.Current;
			if (state == null) {
				//it's just cooperatively yielding, state unchanged 
				continue;
			} else if  (state is TimeSpan) {
				//it wants to sleep, move to the sleeping list. we use the Data property for the wakeup time 
				en.Current.Data = nowTicks + ((TimeSpan)state).Ticks;
				en.MoveCurrentToList (sleeping);
			} else if (state is IEnumerable) {
				throw new NotImplementedException ("Nested tasks are not supported yet");
			} else {
				throw new InvalidOperationException ("Unknown task state returned:" + state.GetType ().FullName);
			} 
		}
	}
 
	internal void AddToActive (TaskItem task)
	{
		active.Append (task);
	}
}

At this point, it looks fairly useful, but let's add a simple synchronization primitive too. Microthreads should never make long blocking calls because they can't be pre-empted. Instead, we're going to let the microthread obtain and yield a signal object, which means it will not be scheduled until the signal has been set. Instead of using blocking APIs, you can use async APIs, create a signal object, wait on that, and have the callback set the signal. Or, thinking back to the initial game example, some game object's controlling microthread might want to sleep until another game object reaches a certain state; the target object can keep a signal accessible via a property that microthreads can read and wait on.

A thread can wait for more that one signal, and more than one thread can wait for a signal. Essentially we're going to have an equivalent of .NET's AutoResetEvent and WaitHandle.WaitAll(WaiHandle[]).

The signal's job is to keep a list of all the tasks that are waiting for it. When tasks start waiting, they move out of the scheduler's lists and are tracked by all the signals instead. Each signal increments/decrements the task's Data property to keep track of how many signals the task is waiting for. When the count reaches zero, the task can be moved back to the scheduler.

public class Signal
{
	static int nextId = int.MinValue;
 
	int id = nextId++;
	List<TaskItem> tasks = new List<TaskItem> ();
	bool isSet = true;
 
	public void Set ()
	{
		if (isSet)
			return;
		isSet = true;
		//decrement the wait count of all tasks waiting for thsi signal
		foreach (TaskItem task in tasks)
			if (--task.Data == 0)
				//if the wait count is zero, the task isn't waiting for any more signals, so re-schedule it
				task.Scheduler.AddToActive (task);
		tasks.Clear ();
	}
 
	internal void Add (TaskItem task)
	{
		//signal only becomes unset when it has tasks
		if (isSet)
			isSet = false;
		//the signal keeps a list of tasks that are waiting for it
		tasks.Add (task);
		//use the task's data for tracking the number of signals it's still waiting for
		task.Data++;
	}
}

And we need to add a couple more checks to the Scheduler.Run() state handling, so that when the task returns a signal or collection/array of signals, it's moved from the scheduler's lists to the tasks' lists.

} else if (state is Signal) {
	TaskItem task = en.RemoveCurrent ();
	task.Data = 0;
	((Signal)state).Add (task);
} else if (state is ICollection<Signal>) {
	TaskItem task = en.RemoveCurrent ();
	task.Data = 0;
	foreach (Signal s in ((ICollection<Signal>)state))
		s.Add (task);
}

And, there it is.

I have a few more ideas to improve this and turn it into a real library:

  • Implement a Signal that's a ManualResetEvent analogue
  • Use a IEnumerable<ThreadState> instead of plain IEnumerable, where ThreadState is a union struct with an enum specifying its type. This could be used to avoid the boxing of TimeSpan and the type checks and casts in the scheduler — just switch on the enum value. It would also prevent consumers returning a zero TimeSpan instead of null.
  • Dispose any disposable task IEnumerators
  • Implement task priorities, probably using different lists
  • Tidy up the accessibility and API a bit
  • Add a Scheduler.Run overload that only runs some number of iterations, not the whole list.

Some people may wonder why I haven't mentioned interactions with real threads. If the scheduler were thread-aware, then you could have multiple real threads on different cores consuming the microthread queue, and it would be faster with multiple cores and avoid blocking on slow tasks. The problem is not just that this increases the complexity, but that the microthreads all would have to be aware of threading too, and would need to lock all the objects they touched, and so on. This scheduler is meant to run in the equivalent of the GUI thread, purely to enable driving high-level game logic (and similar things) with an intuitive thread-like imperative programming model, and minimal overhead. If it doesn't fit easily on one core you're probably using it for the wrong thing. There might be cases where it's useful to create multiple Schedulers, and run them in different thread, but be careful about what the microthreads touch.

This is part of the Catchup 2010 series of posts.

Integrating a GTK# Application with the Mac

In this follow-up to my post on turning a GTK# app into a Mac app bundle, I describe how to integrate your application with Mac-specific features such as the main menu, the dock, and file/URL events. This is based on the work I did to integrate MonoDevelop and MonoDoc with the Mac, and largely involves cherry-picking code snippets from these projects. Although it would be nice to isolate this code into a library, I don't have the time at the moment to maintain such a library myself.

MonoDevelop with Mac main menu support

GTK# is a nice toolkit, and in my opinion the best cross-platform toolkit for Mono/.NET, but there are some things that just don't have direct cross-platform analogues, such as the Mac main menu. To integrate with such features, you need to implement platform-specific code paths. MonoDevelop does this in some cases using runtime checks:

if (PropertyService.IsMac) {
    //some mac-specific behaviour
} else if (PropertyService.IsWindows)
    //some windows-specific behaviour
} else {
    //some default behaviour
}

This is great when the platform-specific code doesn't introduce any dependencies, because you have one binary that works across all platforms. However, you can't rely on checking System.PlatformID.MacOSX so we have to use some other techniques for platform detection. In MonoDevelop's PropertyService we can find code based on Mono's Managed.Windows.Forms/XplatUI that detects whether the program is running on Mac or Windows. Here are the pertinent parts, copied into a new class:

public static class PlatformDetection
{
	public readonly static bool IsWindows;
	public readonly static bool IsMac;
 
	static PlatformDetection ()
	{
		IsWindows = Path.DirectorySeparatorChar == '\\';
		IsMac = !IsWindows && IsRunningOnMac();
	}
 
	//From Managed.Windows.Forms/XplatUI
	static bool IsRunningOnMac ()
	{
		IntPtr buf = IntPtr.Zero;
		try {
			buf = System.Runtime.InteropServices.Marshal.AllocHGlobal (8192);
			// This is a hacktastic way of getting sysname from uname ()
			if (uname (buf) == 0) {
				string os = System.Runtime.InteropServices.Marshal.PtrToStringAnsi (buf);
				if (os == "Darwin")
					return true;
			}
		} catch {
		} finally {
			if (buf != IntPtr.Zero)
				System.Runtime.InteropServices.Marshal.FreeHGlobal (buf);
		}
		return false;
	}
 
	[System.Runtime.InteropServices.DllImport ("libc")]
	static extern int uname (IntPtr buf);
}

For certain other cases in MonoDevelop, particularly ones that introduce dependencies that only exist on some platforms, we have a "platform service" extension point using Mono.Addins, where a platform-specific addins can provide a platform-specific implementation of our PlatformService abstract class, and if none is found we fall back to a default implementation. This means that just one of our binaries is platform-specific. It's also nice because a lot of the platform-specific code is in one place. But there are still many places where certain platforms just need a small behavioural tweak and the overhead of pulling something out into this abstraction layer isn't worthwhile. In such cases we use a quick runtime check instead.

A third option is do build a different version of your binary for Mac, using ifdefs. This is what I did for MonoDoc, because it was easier for a quick hack, but I'll probably go back later and change it to use runtime checks.

#if MACOS
    //some mac-specific behaviour that only gets included when you pass /define:MACOS to the C# compiler
#endif

The first platform-specific codepath we'll add is the main menu, because that's the one your users are going to notice. There is a GTK library called ige-mac-integration that provides some platform integration features for the main menu and dock, and is included in the Mono framework for Mac. If you exported the DYLD_FALLBACK_LIBRARY_PATH like I described in my post about building the app bundle, you should be able to P/Invoke it. Unfortunately it's quite limited in what it can do, so we don't use it at all in MonoDevelop, but its menu integration is effective for simpler apps like MonoDoc. In the MonoDoc source you can find a small IGE wrapper in a single file that you can copy into your app. Since P/Invokes are only resolved at runtime if they're used, you can include the code on all platforms provided you only call it on Mac .

if (PlatformDetection.IsMac) {
    //enable the global key handler for keyboard shortcuts
    IgeMainMenu.GlobalKeyHandlerEnabled = true;
 
    //Tell the IGE library to use your GTK menu as the Mac main menu
    IgeMainMenu.MenuBar = yourGtkMenuBar;
 
    //tell IGE which menu item should be used for the app menu's quit item
    IgeMainMenu.QuitMenuItem = yourQuitMenuItem;
 
    //add a new group to the app menu, and add some items to it
    var appGroup = IgeMainMenu.AddAppMenuGroup ();
    appGroup.AddMenuItem (yourAboutItem, "About FooApp...");
    appGroup.AddMenuItem (yourPrefsItem, "Preferences...");
 
    //hide the menu bar so it no longer displays within the window
    yourGtkMenuBar.Hide ();
}

The IGE library automatically converts your menu's shortcuts to use Command instead of Control, but if you explicitly check modifiers elsewhere in your code, for example in custom widgets, you may wish to make Mac-specific tweaks to behaviour. If you do need to access modifiers from key events directly, beware that the Mac GTK modifier mappings are very strange; see the MapRawKeys method on MonoDevelop's KeyBindingManager for details.

Next we'll handle the Apple Events that Mac applications in App Bundle form are expected to handle.

The Quit event is sent when the Quit command on your app's the Dock icon is used (among other things), and obviously should cause your app to quit. The Reopen event is sent when the dock is clicked, and should cause your app to show its windows if they're hidden, or if you follow the Mac document-per-window model and no document is open, optionally create a new empty document.

The strange events for developers from other OSes are the file open and URL open events that your app gets set if it's registered to handle file types or URL types. Because the Mac only expects to have a single instance of a bundle app running at once, each app handling multiple documents, Launch Services sends the files or URLs as events to the running instance of the app, or if the app is not running, starts it then sends the events. They are not ever passed to your app as command-line arguments.

MonoDevelop's MacPlatform addin has a wrapper for these events that exposes them as simple static .NET events on a static class called OSXIntegration.Framework.ApplicationEvents. Simply copy the entire contents of the MacPlatform/MacInterop directory and include it in your app, then connect handlers to the ApplicationEvents class's static events sometime before starting the GTK mainloop:

if (PlatformDetection.IsMac) {
	ApplicationEvents.Quit += delegate (object sender, ApplicationEventArgs e) {
		Application.Quit ();
		e.Handled = true;
	};
 
	ApplicationEvents.Reopen += delegate (object sender, ApplicationEventArgs e) {
		mainWindow.Deiconify ();
		mainWindow.Visible = true;
		e.Handled = true;
	};
 
	//optional, only need this if your Info.plist registers to handle urls
	ApplicationEvents.OpenUrls += delegate (object sender, ApplicationUrlEventArgs e) {
		if (e.Urls != null || e.Urls.Count > 0) {
			OpenUrls (e.Urls);
		}
		e.Handled = true;
	};
 
	//optional, only need this if your Info.plist registers to handle files
	ApplicationEvents.OpenFiles += delegate (object sender, ApplicationFileEventArgs e) {
		if (e.Files != null || e.Files.Count > 0) {
			OpenUrls (e.Files);
		}
		e.Handled = true;
	};
}

You might wonder how to register to handle files and URLs in your Info.plist. The Apple docs have the full details, but we can look at the MonoDoc Info.plist and the MonoDevelop Info.plist for some basics.

To register URLs, add the CFBundleURLTypes key to the Info.plist's main dictionary. Its value should be an array of URL types, where each one is a dictionary defining the name and the schemes (prefixes):

<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleURLName</key>
		<string>MonoDoc</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<string>monodoc</string>
		</array>
	</dict>
</array>

Notice that you're not just registering to handle the URL types, you're also defining them. Launch Services automatically scans app bundles on the system and in the user's directory to build system-wide and per-user databases of URL types and their handlers. So, as soon as your app is present, it could be used to handle the things it claims. You can even define the icon, as an icns file to be loaded from your app's Resources directory, just like the bundle's icon, and this will show up in Finder.

Registering file types is similar, but there are more keys. You can also register whether your bundle is the default editor, and whether it's an editor or viewer.

<key>CFBundleDocumentTypes</key>
<array>
	<dict>
		<key>CFBundleTypeIconFile</key>
		<string>monodevelop.sln.icns</string>
		<key>CFBundleTypeExtensions</key>
		<array>
			<string>sln</string>
		</array>
		<key>CFBundleTypeName</key>
		<string>MonoDevelop Solution</string>
		<key>CFBundleTypeRole</key>
		<string>Editor</string>
		<key>LSIsAppleDefaultForType</key>
		<true/>
	</dict>
</array>

With these examples, you should now be able to make your application fit in much better on Mac.

I'm still investigating other Mac integration points for MonoDevelop, such as native file dialogs, but that's likely to take some time, as binding the native toolkits and getting them to play nicely with the GTK mainloop is likely to be difficult, and there are other important things keeping me busy. When they're done I'll be sure to share that code too.

This is part of the Catchup 2010 series of posts.

Creating a Mac App Bundle for a GTK# Application

While making the MonoDevelop and MonoDoc packages for Mac I learned a few things about adapting GTK# apps for Mac, and I'd like to share them so that anyone else who's built a GTK# app on Windows or Linux can provide a nice self-contained Mac app bundle for their Mac users. This first part will cover building an app bundle, and a later post will cover building platform-specific code paths so that your app integrates with the main menu and dock.

If you're not a Mac developer, you might be wondering exactly what an app bundle is. Well, it's simply a directory with a ".app" extension that contains an application and everything it needs. The Mac GUI shell treats this folder as a self-contained application that can be run directly. It never has to be "installed" as such, but can simply kept wherever the user wants, typically in the system's Applications folder, and to "uninstall", it's simply deleted. To look inside an app bundle, use the context menu on the bundle in Finder.

The most important part of the app bundle is the Info.plist manifest, which lives in the Contents subdirectory of your app bundle, and describes the application. A plist ("property list") is an Apple structured data format that can be represented in binary or xml formats. We'll be using the xml format, which is fairly simple, if a little odd. Let's look at a simplified version of the MonoDoc app bundle's manifest:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>English</string>
	<key>CFBundleExecutable</key>
	<string>monodoc</string>
	<key>CFBundleIconFile</key>
	<string>monodoc.icns</string>
	<key>CFBundleIdentifier</key>
	<string>com.novell.monodoc</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>MonoDoc</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>2.2</string>
	<key>CFBundleSignature</key>
	<string>xmmd</string>
	<key>CFBundleVersion</key>
	<string>2.2</string>
	<key>NSAppleScriptEnabled</key>
	<string>NO</string>
</dict>
</plist>

As you can see, it simply contains key/value pairs to set properties of the app bundle. This is just a subset of the keys you can set, but they're the important ones for our purposes. If you need more, see the Apple docs.

The one you need to change are:

CFBundleExecutable
The name of the executable within the Contents/MacOS subdirectory of your bundle that Launch Services will run when opening your app. We'll be using a shell script to run your real executable using Mono.
CFBundleIconFile
The icon of your app bundle, shown in Finder and the Dock. It should be an icns file created using Icon Composer, and placed in the Contents/Resources subdirectory of your bundle.
CFBundleIdentifier
A namespaced unique ID for your app.
CFBundleName
The name for your app that will be shown by the dock and menu bar.
CFBundleShortVersionString
A short display string for your app bundle's version.
CFBundleVersion
Your app bundle's version.

Treat the other keys as boilerplate that must be included. Much of the app bundle layout and properties are designed for apps using the native toolkits; we are using just enough of them to fit in and work correctly.

If you read the descriptions of the keys, you'll see that your app bundle directory should have the following structure:

  • YourAppName.app
    • Contents
      • Info.plist
      • Resources
        • yourappname.icns
      • MacOS
        • yourappname
        • Your app's real executable, dll, and other files

After you've written your Info.plist, made an icon file using Icon Composer (from the Apple Developer Tools), and copied all your app's real file into the bundle's MacOS folder, all we're missing is the launch script. The Apple Launch services can run shell scripts, but doesn't know how to open Mono programs directly, so we use a shell script to start Mono. Let's look at the MonoDoc launch script:

#!/bin/sh
# Author: Michael Hutchinson (mhutchinson@novell.com)
 
DIR=$(cd "$(dirname "$0")"; pwd)
MONO_FRAMEWORK_PATH=/Library/Frameworks/Mono.framework/Versions/Current
export DYLD_FALLBACK_LIBRARY_PATH="$DIR:$MONO_FRAMEWORK_PATH/lib:/lib:/usr/lib"
export PATH="$MONO_FRAMEWORK_PATH/bin:$PATH"
 
exec mono "$DIR/browser.exe"

This is very straightforward. It simply gets a full path to the bundle's MacOS directory and puts it in the DIR variable so that can be used later in the script, and sets DYLD_FALLBACK_LIBRARY_PATH to include this directory and the Mono framework directory, so that you can P/Invoke native libraries in your MacOS directory and the Mono framework lib directory. It also sets the PATH environment variable to include the official Mono framework's bin directory, ensuring that the official Mono is used. Using the official Mono is important, because it means you avoid issues specific to users who have MacPorts or some other custom Mono in their PATH, which are likely to be difficult to reproduce or fix. It then executes the app using Mono.

This is a simple script, but it has some deficiencies. We can improve it by taking some code and tricks from the MonoDevelop Mac launch script.

First, we'll detect whether the Mono Framework is installed, and if it's not, show a message using AppleScript telling the user to download it. This prevents your app from dying silently if Mono isn't installed. Put this in your script before the exec call.

#mono version check
 
REQUIRED_MAJOR=2
REQUIRED_MINOR=4
APPNAME="MonoDevelop"
 
VERSION_TITLE="Cannot launch $APPNAME"
VERSION_MSG="$APPNAME requires the Mono Framework version $REQUIRED_MAJOR.$REQUIRED_MINOR or later."
DOWNLOAD_URL="http://www.go-mono.com/mono-downloads/download.html"
 
MONO_VERSION="$(mono --version | grep 'Mono JIT compiler version ' |  cut -f5 -d\ )"
MONO_VERSION_MAJOR="$(echo $MONO_VERSION | cut -f1 -d.)"
MONO_VERSION_MINOR="$(echo $MONO_VERSION | cut -f2 -d.)"
if [ -z "$MONO_VERSION" ] \
	|| [ $MONO_VERSION_MAJOR -lt $REQUIRED_MAJOR ] \
	|| [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ] 
then
	osascript \
	-e "set question to display dialog \"$VERSION_MSG\" with title \"$VERSION_TITLE\" buttons {\"Cancel\", \"Download...\"} default button 2" \
	-e "if button returned of question is equal to \"Download...\" then open location \"$DOWNLOAD_URL\""
	echo "$VERSION_TITLE"
	echo "$VERSION_MSG"
	exit 1
fi

We can also take some code to use the "-a" argument to exec to set the process name that will be see by the "ps" commandline tool. Note that this doesn't work on OS 10.4, so we check the OS version and define an exec command in a variable that we can use later.

# Work around a limitation in 'exec' in older versions of macosx
OSX_VERSION=$(uname -r | cut -f1 -d.)
if [ $OSX_VERSION -lt 9 ]; then  # If OSX version is 10.4
	MONO_EXEC="exec mono"
else
	MONO_EXEC="exec -a appname mono"
fi

Finally, change the exec call from the original version to use our new MONO_EXEC variable, write all console output to a log file in a subdirectory of ~/Library/Logs, and pass the value of the MONO_OPTIONS environment variable to Mono. The MONO_OPTIONS environment variable is useful to enable you to to pass diagnostic options to the Mono runtime, such as "--debug", without altering your script.

EXE_PATH="$DIR\appname.exe"
LOG_FILE="$HOME/Library/Logs/$APPNAME/$APPNAME.log"
mkdir -p "`dirname \"$LOG_FILE\"`"
$MONO_EXEC $MONO_OPTIONS "$EXE_PATH" $* 2>&1 1> "$LOG_FILE"

Let's tidy this all up into a single script, with all the values you need to change for your specific app in one place at the top.

#!/bin/sh
 
#get the bundle's MacOS directory full path
DIR=$(cd "$(dirname "$0")"; pwd)
 
#change these values to match your app
EXE_PATH="$DIR\appname.exe"
PROCESS_NAME=appname
APPNAME="AppName"
 
#set up environment
MONO_FRAMEWORK_PATH=/Library/Frameworks/Mono.framework/Versions/Current
export DYLD_FALLBACK_LIBRARY_PATH="$DIR:$MONO_FRAMEWORK_PATH/lib:/lib:/usr/lib"
export PATH="$MONO_FRAMEWORK_PATH/bin:$PATH"
 
#mono version check
REQUIRED_MAJOR=2
REQUIRED_MINOR=4
 
VERSION_TITLE="Cannot launch $APPNAME"
VERSION_MSG="$APPNAME requires the Mono Framework version $REQUIRED_MAJOR.$REQUIRED_MINOR or later."
DOWNLOAD_URL="http://www.go-mono.com/mono-downloads/download.html"
 
MONO_VERSION="$(mono --version | grep 'Mono JIT compiler version ' |  cut -f5 -d\ )"
MONO_VERSION_MAJOR="$(echo $MONO_VERSION | cut -f1 -d.)"
MONO_VERSION_MINOR="$(echo $MONO_VERSION | cut -f2 -d.)"
if [ -z "$MONO_VERSION" ] \
	|| [ $MONO_VERSION_MAJOR -lt $REQUIRED_MAJOR ] \
	|| [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ] 
then
	osascript \
	-e "set question to display dialog \"$VERSION_MSG\" with title \"$VERSION_TITLE\" buttons {\"Cancel\", \"Download...\"} default button 2" \
	-e "if button returned of question is equal to \"Download...\" then open location \"$DOWNLOAD_URL\""
	echo "$VERSION_TITLE"
	echo "$VERSION_MSG"
	exit 1
fi
 
#get an exec command that will work on the current OS version
OSX_VERSION=$(uname -r | cut -f1 -d.)
if [ $OSX_VERSION -lt 9 ]; then  # If OSX version is 10.4
	MONO_EXEC="exec mono"
else
	MONO_EXEC="exec -a \"$PROCESS_NAME\" mono"
fi
 
#create log file directory if it doesn't exist
LOG_FILE="$HOME/Library/Logs/$APPNAME/$APPNAME.log"
mkdir -p "`dirname \"$LOG_FILE\"`"
 
#run app using mono
$MONO_EXEC $MONO_OPTIONS "$EXE_PATH" $* 2>&1 1> "$LOG_FILE"

Now open a terminal, make the script executable, and execute it.

chmod +x AppName.app/Contents/MacOS/scriptname
./AppName.app/Contents/MacOS/scriptname

It's useful to run the script directly like this because you will immediately see the result of any errors in it. Assuming the script worked, your app bundle is complete, and you can double-click on it to run it.

In my follow-up post I cover writing platform-specific code paths to integrate your new bundle better with the platform, including using the Mac main menu, and handling Apple events, which will enable your app to handle files and URLs.

This is part of the Catchup 2010 series of posts.

Long Time No Blog

It's strange to think that's it's 2010 already and I haven't blogged since June. It certainly hasn't been for lack of things to blog about — if anything, I've been too busy working on things to blog about them, so whenever I've had something to share I've usually used Twitter because much easier to fire off a quick tweet than to write a blog post. However, I'm determined to start blogging again so that I can share detailed information to which I can refer people more easily.

So, over the next few weeks, I'm going to try to catch up a bit. I intend to blog about the following topics:

I don't have any particular order planned, so if there's something you'd particularly like to see, leave a comment and I'll try to take it into consideration. I'll link the items in the list through to the posts as I make them.