Un serveur WEB en OpenScheme
Par :
Guilhem de Wailly (gdw@erian-concept.com)
et :
Fernand Boéri (boeri@unice.fr)
Résumé
Dans cet article, nous allons commencer la programmation d'un serveur WEB élémentaire en Scheme. Nous utilisons l'environnement Open-Scheme disponible librement sur le site www.open-scheme.com. Certaines fonctionnalités utilisées sont des extensions à la norme du langage qui peuvent être assez facilement portées vers d'entres systèmes Scheme.
L'objectif à atteindre est de programmer un serveur WEB compatible avec le protocole HTTP/1.0, et supportant les scripts CGI. Dans un premier temps, ce serveur fonctionnera avec les entrées/sorties standards, et il sera mono-processus, mono-thread. Au fur et à mesure de nos avancées, il sera doté des fonctionnalités de haut niveau.
Nous permettrons aussi d'exécuter du code Scheme directement dans les pages WEB, un peu à la manières des scripts CGI que nous avons examinés la dernière fois.
Ce petit serveur sera multi-plateforme, simple à maintenir, simple à étendre et efficace.
Description
Nous avons présenté dans un Linux Magazine précédant les rudiments du protocole HTTP pour interroger les serveurs WEB. Cette fois-ci, nous nous plaçons de l'autre coté, du coté du serveur, pour réaliser un petit serveur WEB.
Comme c'est sujet assez ambitieux, nous allons présenter ce projet par étapes, en essayant de simplifier au maximum les descriptions. C'est pour cette raison que notre serveur ne fonctionnera pas dans un premier temps sur le réseau, mais à partir des ports d'entrées / sorties standards. Nous ne traiterons pas non plus les scripts CGI, ni le code embarqués dans les pages HTML.
Comme ce projet fait partie du projet Open-Scheme, ce dernier évolue en fonction des nécessités imposées par ce serveur. Il sera donc indispensable d'obtenir les dernières versions de l'environnement sur son site WEB et/ou sur le CDROM de LinuxMag. Ce mois-ci, les versions 1.3.5 ou 1.3.4 suffiront.
De plus, dans la mesure ou le projet est réalisé au fur et à mesure de la parution des articles, il est fort probable que certaines parties de code initiale soient remises en cause par la suite. Nous essaierons cependant d'éviter au maximum ces cas de figure.
Le fichier source de ce serveur est disponible sur le site http://www.erian-concept.com/pub/ows.tgz.
Le serveur
Le projet sera réalisé dans un seul fichier source, que nous appellerons ows.osm, pour Open-Scheme Web Server :)
Le serveur est une structure de donnée utilisant les classes d'Open-Scheme définie par :
; classe des
serveurs
(define-class <server> #f
;
nom publique du serveur
(name :initform
"")
; adresse IP publique
(address :initform "")
;
port (en général 80)
(port
:initform 0)
; répertoire racine du
serveur
(root-directory :initform "")
;
indexes par défaut
(default-indexes :initform
'())
; continuation sur erreur
(error
:initform #f))
Dans cette classe, les champs sont :
ï‚· name : nom du serveur sur l'Internet. Le nom du serveur sera utilisé par la suite pour réaliser des hôtes virtuels, c'est à dire un serveur WEB capable d'héberger plusieurs sites sans aucune relation ;
ï‚· address : adresse Internet du serveur. Cette adresse sera utilisée pour l'adresse de retour des requêtes lorsque notre serveur sera capable de gérer les clusters, c'est à dire les serveurs en grappe ;
ï‚· port : port Internet du serveur ; en général, les serveurs WEB répondent sur le port 80, mais nous utiliserons d'autres ports pour la mise au point ;
ï‚· root-directory : répertoire racine des pages WEB du serveur.
ï‚· default-indexes : liste des indexes HTML par défaut ; en générale, les indexes peuvent être les fichiers index.htm, index.html ou index.cgi ;
ï‚· error : routine à exécuter en cas d'erreur dans le protocole HTTP. Cette routine sera une continuation Scheme permettant de redémarrer la lecture des requêtes.
Le serveur est organisé autour d'une boucle principale qui est placée à la fin du fichier :
; programme principal du
serveur WEB
(let (; construction du serveur
[server
(build
<server>
:name "http://www.dummy.com"
:address "1.1.1.1"
:port
8080
:root-directory "/home/gdw/erian/doc/html"
:default-indexes
'("index.html"
"index.htm"
"index.cgi"))])
; Définition de la continuation de traitement
; des
erreur. Elle agira un peu à la manière
; des
exceptions C++
(call/cc (lambda (return)
(<server>:error! server return)
(return #f)))
; boucle infinie du serveur WEB
(let
loop ()
; lecture de la requête
(let
([query (read-request server)])
;
traitement de la requête
(process-query server
query))
; itération
(loop)))
Le programme construit tout d'abord l'objet server en fonction des attributs de la classe <server>. Puis il construit une continuation qu'il affecte à l'attribut error du serveur. Lorsque cette continuation sera invoquée en cas d'erreur, tout se passera comme si on retournait de l'appel call/cc. Cette continuation pourra être invoquée indéfiniment.
Nous entrons ensuite dans le coeur du serveur, l'itération. Le programme lit une requête et la traite pour enfin reboucler. Elémentaire !
Les requêtes
Les requêtes sont elles aussi des objets instanciés à partir de la classe suivante :
; classe des
requêtes
(define-class <query> #f
;
date de création de la requête
(date
:initform 0.0)
; commande HTTP
(command :initform "")
; URI
(texte complet de la requête)
(uri :initform
"")
; protocole de la requête
(protocol :initform "")
;
entêtes dans la requête
(headers :initform
'())
; URL de la requête
(url
:initform "")
; argument dans
l'URI
(string :initform "")
;
nom complet de la ressource
(resource :initform
""))
L'attribut date de la classe est la date à laquelle la requête a été créée. Ceci nous servira pour les fichiers de journalisation (connus aussi sous le nom de fichiers de logs). Nous verrons la signification des autres attributs au fur et à mesure.
Pour initialiser la date de création de la requête, nous redéfinissons la méthode oo:create de création de la classe par :
; méthode de
création
(define-method (oo:create (self
<query>) args)
(<query>:date! self (date)))
La fonction date est une extension d'Open-Scheme qui retourne la date du jour. Cette date est affectée à l'objet. Cette date peut être manipulée par les fonction date->string ou date->vector.
Lecture des requêtes
La lecture des requêtes est effectuée par la fonction read-query définie par :
; lecture d'une
requête
(define (read-request server)
(let*
(; construction de la requête
[query
(build <query>)]
;
lecture de la ligne de la requête
[line
(get-line)]
;
séparation des mots
[request (if
(string? line)
(str:separate
line #\space)
"")])
On commence par construire une requête vierge. Puis on lit une ligne entière avec la fonction get-line définie plus pas. Enfin, on sépare la requête en mots avec la fonction str:separate. Ainsi, si la ligne lue est "GET /home/index.html HTTP/1.0", la variable request contiendra la liste de chaînes de caractères '("GET" "/home/index.html" "HTTP/1.0"). Il peut arriver que la ligne lue ne soit pas valide, en cas d'erreur.
Nous avons ensuite :
;
extraction des composants de la requête
(<query>:command! query (list-ref* request 0))
(<query>:uri! query (list-ref* request 1))
(<query>:protocol! query (list-ref* request 2))
Nous prenons le premier élément de la liste request avec la fonction list-ref* définie plus bas pour l'affecter à l'attribut command de l'objet query. Nous faisons de même avec les éléments de la liste suivants. La fonction list-ref* fonctionne comme son homologue de la bibliothèque standard du langage Scheme, avec comme différence quelle retourne #f si la liste est trop petite, au lieu de provoquer une erreur.
Ainsi, si la requête est "GET /home/index.html HTTP/1.0", l'attribut command vaudra "GET", l'attribut uri vaudra "/home/index.html" et l'attribut protocol vaudra "HTTP/1.0". De même, si la requête est "GET /home/index.html", l'attribut command vaudra "GET", l'attribut uri vaudra "/home/index.html" et l'attribut protocol vaudra #f.
Nous vérifions qu'au minimum, nous avons une commande et une URI :
; au
moins une commande et une URI
(if (or (not
(<query>:command query))
(not
(<query>:uri query)))
(print-error server
query
501
"Not implemented"
".
request="
line))
Dans le cas où la requête est incomplète, nous invoquons la fonction print-error, définie plus bas. Cette fonction affichera un message d'erreur au format HTML, ajoutera une ligne dans le fichier de journalisation des erreurs du serveur et invoquera la continuation d'erreur du serveur pour passer à la requête suivante. La fonction print-error prend en arguments le serveur, la requête, le code d'erreur HTTP et le texte de l'erreur. La liste des codes d'erreurs est donnée dans les RFCs dont la liste est dans les références.
Nous ajoutons maintenant une ligne au fichier de journalisation des accès :
;
journalisation de l'accès
(format
*current-error-port*
"[~a]: ~a ~a\n"
(format-date (<query>:date query) )
(<query>:command query)
(<query>:uri
query))
Pour l'instant, la ligne de journalisation est affichée dans le fichier standard des erreurs, par défaut, l'écran. La date de la requête doit être formatée pour retourner une chaîne de caractères par la fonction format-date définie plus bas.
Les protocoles HTTP/1.0 et HTTP/1.1 permettent d'envoyer sous la ligne de requêtes des entêtes (headers) qui donne un certain nombre de renseignements sur la requête ; une ligne blanche doit suivre, qu'il y ait ou non des entêtes. Le protocole HTTP/0.9, quant à lui, ne reconnaît pas les entêtes et ne nécessite pas de ligne blanche ; le texte donnant la version du protocole n'est même pas nécessaire. Ainsi, une requête HTTP/1.0 ou HTTP/1.1 aura la forme :
GET /home/index.html
HTTP/1.0
Referer:www.toto.com
Notez la présence d'une ligne blanche à la fin de la requête. Une requête selon le protocole HTTP/0.9 aura la forme :
GET /home/index.html HTTP/1.0
Nous lisons donc ensuite les entêtes, en fonction de la version de protocole utilisé :
;
lecture des entetes, le cas échéant
(cond
[; pas de protocole --> protocole HTTP/0.9
(not
(string? (<query>:protocol query)))
(<query>:headers! query '())
(<query>:protocol!
query "HTTP/0.9")]
[; protocole HTTP/1.0 ou
HTTP/1.1
(or
(string-ci=? (<query>:protocol query)
"http/1.0")
(string-ci=?
(<query>:protocol query)
"http/1.1"))
(<query>:headers!
query
;
itérer: lire une ligne
(let loop ([line
(get-line)])
(if (or (eof-object?
line)
(zero? (string-length
line)))
'()
;
séparer le nom de la valeur
(let*
([list (str:separate line #\:)]
[name (str:upper-case!
(list-ref list 0))]
[data
(list-ref* list 1)])
;
construire la liste de couple
(cons
(cons name data)
(loop
(get-line)))))))]
[else
;
protocole inconnu, produire erreur
(print-error
server
query
501
"Not implemented"
(<query>:protocol query))])
Si le protocole est absent de la requête, nous sommes en HTTP/0.9 ; dans ce cas, il n'y a pas d'entête. Si le protocole vaut HTTP/1.0 ou HTTP/1.1, nous devons lire les entêtes et une ligne blanche. Les entêtes sont lus ligne à ligne ; chaque ligne contient deux mots séparés par le caractères #\: avec, à droite le nom de l'entête et à gauche sa valeur. Nous fabriquons une liste de couples nom-valeur que nous affectons à l'attribut header de la requête.
Nous séparons maintenant L'URL (Uniform Resource Locator) des arguments dans l'URI (Uniform Resource Identifier). En effet, une requête peut avoir la forme "GET /pgm.cgi?name=3" ; dans ce cas, l'URI est "/pgm.cgi?name=3", l'URL est "/pgm.cgi" et les arguments sont "name=3".
;
séparation de l'URI en URL et arguments
(let*
([list (str:separate (<query>:uri query) #\?)]
[url (list-ref* list 0)]
[str (list-ref* list
1)])
(<query>:url! query (if url url #f))
(<query>:string! query (if str str "")))
Il ne nous reste plus qu'à retourner la valeur de retour, l'objet query si la commande est valide, ou à invoquer la fonction de traitement des erreurs :
; si
la commande est valide, retourner la requête
;
sinon produire une erreur
(if (or
(string-ci=? (<query>:command query) "get")
(string-ci=? (<query>:command query)
"head"))
query
(print-error server
query
501
"Not implemented"
(<query>:command query)))))
La fonction de lecture des requêtes est maintenant terminée. La fonction de lecture des lignes get-line est définie par :
; lecture d'une ligne de
texte sur le port
; d'entrée standard. Sera redéfinie
plus tard.
(define (get-line)
(read-line))
La fonction list-ref* est définie par :
; retourne un élément
de la liste à partir de
; son numéro, ou #f, si la
liste est trop courte
(define (list-ref* list ref)
(let loop ([ref ref]
[list list])
(if
(pair? list)
(if (zero? ref)
(car list)
(loop (- ref 1)
(cdr list)))
#f)))
Si la liste est assez longue, elle retourne l'élément à la position ref (base zéro), sinon, elle retourne faux.
Traitement des requêtes
Nous proposons ici une version très simplifiée de traitement des requêtes :
; traitement d'une
requête
(define (process-query server query)
;
vérification que le répertoire racine existe
(if (not (os:directory?
(<server>:root-directory server)))
(print-error
server
query
404
"Not found"
"No
such file or directory: "
(<server>:root-directory server)))
; fabriquer le
nom de la ressource demandée
(<query>:resource!
query
(path! (<server>:root-directory
server)
(<query>:url query)))
; vérifier que l'on a bien les droits pour accéder à
la ressource
(check-access server query)
;
obtention de la ressource
(let ([err (trap
(output-resource server query))])
(if err
(format *current-error-port* "*** Error ~a\n"
err))))
La fonction commence par vérifier que le répertoire racine existe bien, puis elle fabrique le nom de la ressource demandée en ajoutant au répertoire racine l'URL de la requête avec la fonction path! définie plus bas.
Nous ne traitons pas ici les indexes par défaut.
La ressource est produite avec la fonction output-resource, définie plus loin ; nous capturons les erreurs qui peuvent se produire à ce stade avec la macro trap. Cette macro retourne faux si aucune erreur ne s'est produite, et une chaîne de caractères décrivant l'erreur, dans le cas contraire.
Vérification des droits
Voici maintenant la fonction de contrôle des accès :
; vérification de
l'accès
(define (check-access server query)
;
extraction des attributs
(let* ([resource
(<query>:resource query)]
[url (<query>:url
query)]
[path (os:dirname resource)])
;
la resource doit être un fichier
(if (not
(os:file? resource))
(print-error server
query
404
"Not found"
"File not found "
url))
; ce fichier
doit être lisible
(if (not
(os:readable? resource))
(print-error server
query
403
"Forbidden"
"Access
prohibited "
(<query>:url
query)))
; vérifier que la
ressource se situe bien à
;
l'intérieur du répertoire racine du serveur
(let ([org (getcwd)])
(chdir path)
(let ([where (getcwd)])
(chdir
org)
(if (not (string-head?
where
(<server>:root-directory
server)))
(print-error server
query
404
"Not found" "File not found "
(<query>:url query)))))))
Cette fonction vérifie que la ressource existe bien est que le serveur y possède des droits en lecture. De plus, elle s'assure que la ressource se situe à l'intérieur du répertoire racine du serveur. Pour se faire, elle se place dans le répertoire contenant la ressource avec la fonction chdir, puis elle lit le répertoire courant avec getcwd ; elle vérifie alors que ce répertoire lu contient en racine le répertoire racine du serveur, avec la fonction string-head? :
; détermine si une
chaîne de caractères est le préfixe
; d'une
autre chaîne
(define (string-head? string head)
(let ([string-len (string-length string)]
[head-len (string-length head)])
(and (<=
head-len string-len)
(string=? head (substring
string
0
head-len)))))
Production de la ressource
La production de la ressource affiche sur la sortie standard le code HTML destiné au navigateur :
; types MIME en fonction de
l'extension du fichier
(define
*mimes*
'(("htm"
. "text/html")
("html" .
"text/html")
("txt" .
"text/plain")
("css" . "text/css")
("gif" . "image/gif")
("jpg" . "image/jpeg")
("jpeg"
. "image/jpeg")
("tif" .
"image/tiff")
("tiff" .
"image/tiff")
("png" . "image/png")
("lrp" .
"application/octet-stream")))
; produit la
resource au format HTML sur la sortie standard
(define
(output-resource server query)
(let* ([resource
(<query>:resource query)]
[protocol
(<query>:protocol query)]
;
extension de la resource
[extension
(os:extname resource)]
[mime (assoc
extension *mimes*)]
[mime (if mime (cdr
mime) "text/plain")])
; si
le protocole est supérieur à 0.9, produire
;
les entêtes
(if (and (or
(string=? protocol "HTTP/1.0")
(string=? protocol "HTTP/1.1"))
(string=? mime "text/plain")
(string=? mime "text/html"))
(begin
(print-header query "200 OK")
(format
#t
"Content-Length:
~a\r\n"
(os:size resource))
(format
#t
"Last-Modified: ~a\r\n\r"
(format-date (os:modified resource)))
(format
#t
"Content-Type: ~a\r\n\r\n"
mime)))
(cond [(string-ci=? (<query>:command
query) "head")
'nothing]
[else
(output-file server query)])))
La fonction extrait les attributs et calcule le type MIME à partir de la variable globale *mime*. Si le protocole est supérieur à la version 0.9 et que la ressource est soit du texte soit une page HTML, la fonction affiche les entêtes HTTP. Le contenu du fichier est ensuite affiché par la fonction output-file définie ci-dessous :
; affiche la ressource sur la
sortie standard
(define (output-file server query)
(with-input-from-file
(<query>:resource query)
(lambda ()
(let loop ()
;
itération: lecture d'un bloc d'octets
(let
([buffer (port-read 1024)])
(if (not
(eof-object? buffer))
(begin
;
affichage de ce bloc d'octets
(port-write
buffer
(string-length buffer))
(loop))))))))
L'entete HTTP est affiché, quant à lui, par la fonction print-header :
; identifiant du
serveur
(define *server* "OpenScheme server")
;
affiche l'entête HTTP sur la sortie standard
(define
(print-header query header)
(format #t
"HTTP/1.1 ~a\r\nServer: ~a/~a\r\n"
header *server* *version*))
(format #t
"Date:
~a\r\nConnection: close\r\n"
(format-date
(<query>:date query))))
La date est simplement formatée par la fonction standard d'Open-Scheme (plus tard, nous aurons à réécrire cette fonction de formatage pour être conforme à la norme HTTP) :
; formatage de la date (sera
redéfinie plus tard)
(define (format-date
date)
(date->string date))
La fonction path! construit un chemin de fichier à partir de deux chemins, en les combinant entre eux. La difficulté est de les séparer par un seul séparateur (caractère /) :
; combine deux chemins entre
eux
(define (path! path1 path2)
(if (path?
path1)
; path1/
(if (char=? (string-ref path2 0)
#\/)
;
/path2
(string-append path1
(substring path2
1
(string-length
path2)))
;
path2
(string-append path1
path2))
; path1
(if (char=? (string-ref path2 0)
#\/)
;
/path2
(string-append path1
path2)
;
path2
(string-append path1
"/"
path2))))
La fonction path? permet de savoir si le nom d'une ressource se termine par le séparateur de répertoire :
; l'argument désigne-t-il
un répertoire ?
(define (path? path)
(let
([length (string-length path)])
(and (>
length 0)
(char=? #\/
(string-ref
path
(- length 1))))))
Erreur
Les erreurs sont affichées par la fonction print-error :
; affichage des erreur
(define
(print-error server query number title . args)
;
affichage de l'entête HTTP
(print-header
query
(format
#f "~a ~a" number title))
(display
"Content-type: text/html\r\n\r\n")
;
affichage de l'erreur, en HTML
(format #t
"<HTML>\n <HEAD>\n <TITLE>~a ~a"
number
title)
(display
"</TITLE>\n</HEAD>\n<BODY>\n")
(format #t
"<H2>~a ~a<H2>\n"
number
title)
(for-each (lambda
(arg)
(format #t " ~a\n" arg))
args)
(display " <! Open-Scheme WEB
Server>\n")
(format #t
" <!
~a - ~a [~a] >\n"
*name* *version* *when*)
(display " <! USE AT YOUR OWN RISK. >\n")
(display " <! THERE IS ABSOLUTLY NOT")
(display " WARANTIES OF ANY
SORT. >\n")
(display "</BODY>\n</HTML>")
;
journalisation de l'erreur
(format *current-error-port*
"[~a]: ~a ~a "
(format-date
(<query>:date query))
number
title)
(for-each (lambda (arg)
(display
arg *current-error-port*))
args)
(newline
*current-error-port*)
; appel à la
continuation de traitement des
; erreurs
((<server>:error server) 0))
La fonction affiche les entêtes HTTP puis le texte de l'erreur au format HTML. Puis on journalise le texte de l'erreur, pour l'instant sur le port standard d'erreur. Enfin, on invoque la continuation sur erreur du serveur, dans l'attribut error. Ceci nous replace dans le programme principal, prêts pour une nouvelle lecture des requête.
Utilisation
La première version (élémentaire) du serveur WEB est maintenant terminée. Elle contient tous les éléments d'un ``vrai'' serveur, mais simplifiés à l'extrême. Dans les prochains articles, nous apporterons quelques modifications pour augmenter sa puissance.
L'utilisation de ce serveur est très simple.
Créez tout d'abord une page WEB nommée par exemple page.html et placez-la dans un répertoire. Remplacez l'option :root-directory par ce répertoire lors de la création du serveur.
Lancez le serveur avec :
$ osm --banner=off --exec ows.osm
Vous pouvez ensuite taper des requêtes :
GET /page.html
devrait afficher le contenu de votre page. Il est aussi possible de formuler une requête selon le protocole HTTP/1.0 avec :
GET /page.html HTTP/1.0
Dans ce cas, le texte de la page est précédé par les entêtes HTTP.
Comme le serveur est conçu pour fonctionner indéfiniment, il est possible de le stopper en tapant simultanément sur les touches CONTROL et C.
Le mois prochain, nous améliorerons le serveur.
Les auteurs
Guilhem de Wailly, directeur de la société Erian Concept : support, formations, configurations, administration, développements Linux. Environnement Open-Scheme.
http://www.erian-concept.com
Pr Fernand Boéri, Professeur à l'Université de Nice - Sophia-Antipolis, membre de l'unité SPORTS du laboratoire I3S, spécialiste en modélisation, évaluation de performances et architectures parallèles.
http://www.i3s.unice.fr
Références
WEB
 TCP/IP, Architecture,
protocoles, applications
Douglas Comer
InterEdition
ï‚· W3C
http://www.w3c.org
ï‚· CGI
http://www.jmarshall.com/easy/cgi/
ï‚· CGI
http://hoohoo.ncsa.uiuc.edu/cgi/
ï‚· CGI
http://www.tsden.org/ryutaroh/fileupload-e.shtml
ï‚· CGI en Français
http://www.scripts-fr.com/
ï‚· CGI+Scheme
http://www.lh.com/~oleg/ftp/Scheme/web.html
ï‚· FastCGI
http://www.fastcgi.com/
ï‚· HTTP en
Français
http://webbo.enst-bretagne.fr/ActiveWebFr/eg-uk-tut.book_29.fr.html
ï‚· HTTP 1.0
http://www.ietf.org/rfc/rfc1945.txt
ï‚· HTTP 1.1
http://www.ietf.org/rfc/rfc2616.txt
Scheme
 Structure et Interprétation
des programmes informatiques
H. Abelson, GJ.
Sussman
InterEdition
 The Scheme Programming
Languages - Ansi Scheme
R. Kent Dybvig
Prentice Hall
 Les langages Lisps -
Christian Queinnec
InterEdition
 Programmer avec Scheme
- J. Chazarin
Thomson Publishing
 Revised4 Report on the
Algorithmic Language Scheme
W. Clinger, J.
Rees
ftp://ftp.nj.nec.com/pub/kelsey
Environnements Scheme Free
 Bigloo -
Manuel Serrano
http://kaolin.unice.fr
Environnement
de programmation Scheme.
 DrScheme -
Rice University
http://www.cs.rice.edu/CS/PLT/
Environnement
Scheme libre très avancé.
 PCScheme - Texas
Instrument
ftp://cui.unige.ch/public/pcs/pcscheme.exe
Un
très bon environnement de programmation Scheme pour DOS, avec
éditeur intégré.
 Scm - A.
Jaffer
http://www-swiss.ai.mit.edu/~jaffer
La
référence des interprètes Scheme. Très
petit, rapide, pour beaucoup de plates-formes, extensible.
 Stk -
Erik Gallesio
http://kaolin.unice.fr
Interprète
Scheme avec la bibliothèque TK.
D'autres liens sont visibles sur le site www.schemers.org.
Environnements Scheme commerciaux
 ChezScheme -
Cadence, Inc
http://www.scheme.com/
Environnement
Scheme très performant.
 EdScheme, 3Dscheme -
Scheme, Inc
http://www.schemers.com/
Environnement
de programmation Scheme pour Windows.
 Inlab Scheme - Inlab
Software GmbH
http://www.munich.net/inlab/scheme/
Environnement
commercial
 Open-Scheme -
Erian Concept
http://www.open-scheme.com
Environnement
professionnel de programmation Scheme comprenant un interprète,
un compilateur et un débogueur symbolique.
Existe en
version libre et commerciale, pour Linux, FreeBSD, Solaris, Windows
et BeOS, sur systèmes Intel.