Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Porting a Mac program to Windows using The Cocotron

In this last of three posts about porting a Mac application to Windows, I look at the steps involved in setting up The Cocotron with a remote debugging session between Xcode and the application running on Windows. I'll also talk about the code that didn't "just work" and some of the approaches I used to fix the program and get it working.

Introduction

If you want some background on what The Cocotron is and alternative ways of porting applications from the Mac to Windows, please read the first and second part in this series.

Setting up The Cocotron for remote debugging

Installing The Cocotron is a three step process:

  1. Download the InstallCDT zip file, decompress it and run the InstallCDT script it contains. I think CDT stands for Cocotron Developer Tools. This installs a copy of gcc and related tools that you will required for cross-compilation. It will also install required MinGW libraries and W32API libraries for linking Windows projects.
  2. Check out a copy of The Cocotron code. The repository has recently moved to Mercurial, so you'll need to have a copy of Mercurial or check out the Subversion mirror. Open the Cocoa.xcodeproj and build the project.
  3. Download a copy of xcxdb-Installation.zip and run the install-xcxdb,sh script it contains. XCXDB probably stands for Xcode Cocotron (X)cross DeBugger.

The third point is not strictly required for using The Cocotron but is required for remote debugging as it installs a version of gdb that will allow remote debugging between Xcode and Windows. The XCXDB install has the added advantage that it adds a Template to the Project Templates that will create a Windows debuggable project without needing to mess around with specific project settings.

You can add The Cocotron and XCXDB settings to an existing project but it takes a bit more effort: apply the Build settings for CDT and then, from an XCXDB project created from the template, copy the manifest, icon, Remote-Target.txt and win-icon.rc files, then copy the run script build phase that includes gdbserver.exe in your debug builds, set the GDB connection to "pipe" and probably a few other things I'm forgetting.

Setting up the project manually is easy to get wrong, so you'll want to get a project running from the "Cocotron" User Template before trying to modify an existing project.

Coordinating on the Windows side

I run Windows XP Home in VirtualBox on the same Mac as Xcode. You could also use VMWare, Parallels or a separate, dedicated Windows machine. VirtualBox is cheaper than these options (it's free) but is also the slowest so you'll want a faster machine.

Running a virtual machine and Xcode on the same computer also uses lots of RAM. You could probably manage in 4GB of RAM but I have 6GB of RAM and have contemplated getting more to accomodate VirtualBox, Xcode and my other regular applications.

To make a virtual machine work, the best approach is to make the virtual machine use a "bridged" network adapter. This gives it its own IP address on the local network. Incidentally, I had to switch from VirtualBox's default AMD network driver to the Intel driver — since the AMD driver would crash periodically while debugging, causing the whole virtual machine to lose network connectivity until I restarted it.

Once the Windows virtual machine has its own IP address, edit the IP address in the "Remote-Target.txt" file from the Xcode project to match this address. Make sure the Firewall on the Windows machine allows access to 999 (the default port that XCXDB uses for debugging). You can use another port if needed, just use a different port in Remote-Target.txt and below when you start gdbserver on the remote machine.

You also need to ensure that the build location for your Xcode project is a shared volume that both the Mac and Windows machines can access. I chose to use VirtualBox's "Shared Folders" to make the build location on my Mac's drive a network volume under Windows.

Running the sample XCXDB project

In Xcode, create a project from the Cocotron User Template. I named mine "XCXDB-Sample".

Select the XCXDB-Sample-Windows target from the targets menu.

From Windows as I configured it, the path to the XCXDB-Sample directory is Z:\Projects\XCXDB-Sample. I chose to use an MSYS terminal client to navigate to the /z/Projects/XCXDB-Sample/build/Debug-cocotron-win32/XCXDB-Sample.app directory (download MSYS and its dependencies from mingw.org).

The reason why I used MSYS instead of a DOS command prompt like cmd.exe, is that MSYS allows the standard out and standard error of any running application (this includes NSLog() messages from Cocoa) to work for applications (otherwise these facilities are only visible to dedicated command-line programs in Windows). MSYS also offers a more familiar (if slightly quirky) bash shell environment.

Once you're in the XCXDB-Sample.app directory, run:

./gdbserver --multi localhost:999

Normally in XCXDB, this command is run from the debug.bat file. However, ".bat" files don't run under MSYS. You can create a shell script if you prefer.

The Windows command shell will now display: "Listening on port 999".

From Xcode, select "Build and Debug" from the "Build" menu. If all has gone well, the status bar at the bottom of the project menu will build and then display "GDB: Stopped". You need to tell GDB to try to connect. With the one of the Xcode project windows selected, type Command-Option-P (Debugger continue).

If the communication between Mac and Windows isn't working you may see an error in the Debugger Console.

mix_cmd_pid_info: the program being debugged is not running.
mix_cmd_exec_interrupt: Inferior not executing.

This indicates that either:

  • The working directory of the Executable in Xcode is not set to the "Project Directory".
  • Remote-Target.txt was not found in the Project Directory.
  • The relative path to the "remote exec-file" did not match a file on the Windows machine.
mix_cmd_pid_info: the program being debugged is not running.

If you get this line without the "Inferior not executing" message shown above, then the RemoteTarget.txt did not contain the correct IP address or the gdbserver.exe isn't running.

A few tips for remotely debugging with The Cocotron and XCXDB

You will encounter unimplemented functions and methods in The Cocotron. You may even encounter bugs.

To help you fix these issues, you'll probably want to build The Cocotron's Cocoa.xcodeproj project in Debug and keep the AppKit.xcodeproj and Foundation.xcodeproj projects open. This will build debug information into the AppKit and Foundation DLLs and will let you debug problems in these frameworks.

Since the debugger is configured to communicate via PIPE instead of the interactive terminal, you can't enter GDB commands into the Command Console in Xcode. You can add extra commands to the Remote-Target.txt file though.

Every time I started listening on a socket, GDB reported a SIGSEGV signal. The program didn't crash (I'm not sure it was a real SIGSEGV) but it paused the debugger. If this happens for you and it gets too annoying, you can add the line "handle SIGSEGV nostop noprint pass" to the start of the Remote-Target.txt file.

I had issues setting breakpoints at times. Often, I could only set breakpoints before the program started or when the program was already stopped at another breakpoint. If anyone finds away around this nuisance, I'd like to know.

Code which won't "just work" in The Cocotron

Here begins a story of the issues I encountered porting ServeToMe to Windows using The Cocotron.

Once I'd gone through the arduous task of adding required settings and files to my existing projects, came the task of getting the program compiling.

I'm not going to discuss the difficulties of compiling and linking third-party dependencies on another platform. Don't misunderstand me: it is very difficult; but since it has little to do with The Cocotron, I won't discuss that work here.

Almost everything involving sockets

ServeToMe includes an HTTP server and does a lot with BSD socket communication. None of this worked initially.

The Cocotron itself doesn't implement sockets — instead that falls directly through to WinSock's implementation. Most socket code just required the Windows headers to be included:

#if __WIN32__
    #undef WINVER
    #define WINVER 0x501
    #import <winsock2.h>
    #import <ws2tcpip.h>
#else
    #import <sys/socket.h>
    #import <netinet/in.h>
    #include <arpa/inet.h>
#endif

and the respective libraries (libwsock32.a, libws2_32.a and libmswsock.a) included from the /Developer/Cocotron/1.0/PlatformInterfaces/i386-mingw32msvc/lib directory.

There are a few common socket-related functions in <arpa/inet.h> and other headers on the Mac that WinSock doesn't include. Examples include inet_ntop and inet_pton. You will need to reimplement these yourself. If you Google for them, you'll find implementations around.

Quirks and omissions in the MinGW libraries

The MinGW libraries implement a lot of POSIX functions but certainly not all.

One of the more annoying omissions for me was mkdtemp() (which creates temporary directories in a safe manner). In the end I had to find an open source implementation of this function in FreeBSD and included that in my project.

A few other functions like random() were missing (using rand() instead was sufficient) and some functions like mkdir() took a different number of arguments (again a simple #define to change the arguments was sufficient).

Objective-C Runtime Differences

The Cocotron's Objective-C runtime does not support +load methods at this time. This meant that I had to move all the code that I previously invoked in +load methods into another location. Not really a big difficulty — I registered service handlers for my HTTP server in a different location.

Unimplemented behaviors

The CoreFoundation functions for generating UUIDs — CFUUIDCreate and CFUUIDCreateString — were not implemented. With functions like this, you need to choose: do you add the functionality to Cocotron and submit the changes back to the trunk, or do you just use the Win32 equivalent inline in your code.

With other functions I used that weren't implemented, like SecKeychainFindGenericPassword, I implemented the changes properly in The Cocotron and committed back to the trunk (I feel bad, looking at my additions, that don't match the indentation of the other code).

With UUIDs though, I couldn't be bothered working out if a platform independent or multi-platform solution was easy or possible, so I just slapped the Win32 version into my code:

#ifdef __WIN32__
    UUID uuid;
    UuidCreate(&uuid);
    unsigned char *cString;
    UuidToString(&uuid, &cString);
    uuidString = [[NSString alloc] initWithUTF8String:cString];
    RpcStringFree(&cString);
#else
    CFUUIDRef uuid = CFUUIDCreate(NULL);
    uuidString = (NSString *)CFUUIDCreateString(NULL, uuid);
    CFRelease(uuid);
#endif

I also use an NSStatusBar menu for ServeToMe (a menu item at the right of the menubar).

statusItem = [[[NSStatusBar systemStatusBar]
    statusItemWithLength:NSSquareStatusItemLength] retain];

[statusItem setMenu:statusMenu];
[statusItem setHighlightMode:YES];
[statusItem setToolTip:@"ServeToMe"];
[statusItem setImage:[NSImage imageNamed:@"StatusBarIcon"]];

This wasn't implemented in The Cocotron either.

As with UUIDs, I didn't have the time to integrate a solution properly into a Cocotron implementation of NSStatusBar, implementing this was not nearly as elegant.

NSString *const NSContentArrayBinding = @"contentArray";
NSString *iconPath =
	[[NSBundle mainBundle] pathForResource:@"ServeToMeSmall" ofType:@"ico"];

ZeroMemory(&niData,sizeof(NOTIFYICONDATA));
niData.cbSize = sizeof(NOTIFYICONDATA); // don't bother using features from DLL V5 or later
niData.uID = TrayIconID;
niData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
niData.hIcon = (HICON)LoadImage(
    NULL,
    [iconPath UTF8String],
    IMAGE_ICON,
    GetSystemMetrics(SM_CXSMICON),
    GetSystemMetrics(SM_CYSMICON),
    LR_LOADFROMFILE | LR_DEFAULTCOLOR);
niData.hWnd = (HWND)[(Win32Window *)[[self window] platformWindow] windowHandle];
niData.uCallbackMessage = TrayIconMessage;

// tooltip message
strncpy(niData.szTip, "ServeToMe",
    sizeof(niData.szTip));

BOOL result = Shell_NotifyIcon(NIM_ADD,&niData);
if (!result)
{
    NSLog(@"Shell_NotifyIcon failed: %d", GetLastError());
}

// free icon handle
if(niData.hIcon && DestroyIcon(niData.hIcon))
    niData.hIcon = NULL;

And all of that is just to show the icon! Responding to clicks in the icon and showing a menu, then minimizing the window into that icon to show that the program is still running took a lot more.

It's very strange to include all this Win32 code in the middle of a Cocoa application.

Windows does not support that

My code previously used -[NSFileHandle waitForDataInBackgroundAndNotify]. The Cocotron does not implement this method.

At first I thought it was just a missing implementation that I could add.

Then I learned that Windows can't wait for a notification on an unnamed pipe. The select function or the WaitForMultipleObjects in Win32 won't accept a file handle from an unnamed pipe.

This actually required refactoring my code to use readInBackgroundAndNotify instead. This isn't a huge change but it is a situation where Windows was actually incapable of replicating the exact behavior and changing the program to work in a slightly different way turned out to be the easiest approach.

Obviously, it is possible to implement an abstraction around the whole concept of pipe and file handles to replicate the same functionality but that's a lot of work, and is antithetical to how the rest of The Cocotron works (it aims to be a thin implementation of Cocoa on top of Win32 without heavy abstraction layers).

Windows filesystem

Obviously, hardcoded paths need to change when you move to Windows.

Unfortunately, there are a couple other quirks to handle.

ServeToMe uses lstat and readdir to traverse directories instead of any higher level function (for performance reasons discussed in an earlier post). MinGW provides a stat implmentation for file information but does not provide lstat, so handling Windows Shortcuts would be tricky — I didn't try.

Unfortunately, stat doesn't handle UNC paths. These are Windows network paths for volumes that aren't mounted as drive letters. For this reason, I decided to move to a higher level API.

The correct way to do this under Win32 is probably GetFileAttributesExW. However, it turns out that it was easier to simply revert to the higher level NSFileManager API for getting file system information. This API is fully implemented by The Cocotron, supports UNC paths and worked fairly efficiently in testing.

Aside: the function name GetFileAttributesExW is amusing evidence of the age of Win32 and its efforts to maintain backwards compatibility. The original function, GetFileAttributes is still there but is just about useless — it was deprecated in 1999 — and Win32 never fully committed to Unicode so the function has to point out: Warning! "Wide" chars!

That's right: in some cases, the Windows version of ServeToMe is more Cocoa than the Mac version. But only in some cases.

Time taken

Porting ServeToMe to Windows took me just over 2 weeks. This time breaks down as:

  • Setting up my Windows virtual machine and The Cocotron and getting remote debugging working: 2 days
  • Commenting out code which doesn't work up-front and getting the program to launch: 2 hours
  • Re-adding functionality that was missing or didn't immediately work: 1.5 days
  • Fixing quirks associated with sockets and port mappings: 1.5 days
  • Building or reimplementing 3rd party libraries for Windows: 4 days
  • Sorting out minor bugs and issues: a day or two (so far)

This two weeks is similar to the amount of time it took me to write the first version of ServeToMe from scratch (admittedly, it was a lot less sophisticated).

Does this mean a full-time Windows developer could have reimplemented the whole program in a native Windows language and environment (e.g. C# .Net) in the same time-period? Yes, a really good C# programmer with existing libraries of code to rely on and access to my original implementation could probably have reimplemented rather than ported within the same time period.

The big advantage of using The Cocotron instead is not the up-front time saving of this approach (which would reduce if I gained practice porting projects in this manner) but the fact that all changes I make to the codebase will now appear in both versions Mac and Windows with no extra effort — they are completely in sync. Also, The Cocotron allowed me to stay in a code environment where I'm an expert (Cocoa/Objective-C), rather than dabbling outside my area of expertise where I'm more likely to make serious mistakes.

Conclusion

When I started as a professional developer, I worked as an MFC and Win32 developer at a large company. It's very weird going back to Win32 after 7 years away from it.

Of course, I'm Cocoa programmer now and very comfortable in Xcode. Being able to develop a Windows application without leaving that comfortable environment was a great relief.

It was not without its difficulties though. Setting up a remote debugging environment is frustrating. There are lots of issues that can crop up until you're familiar with the setup.

I certainly encountered missing functionality in The Cocotron but it was all very simple to implement as needed. I even encountered a couple of bugs in The Cocotron but likewise, they were simple to address (no harder than most bugs in my own code).

The final result was not necessarily the fastest path to functionality on Windows but one which achieved maximum shared functionality between the two codebases (both now and moving forward) and only small amounts of work outside of my normal Cocoa skillset.

Design of a multi-platform app using The Cocotron

In this post, I'll talk about how multi-platform applications are structured and talk about some of the different ways that applications separate core logic from platform specific behaviors. I'll talk about how using a porting layer like The Cocotron fits into these designs and why using The Cocotron doesn't necessarily mean that you're ignoring best-practice or creating a second-class application.

Disclaimer: I'm not associated with The Cocotron, I just think it's really cool.

Multi-platform abstractions

In my previous post, I talked about different libraries, APIs and SDKs that can be used to help adapt and link a Cocoa application for running under Windows.

Commenters on that post pointed out that I omitted any discussion of the structural and design considerations in abstracting platform specific behaviors from the core logic of your program.

A simple model of platform abstractions

Generally, the aim is to structure a multi-platform program like this:

drawing.png

The idea is that your program should never need to write any platform specific code outside of the platform implementations. The platform abstracted layer is the layer to which your core application talks. This abstracted layer is then connected to multiple implementations underneath whose responsibility it is to perform the correct platform specific actions corresponding to the abstracted invocations above.

As easy as it is to draw a diagram like this, it says very little about how it will actually work.

The reality is that a high-quality, multi-platform application is dependent upon how well the platform specific implementation works and whether it follows the rules and guidelines of the target platform.

In order that a platform specific implementation correctly behaves as a native application on specific platforms, you need to add further code at the platform implementation layer — in some cases, moving the entire user-interface layout and structure into this layer is required to properly create the look-and-feel for the platform.

However, growing the platform implementation level in this way, you eventually end up with unrelated applications on each platform that just happen to share a common library to perform some internal work.

Have a look at how open source applications like Transmission and VLC are developed: they share a library to handle most of the processing or data manipulation work but the actual application is completely rewritten from platform to platform — all of what the user sees is platform specific. They may be layed out to look similar from platform to platform but this is because they have been separately implemented to replicate each other.

Increasing common functionality between platform implementations

Writing a completely different application for every platform that just happens to share a core library is not a bad way to write a multi-platform application but it takes a huge amount of effort and the platform implementations may tend to drift away from each other in terms of appearance and functionality since they are so different at a user level.

The reality is that you will probably want to forgo some platform distinctiveness for common behavior. A very good abstraction and implementation layer can still adapt this back to the native style of the platform to the point where the user cannot tell the difference.

As an example Firefox choose to forgo all standard platform controls and draw everything themselves. This allows Firefox to use its own user-interface layout and design and expose control over these traits to the core application.

However, early versions of Firefox were critically attacked for not sufficiently mapping its own user-interface design onto the Mac platform. Windows didn't highlight and unhighlight correctly. Popup menus didn't match native popup menus. Drawing speed was poor. The reality was not that abstracting user-interface controls was inherently flawed but simply the platform specific implementation needed to significant work to correctly map the abstraction onto something that meshed with the native platform.

It is not an inherently bad idea to abstract some aspects of the platform implementation but you need to remember that a poor implementation of an abstraction may end up taking more work to fix than the abstraction saved in the first place. Multi-platform is a struggle to decide which functionality should be abstracted and which should be re-implemented.

How does this relate to porting to Windows with Cocotron?

The Cocotron treats Cocoa itself as though it is the platform abstracted layer. It then uses a series of platform-specific class implementations (for example NSTask has a platform specific implementation: NSTask_win32) to map functionality onto target platforms.

This may seem strange if you're accustomed to considering Cocoa as though it is the Mac OS. The reality is though that AppKit and Foundation do not expose much of the underlying operating system at all. In a Cocoa application, you will never write directly to the Window Server, you will rarely use direct hardware access like IOKit and you should never hardcode paths to locations on your hard drive.

A well-written application framework is already an abstraction layer. It needs to be so that the operating system can be updated without breaking all existing programs.

Cocoa is implemented on the Mac upon Apple's Objective-C runtime, the Mach/XNU kernel and ABI, the Mac OS X Window Server and various private frameworks, BSD and POSIX components that comprise the greater operating system.

The Cocotron takes the same Cocoa API but instead chooses to implement it on top of its own runtime, the Windows ABI, GDI drawing and Windows messages, and the the other Win32 DLLs as prepared by MinGW for cross-compilation.

Cocotron's Win32 implementation

Windows messages are drawn out of a standard Win32 WindowProcs, PeekMsg and MsgWaitForMultipleObjects calls. The messages are transformed into the equivalent NSEvents and are rerouted to follow the event routing as required by Cocoa.

All drawing at the AppKit level is implemented using CoreGraphics, which is itself implemented using a set of classes that The Cocotron calls "O2Graphics" of which the platform implementation O2Context_gdi is just a regular GDI HWND and O2DeviceContext_gdi is an ordinary HDC, all of which are drawn using standard GDI drawing commands.

NSFileHandle_win32 uses standard Windows HANDLEs internally (in place of the file descriptors used on the Mac), BSD sockets become WSA sockets, NSFonts become HFONTs and paths are separated by backslashes.

In many respects, it is amazing how seamlessly a high level application framework like Cocoa can be remapped onto a completely different low-level implementation like Win32. Of course, the experience is unlikely to be totally seamless but much of that is due to The Cocotron's work-in-progress status; in a reasonable sized program, you're likely to encounter classes and functions which haven't been implemented yet — or whose implementation has not been fully tested.

A corollary here: don't expect that The Cocotron will let you port your application to Windows without you knowing (or at least learning) some Win32. It is highly likely that you'll need to write at least some of the Windows implementation for your application yourself.

The Cocotron versus WINE

The Cocotron treats the Cocoa frameworks like a platform abstracted layer and reimplement them under Windows.

Isn't this the same thing that WINE does to reimplement Win32 DLLs on Linux?

Yes and no.

Firstly: Cocoa is a higher-level API than Win32. The high level nature of Cocoa makes it easier to abstract from its native environment and reimplement. As I stated before: Cocoa already has an under-the-hood implementation on the Mac and The Cocotron is simply filling in for this. By comparison, Win32 doesn't necessarily have an under-the-hood implementation; in many respects, it is the under-the-hood implementation upon which higher level APIs in Windows are built. WINE must work much harder to allow Win32 applications to run outside of their native environment.

Secondly: you can make a special build of The Cocotron to work in a way that is most appropriate for your application. This allows you to create a higher quality of native application by adding the native behaviors your application needs in its target environment. You can also implement or bug fix features that your application uses; you can make sure there are no problems with your program running under Windows.

Thirdly: You use The Cocotron by compiling and linking against it; it's not simply swapped in place of the native Cocoa frameworks. This means that at compile time, significant portions of the program can be changed and be aware of a Windows environment.

"Native-ness" of the end result

Does the Cocotron appear as a native windows application?

The short answer is: not initially.

The reality is that The Cocotron interprets your XIB files and menus literally — it makes the best attempt to have the Windows application look exactly like the Mac application. The end result is something that looks a lot like a Mac application that has suddenly appeared on Windows.

If you don't mind how the application looks and behaves, then it probably doesn't matter — once the application is functional you can stop.

However, if you're looking to properly integrate as a Windows application, you'll want to generate different XIB files (yes, that's right, you can still use Mac XIB files to create a better Windows application — just change the layout) to rework the interface so that controls are positioned more like Windows applications. At the same time, you can change images, colors and other traits so that the user-interface looks less like a Mac skin.

Rethinking how menus are used on Windows is also an important step: Windows has been slowly deprecating menu bars over the last few years, whereas menus are still an important user-interface feature on the Mac.

You'd probably also want to abstract away traits like "Sheets" (modal dialogs that are stuck to the front of an existing window) since these appear quite foreign on Windows.

You might also want to remove the custom button drawing that The Cocotron uses to simulate the different Mac OS button styles and instead revert to natively drawn buttons where appropriate.

Appropriateness of using The Cocotron for a multi-platform application

The biggest advantage that The Cocotron offers over other means of writing a multi-platform application is simple, low effort porting of simple applications from the Mac to Windows. In this capacity, it is a huge time saver.

Outside this scenario, it is less compelling.

While Cocoa works fairly well as an abstraction layer, it certainly does a number of things that are not appropriate on all platforms and for which some run-around is required.

Further, The Cocotron targets Win32 on Windows. Very few people are writing programs in raw Win32 any more. Few people would want to; it's horrific! This isn't a poor choice made by The Cocotron — it's the appropriate API on Windows for a low-level implementation — but it's not exactly programmer friendly. Extending and expanding bottom level features in Win32 is slow and uncomfortable.

Long story short: The Cocotron is great for porting small Cocoa applications to Windows quickly. It does a good job of that, but for larger programs, for platforms other than Mac/Windows, or for applications that are not predominantly Cocoa, you would probably want to draw the platform abstraction line at a different point and keep AppKit and Foundation out of your application's core.

Conclusion

I didn't intend to have another post full of words rather than code so soon after the last one. It looks like I failed miserably.

However, last week I received numerous emails, and a few comments on the post itself, questioning if using an API like The Cocotron was ever a good idea. The question asked was: surely the best practice is to avoid a porting library like The Cocotron and instead focus on building a platform independent core and then write completely native implementations for each platform?

There's no right-in-all-cases answer.

If your application is already written in Objective-C, then The Cocotron is certainly the fastest way to bring your application to Windows and it is as capable as any other Win32 implementation of delivering a first rate experience.

I'm not pretending that there is no additional work required to make the end result look properly at home on Windows. The Cocotron ports your program somewhat literally. As you'd expect, reinterpretation — in this case, to match the style of the target platform — must always be done by a human and an extra step.

Options for porting Objective-C/Cocoa apps to Windows

There are a few different options for porting Objective-C/Cocoa applications to Windows. Each option has different advantages and offers different capabilities. In this post, I'll give an overview of some of these options, their advantages and disadvantages.

Introduction

In this post, I'll look at the options available for porting Objective-C/Cocoa applications to Windows. There are other ways of writing compiled programs that are designed to be portable across multiple platforms (Qt, WxWidgets, Java, pure command-line C, etc) and numerous virtualized environments that are inherently platform independent (Java, Mono, etc) but this is a Cocoa blog so I'm going to ignore all of these.

A warning: porting software from the Mac to Windows is tricky and this post is not intended to solve those tricky situations. If you decide to port an Objective-C program to another platform, expect to build a lot of software via the command-line, track down bugs in libraries you didn't write and handle weird and cryptic error messages at every stage. Keeping a common codebase across multiple platforms is good in the long-run but can actually be slower than a complete re-write to set up in the first place.

Recreating POSIX on Windows

Before I actually get to discussing porting Objective-C/Cocoa programs to Windows, I need to discuss the target environment. Specifically, the difficulty of getting the POSIX layer from the Mac to run on Windows.

Much of what is considered to be "standard C" on the Mac actually falls outside the minimalist standard C library and is part of the BSD system on the Mac. TCP/IP sockets, many file I/O calls, getting information about the current user; these are all traits of the BSD-derived POSIX layer on the Mac.

The first issue to solve when porting to Windows often involves looking at how to replace this BSD derived functionality. The short answer is that you normally need an API that will offer you POSIX-like behavior on Windows.

Generally, this involves one of two approaches: Cygwin or MinGW.

Cygwin

Cygwin is the better known of the two because it is intended as a user-environment. It aims to recreate a full POSIX system, with full POSIX compatibility on top of Windows. Many ports of X-Windows applications from *nix systems (Linux, UNIX, etc) to Windows run on top of Cygwin because it comes the closest to offering source-level compatibility with *nix systems.

However, Cygwin comes with a disadvantage: if you write your project using Cygwin, then your user must also have Cygwin installed. Cygwin is not really a way to write native Windows applications, it is a way to write native Cygwin applications (and Cygwin runs on top of Windows).

MinGW

The alternative to Cygwin is normally MinGW. MinGW is not a user environment like Cygwin. Although MSYS — a light terminal and user system — can be run on MinGW, generally the MSYS environment is only used by developers for building and testing applications, not by end users.

MinGW uses a different philosophy to Cygwin when it comes to implementing the POSIX subsystem: MinGW attempts to implement those parts of a POSIX system that can be easily mapped onto calls within the native WIN32 API on Windows. This has the huge advantage that your target users do not need to have anything except Windows installed. Unfortunately, it also means that some features (like sockets) will behave slightly differently because they will obey the native Windows behaviors, rather than the standard POSIX behaviors.

Where Cygwin programs are normally compiled on Windows, MinGW includes significant support for cross-compiling so that you can build Windows applications from different platforms.

CoreFoundation

The minimalist approach to compiling an Objective-C program for Windows is to use Apple's own CoreFoundation APIs and Objective-C runtime and compile them for Windows.

CoreFoundation and the Objective-C runtime are both open source under Apple's APSL license:

This solution is not real "Cocoa" (it is only the very simple subset of CoreFoundation and a few supporting frameworks) but it does have the major advantage of being efficient, thoroughly tested and maintained by Apple.

In fact, Apple themselves have a guide on how to use these files to compile on Linux and Windows under Cygwin. You can also look at an alternate setup process on CFLite.

Advantages

CoreFoundation is thoroughly tested and efficiently written. It also offers a high degree of compatibility between Mac to Windows.

Disadvantages

Not really Cocoa; only CoreFoundation (no Foundation or AppKit). Old version of CFNetwork.

Historically, many people have had objections to the specific terms of the APSL. The Free Software Foundation have withdrawn their objections following version 2.0 of the APSL (although they still wish it was a copy-left license, that's an issue for the licensors, not the users of the license).

GNUstep

GNUstep is the oldest open source implementation of OpenStep, which became Cocoa. It is well established and fairly stable. You can build GNUstep applications on Linux, Windows under Cygwin or Visual Studio or on the Mac.

The easiest way to get started with GNUstep on Windows is to use the GNUstep Windows Installer. You will need to install the System, Core and Devel components to begin coding. The advantage is that this will install all the MSYS/MinGW components required. For help with the installation process, there's a futher guide to Installation on Windows.

Advantages

GNUstep is a complete application framework and has been in regular development for over a decade. It will integrate with most development environments and can be built in its entirety on Windows, Linux and many other *nix systems.

GNUstep also has a (relatively) large development community and a huge amount of developer documentation (a real rarity in the world of OpenSource).

Disadvantages

Any application written for GNUstep must be distributed with the GNUstep installer (since the whole GNUstep system must exist to allow functionality).

GNUstep is more like its own operating system (or at least its own windowing system) than a toolkit within an existing operating system. GNUstep does not use native window drawing commands on any platform.

The Cocotron

The Cocotron had its first release in 2006. Its primary goal is to enable cross-compiling from Xcode on the Mac to other platforms (primarily Windows).

This cross-compiling from Xcode is The Cocotron's key defining feature: you must build all your projects in Xcode running on a Mac, even if the deployment targets are Windows or Linux. However, this means that The Cocotron's main pipeline concentrates exactly on the task of porting Mac programs to Windows.

The installation for The Cocotron is a multi-step process but is relatively straightforward.

By default, The Cocotron encourages you to debug your Windows products under Windows using Insight-GDB (a reasonably user-friendly wrapper around GDB running under MinGW) directly on Windows.

An alternative approach for debugging is to use XCXDB (download the XCXDB Installation here). This allows you to remotely debug from Xcode on the Mac to your Windows machine, so you can handle all building and debugging from Xcode. XCXDB has the added advantage that it adds project templates to Xcode to make cross-compiling projects easier to create.

Advantages

Build and potentially debug everything from the original project in Xcode. The framework was designed with porting from the Mac to Windows in mind.

Natively builds for WIN32, meaning that final products look and feel like native Windows applications and the installation is not dependant on installing another large project (AppKit, Foundation and other .DLLs must be included with the executable but they are very small).

Simple, in-built support for NIB files, CoreData, fast enumeration, Objective-C 2.0 properties and other features that are either missing from GNUstep, external packages or still in-progress.

Aims for source level compatibility with Mac OS X, something which is not necessarily a goal of GNUstep.

Disadvantages

The least mature of the options listed so far. It is possible to encounter bugs at low levels that you may need to fix for yourself.

While a huge amount is implemented, you won't have to go far to encounter APIs that are not. Significant portions of some classes are not implemented at all. While the interfaces exist, they will lead to a runtime exception as the program runs over a NSUnimplementedMethod().

The current state of XCXDB remote debugging and Insight debugging are inferior to native debugging in an IDE.

Conclusion

If you're only porting a command-line program to Windows, then you're spoiled for choice — all of the options I've listed will handle that (although CoreFoundation does not offer a full Foundation API).

I chose to implement the current public beta of ServeToMe for Windows using The Cocotron as I think The Cocotron offers the best experience for porting a graphical user-interface Mac application to Windows. The ability to build and debug the entire project from the original Xcode project was the key feature I sought. However, it was not without issues. I'll discuss how I got ServeToMe running in The Cocotron with XCXDB next week, the issues I had and their solutions.

Despite The Cocotron's immaturity relative to GNUstep and many missing APIs, enabling a Cocoa project to be built from the Mac and deployed and debugged on Windows in a single step is a huge advantage. Further, native WIN32 at the back end (as annoying as WIN32 is to write) is better for your end-users than a system that requires Cygwin underneath.

Of course, if you needed to target Linux with an Objective-C application or you need to build on platforms other than the Mac, you'd want to use GNUstep instead. While Cocotron has Linux/X11 support underway, it is not really ready for mainstream adoption at this time.

Network data requirements on iPhone OS devices

If your iPhone OS application makes heavy use of the network, there are a few extra settings your application will require to ensure the network works correctly and Apple will approve your application. These requirements are not always obvious (some are documented, others are only implied in documentation). I thought I'd share them so that you can avoid network dropouts and unnecessary App Store rejections.

Info.plist settings

If your iPhone/iPad/iPod Touch application uses a WiFi network connection at all, you should enable UIRequiresPersistentWiFi (Application uses Wi-Fi) in your application's Info.plist. Without this setting, the device will put its WiFi connection to sleep after 5-30 minutes.

I've mentioned this setting in a previous post but I feel it is worth repeating since it is an easy flag to overlook and it leads to unexpected network dropouts if it isn't set.

There are other Info.plist settings, including the UIRequireDeviceCapabilities (Required device capabilities) value "wifi" which you can enable if you choose but it does not appear to have any effect at this time.

Reachability

If your application runs on WiFi only or 3G only, then you are required to test this and present an error to the user if the wrong network is in use. This is a usability requirement that Apple enforces and your application will be rejected if it isn't followed.

Apple provide the Reachability sample application to show how you can test for network availability. Internally, this class uses the SCNetworkReachability API but it's much easier to borrow the entire Reachability class from this example program and invoke:

if ([[Reachability reachabilityForLocalWiFi] currentReachabilityStatus] == ReachableViaWiFi)
{
    // perform action that requires a local WiFi connection
}
else
{
    // give a message that local WiFi is required
}

A quick warning about this: the WiFi connection can take 10 seconds or more to wake up after the device is woken up from sleep. If WiFi is not available, you may want to set a timer and try again for the next 5-10 seconds before presenting an error.

3G data throttling

If your application uses continuous data over 3G, you are required to throttle this data, rather than perpetually use the maximum available.

What do I mean by this?

As an example: I wrote an application for a client that downloaded music tracks to the phone and then played these tracks from the locally stored cache. The download of the track was not speed limited and Apple initially rejected the application for making excessive use of 3G.

This guideline is somewhat fuzzy. Apple did not give a specific data rate to which the application should limit itself. I do wonder if they didn't realize that the audio wasn't actually streaming but a progressive download.

In any case, throttling the download at 128kbps allowed the application to be approved.

How do you throttle a download? With an NSURLConnection, you can't.

I used the following crude but easy to implement approach. First, I switched from NSURLConnection to a CFReadStream. Then, in the ReadStreamCallback, I tracked the data downloaded over a small time window and if data downloaded exceeded the allowance for that window, simply removed the CFReadStreamRef from the run loop and scheduled a timer to put it back at the end of the windowed allowance period:

CFReadStreamUnscheduleFromRunLoop(
    downloadStream,
    CFRunLoopGetCurrent(),
    kCFRunLoopCommonModes);

NSTimeInterval delay =
    THROTTLE_WINDOW_DURATION * 
    (totalDataDownloaded - startOfThrottleWindow) / THROTTLE_WINDOW_QUOTA;
throttleTimer =
    [NSTimer
        scheduledTimerWithTimeInterval:delay
        target:self
        selector:@selector(continueDownload)
        userInfo:nil
        repeats:NO];

and in the implementation of continueDownload I used CFReadStreamScheduleWithRunLoop to reschedule the download again.

By removing the CFReadStreamRef from the run loop, it doesn't get processed and the connection is throttled. Put it back into the run loop when it's under quota again.

Progressive download limit for 3G video

This limitation is pretty simple: if you are playing an MP4/MOV downloaded from a 3G network, it must be shorter than 10 minutes and a lower data rate than 5MB in 5 minutes (that's about 192kbps). Apple have started rejecting applications that significantly exceed these quoted limits.

The answer is that you are expected to convert any such application over to the new HTTP video streaming approach which can offer a more responsive performance over 3G.

HTTP streaming video 64kbps stream

On the topic of HTTP streaming video, Apple have also added a new requirement that any HTTP video streaming that is intended for use over 3G must offer one stream quality that is 64kbps or less.

Apple suggest that you offer an audio-only stream quality to satisfy this requirement. Personally, I think that 4 frames per second is still better than none if you can find a way to squeeze in the video too. The iPhone doesn't like to play video at frame rates lower than about 2.5fps so you can't go much lower than this.

Conclusion

There are a lot of little quirks to using a mobile, low power or 3G network device and these quirks won't always appear during testing on the simulator or during brief test runs on your office WiFi.

Apple have also added a lot of limits to how you can use 3G. None of this is really heavy-handed on their part — it appears they really are just trying to make sure that 3G connected applications will work in real-world situations — but it does create a collection of extra requirements you need to satisfy.

I hope some of these points are helpful. Every example on this page represents a point that I've had to fix in an application (sometimes during beta but sometimes during App Store submission or after release to customers). The sooner the better — it's always best to know before you start.

StreamToMe is available for the iPad!

StreamToMe is on the App Store for the iPad from day 1, so you can watch video and play music stored on your Mac using your iPad wherever you are.

And to all who are still waiting for the Windows version of ServeToMe... I'm hoping that I'll have a public beta ready in a day or two.