This is a tutorial introduction to using Guile, the GNU extension language. I'm aiming at people who want to use Guile, and who don't want to mess around in the guts of Guile itself.
For example, if you've just written a spiffy new word processor/image manipulator/window manager, and you want your users to be able to run cunning scripts, then Guile is the library for you and (hopefully) this is the tutorial for you.
I'm going to assume that you know a little bit about the Scheme programming language, and I'm also going to assume that you have managed to get Guile successfully built and installed on your system. I'm also going to assume that you have Guile version 1.3 or higher.
Guile is an implementation of the Scheme programming language, made available as a library, together with conventions for interfacing to the interpreter from other programming languages.
The point of this is to allow extensibility. You can write a big piece of software in C, and then allow the users of your software to write Scheme scripts which call various C functions in your software. I'll show a simple example of this later on.
That's not all. One of the things that Scheme as a programming language is particularly good at is the emulation of other programming languages. This means that your users don't actually have to write their extension scripts in Scheme; instead, they can write the scripts in Tcl or Python, and just load up the appropriate chunk of Scheme code to emulate that language. (Or at least they will be able to, when the appropriate translators have been written.)
In concrete terms, you'll have a number of things available once you have Guile correctly installed.
Firstly, you should have guile
, which is the Guile
Scheme interpreter as a stand-alone executable. This is useful if you
want to just write Scheme code; it's also useful for testing out bits
of extension Scheme code interactively. This should be very small,
since all it contains is a thin wrapper for the Guile shared library
(on my system, it's less than 4k).
Secondly and most importantly, you should have
libguile.so
. This is the Guile shared library, which you
can link to from your C (or C++ or whatever) code.
Thirdly, you'll probably also have some standard Scheme library
functions installed somewhere. These will typically have a
.scm
filename extension; on my (RedHat 6.0 Linux) system
these are installed under /usr/share/guile
.
The things I've described so far make up the primary parts of
Guile, that is the things you'll need if you want to run a program
which uses Guile as its extension language. (In RedHat
package terms, these are all in the guile
package).
Given that we want to write code, we need a few more things (which
are kept in the guile-devel
RedHat package). . . .
Fourth is a collection of header files which cover the process of
calling Guile from C and vice-versa. These might get installed into
/usr/include/guile
and /usr/include/libguile
,
for example.
Fifth is the utility function guile-snarf
. This
utility is used to ease the process of getting Guile and C to talk to
one another, provided you stick to some coding conventions.
Sixth and last is the utility guile-config
.
This program scans your local configuration and tells you what options
you need to compile and link Guile code into your programs.
To demonstrate Guile, we are going to write a very simple graphics program. It's written for the X Window system; however, it is so simple it should be easy to convert to other graphics systems.
The program draws graphics by assuming that there is a tortoise sitting in the middle of the screen. This tortoise is quite stupid, and can only understand a small number of instructions.
You can ask the tortoise to turn to the right by a number of degrees (negative numbers will get him to turn to the left). You can ask the tortoise to walk forward a certain number of steps. And you can ask him to hold a pen either in his paws or behind his ear, so that when he moves he will either leave a mark on the ground or not (respectively).
Finally, in case the tortoise gets utterly confused, you can ask the tortoise to return to the middle of the screen and turn to face the top of the screen again
To program this up, I'm going to write a C program where each of the tortoise instructions corresponds to a function call.
First, lets take care of the bookkeeping. The main()
function will deal with the mechanics of getting some graphics -
getting a window and a GC, in X terms.
#include <X11/Xlib.h> #define WINDOW_SIZE 500 Display *theDisplay; Window theWindow; Screen *theScreen; GC theGC; int main(int argc, char *argv[]) { theDisplay = XOpenDisplay(NULL); XSynchronize(theDisplay, True); theScreen = DefaultScreenOfDisplay(theDisplay); theWindow = XCreateSimpleWindow(theDisplay, RootWindowOfScreen(theScreen), 0, 0, WINDOW_SIZE, WINDOW_SIZE, 0, BlackPixelOfScreen(theScreen), WhitePixelOfScreen(theScreen)); theGC = XCreateGC(theDisplay, theWindow, 0L, NULL); XSetForeground(theDisplay, theGC, BlackPixelOfScreen(theScreen)); XMapWindow(theDisplay,theWindow); /* more stuff to come here . . */ return 0; }
Next, we have some variables which hold the current state of the tortoise:
double currentX; double currentY; double currentDirection; int penDown;
We need to initialize this data, so we add the following function
invocation to main()
:
tortoise_reset();
The function being invoked here implements one of the things that the tortoise can do. There are similar functions which implement all of the other things that the tortoise can do:
#include <math.h> #define DEGREES_TO_RADIANS (3.1415926535897932384626433832795029L/180.0) void tortoise_reset() { currentX = currentY = WINDOW_SIZE/2; currentDirection = 0.0; penDown = 1; } void tortoise_pendown() { penDown = 1; } void tortoise_penup() { penDown = 0; } void tortoise_turn(int degrees) { currentDirection += (double)degrees; } void tortoise_move(int steps) { double newX, newY; /* first work out the new endpoint */ newX = currentX + sin(currentDirection*DEGREES_TO_RADIANS)*(double)steps; newY = currentY - cos(currentDirection*DEGREES_TO_RADIANS)*(double)steps; /* if the pen is down, draw a line */ if (penDown) XDrawLine(theDisplay, theWindow, theGC, (int)currentX, (int)currentY, (int)newX, (int)newY); /* in either case, move the tortoise */ currentX = newX; currentY = newY; }
We can test that we have written our primitives correctly by
temporarily including some test code at the end of main()
:
{ int ii; tortoise_pendown(); for (ii=0; ii<4; ii++) { tortoise_move(100); tortoise_turn(90.0); } /* sleep for a bit so the window stays visible */ sleep(10); }
As expected, this draws a square in the window. (Compilation instructions here)
That's it. We have all the code we need. Except, of course, that we have no way to get our tortoise to do anything. Now we want to let the user instruct the tortoise directly.
This is where Guile comes into play. Without Guile, we would have to write code to read input from the user, interpret it, and call the appropriate tortoise primitive.
Even if we did write this code, the user would only be able to instruct the tortoise directly. There would be no way for the user to perform loops or iterations; they would just have to do a lot of typing.
In fact, in order to get more programmable function out of the tortoise we would need to write an interpreter for a tortoise programming language.
This would be wasted effort; Guile already provides a perfectly good way of making our tortoise programmable. The next section talks about how to do this.
The first step is to get access to the Guile library from within the program. To do this, we first need to include the master header file:
#include <guile/gh.h>
You may need to add an extra include directory to your path to get
at this header file; the command guile-config compile
should tell you where to find it.
Next, we need to kick off the Guile interpreter. This is done in
two steps; firstly, for arcane reasons we need to call
gh_enter
at the end of main()
.
gh_enter(argc, argv, inner_main); return(0); /* never reached */
Secondly, gh_enter
will call back into the
inner_main
function passed to it, and it is this
inner_main
function which actually calls
gh_repl
to start the Scheme interpreter
(the repl
stands for Read-Evaluate-Print Loop, in case you
were interested).
void inner_main(int argc, char **argv) { register_procs(); gh_repl(argc, argv); }
This code refers to the function register_procs
. We'll
come to this below
Finally, we need to link with the libguile.so
library.
On my system, this simply involved adding -lguile
to the
link line. Running guile-config link
is a good way of
finding out what you might need to do on your system.
At this point, we now have our tortoise program invoking the Guile interpreter, but the user has no way of accessing all of our tortoise functionality. When the program starts, the user can interactively write programs in Scheme, but can't get the tortoise to wake up and do anything.
What we need to do is to make all of our tortoise primitives available from the Scheme interpreter.
To recap, these were:
tortoise_reset()
to return the tortoise to the
starting positiontortoise_pendown()
to lower the pentortoise_penup()
to raise the pentortoise_turn(int degrees)
to turn
degrees
clockwisetortoise_move(int steps)
to move steps
steps.We make Scheme accessible versions of all of these.
#include <math.h> #define DEGREES_TO_RADIANS (3.1415926535897932384626433832795029L/180.0) SCM tortoise_reset() { currentX = currentY = WINDOW_SIZE/2; currentDirection = 0; penDown = 1; return SCM_EOL; } SCM tortoise_pendown() { penDown = 1; return SCM_EOL; } SCM tortoise_penup() { penDown = 0; return SCM_EOL; } SCM tortoise_turn(SCM s_degrees) { int degrees = SCM_INUM(s_degrees); currentDirection += (double)degrees; return SCM_EOL; } SCM tortoise_move(SCM s_steps) { double newX, newY; int steps = SCM_INUM(s_steps); /* first work out the new endpoint */ newX = currentX + sin(currentDirection*DEGREES_TO_RADIANS)*(double)steps; newY = currentY - cos(currentDirection*DEGREES_TO_RADIANS)*(double)steps; /* if the pen is down, draw a line */ if (penDown) XDrawLine(theDisplay, theWindow, theGC, (int)currentX, (int)currentY, (int)newX, (int)newY); /* in either case, move the tortoise */ currentX = newX; currentY = newY; return SCM_EOL; }
In this code, there are a few key points
SCM
, which is the universal type
for representing Scheme values inside C code. For now, in all of these
functions we return the specific value SCM_EOL
, which
is the way to say the Scheme value ()
in C.SCM
type once moreSCM_INUM
converts a Scheme object into a
regular C integer, on the assumption that the object is
actually an integer. (If it isn't, what happens is undefined)Having set up all of these C functions in a way which is accessible
from Guile, we now need to tell Guile that they are there. To do
this, we define the function register_procs
which was
mentioned in the previous section.
void register_procs(void) { gh_new_procedure("tortoise-reset", tortoise_reset, 0, 0, 0); gh_new_procedure("tortoise-pendown", tortoise_pendown, 0, 0, 0); gh_new_procedure("tortoise-penup", tortoise_penup, 0, 0, 0); gh_new_procedure("tortoise-turn", tortoise_turn, 1, 0, 0); gh_new_procedure("tortoise-move", tortoise_move, 1, 0, 0); }
The registration of the new Guile-accessible primitives is done by
the calls to gh_new_procedure
. This function takes the
following parameters
Amazingly, that's it - we have the entire program. If we now compile and run the program, we get a Guile interpreter prompt:
guile>
We can now try a few commands:
guile> (tortoise-move 100) () guile> (tortoise-turn 90) () guile> (tortoise-move 100) ()
At this point, a couple of lines should have appeared in the program's window.
Given that this is Scheme, we can define a new top-level procedure.
guile> (define (move-n-turn angle) ... (tortoise-move 100) (tortoise-turn angle)) guile> (for-each move-n-turn '(80 80 80 80 80 80 80 80 80))
All of the primitives so far have returned the empty list, because this was easy to do. However, we'd now like to pass some information back from C to Scheme - in particular:
tortoise-turn
should return the angle that the
tortoise was at before this turn took effecttortoise-penup
and tortoise-pendown
should both return the whether the pen was down before the calltortoise-move
should return the previous position
of the turtle as a Scheme list, for example (200 200)
Also, we've noticed that tortoise-move
and
tortoise-turn
only allow an integer argument. It's time
to change this so that we can specify floating point values for these
arguments.
#include <math.h> #define DEGREES_TO_RADIANS (3.1415926535897932384626433832795029L/180.0) SCM tortoise_reset() { currentX = currentY = WINDOW_SIZE/2; currentDirection = 0; penDown = 1; return SCM_UNSPECIFIED; } SCM tortoise_pendown() { int prevValue = penDown; penDown = 1; return (prevValue ? SCM_BOOL_T: SCM_BOOL_F); } SCM tortoise_penup() { int prevValue = penDown; penDown = 0; return (prevValue ? SCM_BOOL_T: SCM_BOOL_F); } SCM tortoise_turn(SCM s_degrees) { int prevValue = (int)currentDirection; double degrees; SCM_ASSERT(SCM_NUMBERP(s_degrees), s_degrees, SCM_ARG1, "tortoise-turn"); degrees = SCM_NUM2DBL(s_degrees); currentDirection += degrees; return SCM_MAKINUM(prevValue); } SCM tortoise_move(SCM s_steps) { double oldX = currentX; double oldY = currentY; double newX, newY; double steps; SCM_ASSERT(SCM_NUMBERP(s_steps), s_steps, SCM_ARG1, "tortoise-move"); steps = SCM_NUM2DBL(s_steps); /* first work out the new endpoint */ newX = currentX + sin(currentDirection*DEGREES_TO_RADIANS)*steps; newY = currentY - cos(currentDirection*DEGREES_TO_RADIANS)*steps; /* if the pen is down, draw a line */ if (penDown) XDrawLine(theDisplay, theWindow, theGC, (int)currentX, (int)currentY, (int)newX, (int)newY); /* in either case, move the tortoise */ currentX = newX; currentY = newY; return gh_cons(gh_double2scm(oldX) , gh_cons(gh_double2scm(oldY), SCM_EOL)); }
So what's different about this version of the code? Well, firstly
the pen up/down functions use SCM_BOOL_F
and
SCM_BOOL_T
to return the Scheme values #f
and #t
, as appropriate. Secondly,
tortoise_turn
uses SCM_MAKINUM
to build a
Scheme integer from a C integer.
Thirdly, the function tortoise-move
builds a list
containing the previous coordinates using
gh_double2scm
, which converts a C
double
to the equivalent Scheme value, andgh_cons
, which conses two C representations
(SCM
s) of Scheme values together.Fourthly, tortoise_move
and tortoise_turn
use SCM_NUM2DBL
instead
of SCM_INUM
to get a floating point value from their single
arguments.
Finally, I mentioned before that the results of SCM_INUM
are
undefined if the Scheme value passed in is not an integer. The same
is true for SCM_NUM2DBL
if the Scheme value passed in is
not some sort of number. However, in this version of the
code I protect against this possibility.
The way to ensure that an argument passed from Scheme to C is of a
particular type is to use SCM_ASSERT
, combined with a
test for the required type such as SCM_NUMBERP
or
SCM_INUMP
. The arguments to SCM_ASSERT
are:
SCM
object which caused the error, in this case the argument to
the C function.SCM_ARG1
.We can take advantage of these changes (this version of the complete program):
guile> (tortoise-move 100) (250.0 250.0) guile> (tortoise-turn 90) 0 guile> (tortoise-move 100) (250.0 150.0) guile> (tortoise-turn (+ 90 45)) 90 guile> (tortoise-move (sqrt (* 2 (* 100 100)))) (350.0 150.0) guile> (tortoise-penup) #t guile> (tortoise-move "abc") ERROR: In procedure tortoise-move in expression (tortoise-move "abc"): ERROR: Wrong type argument in position 1: "abc" ABORT: (wrong-type-arg)
In the examples so far, the program has kicked off the Guile Scheme interpreter, allowing the user to interact with the program dynamically. For many of the potential uses of Guile, this is not what is needed; the program will only need to read in a configuration script at start-of-day, and then proceed with its normal processing
So we're now going to change the program so that it reads in and
runs a .tortoise
file at start-of-day. To do this, we
still need to use gh_enter
to get to our
inner_main
function. However, when we get there, we
don't want to hand over control of the program to Guile with
gh_repl
.
Instead, we want to open the .tortoise
file and
explicitly pass the lines from it to the Scheme interpreter, one by
one. When we're done with the file, we're done with the interpreter
and we can get on with the main business of the program.
#define CONFIGFILENAME ".tortoise" #define DIRECTORYSEPARATOR "/" void read_config_file(void) { char *homedir; char *filename; FILE *file; char inputLine[1024]; /* hack: assume each line less than 1024 chars */ char *p; /* open $HOME/.tortoise */ homedir = getenv("HOME"); if (homedir == NULL) return; filename = (char *)malloc(strlen(homedir) + strlen(CONFIGFILENAME) + strlen(DIRECTORYSEPARATOR) + 1); if (filename == NULL) return; sprintf(filename, "%s%s%s", homedir, DIRECTORYSEPARATOR, CONFIGFILENAME); file = fopen(filename, "r"); free(filename); if (file == NULL) return; /* spin through the file */ while (1) { p = fgets(inputLine, sizeof(inputLine), file); if (p == NULL) return; gh_eval_str(p); } fclose(file); } void inner_main(int argc, char **argv) { register_procs(); read_config_file(); /* now get on with the main business of the program . . . */ sleep(20); }
Here, the key function is gh_eval_str
, which gets the
Scheme interpreter to evaluate one particular string and then
return. (The complete program is here)
We can test this out with a sample .tortoise
file,
which defines a Scheme procedure for drawing a polygon with a given
number of sides:
(define (move-n-turn angle) (tortoise-move 100) (tortoise-turn angle)) (define (polygon n) (do ((i n (- i 1))) ((zero? i)) (move-n-turn (/ 360 n)))) (polygon 7)
Unfortunately, there is a downside to this approach. Each line of
our .tortoise
file has to be a complete Scheme expression.
It's very easy to fix this - we just let Guile do more of the work
for us. There is a function gh_eval_file
which will load
an entire file and evaluate it:
#define CONFIGFILENAME ".tortoise" #define DIRECTORYSEPARATOR "/" void read_config_file(void) { char *homedir; char *filename; /* build the $HOME/.tortoise filename */ homedir = getenv("HOME"); if (homedir == NULL) return; filename = (char *)malloc(strlen(homedir) + strlen(CONFIGFILENAME) + strlen(DIRECTORYSEPARATOR) + 1); if (filename == NULL) return; sprintf(filename, "%s%s%s", homedir, DIRECTORYSEPARATOR, CONFIGFILENAME); /* get Guile to do all of the work */ gh_eval_file(filename); free(filename); } void inner_main(int argc, char **argv) { register_procs(); read_config_file(); /* now get on with the main business of the program . . . */ gh_eval_str("(tortoise-turn 30)"); /* example: call C-defined procedure */ gh_eval_str("(polygon 3)"); /* example: call Scheme-defined procedure */ sleep(20); }
With this version of the code, we can re-write our
.tortoise
file in a more easily readable version:
(define (move-n-turn angle) (tortoise-move 100) (tortoise-turn angle)) (define (polygon n) (do ((i n (- i 1))) ((zero? i)) (move-n-turn (/ 360 n)))) (polygon 7)
Let's recap on what we've seen so far. We can now:
gh_new_procedure
SCM_INUM
and
SCM_NUM2DBL
SCM_ASSERT
and SCM_NUMBERP
SCM_EOL
SCM_UNSPECIFIED
SCM_BOOL_F
and SCM_BOOL_T
SCM_MAKINUM
gh_double2scm
gh_cons
gh_eval_str("(procname args)")
At this point, you probably need to find some more detailed
information on Guile. Bizarrely, the official Guile documentation is
not currently available on the Guile homepage, so you'll either have
to scan the internet for guile-doc
, or you'll have to get the
current guile-doc
images via anonymous CVS (I couldn't do
this because my internet proxy won't allow it).
The Guile documentation includes a tutorial
(guile-tut.info
), a reference manual
(guile-ref.info*
) and a copy of the Revised^4 Report on
the Algorithmic Language Scheme (r4rs.info*
).
Some things you might need to find out next:
gh_lookup
).tortoise
file from
terminating your program (hint: use gh_catch
)Have fun . .
I wrote this page because I really needed something like it when I started playing with Guile - I actually wrote the page as I figured out how to do things.
Hopefully, this page will be useful to other people who are in the same position that I was. If you're one of those people, please let me know if it has been useful, and if not, how it could be improved so that it is. (Thanks to Neil Jerram and others who have already provided some useful feedback).
If you want to know the answers to deep tricky Guile questions, I'm probably not the best person to ask. Try joining one of the Guile mailing lists (see info here).
Copyright (c) 2000 David Drysdale
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is available here.