Tuesday, May 25, 2010

DLL wrapper for Ruby 1.9 in Windows

I found myself in a situation where I had to access a Windows DLL to control some external PC peripheral. Hey I thought, this is a good opportunity to hone my Ruby skills.

At first I used Ruby's Win32API. It works fine, provided that the dll function does not return a pointer. For example, if the DLL function is

char * controlx (int a, int b, some_struct * c)

In ruby you can process arguments a, b, and even c, but I almost vomited blood trying to process the return pointer. The thing is, Ruby do not use pointers ! This makes it very tricky to process return pointers from functions.

I was about to use plain old C to do the job, or maybe even Python (I got a Python friend - or fiend - who is telling me that this is no prob in Python, even though Python doesn't use pointers too). Then I googled upon the SWIG project (www.swig.org).

It do me a while to get SWIG working for Ruby. SWIG is pretty well documented, but the below gives the juice of the process and some gotchas. SWIG essentially generates a Ruby wrapper module (in c) for the DLL.

To save hassles, get the same c compiler and environment that created the Ruby windows version you are using. Ruby 1.9 uses MINGW compiler (basically an open source Windows C compiler that uses the Windows libraries, unlike CYGWIN that uses the POSIX libraries). You also need the make program. So I installed MINGW and MSYS, which gives a POSIX-like shell with the essential make program and other usual shell goodies. I think it may be possible to use Microsoft Visual C and nmake.exe (same Windows library calls right?), but I didn't to bother to try that.

You have to create a interface.i file that lists down the DLL functions. It looks like this.

%module your_desired_ruby_name
%{
/* Put header files here or struct declarations that are needed by the functions below */
%}

%feature("autodoc", "1"); /* this is for Ruby's rdoc */
extern char * DLL_function1(void);

%feature("autodoc", "1");
extern bool DLL_function2(void);
...............

Then just execute

> swig -ruby interface.i

or for c++

> swig -c++ -ruby interface.i

SWIG is supposed to be able to wrap both C and C++ functions for Ruby. However when I tried it with the c++ option, the generated c++ wrapper code is not compilable. No problem, I just changed all the bool types in my interface to char, and I had switched to a nice C interface. Anyway why would anyone export C++ functions in a DLL? DLLs are meant to be called from any language.

SWIG generates a c program named interface_wrapper.c. This is just a Ruby c module. So you can now use the standard Ruby ways to compile the module. You can either go the gcc way, or use Ruby built-in mkmf class. mkmf is recommended because it handles all the gcc settings for your Ruby installation (mostly the include dirs etc).

To use mkmf, create a extconf.rb file that looks like this

require 'mkmf'
$libs = append_library($libs, "your_dll_name")
create_makefile('your_ruby_module_name')

Now execute

>ruby extconf.rb

This creates a Makefile in the local directory

>make

This compiles the wrapper Ruby module. Put the DLL in the same directory as the wrapper c code to avoid any linking problems. Then just

>make install

Viola! Your Ruby wrapper module had been compiled and copied to your Ruby installation. In my case it is C:\Ruby19\lib\ruby\site_ruby\1.9.1\i386-msvcrt

To use your DLL functions, just fire up irb or type in a ruby program

irb> require 'your_module_name'
irb> YOUR_MODULE_NAME_IN_CAPITAL.dll_function_xxxx

Oh yeah one last thing. Use rdoc to generate some nice documentation of what the DLL input/output parameters are.

rdoc -f html interface_wrap.c