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…
- The Visual Studio 8.0 Runtimes If you don’t already have them installed
- Microsoft Platform SDK (Windows Server 2008)
- The Ruby One-click Installer for Windows
- AutoIt3
- SWIG for Windows (swigwin) Search the page for “Windows users”. The link is kind of hard to see at first.
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);2to 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" endThe 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 compileAutoItX3.libinto our project.The last chunk of code after
# For SWIGboth adds the SWIG directory to the build PATH and adds a dependency forautoit_wrap.cxx. This way if I modifyautoit.ithe fileautoit_wrap.cxxwill automatically be regenerated bynmake.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.rbBuild the extension library
Now in the Platform SDK command prompt run this.
nmakeIf you crossed your fingers hard enough you will end up with a
autoit.sofile 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 installThis copies
autoit.soto Ruby’s Windows library path. With the one click install defaults that would bec:/ruby/lib/ruby/site_ruby/1.8/i386-msvcrt/autoit.soCopy 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\system32and 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:1Make 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
LoadErrorexception. 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
nmakeends without compiling anything, you more than likely generated the Makefile before any source files were in your project directory. Runswig -c++ -ruby autoit.ito generateautoit_wrap.cxxand then runnmakeagain.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.


