Thursday, August 11, 2011

.NET LibCurl using pure C#

I needed to compare the performance of libcurl against NSUrlDownload in my MonoMac application.  I searched for a .NET implementation of libcurl and ran across http://sourceforge.net/projects/libcurl-net/.  I was disappointed when I realized this required a C library.

Of course I decided to create a pure C# implementation. This was my goal for the implementation. See the project here.
            using (Curl c = new Curl())
            {
                CURLcode ret = c.SetUserAgent("MyUserAgent");
                ret = c.SetUrl(urlToDownload);
                
                c.OnWriteCallback = c_OnWriteCallback;
                c.OnProgressCallback = c_OnProgressCalback;

                using (FileStream downloadDestFile = new FileStream(downloadDestPath, FileMode.Append))
                {
                    c.SetWriteData(downloadDestFile);
                    c.SetProgressData(fileInfo);

                    ret = c.Perform();
                }
            }

       int c_OnWriteCallback(byte[] buffer, int size, object userdata)
       {
            FileStream downloadDestFile = (FileStream)userdata;
            downloadDestFile.Write(buffer, 0, size);

            return size;
       }

The first task was understanding what the C library was being used for. One of the main uses was for the libcurl callbacks. So how do you get a C library to invoke a method within your C# code. I read a few articles on this subject. Here is one. All were convoluted in some way. I wanted something streamlined.
// this matches the C method signature
private delegate int _GenericCallbackDelegate(IntPtr ptr, int sz, int nmemb, IntPtr userdata);

[DllImport(m_libCurlBase, CallingConvention = CallingConvention.StdCall)]
private static extern CURLcode curl_easy_setopt(IntPtr pCurl, CURLoption opt, _GenericCallbackDelegate callback);
...
   _GenericCallbackDelegate cb = new _GenericCallbackDelegate(internal_OnWriteCallback);
   cbHandle = GCHandle.ToIntPtr(GCHandle.Alloc(cb, GCHandleType.Pinned));
   curl_easy_setopt(m_pCURL, CURLoption.CURLOPT_WRITEFUNCTION, cb);
...
   int internal_OnWriteCallback(IntPtr ptrBuffer, int sz, int nmemb, IntPtr ptrUserdata)
   {
   }

The solution is to pin the callback object so it can be found by the C library. So now curl invokes its write function and that will callback into internal_OnWriteCallback. The full implementation is like this.
IntPtr GetHandle(object obj)
{
   if (obj == null)
      return IntPtr.Zero;
   return GCHandle.ToIntPtr(GCHandle.Alloc(obj, GCHandleType.Pinned));
}

public delegate int GenericCallbackDelegate(byte[] buffer, int size, object userdata);
public GenericCallbackDelegate OnWriteCallback
{
   set
   {
      if (_OnWriteCallbackHandle != IntPtr.Zero)
      {
         FreeHandle(ref _OnWriteCallbackHandle);
         _OnWriteCallback = null;
      }
      if (value != null)
      {
         _OnWriteCallback = value;
         _GenericCallbackDelegate cb = new _GenericCallbackDelegate(internal_OnWriteCallback);
         _OnWriteCallbackHandle = GetHandle(cb);
         curl_easy_setopt(m_pCURL, CURLoption.CURLOPT_WRITEFUNCTION, cb);
      }
   }
}

int internal_OnWriteCallback(IntPtr ptrBuffer, int sz, int nmemb, IntPtr ptrUserdata)
{
    if (_OnWriteCallback != null)
    {
        int bytes = sz * nmemb;
        byte[] b = new byte[bytes];
        Marshal.Copy(ptrBuffer, b, 0, bytes);

        object userdata = GetObject(ptrUserdata);
        return _OnWriteCallback(b, bytes, userdata);
    }
    return 0;
}

When internal_OnWriteCallback is called from the C library we turn around and invoke the C# delegate.

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.