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.