I had a need to do some automation in Windows. AutoIt is an excellent tool for the job. It comes with its own scripting language and, more importantly, a well documented DLL interface called AutoItX. Why settle for learning a new, limited-scope, scripting language when you can build a Ruby Extension to wrap the API? On top of getting all of AutoIt’s automation functions, I get everything that comes with Ruby: Modules, classes, functions, mix-ins, inheritance, and access to hundreds of pre-written libraries.
I’m going to be using SWIG to semi-automatically build the C extension code that will wrap the AutoItX DLL. SWIG can take a chunk of C/C++ code and generate the C/C++ code necessary to build support in Ruby. It has support for several other dynamic languages as well and it is actually rather useful.
Pre-requisites
Remember that this is going to be built on Windows. We’re going to run into some snags because of this. Setting up the build environment is actually going to take more time than writing the extension with SWIG. Download and install the following…
SWIG’s Windows installer isn’t really an installer at all. I extracted it into C:\Program Files\SWIG. I and then added that to my PATH in my Makefile, which we’ll do shortly.
Set up the project directory
In my project directory at My Documents\Projects\Autoit I have the following.
AutoIt.h # Copy from C:\Program Files\AutoIt3\AutoItX\StandardDLL\VC6
AutoItX3.lib # Copy from C:\Program Files\AutoIt3\AutoItX\StandardDLL\VC6
AutoItX3.dll # Copy from C:\Program Files\AutoIt3\AutoItX
# We will make the following three files
autoit.i # SWIG configuration file
extconf.rb # Ruby file to create our Makefile
Makefile # Will be generated by extconf.rb
Modify Ruby’s config.h
The one-click install of ruby is compiled with Visual Studio 6.0 which I would be surprised if you were using (it was released 10 years ago). I do not know why the One-click Windows Rubyists decided to go with Visual Studio 6.0 and I really don’t care. I’ve seen pretty dumb compatibility arguments from Rubyists before and I don’t feel like reading through another one.
This won’t hurt us until we try to compile our extension, but let’s take care of it now. Open up C:\ruby\lib\ruby\1.8\i386-mswin32\config.h. Right at the top you see this.
#if _MSC_VER != 1200
#error MSC version unmatch
#endif
This says, “If you’re not using VC 6.0 bail.” Why? I don’t know. Comment it out.
Many thanks to anelson and his blog The Totally Bullshit Ruby Extension Experience on Windows. He helped me with the config.h problem and the writing of much of this guide.
Create the SWIG configuration file
You can check the SWIG documentation for more details, but the syntax is rather easy. Put this in a file called autoit.i in your project directory.
%module autoit
%include typemaps.i
%inline %{
#include "AutoIt3.h"
%}
#define WINAPI // Only needed for AutoIt3.h
long WINAPI AU3_Run(const char *INPUT, const char *INPUT, long nShowFlags);
%include "AutoIt3.h"
Here’s a quick run down of the SWIG source.
%module% autoit defines the name of the resultant module. This means that eventually we will include our module with a call of require 'autoit'.
%include typemaps.i is required for the INPUT macros you see below in the AU3_Run line. In C-style programs you will often see pointers used for function input and output. The typemaps.i macros will take care of making this work in Ruby for you. In very simple terms, you will want to do the following.
- If you see a pointer used for function input, change the function prototype to use
INPUT
- If you see a pointer used for function output, change the function prototype to use
OUTPUT
- If you see a pointer used for function input and output, change the function prototype to use
INOUT
You can read more about these macros in the SWIG documentation for Ruby.
The %inline block of code will insert everything between %{ and %} verbatim into the resultant extension code. Here, we’re making sure that the extension code includes AutoIt3.h.
#define WINAPI was included because there are WINAPI macros all over the AutoIt3.h file. This lets us %include "AutoIt3.h" later without modifying it.
The AU3_Run line is an example of how to use the INPUT SWIG macro. The first two parameters are input pointers, so I have changed the name of the variable to be INPUT.
Finally, %include "AutoIt3.h" copies the contents of AutoIt3.h into our SWIG file verbatim, much like how #include works in C. This saves us from having to manually type out all the function prototypes in AutoIt3.h.
Run SWIG part of our build by running this in the Platform SDK command prompt that we modified previously.
swig -c++ -ruby autoit.i
This should not report any errors and create a file called autoit_wrap.cxx in your project directory. You must do this before you create and run the Makefile we generate below. If you don’t have any C/C++ files in your project when you generate the Makefile, your project will be very boring and very empty.
Set up mkmf configuration file
Create a file called extconf.rb in your project directory and put in the following. I got this code from Peter Cooper’s guide to creating a Ruby C Extension and then modified it.
extconf.rb will create a file Makefile in the project directory. You should be able to just run nmake from here to build the Ruby extension, but because we’re working in Windows and not *nix we have to take some extra steps. Again, you can read all about Ruby’s problems with building extensions in Windows in the blog post The Totally Bullshit Ruby Extension Experience on Windows. Basically, the Makefile generated by mkmf does NOT work on Windows. The documentation for mkmf isn’t very helpful either. For Windows, we need to add a .manifest file into the final autoit.so output. For my project, we need to add the SWIG program directory to our build path, add a dependency line for autoit_wrap.cxx, and add AutoItX3.lib as library dependency.
I found that the best way to embed the .manifest file is to edit the mkmf source code. You can modify the Makefile that is generated, but if you change the source code you’ll never have to worry about it again. If you used the Ruby One-click installer, mkmf.rb can be found at C:\ruby\lib\ruby\1.8\mkmf.rb.
# Line 1324
mfile.print "$(RUBYARCHDIR)/" if $extout
mfile.print "$(DLLIB): ", (makedef ? "$(DEFFILE) " : ""), "$(OBJS)\n"
mfile.print "\t@-$(RM) $@\n"
mfile.print "\t@-$(MAKEDIRS) $(@D)\n" if $extout
# link_so = LINK_SO.gsub(/^/, "\t")
# mfile.print link_so, "\n\n"
# Add the lines below
link_so = LINK_SO.gsub(/^/, "\t")
mfile.print link_so, "\n"
mfile.print "\tmt.exe -manifest $(DLLIB).manifest -outputresource:$(DLLIB);2\n" if $mswin
mfile.print "\n\n"
This will add mt.exe -manifest $(DLLIB).manifest -outputresource:$(DLLIB);2 to the Makefile dependency for our output .so file if we’re building for Windows.
Below is the code for extconf.rb.
# Loads mkmf which is used to make makefiles for Ruby extensions
require 'mkmf'
# Give it a name
extension_name = 'autoit'
# The destination
dir_config(extension_name)
# Add autoitx.lib
$libs = append_library($libs,"AutoItX3")
# Do the work
create_makefile(extension_name)
# For SWIG
File.open("Makefile", "a") do |mf|
mf.puts "PATH=$(PATH);C:\\Program Files\\SWIG"
mf.puts "\n\n"
mf.puts "autoit_wrap.cxx: autoit.i\n"
mf.puts "\tswig -c++ -ruby autoit.i\n"
end
The lines I modified are just for my project. Explanations follow.
The line $libs = append_library($libs,"AutoItX3") is something I gleaned off of the SWIG Ruby documentation. This tells the Makefile to compile AutoItX3.lib into our project.
The last chunk of code after # For SWIG both adds the SWIG directory to the build PATH and adds a dependency for autoit_wrap.cxx. This way if I modify autoit.i the file autoit_wrap.cxx will automatically be regenerated by nmake.
Generate Makefile using mkmf
Run the command line shortcut for Windows XP 32-bit that was installed into the Start menu by the Platform SDK. This is in Microsoft Windows SDK 6.1 -> CMD Shell. Run the following to generate the Makefile.
ruby extconf.rb
Build the extension library
Now in the Platform SDK command prompt run this.
nmake
If you crossed your fingers hard enough you will end up with a autoit.so file in your project directory. You WILL get errors from the compiler about depreciated and unknown options. Blame this on mkmf, but the build should still work.
Install the library
Cross your fingers again and run this from the command prompt.
nmake install
This copies autoit.so to Ruby’s Windows library path. With the one click install defaults that would be c:/ruby/lib/ruby/site_ruby/1.8/i386-msvcrt/autoit.so
Copy AutoitX3.dll into a system path
Because our Ruby library depends on AutoItX3.dll, Windows needs to be able to find AutoItX3.dll. It can either be somewhere in your PATH or in the current directory of your Ruby script. I didn’t really care about dirtying up my system, so I copied it into C:\windows\system32 and forgot about it.
Test the extension with irb
C:\>irb
irb(main):001:0> require 'autoit'
=> true
irb(main):002:0> Autoit.AU3_Run "notepad.exe", "", 1
=> 912
irb(main):003:0>
This should open up Notepad. Magic!
Troubleshooting
If Windows cannot find AutoItX3.dll, Ruby will error out with the following uninformative error.
c:/ruby/lib/ruby/site_ruby/1.8/i386-msvcrt/autoit.so: 126: The specified module could not be found. - c:/ruby/lib/ruby/site_ruby/1.8/i386-msvcrt/autoit.so (LoadError)
from c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
from MyTest/autoit.rb:1
Make sure AutoItX3.dll is in a system path or in the current directory.
If the manifest was not correctly embedded you will get errors about not being able to find MSVC80.dll when we finally run our extension. Ruby itself will error out with a LoadError exception. If you run the script from a command prompt, Windows will popup complaining about missing MSVC80.dll. This happens even if you do have the MFC 8.0 run times installed.
If running nmake ends without compiling anything, you more than likely generated the Makefile before any source files were in your project directory. Run swig -c++ -ruby autoit.i to generate autoit_wrap.cxx and then run nmake again.
Currently, I cannot get OUTPUT parameters to work. The solution to this lies in SWIG typemaps. I’m going to write up another post about that.
Wrap the wrapper (optional)
You may not like the interface that the dll gives you. You can of course write wrappers around the extension to make things easier on yourself. For example.
module Autoit
def Autoit.run(file,directory="",flag=1)
Autoit.AU3_Run(file,directory,flag)
end
end
# These two are now equivalent
Autoit.AU3_Run("notepad.exe", "", 1)
Autoit.run "notepad.exe"
A whole bunch of convenience functions and error handling can all be handled in Ruby itself. You barely have to know any C/C++ to make working with the third party library much more convenient. For example, the AutoIt API will set an error flag to 1 if a function fails. You can retrieve the value of the error flag by calling AU3_Error. I can write wrappers that will check this error code and throw a Ruby exception if it is set. This saves me from having to manually check the error code every time. Neato.
Conclusion
I hope this helps anyone that might be trying to wrap third party DLLs into a Ruby extension. This is not a very simple process and there is a lot of work to be done just to get to the point of creating the Ruby module library. You’ll have to deal with several things not being written with Windows in mind: SWIG not having a real installer, mkmf not creating a very useful Makefile, and the manifest issue. If you have any problems getting this to work, please leave a comment. I’ll see if I can help and, in turn, hopefully make this guide better.