What is Marzipan?
Marzipan is a technology that makes it easier to run managed (.NET) code in your native Mac (Cocoa) applications by embedding Mono.
Marzipan is in early development, and was mainly conceived due to the need to host Mono and the managed compiler inside Fire, a project currently under development here at RemObjects. Marzipan is made to do exactly what is needed by Fire, and not much more. That said, it is usable, and we want to make it available for everyone to use. Feedback and contributions are appreciated, and we plan to improve this project moving forward.
Mono is made for embedding, but traditionally, interaction between the native and managed side has been clumsy, with an awkward C-based API. Marzipan fixes this by providing wrapper classes that allow you to (a) interact with the Mono runtime using object oriented APIs and more importantly, (b) interact with your ‘’own’’ classes directly.
Marzipan comes in two parts. ‘’’Part one’’’ is a native Cocoa library you can link into your app that makes it easy to embed Mono, launch up an instance of the Mono runtime, and also takes care of a lot of the background tasks for making everything work. ‘’’Part two’’’ is a code generator that takes your managed .dlls and a small config file that describes what classes you want to expose and generates Cocoa source files for wrapper classes you can use in your native app.
Right now, this second part generates Oxygene or RemObjects C# code, but eventually, we will expand it to support generating Objective-C and Swift code as well.
The following requirements exist to run and use Marzipan:
- a 64-bit version of Mono to be either installed globally on the Mac or embedded into your .app (the latter requires a bit of manual work, described below)
- Elements (because as mentioned above, we currently only generate Elements code, no Objective-C or Swift)
Note that currently, Mono only ships as 32-bit version. Since you’re
most likely (especially if you’re using Elements) building 64-bit Mac
apps, you will need to manually build Mono for 64-bit. It’s pretty easy,
and described here,
but it comes down to four simple command line steps to run in Terminal
after you check out mono from git
someplace" with a path of your choice):
./autogen.sh --prefix=/someplace/Mono --disable-nls make make install install_name_tool /someplace/Mono/lib/libmono-2.0.dylib -id @loader_path/../Frameworks/libmono-2.0.dylib
The last part is only needed if you want to later embed Mono into your .app bundle (which is recommended if you actually want to ship your app to users without them needing to have Mono installed themselves).
The next step is to have some .dll(s) with managed code that you want to
expose to your native app, and to create a small .xml config file that
describes what you want to export. Note that Marzipan is pretty good at
marshaling stuff, but there are limitations to what it can do. In
general, most classes that don’t do anything too awkward should export
fine and be usable. If your classes expose other classes as properties
(or expect them as parameters), make sure to include all the classes in
your export config. Any class not exported by Marzipan will be shown
as a black box
MZObject type when used as parameter or result.
An example XML config looks something like this (this is taken from Fire):
<?xml version="1.0" encoding="utf-8"?> <import> <namespace>RemObjects.Fire.ManagedWrapper</namespace> <outputfilename>FireManagedWrapper\ImportedAPI.pas</outputfilename> <outputtype>Oxygene</outputtype> <libraries> <library>..\..\Bin\RemObjects.Oxygene.Tools.dll</library> <library>..\..\Bin\RemObjects.Oxygene.dll</library> <library>..\..\Bin\RemObjects.Oxygene.Fire.ManagedHelper.dll</library> ... </libraries> <types> <type> <name>RemObjects.Oxygene.Fire.ManagedHelper.LogLevel</name> </type> <type> <name>RemObjects.Oxygene.Fire.ManagedHelper.XBuilder</name> </type> <type> <name>RemObjects.Oxygene.Code.Compiler.CodeCompletionCompiler</name> </type> ... </import>
Essentially, you specify the namespace and language type; valid right now are “Oxygene” and “Hydrogene” (for RemObjects C#) to use at the top, followed by the list of .dlls and then the list of types. It does not matter what language or compiler those .dlls were compiled with, as long as they are pure IL assemblies.
You then run MarzipanImporter.exe against this file (you can run it
mono MarzipanImporter.exe on the Mac, if you wish), and the
result will be a .pas or .cs file with Cocoa wrappers for all the
classes and types you specified.
Don’t worry about the details of the implementation for these classes — they will look pretty messy, because they do a lot of C-level API fiddling to hook everything up. What matters is the public API of these classes — and you should see all your methods and properties.
You can now add this file to your Mac .app project, add a reference to libMarzipan, and you're ready to use it.
Start by adding “RemObjects.Marzipan” to your uses/using clause (or ‘’import’’ing it or the respective libMarzipan.h header file in Swift or Objective-C).
Next, you will want to initialize the Mono runtime and load your dlls. All the following code snippets are RemObjects C#, but the same principles apply no matter what language you use:
var fRuntime: MZMonoRuntime; // class field ... fRuntime := new MZMonoRuntime withDomain('MyApp') appName('"MyApp') version('v4.0.30319') lib(/path/to/mono/etc') etc(/path/to/mono/etc');
MZMonoRuntime _runtime; // class field ... _runtime = new MZMonoRuntime withDomain("MyApp") appName("MyApp") version("v4.0.30319") lib("/path/to/mono/etc") etc("/path/to/mono/etc");
var _runtime: MZMonoRuntime // class field ... _runtime = MZMonoRuntime(domain: "MyApp", appName: "MyApp",version: "v4.0.30319", lib: "/path/to/mono/etc", etc:"/path/to/mono/etc")
Rather than hardcoding the paths, you will probably determine them at
runtime — for example by looking into your bundle to find the embedded
Mono folder in its resources (see below). You will want to hold on to
_runtime instance in a global object, so that it does not get
released. That said, once a runtime was instantiated, you can also
always access it globally via
Next, load in the .dll or .dlls that contain your code, as well as any dependent .dlls that won’t be found on their own:
Once again you’ll probably want to determine the paths dynamically.
Finally, as the very last step, you need to make sure to attach Mono to each thread that you want to use it on. If all your code is on the main thread, just call this once; if you create threads or use GCD queues, you’ll need to call it at least once (you can call it again without harm) the first time you call into managed code on any given thread.
Keep in mind that GCD queues will use random/new threads. Even serial queues do not always use the same thread for each block.
And with that, you’re set up and ready to use your own classes as imported. Just new them up (or alloc/init them up) as you always do and call their methods as if they were native Cocoa classes.
Building your .app
There are a couple of items to note for building your .app:
- Most likely, you'll want to embed the Mono folder into your bundle as resource. Just add it to your project. In Visual Studio or Fire, set the build action to "AppResource". In Xcode, make sure to add it as "Folder Reference" (it will show up as blue folder icon, not yellow) and add it to the Copy Files build phase, alongside your other resources.
You will need to link against libmono-2.0.dylib (or
libmono-2.0.fx) and have libmono-2.0.dylib copied into your
app bundle into the
Frameworksfolder. In Xcode, you will need to create a new build phase for it. In Visual Studio or Fire, just set the build action to AppFramework after adding the file to the project (you'll want to add both the .fx file as reference and the .dylib file as resource with the AppFramework build action). Make sure to use the version of libmono-2.0.dylib that's part of your actual Mono build, as the versions need to match.
- You will also need to embed all your .dlls to be packaged into the resource folder, as well (just as regular AppResource file resources).
You can use the following code to locate the Mono folder at runtime for
passing to the
new MZMonoRuntime ... call shown above:
var lMonoPath := NSBundle.mainBundle.pathForResource('Mono') ofType('');
var monoPath = NSBundle.mainBundle.pathForResource("Mono") ofType("");
let monoPath = NSBundle.mainBundle.pathForResource("Mono", ofType: "")
The same works for locating your .dlls:
var lMyDll := NSBundle.mainBundle.pathForResource('MyManagedAssembly') ofType('dll') inDirectory('');
var myDll = NSBundle.mainBundle.pathForResource("MyManagedAssembly") ofType("dll") inDirectory("");
let myDll = NSBundle.mainBundle.pathForResource("MyManagedAssembly", ofType: "dll", inDirectory: "");