Der Wunsch: Systemnahes Programmieren mit Lisp

Nehmen wir an, wir möchten für ein kleines, in seinen technischen Fähigkeiten eingeschränktes Gerät Code schreiben, zum Beispiel für ein Handy. In diesem Fall wird man vor allen Dingen versuchen müssen, den Arbeitsspeicherverbrauch in engen Grenzen zu halten. Nehmen wir weiters an, wir wären ein leidenschaftlicher Lispprogrammierer. Welche Programmiersprachen eignen sich für uns in dieser Situation?

Erster Versuch: Scheme

Auf dem Scheme-Wiki liest man, Scheme habe im Gegensatz zu Common Lisp eine kleine Laufzeitumgebung, die leicht in C-Code einbettbar sei. Machen wir uns also zunächst auf die Suche nach einer passenden Scheme-Implementierung. Das scheint am erfolgversprechendsten.

Am sinnvollsten erscheinen zunächst die Implementierungen, die C-Code erzeugen. Diejenigen darunter, die eine einigermaßen hinreichende Auswahl an Bibliotheken bereitstellen, sind, wie es scheint, Chicken, Gambit und Bigloo. Besonders letztere Implementierung scheint C-nah zu sein, ein gutes Zeichen.

Kompiliert man die Implementierungen aber, so zeigt sich ein ernüchterndes Bild: Die Laufzeitumgebungen sind nicht gerade klein, wenn man von einem eingeschränkten Zielgerät ausgeht. Die Runtime von Chicken schlägt mit fast 4 MB zubuche, die von Bigloo mit über 4,5 MB und die von Gambit sogar mit rund 5,5 MB. Damit ist die Option Scheme wohl erledigt.

Zweiter Versuch: Common Lisp

Der Gedanke, den man an diesem Punkt hegen kann, ist: Wenn die Runtimes der Schemes ohnehin recht groß sind, können wir auch gleich Common Lisp verwenden. Das wird zwar nicht kleiner, aber zumindest komfortabler sein.

Wir laden also ECL herunter, das „einbettbare Common Lisp“, kompilieren es und sind erstaunt: Die Runtime ist gerade einmal 2 MB groß — um die Hälfte kleiner als das kleinste praktikable Scheme also. Wir vermuten, daß die Scheme-Seite bis dato einiges an Optimierungspotential ungenutzt gelassen hat.

Wie auch immer: Wir merken uns ECL vor, suchen aber weiter. Kann es wirklich sein, daß man mindestens 2 MB an Runtime mit sich herumschleppen muß, um ein wenig von C wegzukommen?

Dritter Versuch: Exotische Systemlisps

Wir landen auf unserer Suche nach einem möglichst C-nahen Lisp bei einigen Ansätzen, die versuchen, die Auslieferung fertiger Software zu vereinfachen, indem nicht zu weit von C weggegangen wird. Diese sind STELLA, eine Art Multicompiler für Lisp, der seinen Lispdialekt in Java, C++ und Common Lisp übersetzen kann, und Lush, ein speziell auf effiziente numerische Berechnungen ausgelegtes Lisp.

Auch, wenn sich beides ganz nett anhört, die Details machen uns erneut einen Strich durch die Rechnung: Wieder sind die Laufzeitumgebungen einfach zu groß.

Vierter Versuch: Back to the Roots

Langsam müde von der langen Recherche, widmen wir uns der letzten Idee: Wir suchen nach etwas, das so nah an C bleibt, wie möglich. Das heißt: Wir opfern Lispigkeit, um Systemnähe zu erhalten. Wir halten Ausschau nach etwas, das letztlich nur ein Präprozessor ist, der Code direkt und ohne Laufzeitumgebung in reinen, rohen C-Code übersetzt.

Es gibt mit Sicherheit einige Ansätze dazu. Wir beschränken uns auf die Lisp-nahen und finden zwei: C-amplify und SC.

C-amplify: Ambitioniert, und zwar zu sehr

C-amplify ist das ambitioniertere der beiden Projekte. Es versucht, alle Informationen zu sammeln, die es kriegen kann. Dafür nimmt es an, daß der C-Präprozessor nicht verwendet wird. Stattdessen muß der Benutzer dafür Sorge tragen, daß Deklarationen aller Funktionen und Datenstrukturen, auf die man zugreifen will, in für C-amplify verständlicher Form vorliegen.

Der Ansatz von C-amplify verhindert natürlich, daß C-Code direkt eingebettet werden kann, weil ansonsten die Informationen über Rückgabewerte und ähnliches verloren gehen könnten. Leider unterstützt C-amplify noch nicht alles, was in C möglich ist, weshalb das dazu führt, daß man manche Dinge einfach nicht tun kann. Außerdem ist es schwierig, Bibliotheken von Drittanbietern oder des Betriebssystems zu verwenden, wenn man den C-Präprozessor nicht ausnutzen kann. Was aber ist C ohne Bibliotheken? Richtig. Versuchen wir etwas anderes.

SC: Keep it Simple, Stupid

Wir sehen uns zuletzt also SC an. Auf den ersten Blick erinnert es uns sehr an C-amplify, nur mit einem kleinen, doch wesentlichen Unterschied: Die Ambitionen sind merklich kleiner.

SC versucht nicht, Typinferenz zu betreiben oder sonstige Daten über das zu kompilierende Programm zu sammeln, um Optimierungen durchzuführen. Es verlangt auch nicht, daß irgendwelche Informationen aus Header-Dateien dupliziert oder in das System eingelesen werden. Stattdessen macht es eine sture Übersetzung aus einer Lisp-artigen Syntax in die entsprechende C-Syntax, und expandiert nebenbei (prozedurale, lispige) Makros. Mehr tut es nicht, seinen Zweck erfüllt es dafür aber klaglos.

Wir schreiben Code wie diesen:

;;;; -*- mode: lisp -*-

(c-exp "#include <stdio.h>")
(c-exp "#include <stdlib.h>")

(def (main argc argv) (fn int int (ptr (ptr char)))
  (if (> argc 1)
      (printf "Hallo, %s!~%" (aref argv 1))
      (printf "Hallo, Welt!~%"))
  (return EXIT_SUCCESS))

Er wird übersetzt in das Folgende:

#include <stdio.h>
#include <stdlib.h>

int 
main(int argc, char **argv)
{
  if (argc > 1)
    printf("Hallo, %s!\n", argv[1]);
  else
    printf("Hallo, Welt!\n");
  return EXIT_SUCCESS;
}

Hübsch! Fehlt nur noch, daß man SC von der Befehlszeile aus aufrufen kann anstatt von Lisp aus, und auch das ist hinzubekommen, indem man folgende drei Befehle in die CLISP-Befehlszeile eingibt. Wir erzeugen dadurch ein Befehlszeilentool mit dem Namen „sc2c“:

(load "init.lsp")
(ext:saveinitmem "sc2c"
                 :norc t
                 :executable t
                 :init-function (lambda ()
                                  (let ((sc-main::*indent-options* '("-i2")))
                                    (mapc 'sc-main:sc2c ext:*args*))
                                  (ext:exit)))
(ext:exit)

Die Übersetzung durch SC ist einfach und effektiv. Wenn man ein Konstrukt findet, von dem man nicht weiß, wie man es in der Lispsyntax schreiben soll, oder wenn man es aus irgendwelchen Gründen schlicht lieber direkt in C ausdrückt, dann geht das mit der C-EXP-Form. Man kann SC leicht in seinem Makefile vor den C-Compiler schalten, es ist unintrusiv, es verlangt einem nichts ab, und es erfordert keine Änderungen an bestehendem Code. Kurz: Es ist ein Meisterwerk.

Ich werde mit SC auf jeden Fall herumexperimentieren. Vielleicht gewöhne ich mich daran. Vielleicht verwende ich es sogar bald produktiv. Man wird sehen.