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
position
tortoise_pendown()
to lower the pen
tortoise_penup()
to raise the pen
tortoise_turn(int degrees)
to turn degrees
clockwise
tortoise_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 more
SCM_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 effect
tortoise-penup
and tortoise-pendown
should both
return the whether the pen was down before the call
tortoise-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, and
gh_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.