Testing C++ in Ruby, continued
For a while I’ve been working with SWIG to generate wrappers for my C++ code. I ran into some problems when using it in a real project, so I’ve made the following adjustments to my method. First of all I’m using mkmf to generate a Makefile for the shared objects. This is how the SWIG documentation shows it, and I have added some lines to link in more libraries and run the swig command. I’ve added the -minherit flag to support C++ inheritance.
require 'mkmf'
# Create wrapper module
`swig -c++ -ruby -minherit -Wall -o units_wrap.cpp units.i`
# Since the SWIG runtime support library for Ruby
# depends on the Ruby library, make sure it's in the list
# of libraries.
$libs = append_library($libs, Config::CONFIG['RUBY_INSTALL_NAME'])
# Also, we need the c++ libraries
$libs = append_library($libs, "stdc++")
# Create the makefile
create_makefile('units') This script, src/extconf.rb is executed by automake by adding the following in src/Makefile.am:
bin_PROGRAMS = example
example_SOURCES = example.cpp
noinst_unit_testsdir = .
noinst_unit_tests_DATA = units.so
EXTRA_DIST = extconf.rb autogen.sh
units.so:
ruby extconf.rb
makeI love this solution because now I only have 2 files to maintain compiler details instead of 3. src/units.i and src/Makefile.am. Also, by using the noinst_ prefix the module will not be included when I run ‘make dist’. You can see how it all fits together in my APR project.
Testing C++ in Ruby with Automake and RSpec
Since I’m a C++ newbie, I felt I needed more confidence in my code. As I’ve been programming in Ruby for almost 2 years now, I decided to look for a way to test my C++ code in Ruby. I found an excellent post by Dean Wampler that deals with just this topic. Please take the time to read this fascinating article before continuing. I happened to have been playing with autotools for a different C++ project last week, so I decided to use them for this one too. I decided not to put the SWIG stuff into the Makefile, but rather use a shell script. Automake will make sure the modules get built the way they need to be built, and I can put everything described in Dean’s article in a little shell script which builds the wrapper object for Ruby. I’m using a file src/units.i which loads some module headers for SWIG to wrap. This code is now irrelevant. I’ve found a way to generate this by using mkmf
#!/bin/sh
CXX=/usr/bin/g++
SRC_DIR=src
CFLAGS="-fPIC -fno-strict-aliasing -g -O2"
BIN_DIR=bin
RUBY_LIBS=/usr/lib/ruby/1.8/i486-linux
SWIG=/usr/bin/swig
cd $SRC_DIR
SRCS=`ls *.cpp | sed "s/.*_wrap\.cpp//"`
OBJS=`ls *.o`
`${CXX} -I. -I${RUBY_LIBS} ${CFLAGS} -c ${SRCS}`
`${SWIG} -c++ -ruby -Wall -o units_wrap.cpp units.i`
`${CXX} -I. -I${RUBY_LIBS} ${CFLAGS} -c units_wrap.cpp`
`${CXX} -shared -L. -rdynamic -Wl,-export-dynamic -L/usr/local/lib -o units.so ${OBJS} -lruby1.8 -lpthread -ldl -lcrypt -lm -lc`You could extend it to take options for which module to build. As in my previous autotools example I have a ‘src’ dir containing my C++ code, and a new ‘spec’ dir containing my RSpec specifications in Ruby. The spec_helper.rb in the spec dir is taken from Dean’s example. Here’s an example of a spec for a FileReader class
require File.dirname(__FILE__) + '/spec_helper'
require 'units'
describe Units::FileReader do
it "should be a constant on module Units" do
Units.constants.should include('FileReader')
end
end
describe Units::FileReader, ".new" do
it "should create a new object of the type FileReader" do
fr = Units::FileReader.new("file.txt")
fr.filename.should_be "file.txt"
end
end
describe Units::FileReader, "#openFile" do
it "should open a file" do
fr = Units::FileReader.new
fr.openFile("file.txt").should_be true
fr.filename.should_be "file.txt"
end
end
describe Units::FileReader, "#closeFile" do
it "should close a file" do
fr = Units::FileReader.new
fr.closeFile.should_be true
fr.filename.should_be ""
end
end
describe Units::FileReader, "#readChar" do
it "should read a char" do
fr = Units::FileReader.new("file.txt")
char = fr.readChar
char.should_be fr.ch
char.is_a?(String).should_be true
char.length.should_be 1
end
end
describe Units::FileReader, "#readLine" do
it "should read a line from the file" do
fr = Units::FileReader.new("file.txt")
line = fr.readLine
line.is_a?(String).should_be true
line.match(/^.*\n$/).should_not be_nil
end
endOne little caveat I ran into: you have to expose the private variables of you C++ class with public accessors to be able to use them. Dean’s example shows these two little methods that do this, but it wasn’t obvious to me as my brain is cooked by programming in Ruby for too long. Hope this was helpful for anyone who wants to get started on testing their C++ code in Ruby easily.
Using autotools with C++ to make GTK apps
I’ve been playing around with GTK+, specifically gtkmm last week. I love the GTK+ GUI and I want to write some portable desktop applications. Luckily there are great docs for newbies to get started with. Although C++ has never been my language of choice, I really want to learn it. I think I will warm up to it given I can find a style that works for me. First up are Makefiles. Oh, Makefile. How slender and seductive you start out. Such warts you grow. So much does your ass inflate. After hacking away on my own Makefile for about 2 days I still had code that compiled. But along with that feat came an ugly Makefile which needed constant pruning. I decided it was enough, and thought I’d give automake a try, as is recommended in the gtkmm docs. Luckily the docs are good. But I think they need a bit of updating. On a new C++ project, here’s what I do. I’m using the first gtkmm example here.
aczid@homer:~/helloworld_gtk$ ls -lR .: total 4 drwxr-xr-x 3 aczid aczid 4096 2008-03-18 20:57 src ./src: total 12 -rw-r--r-- 1 aczid aczid 362 2008-02-09 11:35 helloworld.cpp -rw-r--r-- 1 aczid aczid 367 2008-02-09 11:32 helloworld.h -rw-r--r-- 1 aczid aczid 228 2008-02-09 11:33 main.cpp aczid@homer:~/helloworld_gtk$
As you see I have a ‘src’ dir conatining my C++ sources, and no Makefile. Now comes the magic.
aczid@homer:~/helloworld_gtk$ autoscan aczid@homer:~/helloworld_gtk$ ls -l total 8 -rw-r--r-- 1 aczid aczid 0 2008-03-18 20:59 autoscan.log -rw-r--r-- 1 aczid aczid 475 2008-03-18 20:59 configure.scan drwxr-xr-x 3 aczid aczid 4096 2008-03-18 20:57 src aczid@homer:~/helloworld_gtk$ mv configure.scan configure.ac
Autoscan has generated the input file for autoconf for you. Let’s take a look:
# -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ(2.61) AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS) AC_CONFIG_SRCDIR([src/main.cpp]) AC_CONFIG_HEADER([config.h]) # Checks for programs. AC_PROG_CXX AC_PROG_CC # Checks for libraries. # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. # Checks for library functions. AC_OUTPUT
Let’s add a few lines.
# -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ(2.61) FULL-PACKAGE-NAME(Hello World) VERSION(1.0) BUG-REPORT-ADDRESS(bugs@aczid.nl) AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS) AC_CONFIG_SRCDIR([src/main.cpp]) AC_CONFIG_HEADER([config.h]) AM_INIT_AUTOMAKE # Checks for programs. AC_PROG_CXX AC_PROG_CC # Checks for libraries. PKG_CHECK_MODULES(DEPS, gtkmm-2.4 >= 2.4 gtk+-2.0 >= 2.2 glib-2.0 >= 2.2) AC_SUBST(DEPS_CFLAGS) AC_SUBST(DEPS_LIBS) # Checks for header files. AC_STDC_HEADERS # Checks for typedefs, structures, and compiler characteristics. # Checks for library functions. AC_CONFIG_FILES([Makefile src/Makefile]) AC_OUTPUT
You see I added some GTK libs here. The next 2 lines after that set up the compiler options for them. Also notice I added references to a Makefile in the config files macro. These files need stubs to be processed by automake. There are a lot of macros in autoconf to do all kinds of crazy things. Edit ./Makefile.am and add:
SUBDIRS = src
And in the src/Makefile.am you put:
bin_PROGRAMS = helloworld helloworld_SOURCES = helloworld.cpp main.cpp helloworld_LDADD = $(DEPS_LIBS) AM_CPPFLAGS = $(DEPS_CFLAGS)
See, those LDADD and CPPFLAGS macros rely on our DEPS_LIBS and DEPS_CFLAGS variables declared in the configure.ac file. Now all you need to do is run some commands!
aczid@homer:~/helloworld_gtk$ aclocal /usr/share/aclocal/libmcrypt.m4:17: warning: underquoted definition of AM_PATH_LIBMCRYPT /usr/share/aclocal/libmcrypt.m4:17: run info '(automake)Extending aclocal' /usr/share/aclocal/libmcrypt.m4:17: or see http://sources.redhat.com/automake/automake.html#Extending-aclocal aczid@homer:~/helloworld_gtk$ autoconf aczid@homer:~/helloworld_gtk$ autoheader aczid@homer:~/helloworld_gtk$ touch NEWS README AUTHORS ChangeLog aczid@homer:~/helloworld_gtk$ automake --add-missing
Phew, that’s it! Now you can run ./confgure and it will generate a Makefile from the Makefile.in, which was in turn generated by Automake.
aczid@homer:~/helloworld_gtk$ ./configure checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for a thread-safe mkdir -p... /bin/mkdir -p checking for gawk... gawk checking whether make sets $(MAKE)... yes checking for g++... g++ checking for C++ compiler default output file name... a.out checking whether the C++ compiler works... yes checking whether we are cross compiling... no checking for suffix of executables... checking for suffix of object files... o checking whether we are using the GNU C++ compiler... yes checking whether g++ accepts -g... yes checking for style of include used by make... GNU checking dependency style of g++... gcc3 checking for gcc... gcc checking whether we are using the GNU C compiler... yes checking whether gcc accepts -g... yes checking for gcc option to accept ISO C89... none needed checking dependency style of gcc... gcc3 checking how to run the C preprocessor... gcc -E checking for grep that handles long lines and -e... /bin/grep checking for egrep... /bin/grep -E checking for ANSI C header files... yes checking for a BSD-compatible install... /usr/bin/install -c checking for pkg-config... /usr/bin/pkg-config checking pkg-config is at least version 0.9.0... yes checking for DEPS... yes configure: creating ./config.status config.status: creating Makefile config.status: creating src/Makefile config.status: creating config.h config.status: config.h is unchanged config.status: executing depfiles commands aczid@homer:~/helloworld_gtk$ make cd . && /bin/sh /home/aczid/helloworld_gtk/missing --run autoheader rm -f stamp-h1 touch config.h.in cd . && /bin/sh ./config.status config.h config.status: creating config.h config.status: config.h is unchanged make all-recursive make[1]: Entering directory `/home/aczid/helloworld_gtk' Making all in src make[2]: Entering directory `/home/aczid/helloworld_gtk/src' g++ -DHAVE_CONFIG_H -I. -I.. -I/usr/include/gtkmm-2.4 -I/usr/lib/gtkmm-2.4/include -I/usr/include/glibmm-2.4 -I/usr/lib/glibmm-2.4/include -I/usr/include/gdkmm-2.4 -I/usr/lib/gdkmm-2.4/include -I/usr/include/pangomm-1.4 -I/usr/include/atkmm-1.6 -I/usr/include/gtk-2.0 -I/usr/include/sigc++-2.0 -I/usr/lib/sigc++-2.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/lib/gtk-2.0/include -I/usr/include/cairomm-1.0 -I/usr/include/pango-1.0 -I/usr/include/cairo -I/usr/include/freetype2 -I/usr/include/libpng12 -I/usr/include/atk-1.0 -g -O2 -MT helloworld.o -MD -MP -MF .deps/helloworld.Tpo -c -o helloworld.o helloworld.cpp mv -f .deps/helloworld.Tpo .deps/helloworld.Po g++ -DHAVE_CONFIG_H -I. -I.. -I/usr/include/gtkmm-2.4 -I/usr/lib/gtkmm-2.4/include -I/usr/include/glibmm-2.4 -I/usr/lib/glibmm-2.4/include -I/usr/include/gdkmm-2.4 -I/usr/lib/gdkmm-2.4/include -I/usr/include/pangomm-1.4 -I/usr/include/atkmm-1.6 -I/usr/include/gtk-2.0 -I/usr/include/sigc++-2.0 -I/usr/lib/sigc++-2.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/lib/gtk-2.0/include -I/usr/include/cairomm-1.0 -I/usr/include/pango-1.0 -I/usr/include/cairo -I/usr/include/freetype2 -I/usr/include/libpng12 -I/usr/include/atk-1.0 -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.cpp mv -f .deps/main.Tpo .deps/main.Po g++ -g -O2 -o helloworld helloworld.o main.o -lgtkmm-2.4 -lgdkmm-2.4 -latkmm-1.6 -lpangomm-1.4 -lcairomm-1.0 -lglibmm-2.4 -lsigc-2.0 -lgtk-x11-2.0 -lgdk-x11-2.0 -latk-1.0 -lgdk_pixbuf-2.0 -lm -lpangocairo-1.0 -lpango-1.0 -lcairo -lgobject-2.0 -lgmodule-2.0 -ldl -lglib-2.0 make[2]: Leaving directory `/home/aczid/helloworld_gtk/src' make[2]: Entering directory `/home/aczid/helloworld_gtk' make[2]: Leaving directory `/home/aczid/helloworld_gtk' make[1]: Leaving directory `/home/aczid/helloworld_gtk' aczid@homer:~/helloworld_gtk$
See? :) I’ve quite enjoyed fooling around with this. It beats the hell out of writing your own Makefiles. It’s nice to know your app will be compatible with GNU standards and there is much less to write and consequently maintain. In the future I think I’ll look at other systems that try to ease the same pains. Autotools were made for much bigger projects than mine. Scons is what I might look into next.










