Durch die Hintertür

Nicht nur im Web-Bereich ist Perl beliebt. Oft kommen auch große Anwendungssysteme mit einem API, so daß man sie durch Perl-Skripte erweitern und steuern kann.

vorlesen Druckansicht
Lesezeit: 11 Min.
Von
  • Andreas Geissel
Inhaltsverzeichnis

Wichtige Funktionen und Daten vieler Anwendungen sind über C-Funktionen oder C++-Klassen zugänglich. Damit können Programmierer Erweiterungen schreiben, indem sie sich dieser Funktionen beziehungsweise Klassen bedienen und die entsprechende Bibliothek zu ihren Programmen binden. Um ein solches API in einem Perl-Skript verwenden zu können, ist es allerdings nicht mit dem Binden einer Bibliothek getan: Da Perl nicht weiß, wie es mit komplexeren C/C++-Datentypen umzugehen hat, benötigt es eine Zwischenschicht zur Behandlung von Parametern und Rückgabewerten.

Zu deren Formulierung dient die Sprache XS. In ihr deklariert der Programmierer für Perl benutzbare Funktionen (XSUBs) und legt fest, welche Parameter übergeben, ob deren Änderungen an den Aufrufenden weitergeleitet und mit welchen Default-Werten sie initialisiert werden sollen. Der Compiler xsubpp setzt XS- in C-Code um und trägt Anweisungen für die Parameterbehandlung ein. Den Umgang mit komplexeren Datentypen wie Strukturen oder Klassen definiert eine Tabelle, für Standarddatentypen wie int oder double existieren vorgefertigte Regeln. Aus dem erzeugten C-Code entsteht während des make-Laufs eine dynamische Bibliothek. Fordert ein Skript das Modul per use oder require an, lädt der Interpreter diese Bibliothek.

Perl bietet zwei Möglichkeiten, mit komplexen Datentypen umzugehen. Der zunächst einfachere Weg ist, die Referenz einer C-Struktur in Perl als Skalar zu behandeln und über Funktionen auf ihre Elemente zuzugreifen. Dabei ist der Skalar explizit als Parameter anzugeben. Da Perl nicht weiß, um was es sich bei einem solchen Skalar handelt, muß sich der Programmierer um eventuell nötige Aufräumarbeiten selber kümmern. Bei diesem Vorgehen ist die Typumsetzung zwischen Perl und C recht einfach.

Folgendes kleine Beispiel zeigt, wie der Zugriff auf eine C-Struktur mit Integer-Feld aussehen könnte:

$scal = &mypack::create(42);
print &mypack::getNum($scal)."\n";
&mypack::del($scal);

Im zweiten Ansatz dient eine Referenz auf C/C++-Objekte als Perl-Objekt. Dieses Vorgehen erlaubt beispielsweise die Verwendung von new, um ein neues Objekt zu erzeugen. Der Garbage-Collector des Perl-Interpreters erfaßt und löscht es wie andere Perl-Variablen, wenn es nicht mehr benötigt wird. Um eventuell gehaltene Ressourcen freigeben zu können, ruft er vor dem Löschen eines Objekts automatisch dessen Destruktor auf, sofern der existiert.

Zwei Beispiele sollen diese beiden Verfahren illustrieren. Sie wurden mit Perl 5.004 unter Linux 2.0.33 und dem gcc 2.7.2 entwickelt. Das erste nutzt eine C-Struktur und entsprechende Zugriffsfunktionen mit der XS-Schnittstelle; das zweite demonstriert, wie Perl mit C++-Objekten verfährt, deren Referenzen zu einem Paket gehören.

Zunächst legt man mit dem Programm h2xs ein Verzeichnis an, das die Quellen für das neue Modul enthält:

h2xs -n c_struct

In c_struct liegen danach verschiedene Dateien, von denen zunächst nur c_struct.xs interessant ist. In ihr deklariert der Programmierer die XSUBs, die das Modul zur Verfügung stellen soll. Auf den ersten Blick gleicht ihr Inhalt C-Code nach Kernighan/Ritchie - mit dem Unterschied, daß gegen Ende

MODULE = c_struct PACKAGE = c_struct

steht. Diese Zeile leitet den Beginn des XS-Codes ein. Alles darüber landet unverändert in der vom XS-Compiler erzeugten C/C++-Datei.

Um das Beispiel einfach zu halten, sind Struktur und Funktionen im C-Teil von c_struct.xs definiert (Listing 1); man könnte auch externe C-Dateien dort einbinden. Der XS-Teil führt die Funktionen auf, die nach außen sichtbar sein sollen. Hier sieht die Struktur so aus:

typedef struct __CStruct { 
char *name;
int num;
} CStruct;
Mehr Infos

Listing 1

...
typedef
struct __CStruct
{
char *name;
int num;
} CStruct;

void setName(CStruct *s, char *n)
{
if (s != NULL)
{
if (s->name != NULL) { free(s->name); }
s->name = (n != NULL) ? strdup(n) : NULL;
}
}

MODULE = c_struct PACKAGE = c_struct

CStruct *
createStruct(name, num)
char* name
int num
CODE:
CStruct *s = (CStruct*) malloc(sizeof(CStruct));
s->name = NULL;
setName(s, name);
s->num = num;
RETVAL = s;
OUTPUT:
RETVAL


void
deleteStruct(s)
CStruct* s
CODE:
if (s != NULL)
{
if (s->name != NULL) { free(s->name); }
free(s);
}

void
setName(s, name)
CStruct* s
char* name

char*
getName(s)
CStruct* s
CODE:
if (s != NULL)
{
RETVAL = s->name;
}
else
{
RETVAL = "";
}
OUTPUT:
RETVAL
...

Für die Datenfelder name und num gibt es jeweils Zugriffsfunktionen und zwei Routinen, um eine Struktur anzulegen beziehungsweise zu löschen: create/deleteStruct(), set/getName() und set/getNum().

XSUB-Deklarationen ähneln denen von C-Funktionen der Vor-ANSI-Zeit. Der Rückgabetyp muß in einer separaten Zeile stehen; die Parameterliste führt lediglich Namen ohne Typen auf. Ein abschließendes Semikolon nach jeder Parameterdeklaration ist nicht notwendig, stört aber nicht. Im Anschluß an die Parameterliste folgt die Implementierung der XSUB.

In der Regel muß der Programmierer für jede XSUB eine C-Funktion mit gleichem Namen und Parameterliste erstellen, die der Interpreter nach dem Umsetzen der Datentypen aufruft. Im Beispiel ist das setName(). Eine XSUB kann aber auch Code enthalten, der Parameter aufbereitet oder den kompletten Rumpf der Routine implementiert. Das Schlüsselwort CODE leitet die Implementierung ein, etwa für getName(). Die folgenden Zeilen übernimmt der XS-Compiler weitgehend unberührt in die erzeugte C-Datei. Als Faustregel kann gelten: Funktionen, die nur in Perl-Skripten benutzbar sein sollen, implementiert man ohne C-Pendant. Sollen sie innerhalb des XS-Moduls verwendet werden, erstellt man die C-Variante. Im Beispiel ist setName() deshalb in C geschrieben, weil createStruct so darauf zurückgreifen kann.

Hat die Funktion einen Rückgabewert, ist nicht das C-return, sondern die XS-Variable RETVAL zu benutzen. Sie muß der Programmierer im OUTPUT:-Teil explizit als Ausgabevariable kennzeichnen, wenn die XSUB einen CODE-Teil besitzt. Fehlt der, deklariert der XS-Compiler RETVAL automatisch als Ausgabevariable und weist ihr das Ergebnis der gleichnamigen C-Funktion zu. Diese vom XS-Compiler automatisch angelegte Variable hat immer den gleichen Datentyp wie die Funktion.

Damit der XS-Compiler CStruct* an Perl übergeben kann, muß er wissen, welcher Datentyp ihm auf der Perl-Seite entspricht. Diese Typkonvertierungen definiert der Programmierer in der Datei typemap, die er in dem Verzeichnis anlegen muß, wo die XS-Quellen liegen. Da Perl keine weiteren Informationen zu CStruct* benötigt, reichen dort zwei Zeilen (die Typen sind durch mindestens ein Tab-Zeichen getrennt):

TYPEMAP 
CStruct* T_PTROBJ

Alle Standardkonvertierungen, etwa von int und double in Perl-Datentypen und zurück, sind in der Datei ExtUtils/typemap im Bibliotheksverzeichnis von Perl definiert. Um das Erweiterungsmodul zu übersetzen und zu testen, braucht man ein Makefile und ein Testskript. Das von h2xs erzeugte Perl-Skript Makefile.PL erstellt ein Makefile mit allen nötigen Einträgen. Um das Erweiterungsmodul testen zu können, muß das ebenfalls von h2xs erzeugte test.pl ergänzt werden. test.pl (Listing 2) ruft createStruct(), die Zugriffs- und schließlich die Aufräumfunktion auf. make test führt es aus. Nachdem das Modul getestet ist, kann es für den normalen Einsatz mit make install installiert werden.

Mehr Infos

Listing 2

# Testskript: "make test" sollte ohne Fehler durchlaufen
...
BEGIN { $| = 1; print "1..4\n"; }
END {print "not ok 1\n" unless $loaded;}
use c_struct;
$loaded = 1;
print "ok 1\n";
...
local $exmp = c_struct::createStruct("abc", 999);

print $exmp != 0 ? "" : "not ", "ok 2\n";
print &c_struct::getName($exmp) eq "abc" ? "" : "not ", "ok 3\n";
print &c_struct::getNum($exmp) == 999 ? "" : "not ", "ok 4\n";

c_struct::deleteStruct($exmp);

Wie man eine C++-Klasse in Perl verwenden kann, zeigt das zweite Beispiel. Ausgangspunkt ist wieder das Anlegen eines Quell-Verzeichnisses mit

h2xs -n cpp_class

cpp_class.xs (Listing 3) enthält wegen der Übersichtlichkeit alle Klassendeklarationen und Methoden. Diesmal müssen die Deklarationen der beiden von h2xs erzeugten C-Funktionen not_here() und constant() leicht geändert werden, da der C++-Compiler die alte K&R-Notation nicht versteht. Die Beispielklasse besitzt zwei Attribute (name und num) mit entsprechenden Zugriffsfunktionen.

Mehr Infos

Listing 3

// Auszüge aus cpp_class.xs

class XsClass {
public: //Konstruktor, Destruktor, Methoden

XsClass(char*, int);
~XsClass();
char *getName();
void setName(char*);
...

private:
char *name;
int num;
};

XsClass::XsClass(char* nm, int n):name(NULL), num(n)
{
this->setName(nm);
}

XsClass::~XsClass() {
if (this->name != NULL) { free(this->name); }
printf("~XsClass()\n");
}

char *XsClass::getName() {
return this->name;
}

void XsClass::setName(char *nm) {
if (this->name != NULL)
{ free(this->name); }
this->name = (nm != NULL) ?
strdup(nm) : (char*)NULL;
}

...

MODULE = cpp_class PACKAGE = cpp_class
...
XsClass*
XsClass::new(s, n)
char * s
int n

void
XsClass::DESTROY()

char*
XsClass::getName()

void
XsClass::setName(s)
char* s

...

XSUBs für die C++-Methoden bestehen nur aus den Funktionsköpfen und haben keinen eigenen Rumpf, da sich hinter der XSUB eine C++-Methode verbirgt, die der Interpreter nach dem Umsetzen der Parameter aufruft. Wiederum heißen die XSUBs so wie die entsprechende C++-Methode. Eine Ausnahme bilden Konstruktor und Destruktor: Die entsprechenden XSUBs sind XsClass::new() und XsClass::DESTROY(). Wie jeder anderen Methode fehlt ihnen der Rumpf, weil der XS-Compiler die entsprechenden C++-Anweisungen erzeugt.

In diesem Beispiel benötigt der XS-Compiler mehr Information zu dem verwendeten Datentyp als vorher: Neben dem Perl-Gegenstück des Datentyps muß er wissen, wie die Datentypen ineinander zu übertragen sind. Dazu enthält die Datei typemap Codestücke, die er zur Umsetzung der Typen in die erzeugte C++-Datei einsetzt. Listing 4 zeigt die typemap für dieses Beispiel. Sie besteht aus drei Bereichen: TYPEMAP teilt dem XS-Compiler mit, welche C++- und Perl-Typen sich entsprechen. Der OUTPUT-Bereich gibt an, was zu tun ist, wenn eine XSUB eine Referenz des entsprechenden Typs an Perl übergibt. Hier wird analog zum üblichen bless()-Mechanismus die Referenz auf ein XsClass-Objekt als zum Paket gehörend angemeldet. Der XS-Compiler setzt die Funktion sv_setref_pv() in den erzeugten C-Code ein und ersetzt dabei $arg und $var textuell durch die entsprechenden, in C bekannten Variablen, wenn Perl ein O_OBJECT empfangen soll. So ersetzt er $var durch RETVAL, und an die Stelle von $arg tritt der Perl-Parameter-Stack. CLASS ist eine vom XS-Compiler gesetzte C-Variable (char*), die den Namen des Pakets enthält. Aus dem INPUT-Bereich nimmt er den Code für die Umsetzung von Perl- in C/C++-Daten und baut ihn in die erzeugte C-Funktion ein. Für Variablen vom Typ O_OBJECT wird hier die Gültigkeit der Referenz geprüft und im Fehlerfall eine Meldung ausgegeben. Xsubpp ersetzt wiederum die Platzhalter (mit führendem Dollarzeichen) wie $arg, $func_name durch die jeweiligen C-Bezeichner oder -Ausdrücke. Die Namen der Datentypen, zum Beispiel O_OBJECT, kann der Programmierer selber vergeben, es ist jedoch üblich, für gesegnete C++-Objekte O_OBJECT zu verwenden.

Mehr Infos

Listing 4

TYPEMAP
XsClass* O_OBJECT

OUTPUT
O_OBJECT
sv_setref_pv($arg, CLASS, (void*) $var);

INPUT
O_OBJECT
if(sv_isobject($arg) &&
(SvTYPE(SvRV($arg)) == SVt_PVMG))
{
$var = ($type) SvIV((SV*) SvRV($arg));
}
else
{
warn(\"${Package}::$func_name() -- $var is not a blessed SV reference\");
XSRETURN_UNDEF;
}

Für dieses Beispiel ist das von h2xs erzeugte Makefile.PL zu ändern, da statt des C- der C++-Compiler gefragt ist. Das Skript besteht nur aus dem Aufruf von WriteMakefile() im Perl-Paket ExtUtils::MakeMaker, der um die folgenden drei Parameter zu erweitern ist:

"CC" => "c++", "LD" => "$ (CC)", 
"XSPROTOARG" => "-noprototypes".

Listing 5 demonstriert, wie man Objekte des neuen Pakets anlegen kann und diese verwendet. Ein explizites Löschen des Objekts ist nicht notwendig, denn dafür sorgt die Garbage Collection von Perl.

Mehr Infos

Listing 5

BEGIN { $| = 1; print "1..3\n"; }
END {print "not ok 1\n" unless $loaded;}
use cpp_class;
$loaded = 1;
print "ok 1\n";

local $c = new cpp_class("Hello", 1212);

printf("name is %s | num is %d\n",
$c->getName(), $c->getNum());

$c->setName("Jean Michel Jarre");
$c->setNum(2048);

printf("name is %s | num is %d\n",
$c->getName(), $c->getNum());

Neben den erwähnten Dateien erzeugt h2xs eine mit der Endung .pm. Ihr kommt eine wichtige Bedeutung zu: Verwendet ein Perl-Skript ein installiertes Modul, so lädt der Interpreter dessen pm-Datei, zum Beispiel cpp_class.pm, und diese sorgt dann für das Binden der dynamischen Bibliothek. Diese Dateien spielen noch aus einem anderen Grund eine Rolle: Der Programmierer hat die Möglichkeit, dort sein Modul zu dokumentieren. Daraus entsteht beim make install eine man-Page.

Für weitere Informationen sei auf die man-Pages perlxstut, perlxs und perlguts von Jeff Okamoto und Dean Roehrich verwiesen. Das Tutorial perlxstut enthält weitere Beispiele, die unter anderem zeigen, wie man Funktionen aus existierenden C-Bibliotheken einbinden kann.

Andreas Geissel
ist Informatiker und und arbeitet als Labor-Assistent am Fachbereich Informatik der FH Karlsruhe.

[1] Michael Schilli; Gesegnet; Objektorientierte Programmierung mit Perl 5; iX 3/96; S. 162ff.

[2] Michael Schilli; Erben und erben lassen; Objektorientierte Anwendungen mit Perl 5; iX 4/96; S. 198ff.

[3] Dean Roehrich; CookBook - Dean's Extension-Building Cookbook, part A und B; 1996 http://www.oasis.leo.org/perl/docs/manuals/CookBook.dsc.html

Mehr Infos

iX-TRACT

  • Zur Anbindung von C- und C++-Routinen stellt Perl die Sprache XS bereit. XS-Programme können C/C++-Code enthalten.
  • Ein Compiler wandelt XS- in C/C++-Programme und fügt dabei nötige Typkonvertierungen ein.
  • C++-Klassen kann man so einbinden, daß sie wie Perl-Objekte erscheinen. Perls Garbage Collection kümmert sich dann um ihre Beseitigung.
Mehr Infos

Benötigte Module

Das aktuelle Perl 5.004_04 für Unix-Plattformen enthält das verwendete Modul File::Copy bereits von Haus aus. LWP::Simple und HTML::TreeBuilder sind Bestandteil der Bibliothek libwww von Gisle Aas, die auf dem CPAN erhältlich ist, wo auch MIME::Base64 zur Base64-Kodierung zu finden ist.

Die Perl-Distribution für Windows 95 und NT, die unter ports/win95/Standard/x86/Perl5.00402-bindist04-bc.tar.gz auf dem CPAN liegt, enthält alle verwendeten Module. Zu beachten ist, daß der unter Windows benutzte DOS-Kommando-Interpreter auf den doppelten Anführungszeichen (") zum Einschließen des Perl-Kommandostrings beharrt, eventuell im Perl-Code vorkommende Anführungszeichen sind mit \" zu maskieren. Auch fehlt die Expansion von Wildcard-Konstrukten wie *.c auf Kommandointerpreterebene - diese Aufgabe ist an Perl zu delegieren, am besten mit Hilfe von for-Schleifen mit File-Globbing-Konstrukten.

Deutsche CPAN-Spiegel sind unter anderem:

(ck)