Perl ist eine tolle Sprache. Leider hat sie auch viele Abhängigkeiten. Besonders bei Embedding muss man im Allgemeinen eine ganze Perl-Installation ``mitschleppen''. Bei einem bestimmten Projekt musste ich ohne externe Dateien oder andere Abhängigkeiten auskommen. Um trotzdem Perl einsetzen zu können, musste alles in das Binary wandern. Die Probleme und Lösungen, die ich fand, sind im Folgenden beschrieben.
Das Projekt, für das Goofed entstand, bestand darin, den Radius-Server eines großen Internet-Providers in einigen Funktionen zu ersetzen. Da die Anforderungen durch viele Jahre hinweg sehr speziell wurden (ein typisches ``Wir setzen den Standard''-Phänomen) und die alte Codebasis neuen Anforderungen nicht mehr gut gewachsen war, musste ein neuer Daemon her.
Recht früh kam die Idee, im Kern für die komplizierten Regeln eine High-Level-Skriptsprache zu verwenden und nur für das eigentliche Radius-Paket-Management C zu verwenden.
Perl schien zuerst an vielen Gründen zu scheitern: kompliziertes Build-System, viele Abhängigkeiten und Dateien, keine gute Kontrolle über den Speicherbedarf (Leaks wären tödlich für einen Daemon, der meherre Monate durchhalten muss) und Last-not-Least stand zu befürchten, daß die Server-Abteilung die Lösung sofort ablehnen würde, sobald bekannt würde, daß sie Perl benutzt (typisches ``Real-World - macht keinen Sinn''-Phänomen).
Trotz dieser Gegenanzeigen versprach Perl auch wichtige Vorteile, der wichtigste war das Vorhandensein einer grossen Codebasis und viele Module. Das und Perl selbst führen meist sehr schnell zu verwertbaren Ergebnissen.
Um die Anforderungen zu erfüllen, habe ich folgende Entscheidungen gefällt:
Dies schließt leider das (ansonsten hervorragende) PAR-Modul aus, da dieses alle Bibliotheksdateien zwar in eine Datei packt, zur Laufzeit diese aber lediglich entpackt und entsprechend viele Dateien schreibt.
Ausserdem kann man, sobald man die Kontrolle über dei Perl-Quellen hat, bedenkenlos Anpassungen vornehmen (ich nenne das die ``Lizenz zum Rumsauen''-Regel).
Es sollte möglich sein, den Perl-Kern upzugraden, wobei aber klar sein sollte, das dies Veränderungen am Quellcode nach sich zieht.
Perl-Module auf CPAN haben - seien wir ehrlich - fast immer kleine und große Bugs. Meistens kann man mit ihnen leben oder Workarounds finden - häufig muss man dazu auf Interna zugreifen, die sich von Version zu Version ändern.
Da der Daemon sehr viele Anfragen erhält und in anderer Form weiterleitet, wäre es schön, wenn man die Bearbeitung der Anfragen verzahnen könnte. Dazu braucht man ein vernünftiges Event-System (der Name sagts schon - das Event-Modul) und etwas, mit dem man effizient und vor allem einfach Pseudoparallelität erreichen kann - Coroutinen, implementiert mit dem Coro-Modul.
Coro greift auf viele Perl-Interna zu und ist eigentlich recht experimentell, ich hatte aber Erfahrungen mit langlaufenden Servern und Coro und wußte daher, das es sehr stabil läuft, sobald es läuft.
Daß das Umfeld stabil ist, gab den Ausschlag.
Nachdem ein paar grundlegende Gedanken gedacht waren, konnte ich an die Implementierung gehen. Flexibilität ist alles, wenn etwas schiefgeht.
Wenig bekannt ist, daß es auch eine Microperl-``Distribution'' gibt, die ein sehr kleines und portables Perl verspricht - ohne Module, ohne Systemabhängigkeiten und ohne interaktives Configure.
Die ``Distribution'' ist sehr klein: sie besteht nur aus den
drei Dateien README.micro, Makefile.micro
und uconfig.sh. Daher ist sie bei Perl gleich mit
dabei.
Ich habe das Makefile und die nötigen Perl-Sourcen in ein
Unterverzeichnis kopiert und dann die Konfiguration, die
standardmäßig nur ISO-C89 + einige wenige Erweiterungen
wie rename() benötigt, solange getuned, bis ich
Zugriff auf die meisten POSIX-Funktionen hatte.
Dazu habe ich die CFLAGS von:
-DPERL_CORE -DPERL_MICRO -DSTANDARD_C -DPERL_USE_SAFE_PUTENV
auf
-DPERL_CORE=1 -DPERL_MICRO=1
geändert und die Konfiguration in uconfig.sh
getuned, compiliert und solange iteriert, bis ich zum Link-Stadium
kam.
Danach hatte ich folgende Dateien in meinem
uperl-Verzeichnis:
EXTERN.h globals.c opnames.h pp_hot.c sv.h INTERN.h gv.c pad.c pp_pack.c taint.c Makefile gv.h pad.h pp_proto.h thrdvar.h XSUB.h handy.h patchlevel.h pp_sort.c thread.h av.c hv.c perl.c pp_sys.c toke.c av.h hv.h perl.h proto.h uconfig.sh config_h.SH intrpvar.h perlapi.c reentr.c universal.c configpm iperlsys.h perlapi.h reentr.h unixish.h cop.h keywords.h perlio.c reentr.inc utf8.c cv.h locale.c perlio.h regcomp.c utf8.h deb.c mg.c perliol.h regcomp.h util.c doio.c mg.h perlsdio.h regexec.c util.h doop.c miniperlmain.c perlvars.h regexp.h warnings.h dump.c nostdio.h perly.c regnodes.h xsutils.c embed.h numeric.c perly.h run.c embedvar.h op.c pp.c scope.c ext.libs op.h pp.h scope.h form.h opcode.h pp_ctl.c sv.c
Beim Bauen bekommt man die libuperl.a, das
Microperl-Äquivalent zur libperl und
microperl, das zum Bauen benötigt wird und auch
beim weiteren Build-Prozess hilfreich sein kann.
microperl wird z.B. benutzt, um aus der
uconfig.sh das Config.pm-Modul zu
basteln.
Mit microperl, libuperl.a,
Config.pm und den Header-Dateien hat man alles, was
man zum Embedden benötigt.
Dieser Schritt war relativ leicht. Er wäre noch leichter
gewesen, wenn ich auf die meisten esoterischen Funktionen wie
getpwnam, mkdir oder Signal-Handling
verzichtet hätte, da ich fast alles über die
POSIX- und Event-Module erreichen kann,
und im Notfall auch auf C zurückgreifen könnte. Im
absoluten Notfall.
Im nächsten Schritt geht es um die Bilbiotheksdateien,
genaugenommen das lib-Verzeichnis, in dem sich die
.pm-Dateien befinden. Theoretisch könnte man
Microperl ganz ohne dieses betreiben: das Ergebnis wäre jedoch
kaum noch ``Perl'' zu nennen, da auch Pragmas als Module
implementiert sind. Und wie man an anderen, einfacher embeddbaren
Sprachen sieht, ist Perl ohne Standardbibliothek eine nutzlosere
embeddete Sprache, als man denkt.
Üblicherweise sind die .pm-Dateien in den
jeweiligen Modul-Distributionen, aber bei Perl sind sie schon
fertig abgepackt im lib-Verzeichnis.
Das größte Problem war es, alle Dateien und
Unterverzeichnisse in CVS einzuchecken, nachdem ich sie aus der
Perl-Distribution kopiert hatte (cp -rp ~/../perl-5.8.x/lib
lib-perl).
Fragt sich noch, wie die Dateien in das Executable kommen.
Dafür sorgt folgende Regel im Makefile:
UPERL = uperl/microperl -Ilib-perl
src/libfiles.c: s/gen_libfiles extensions uperl/microperl
$(UPERL) s/gen_libfiles >$@
Die sorgt dafür, daß das Perl-Skript
s/gen_libfiles aus allen Dateien die Datei
src/libfiles.c generiert (s heißen
bei mir Verzeichnisse mit Skripten, eine alte
Amiga-Gewohnheit).
Die erzeugte Datei sieht so aus:
const char *embed_files[] = {
/* 0 */
"\025Attribute/Handlers.pm\000\000\030Apackage Attribute::Handlers;\01...
" "data eq 'ARRAY';\012\011\011\011\\$data = [ \\$data ] unless \\$wa...
...
/* 1 */
"\007Carp.pm\000\000\016\017package Carp;\012\012our $VERSION = '1.01'...
...
...
,
};
const int embed_file_num = 208;
Die Datei ist 1139622 Bytes groß und mußte hier leider zweidimensional gekürzt werden. Wie man sofort sieht, folgt sie C99 und nicht C89, aber das habe ich in Kauf genommen.
Was man nicht sofort sieht: Alle Dateien werden in einem
großen String-Feld gespeichert, pro Datei ein String. Am
Anfang steht die Länge des Dateinamens, der Dateiname, danach
die Länge der Datei und danach der Inhalt. Dieses Format habe
ich gewählt, damit man die Datei schnell mittels binärer
Suche finden kann und nicht einmal strlen aufrufen
muß, bzw. auch binäre Dateien ablegen kann.
Die Funktion zum Suchen eines Eintrages sieht so aus (etwas umformatiert, damit sie besser paßt):
SV *
libfile(char *path)
PREINIT:
const char *s = 0, *f;
int c, l = 0, m, r = embed_file_num;
CODE:
do
{
m = (l + r) >> 1;
f = embed_files[m];
c = strncmp (f + 1, path, *f);
if (c > 0) r = m - 1;
else if (c < 0) l = m + 1;
else
{
s = f + 1 + f[0];
l = ((unsigned char)s[0] << 24) | ((unsigned char)s[1] << 16)
| ((unsigned char)s[2] << 8) | ((unsigned char)s[3] );
s += 4;
break;
}
}
while (l <= r);
RETVAL = s ? newSVpvn (s, l) : &PL_sv_undef; OUTPUT: RETVAL
Sie implementiert eine ganz normale binäre Suche und
liefert den Inhalt der Datei als String, oder bei Nichtfinden
undef. Ursprünglich habe ich eine lineare Suche
benutzt (bei Rapid Prototyping bleibt manchmal etwas auf der
Strecke, wenn man interessantere Stadien der Entwicklung erreichen
will und eine binäre Suche auch nach Jahrzehnten
Programmiererfahrung nicht auf Anhieb hinkriegt) und erwartet, das
es keinen Unterschied macht. Die binäre Suche macht das
Ergebnis dennoch einige Prozent schneller beim Starten, also
scheint Perl beim Parsen recht flott zu sein.
Das Erzeugen der C-Strings aus den Dateien hat sich übrigens als weit komplizierter herausgestellt, als ich dachte. Zum einen muß man solche nervigen Sachen wie die maximale Zeilenlänge (4096) beachten, zum anderen ist das Quoting garnicht so einfach. Folgende Funktion quoted den String:
# print a c-escaped string
sub str {
my $str = $_[0];
while (length $str) {
local $_ = substr $str, 0, 2000, ""; # 4096 is ISO-C max.
s/\\/\\\\/g;
s/([^\x20-\x7e])/sprintf "\\%03o", ord $1/ge;
s/"/\\"/g;
s/\\x0a/\\n/g;
print " \"$_\"\n";
}
print " ,\n";
}
Das andere Problem war, daß ich die Unmengen an
Dokumentation nicht unbedingt im fertigen Programm brauche. Die
Lösung ist Pod::Stripper, das sich der Benutzung
allerdings widersetzt, da es unbedingt einen FileHandle möchte
und String-Streams in Microperl nicht zur Verfügung
stehen:
open my $ifh, "<", "$base/$path$_"
or die "$base/$path$_: $!";
pipe my ($r, $w);
if (fork == 0) {
close $r;
Pod::Stripper->new->parse_from_filehandle ($ifh, $w);
exit;
} else {
close $w;
}
local $/;
$file{"$path$_"} = <$r>;
Ein fork() pro Datei und Parsen ist zwar nicht
kostenlos, aber ich baue ja nicht unter Cygwin. Auf meinem Rechner
braucht es 3,5s, was leider etwas lang ist, da ich schlecht eine
Makefile-Regel bauen kann, die von allen Dateien in
lib-perl abhängt und daher jedesmal das Skript
aufrufe, wenn ich linke.
Für goofed brauche ich im Moment folgende
Module:
Compress-LZF Coro-Event Coro-State Crypt-Rijndael Crypt-Twofish Cwd Data Digest Event Fcntl File Filter IO List MIME POSIX PerlIO Pod-Stripper Socket Spread Sys Time attrs daemon re
Header und Library sind zwar genug, um diese zu bauen, aber
Perl-Module wollen ein lauffähiges Perl und Zugriff auf
Kleinkram wie ExtUtils::MakeMaker, damit man sie
übersetzen kann.
Zum Glück bietet ExtUtils::MakeMaker extra
Unterstützung für diesen Fall. Das
Extension-Makefile.PL führe ich so aus:
$TOP/uperl/microperl -I$TOP/lib-perl Makefile.PL INSTALLDIRS=perl \
PERL_CORE=1 PERL_SRC=$TOP/uperl PERL_LIB=$TOP/lib-perl
Anscheinend sind PERL_CORE=1 und SRC=
der Trick. Perl machts beim Übersetzen genauso und es
funktioniert, mehr Gedanken habe ich mir nicht gemacht.
Zum Übersetzen verwende ich folgenden
make-Aufruf:
make CC=$(CC) OPTIMIZE=$(OPTIMIZE) LINKTYPE=static CCCDLFLAGS= static
Das und noch mehr ist in einem Skript namens
s/make_ext verewigt, das nach dem Perl-Vorbild
ext/util/make_ext geschrieben wurde. Es macht wenig
mehr als die beiden Kommandos auszuführen: es baut automatisch
das Makefile, falls es keines gibt und akzeptiert z.B.
clean als Argument. Nichts, was man mit einem
gescheiten Makefile nicht sauberer lösen
könnte.
Dies baut für jede Extension, die XS benutzt, eine
libextension.a, gegen die man linken muss.
Erweiterungen besitzen fast immer auch eigene
.pm-Dateien, die in das fertige Executable
gehören. Mein (nicht ganz perfekter) Weg, dies zu erreichen
war, make install in den fertig gebauten
Erweiterungsmodulen zu machen. Dies installiert die
.pm-Dateien (und eventuell andere Bibliotheksdateien)
im lib-perl-Verzeichnis, wo sie beim Bauen automatisch
gefunden werden. Ich darf dann bloss nicht vergessen, sie ins CVS
einzuchecken.
Das Linken geht relativ einfach:
goofed: src/main.o src/xsinit.o src/libfiles.o extensions
$(CC) $(CFLAGS) -o $@ src/xsinit.o \
`find lib-perl/auto -name '*.a' -print` \
src/main.o src/libfiles.o uperl/libuperl.a $(LIBS)
Es werden einfach alle statischen Libraries,
libuperl.a und die übersetzten Dateien
libfiles.c, xsinit.c und
main.c zusammengelinkt.
main.c enthält das Hauptprogramm. Es macht im
wesentlichen das gleiche, das auch Perl macht (hier ohne
error-checking, das Dokument perlembed enthält
genauere Erläuterungen zum Embedding allgemein). Darin sind
zwei wichtige Aufrufe:
perl_parse (my_perl, xs_init, EMBED_ARGC, EMBED_ARGV, (char **) NULL); perl_run (my_perl);
perl_parse parsed die Kommandozeilenargumente,
genau wie perl, und initialisiert die eingelinkten
Erweiterungsmodule.
Üblicherweise (d.h. heutzutage) werden Erweiterungen als
Shared-Objects gebaut und dynamisch (mit DynaLoader
oder XSLoader) geladen. Der DynaLoader
generiert aus dem Package-Namen einen Dateinamen und versucht,
diese Datei als Shared-Object zu öffnen. Danach sucht es darin
eine Funktion namens boot_Name, z.B.
boot_Data__Dumper), registriert sie bei Perl und
führt sie aus.
Diese Funktion registriert alle XS-Funktionen als
Perl-Funktionen und führt den BOOT:-Abschnitt
aus.
Eine statisch eingelinkte Erweiterung kann nicht dynamisch nach
einem Namen durchsucht werden, daher muss man die Funktionen ``per
Hand'' bei Perl registrieren (das Aufrufen geschieht erst beim
Laden des Perl-Moduls). Dies erledigt die
xs_init-Funktion in der Datei
xsinit.c.
Diese Datei kann man sich mit Hilfe des
ExtUtils::Embed-Moduls schreiben lassen (siehe
perlembed), ich hatte aber wenig Glück, dies
automatisch zu tun (manche Module waren doppelt, andere haben
gefehlt). Seither Pflege ich sie per Hand. Die Datei sieht in etwa
so aus:
#include "EXTERN.h" #define PERL_IN_MINIPERLMAIN_C #include "perl.h"
EXTERN_C void boot_Compress__LZF (pTHX_ CV *cv); ... EXTERN_C void boot_Spread (pTHX_ CV *cv); EXTERN_C void boot_daemon (pTHX_ CV *cv);
EXTERN_C void
xs_init (pTHX)
{
char *file = __FILE__;
newXS ("daemon::bootstrap", boot_daemon, file);
newXS ("Spread::bootstrap", boot_Spread, file);
...
newXS ("Compress::LZF::bootstrap", boot_Compress__LZF, file);
}
Wenn das Parsing vorbei ist, baue ich noch @ARGV
auf - mein Aufruf von perl_parse benutzt ja nicht die
echten Kommandozeilenargumente - und rufe perl_run
auf, das die Kontrolle endgültig an Perl abgibt.
Welches sind nun die Argumente für den Perl-Interpreter?
/* this array contains the initial startup code for the perl interpreter */
char *embed_argv[EMBED_ARGC] = {
"goofed",
"-e",
"\n"
" daemon->bootstrap (0);\n"
... weiterer Perl-Code ...
};
Das erste Argument ist der Name des Programms. Danach kommt eine
-e-Option mit dem Startprogramm. Es sieht so aus:
daemon->bootstrap (0);
PerlIO::scalar->bootstrap (0);
@INC = sub {
open my $fh, '<', \(daemon::libfile ($_[1]))
or die "FATAL: loader can't in-memory stream\n";
$fh;
};
require daemon;
daemon->init;
Die ersten beiden Zeilen initialisieren die beiden Module
daemon (ein Modul, in das ich die meisten
Spezial-C-Funktionen für goofed gepackt habe) und
(ausgerechnet!) PerlIO::scalar.
Ersteres enthält die libfile-Funktion, mit dem
ich die Perl-Sourcen bekomme und letzteres wird von Perl
benötigt um Memory-Streams (open FH, "<",
\$scalar) zu supporten. Die beiden
bootstrap-Funktionen registrieren allerdings
nur die XS-Funktionen, die zugehörigen Perl-Module
kann man zu diesem Zeitpunkt nicht laden.
Um das zu ermöglichen, überschreibe ich
@INC mit einer Funktion. Versucht Perl nun eine Datei
zu laden durchsucht es @INC, findet die Funktion und
führt sie aus. Diese holt sich die entsprechende Datei und
gibt einen Filehandle zurück. Perl liest und parsed sie
dann.
Danach kann man die Perl-Module ganz normal mit use
oder require laden, was auch prompt gemacht wird,
indem das daemon-Modul geladen und dessen
init-Funktion ausgeführt wird.
An diesem Punkt hat man ein Perl, in das man relativ einfach CPAN-Module einbinden kann und keinerlei Abhängigkeiten im Filesystem besitzt, schnell bootstrapped, Support für binäre Objekte (Bilder etc.) hat und die gesamte Power von Perl zur Verfügung stellt.
Nachdem ein solcher Aufwand getrieben wurde, wollte ich noch die Frage klären, ob es sich gelohnt, Perl statt C zu benutzen.
Die Hauptaufgabe war es, einen Radius-Server zu bauen (wobei man
mit goofed natürlich noch andere Aufgaben
erfüllen kann, es dient also eher als Anwendungsplattform
:)
Das Parsen von Radius-Paketen ist eine Byte-Pfriemelei, braucht aber sehr viel Konfigurationsdaten. Das komplette Modul zum Parsen und Erzeugen von Radius-Paketen ist (inklusive reichlich Dokumentation) 307 Zeilen lang. Ähnlicher Code im Merit-Radiusd besteht aus ca. 8000 Zeilen C. Ohne Dokumentation.
Allein hier hat sich Perl gelohnt. Richtig interessant wird es allerdings bei den speziellen Anforderungen: eine Authentifizierung benötigt ca. zehn LDAP-Anfragen pro Request. Im alten Radius-Daemon waren diese blockierend und haben den Großteil der Realzeit verbraucht.
In goofed sehen sie zuerst auch blockierend
aus:
my $ldap_user = $self->{ldap}->search ("uuRadiusUser=$user, uuRadiusRealm=$realm, $ldap_root")
or return $self->nak ("user unknown");
... tue etwas
my $ldap_profile = $self->{ldap}->search ("uuRadiusProfile=$profile, $ldap_root");
... tue noch mehr
$self->{ldap}->search ("uuRadiusNAS=$nas_ip, uuRadiusRealm=$realm, $ldap_root")
or return $self->nak ("roaming not allowed");
... usw.
Auch hier wird eine Anfrage gebastelt und auf das Ergebnis gewartet. Der Unterschied ist, das die Anfrage asynchron verschickt wird und in der gleichen Zeit andere Radius-Pakete entgegengenommen werden können und Anfragen gestellt bzw. verarbeitet werden können.
Da das LDAP-Protokoll selbst asynchron ist, besteht bei einem entsprechend intelligenten LDAP-Server (ich kenne keinen) sogar die Möglichkeit, einfache Anfragen sofort zu beantworten während komplizierte im Hintergrund erledigt werden.
Das LDAP-Modul verwendet nicht das Net:LDAP-Modul
von CPAN, da ich dieses nicht stabil mit OpenLDAP zusammenbringen
konnte, bzw. es sich extrem gegen Event-basierte Programmierung
gewehrt hat. Es benutzt stattdessen direkt die libldap
von OpenLDAP (durch Xs-Funktionen in
daemon.xs implementiert).
Das erste, was es macht, ist, eine Verbindung zum LDAP-Server aufzumachen und einen Event-Handler darauf zu binden:
$self->{ldap} = new ldap::conn $self->{host}, $self->{port} || 389
or die "unable to connect to ldap server $self->{host}:$self->{port}: $!\n";
...
$self->{w} = Event->io (
fd => $self->{ldap}->fd,
poll => 'r',
cb => [$self, "_recvresp"],
);
Normalerweise ist es mein Stil, Callback-Funktionen als anonyme
Sub-Funktionen auszulegen. Nur leider gibt dies mit aktuellen
Coro-Versionen manchmal Memory-Leaks, die ich nichtmal mit
valgrind finde.
Die _recvresp-Methode holt einen Ergebnis-Record
vom Server ab, baut bei Abbruch die Verbindung neu auf, und ruft
den entsprechenden Callback für die ursprüngliche Anfrage
auf.
Anfragen sind etwas aufwendiger: Einerseits mag es der LDAP-Server nicht, wenn zu viele Anfragen gleichzeitig ausstehen (warum, weiss ich nicht, aber da wenige Anwendungen asynchron Anfragen stellen, ist dies vielleicht nicht gut getestet): es treten Fehler auf und sie müssen wiederholt werden; andererseits darf immer nur einer gleichzeitig eine Anfrage stellen (die C-Library ist zwar reentrant, nicht aber der FileHandle, auf den sie schreibt).
Sowohl für die globale Limitierung als auch für das
gleichzeitige schreiben verwende ich eine
Coro::Semaphore:
# in sub new
my $self = bless {
host => $host,
lock => (new Coro::Semaphore),
limit => (new Coro::Semaphore 20),
%arg,
}, $class;
Der Abfragecode macht dann:
$self->{limit}->down; # limit # of simult. requests
my $sem = new Coro::Semaphore 0;
$self->{lock}->down; # ensure that there is only a single writer
Uff, drei Semaphoren! Die erste sorgt dafür, das nur eine bestimmte Anzahl von Abfragen gleichzeitig ausstehen können. Sie wird erst nach dem Lesen des Ergebnisses wieder freigegeben.
Die zweite Sempahore wird schon gelockt erzeugt und wird nur benutzt, um auf das Ergebnis zu warten (mehr dazu gleich).
Und die dritte Semaphore-Operation läßt immer nur einen gleichzeitig Anfragen zum Server schicken.
Dies geschieht danach: (Achtung: stark vereinfacht :)
my $id = $self->{ldap}->search ($base);
# registriere Callback
$self->{id}{$id} = sub {
... parse Ergebnis
$sem->up;
}
Danach werden die Semaphoren wieder gelöst:
$self->{lock}->up;
$sem->down; # sleep till result is there
$self->{limit}->up;
Dies geschieht in umgekehrter Reihenfolge (das ist notwendig, um einen Deadlock zu verhindern) und definiert den Zeitablauf:
Zuerst wird die Schreibzugriff-Semaphore freigegeben, da die
Anfrage mit search zum Server geschickt wurde und nun
andere Anfragen schicken dürfen.
Danach wird auf das Ergebnis gewartet: Die
$sem-Semaphore ist gelockt und wird erst im Callback
wieder gelöst ($sem->up), wenn das Ergebnis
eingetrudelt ist.
Zuletzt wird die Semaphore, die die maximale Anzahl ausstehender Anfragen begrenzt, gelöst, da ja die Anfrage durchgeführt wurde und das Ergebnis vorliegt.
Das sind 177 Zeilen (die XS-Schnittstelle zur OpenLDAP-Library nicht mitgezählt). Dadurch ist dir LDAP-Funktionalität abgekapselt und leicht einzeln testbar.
Der Vorteil ist, daß man den eigentlichen Authentifizerungscode sehr natürlich schreiben kann, nämlich sequentiell ohne zwischendurch irgendwelche Locks beachten zu müssen oder irgendwelche Zustände zwischenzuspeichern, und trotzdem in den Genuss voller Parallelität von LDAP-Server und Radiusd gelangt. In der Praxis ist deshalb der LDAP-Server der Engpass bei der Authentifizierung.
Quellcode für goofed ist leider nicht frei
erhältlich. Ich hoffe aber, das man ähnliches jetzt
selbst nachbauen kann.
Handarbeit ist nicht immer das Beste. Man kann auch einfacher in
den Genuß von einzelnen Binaries kommen. Z.B. mit dem
PAR-Modul (von CPAN), das aus allen Modulen eines
Programms eine Art Archiv mit Perl-Lader bauen kann. Es kommt dem
obigen sehr nahe, da es ebenfalls ein einzelnes Binary erzeugen
kann. Allerdings entpackt es zur Laufzeit die Dateien und kann auch
keine Bibliotheken einlinken (Shared Objects bleiben Shared
Objects).
Mit makeaperl aus der Perl-Distribution kann man
sich ein statisches Perl-Binary mit allen Modulen bauen, die man
braucht. Dies beinhaltet aber nicht die Perl-Quellen dazu.