Sunday, February 25, 2007

C Sharp, Unmanaged DLLs and Python Avoidance

First off, let me say that I'm not anti-python. Far from it. I think Python is a very interesting language and has a great deal of use in game development.

However, trying to build visual tools for it sucks. There are some decent alternatives out there for GUI builders for python, like boa, but none of them have the level of ease of use as Dev Studio. And I've never been sold on QT, especially the licensing agreement that they've created for us windows users (it's honestly like they're purposefully making it hard for us because we're windows users).

So I've been playing around with CSharp, for tool development. And I've got to say, I like it a lot. I'm still getting the language intrinsics down, but so far, it's been a nice, easy move.

However, a lot of our internal tools at HotHead are done using unmanaged DLLs and Swig bindings to Python. Again, this isn't bad. It's just different.

What I want to do is use the existing DLLs that our asset pipeline uses via Python bindings, but with CSharp. And it's relatively painless. So I thought I would share my experience here.

Please note, there isn't any work related code in this example. This was all done at home, on my own time, yadda yadda legal mumbo jumbo.

First off, I needed to create a DLL that I would use as my test bed. Using Dev Studio 2005, creating a DLL project automatically injects the appropriate Managed code bindings. I don't want that, so I had to prune them out. What I was left with was the following source file:

// This is a simple DLL that can be invoked from any language
// supporting standard DLL exports
#pragma warning(disable: 4996)

static char *buffer;

char* __stdcall DecorateString(unsigned int level, char* srcString)
{
   switch( level )
   {
      case 0:
         sprintf(buffer, "{Warn} %s\n", srcString);
         break;
      case 1:
         sprintf(buffer, "{Error} %s\n", srcString );
         break;
      default:
         sprintf(buffer, "{Undefined} %s\n", srcString );
         break;
   }
   return;
}

BOOL __stdcall DllMain(HINSTANCE hInst, DWORD dwReason, LPVOID resvd)
{
   switch(dwReason)
   {
      case DLL_PROCESS_ATTACH:
         buffer = new char[1000];
         memset(buffer,0,1000);
         printf("CPPDLL loaded\n");
         return TRUE;
      case DLL_PROCESS_DETACH:
         delete [] buffer;
         printf("CPPDLL unloaded\n");
         return TRUE;
      default:
         return FALSE;
   }
}

It's nothing more than a DLL function that allows me to decorate a string with either a '{Warn}' or '{Error}' string, depending on the value passed in on the first argument. This could have done other fancy things, like colour coded the output, but you get the idea.

Additionally, I'm going to need a module definition file for this project, defining the exported functions. It looks like this:

LIBRARY "CPPDLL"
EXPORTS
   DecorateString @1
   DllMain @2

Finally, I need a CSharp application that uses this. I'm essentially going to create a default CSharp project and add some small bits of code to it. I won't go into the details of creating the form, since all I did was run the default project wizard. The code that actually allows me to do the binding to the dll file looks like this:

using System;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Windows.Forms;

namespace cppdlluser
{
   static class Program
   {
      [DllImport("cppdll.dll")]
      [return:MarshalAs(UnmanagedType.LPStr)]

      public static extern String DecorateString(int level, String msg);

      ///  The main entry point for the application.
      ///  [STAThread]
      static void Main()
      {
         Console.WriteLine(DecorateString(0, "This is a warning."));
         Console.WriteLine(DecorateString(1, "This is an error"));
         Application.EnableVisualStyles();
         Application.SetCompatibleTextRenderingDefault(false);
         Application.Run(new Form1());
      }
   }
}

The form that is created doesn't do anything, but the output spew in the debug console shows the following:

   The thread 0x8c4 has exited with code 0 (0x0).
   The thread 0xe64 has exited with code 0 (0x0).
   The thread 0xb70 has exited with code 0 (0x0).
   'cppdlluser.vshost.exe' (Managed): Loaded 'D:\development\CSharp\CPPDLLFromCSharp\cppdlluser\bin\Debug\cppdlluser.exe',     Symbols loaded.
   {Warn} This is a warning.
   {Error} This is an error
   CPPDLL loaded
   CPPDLL unloaded
   The thread 0xae8 has exited with code 0 (0x0).
   The thread 0xb54 has exited with code 0 (0x0).
   The thread 0x150 has exited with code 0 (0x0).

That's pretty much all there is to accessing functions from CSharp. I'm now off to try this using some of our production tool dlls. More on that later.