Figure 17.2 on page 182 shows the overall workflow when building an
extension.
The key to the whole process is the
extconf.rb
program which you, as a developer, create. In
extconf.rb
, you
write a simple program that determines what features are available on
the user's system and where those features may be located. Executing
extconf.rb
builds a customized
Makefile
, tailored for both your application and the system on
which it's being compiled. When you run the
make
command against
this
Makefile
, your extension is built and (optionally) installed.
The simplest
extconf.rb
may be just two lines long, and for many
extensions this is sufficient.
require 'mkmf'
create_makefile("Test")
|
The first line brings in the
mkmf
library module
(documented beginning on page 451). This contains all the
commands we'll be using. The second line creates a
Makefile
for an
extension called ``Test.'' (Note that ``Test'' is the name of the
extension; the makefile will always be called ``Makefile.'')
Test
will be built from all the C source files in the
current directory.
Let's say that we run this
extconf.rb
program in a directory
containing a single source file,
main.c
. The
result is a
Makefile
that will build our extension. On our system,
this contains the following commands.
gcc -fPIC -I/usr/local/lib/ruby/1.6/i686-linux -g -O2 \
-c main.c -o main.o
gcc -shared -o Test.so main.o -lc
|
The result of this compilation is
Test.so
, which may be
dynamically linked into Ruby at runtime with ``
require
''. See how
the
mkmf
commands have located platform-specific libraries and
used compiler-specific options automatically. Pretty neat, eh?
Although this basic program works for many simple extensions, you may
have to do some more work if your extension needs header files or
libraries that aren't included in the default compilation environment,
or if you conditionally compile code based on the presence of
libraries or functions.
A common requirement is to specify nonstandard directories where
include files and libraries may be found. This is a two-step process.
First, your
extconf.rb
should contain one or more
dir_config
commands.
This specifies a tag for a set of directories. Then, when you run the
extconf.rb
program, you tell
mkmf
where the corresponding
physical directories are on the current system.
If
extconf.rb
contains the line
dir_config(
name
)
,
then you give the location of the corresponding directories with the
command-line options:
-
--with-name-include=directory
-
*
Add directory/include
to the compile command.
-
--with-name-lib=directory
-
*
Add directory/lib
to the link command.
If (as is common) your include and library directories are both
subdirectories of some other directory, and (as is also common) they're
called
include
and
lib
, you can take a shortcut:
-
--with-name-dir=directory
-
*
Add directory/lib
and directory/include
to the link
command and compile command, respectively.
There's a twist here. As well as specifying all these
--with
options when you run
extconf.rb
, you can also use the
--with
options that were specified when Ruby was built for your machine. This
means you can find out the locations of libraries that are used by
Ruby itself.
To make all this concrete, lets say you need to use libraries and
include files for the CD jukebox we're developing. Your
extconf.rb
program might contain
require 'mkmf'
dir_config('cdjukebox')
# .. more stuff
create_makefile("CDJukeBox")
|
You'd then run
extconf.rb
with something like:
% ruby extconf.rb --with-cdjukebox-dir=/usr/local/cdjb
|
The generated
Makefile
would assume that the libraries were in
/usr/local/cdjb/lib
and the include files were in
/usr/local/cdjb/include
.
The
dir_config
command adds to the list of places to search
for libraries and include files. It does not, however, link the
libraries into your application. To do that, you'll need to use one
or more
have_library
or
find_library
commands.
have_library
looks for
a given entry point in a named library. If it finds the entry point,
it adds the library to the list of libraries to be used when linking
your extension.
find_library
is
similar, but allows you to specify a list of directories to search for
the library.
require 'mkmf'
dir_config('cdjukebox')
have_library('cdjb', 'CDPlayerNew')
create_makefile("CDJukeBox")
|
On some platforms, a popular library may be in one of several places.
The X Window system, for example, is notorious for living in different
directories on different systems. The
find_library
command will
search a list of supplied directories to find the right one (this is
different from
have_library
, which uses only configuration
information for the search). For example, to create a
Makefile
that uses X Windows and a jpeg library,
extconf.rb
might contain
require 'mkmf'
if have_library("jpeg","jpeg_mem_init") and
find_library("X11", "XOpenDisplay", "/usr/X11/lib",
"/usr/X11R6/lib", "/usr/openwin/lib")
then
create_makefile("XThing")
else
puts "No X/JPEG support available"
end
|
We've added some additional functionality to this program. All of the
mkmf
commands return
false
if they fail. This means that
we can write an
extconf.rb
that generates a
Makefile
only if
everything it needs is present. The Ruby distribution does
this so that it will try to compile only those extensions that are supported
on your system.
You also may want your extension code to be able to configure the
features it uses depending on the target environment. For example, our
CD jukebox may be able to use a high-performance MP3 decoder if the
end user has one installed. We can check by looking for its header
file.
require 'mkmf'
dir_config('cdjukebox')
have_library('cdjb', 'CDPlayerNew')
have_header('hp_mp3.h')
create_makefile("CDJukeBox")
|
We can also check to see if the target environment has a particular
function in any of the libraries we'll be using. For example, the
setpriority
call would be useful but isn't always
available. We can check for it with:
require 'mkmf'
dir_config('cdjukebox')
have_func('setpriority')
create_makefile("CDJukeBox")
|
Both
have_header
and
have_func
define
preprocessor constants if they find their targets. The names are
formed by converting the target name to uppercase and prepending
``HAVE_''. Your C code can take advantage of this using constructs
such as:
#if defined(HAVE_HP_MP3_H)
# include <hp_mp3.h>
#endif
#if defined(HAVE_SETPRIORITY)
err = setpriority(PRIOR_PROCESS, 0, -10)
#endif
|
If you have special requirements that can't be met with all
these
mkmf
commands, your program can directly add to the global
variables
$CFLAGS
and
$LFLAGS
, which are passed to the
compiler and linker, respectively.