Wednesday, July 13, 2011

Embedding Mono runtime in a Cocoa app

The first step is of course to download Mono.  Be sure to get the SDK.  For more details read this page.

I opted to create a dynamic library that initializes Mono.  This allows for reusing the embedding logic across Cocoa applications.  Modify your main.m as below.

extern void InitMono(NSString *exeName);
int main (int argc, char *argv[]) 
{ 
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   InitMono(@"MacMonoClient.exe");
   [pool drain];
   return NSApplicationMain(argc, (const char **)argv);
}

The dynamic library InitMono then initializes the Mono runtime.  The directory structure is described below. 

#import <Cocoa/Cocoa.h>
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/mono-config.h>
#include <mono/metadata/mono-debug.h>
#include <mono/utils/mono-logger.h>

void InitMono(NSString *exeName)
{
   NSString *mPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents/Libraries/lib"];

   MonoDomain *domain;

   NSString *libraryPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents/Libraries/"];

   NSString *sampleAssemblyPath = [libraryPath stringByAppendingPathComponent:exeName];

   // optionally set a mono config file to use
   // NSString* configFile = tempFileName;
   // mono_config_parse ([configFile UTF8String]);

   // optionally set DEBUG and trace levels
   // mono_debug_init (MONO_DEBUG_FORMAT_MONO);
   // if (getenv ("PMONO_TRACE_LEVEL") != NULL)
   //   mono_trace_set_level_string(getenv ("PMONO_TRACE_LEVEL"));

   mono_assembly_setrootdir([mPath UTF8String]);
   domain = mono_jit_init ([sampleAssemblyPath UTF8String]);
   MonoAssembly *monoAssembly = mono_domain_assembly_open(domain, [sampleAssemblyPath UTF8String]);

    char *pExeName = strdup([exeName UTF8String]);
    char *argv[1] = { pExeName };
    mono_jit_exec (domain, monoAssembly, 1, argv);
    free (pExeName);
}

Use pkg-config to determine what "Other Linker Flags" and "Other C Flags" to set.

$ pkg-config --cflags mono-2
-D_THREAD_SAFE -I/Library/Frameworks/Mono.framework/Versions/2.8.2/include/mono-2.0  
$ pkg-config --libs mono-2
-pthread -L/Library/Frameworks/Mono.framework/Versions/2.8.2/lib -lmono-2.0 -lpthread

Change "Dynamic Library Install Name" to be @loader_path/../Libraries/InitMono.dylib.

Once you compile your dynamic library run otool -L on the library:

$ otool -L InitMono.dylib
InitMono.dylib:
 @loader_path/../Libraries/InitMono.dylib (compatibility version 1.0.0, current version 1.0.0)
 /Library/Frameworks/Mono.framework/Versions/2.8.2/lib/libmono-2.0.1.dylib (compatibility version 2.0.0, current version 2.0.0)
 /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.4)
 /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 12.0.0)
 /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
 /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 227.0.0)
 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 476.19.0)
 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 677.26.0)

For embedding this has to be changed.  The InitMono dynamic library is loading libmono-2.0.1.dylib from Mono.framework.  I have created a run script build phase:


install_name_tool -change /Library/Frameworks/Mono.framework/Versions/2.8.2/lib/libmono-2.0.1.dylib @loader_path/../Libraries/libmono-2.0.1.dylib $TARGET_BUILD_DIR/$PRODUCT_NAME.dylib

The next step is to modify the Xcode project for your Cocoa app so that it adds the needed DLLs and libraries to fully embed the Mono runtime.  Create a folder in your Xcode project named Mono.  Copy libmono-2.0.1.dylib and InitMono.dylib into that folder.  Add InitMono.dylib so that your Xcode project links with this library.

Add a copy files build phase.  I have set Destination to Executables and Subpath to ../Libraries.  This will create a folder structure like this:

Cocoa.app
   Contents
      Libraries
      MacOS
      Resources

Libraries will contain all the DLLs as well as the files needed to embed Mono.  Add both InitMono.dylib and libmono-2.0.1.dylib to this build phase.

Add another copy files build phase.  Set Destination to Executables and Subpath to ../Libraries/lib/mono/2.0.  Add mscorlib.dll to your Xcode project from /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/2.0.

I found it easier to have the InitMono code load an executable.  For MacMonoClient.exe I have this code below.  Using either mobjc or MonoMac this will cause the C# libraires to register with the Cocoa runtime.

public static void Main(string[] args)
{
#if MOBJC

    Registrar.CanInit = true;

#elif MONOMAC

    NSApplication.Init();

#endif


    Session.initialize();

}


Add MacMonoClient.exe and your main DLLs (and either the MonoMac or mobjc dlls) to the copy build phase for your Xcode project.  At this point you should be able to run your app.  However, it is not fully embedded.  All of the system DLLs will still be referenced from the Mono.framework folders.

One method to determine what DLLs you need to include is using Activity Monitor.  While your app is running click on Inspect, then click on Open Files and Ports.  Look for DLLs that are located in the /Library/Frameworks/Mono.framework folder.  All of these DLLs must be added to the Xcode project and the copy build phase that places them in the Libraries folder.

I recommend you add I18N.West.dll and I18N.dll even if they do not show up while running.

No comments:

Post a Comment