Lyx – Foreign Function Interface (FFI)
Das Foreign Function Interface ruft C-Funktionen aus Lyx auf. Die Grundlage ist die C-ABI der Zielplattform: System V AMD64 auf Linux/macOS, AAPCS64 auf ARM64.
Philosophie: Die Units instd/unddata/kommen ohne externe C-Bibliotheken aus. FFI ist für Fälle gedacht, in denen eine vorhandene C-Bibliothek genutzt werden soll.
FFI ist in Lyx kein offenes Tor: Der Compiler klassifiziert jedes Symbol und weist gefährliche Aufrufe ab, bevor sie in den Build gelangen. Diese Seite beschreibt beides — die Deklarationsform und das Sicherheitsmodell dahinter.
→ Capabilities (LCBS) · Attribute & Pragmas · Rohspeicher
1. Externe Funktionen deklarieren
Eine externe Funktion wird mit dem Schlüsselwort extern deklariert — ein Attribut @extern gibt es nicht. Die link-Klausel nennt die Bibliothek und ist Pflicht.
@capabilities([system.exit, system.memory.heap])
import std.io;
extern fn strlen(s: pchar): int64 link "libc.so.6";
fn main(): int64 {
PrintLn(IntToStr(strlen("hallo"))); // 5
return 0;
}
- Kein Funktionskörper — nur die Signatur, abgeschlossen mit
; - Die Bibliothek wird mit vollständigem Dateinamen angegeben:
„libc.so.6“, nicht„c“ - Aufrufkonvention: System V AMD64 (Linux/macOS), AAPCS64 (ARM64)
Dielink-Klausel ist seit lyxc 1.0.17C Pflicht (#1179). Fehlt sie, bricht schon der Parser ab:extern fn: die link-Klausel fehlt. Bis 1.0.16x übersetzte die Deklaration auch ohne, das Symbol wurde nie gebunden und der Aufruf lieferte still0— bei C-Funktionen ein plausibler Wert, der Fehler fiel damit kaum auf. Die eigene Standardbibliothek trug 33 solcher Deklarationen (std.time.Now()gab 0,env_getimmer„“,abort()beendete nichts); alle sind ersetzt.
Ein falscher Bibliotheksname wird weiterhin nicht zur Übersetzungszeit gemeldet — den vollständigen Dateinamen angeben („libc.so.6“, nicht„c“) und einen neuen FFI-Aufruf einmal gegen einen bekannten Wert prüfen.
2. Das Sicherheitsmodell: fünf Klassen
Der Compiler ordnet jedes extern-Symbol einer Klasse zu und entscheidet danach, ob und unter welcher Bedingung es zugelassen wird.
| Klasse | Bedeutung | Bedingung |
|---|---|---|
| 0 – Safe | Reine Berechnung oder lesender Zugriff ohne Rechteausweitung | ohne weitere Angabe zugelassen |
| 1 – OS-Ressource | Datei-, Netz-, Umgebungs- oder Speicherzugriff | @capabilities([…]) erforderlich |
| 2 – Prozess | Prozesssteuerung und Rechteänderung | @capabilities([…]) erforderlich |
| 3 – Verboten | Puffer-Überlauf-anfällig oder Kommando-Ausführung | immer abgewiesen |
| 4 – Unbekannt | Nicht klassifiziertes Symbol | in Nutzercode abgewiesen, in std.*-Units zugelassen |
Klasse 0 — ohne Capability nutzbar
strlen · strlcpy · strlcat · snprintf · vsnprintf · memcpy · memmove · memset · memcmp · malloc · free · realloc · calloc · time · clock_gettime
Klasse 1 — OS-Zugriff
open · close · read · write · fopen · fclose · fread · fwrite · socket · bind · listen · connect · getenv · setenv · exit · abort · mmap · munmap · mprotect · mlock · munlock · die strncpy/strncat-Familie
Klasse 2 — Prozesssteuerung
fork · kill · wait · waitpid · clone · prctl · unshare · setuid · setgid · setresuid · setresgid
Klasse 3 — immer abgewiesen
gets · system · popen · sprintf · vsprintf · strcpy · strcat · wcscpy · wcscat · dlopen · die gesamte exec-Familie (execve, execvp, execl, …)
sema error: extern fn in FFI-Blacklist (Klasse 3)
Diese Sperre lässt sich nicht durch eine Capability aufheben — sie gilt unbedingt.
Klasse 4 — das unbekannte Symbol
Jedes Symbol, das der Compiler nicht in einer der vier Listen findet, fällt hierher. Das ist der Regelfall, sobald eine fremde Bibliothek eingebunden wird: sqrtf, png_create_read_struct, curl_easy_init — nichts davon steht in einer Liste.
sema error (line 6): extern fn: unbekanntes FFI-Symbol erfordert eine Capability-Angabe (FFI-Sandbox Fail-Closed)
Drei Dinge sind daran zu wissen, und alle drei kosten sonst Zeit:
- Es ist keine bestimmte Capability gemeint. Die Angabe ist eine Absichtserklärung — jede gültige Liste hebt die Sperre auf. Der Compiler kann das Symbol nicht einordnen und verlangt deshalb, dass der Autor die Verantwortung sichtbar übernimmt.
- Die Annotation gehört unmittelbar vor die Deklaration, nicht an den Unit-Kopf. Auf Modulebene wirkt sie für diesen Zweck nicht.
- Für genau eine Capability gibt es die Kurzform
@cap(…).
unit main;
import std.io;
import std.string;
@cap(system.exit)
extern fn sqrtf(x: f32): f32 link "libm.so.6";
fn main(): int64 {
var a: f32 := 16.0;
var r: f32 := sqrtf(a);
Print("sqrtf(16) = "); PrintF64(r as f64);
return 0;
}
sqrtf(16) = 4.000000
Instd.*-Units gilt die Sperre nicht. Der Bindungs-Layer der Standardbibliothek zählt zur vertrauenswürdigen Basis und darf unbekannte Symbole ohne Angabe deklarieren. Wer eigenen Code gegen eine Bibliothek schreibt, ist davon nicht erfasst — dort greift Fail-Closed.
Der Sinn: ohne diese Sperre wäre jedes beliebige Symbol als „Klasse 0„ durchgewunken worden, und die Capability-Prüfung ließe sich vollständig umgehen.
3. Weitere Prüfungen
Format-String-Funktionen
printf, fprintf, scanf, sscanf und ihre v-Varianten bilden eine eigene Gruppe. Sie sind nur mit der Capability system.unsafe.format_string zugelassen, sonst gelten sie als Klasse 3:
@capabilities([system.exit, system.memory.heap, system.unsafe.format_string])
Tarnung über die link-Angabe
Der Bibliotheks- und Symbolname in der link-Klausel wird mitgeprüft. Ein verbotenes Symbol lässt sich also nicht unter anderem Funktionsnamen einschleusen:
extern fn harmlos(cmd: pchar): int64 link "system";
sema error: extern fn: link-Zielsymbol in FFI-Blacklist
Signaturprüfung
Eine nicht klassifizierte Funktion mit zwei oder mehr pchar-Parametern wird abgewiesen — das ist die typische Form von strcpy-artigen Funktionen ohne Längenbegrenzung:
sema error: extern fn: ≥2 pchar-Parameter ohne Größenlimit
Ein Längenparameter hebt die Sperre seit lyxc 1.0.17K auf (#1180):probe(a: pchar, b: pchar, n: int64)wird angenommen,probe2(a: pchar, b: pchar)weiterhin abgewiesen (≥2 pchar-Parameter ohne Größenlimit (Klasse 3 via Signatur)). Eigene C-Funktionen mit zwei Strings sind damit einbindbar, sobald die Signatur eine Längenangabe trägt; der frühere Ausweg überint64-Adressen ist nicht mehr nötig.
(Issue #1180)
4. Typ-Zuordnung Lyx ↔ C
| C-Typ | Lyx-Typ | Anmerkung |
|---|---|---|
int | int32 | |
long, int64_t | int64 | |
unsigned char | uint8 | |
size_t, ssize_t | int64 | auf allen Zielplattformen 64 Bit |
double | f64 | |
float | f32 | seit der Behebung von #1566 nutzbar |
char* | pchar | nullterminiert |
void* | int64 | Adressen sind Ganzzahlen — Rohspeicher |
void | void | oder Rückgabetyp weglassen |
Einen Zeigertyp gibt es in Lyx nicht: Jeder void*- oder Struct-Zeiger wird als int64 übergeben, der Zugriff läuft über peek/poke.
Breite der Rückgabe angeben
extern fn kann die Breite des Rückgabewerts ausdrücken: int8, int16, int32 und die vorzeichenlosen Gegenstücke. Der Codegen erweitert dann vorzeichenrichtig (movsx/movsxd) beziehungsweise füllt mit Nullen (movzx) — #1607.
@cap(system.exit)
extern fn atoi(s: pchar): int32 link "libc.so.6";
fn main(): int64 {
Print("atoi(\"-1\") = "); PrintLn(IntToStr(atoi("-1"c)));
return 0;
}
atoi("-1") = -1
Ohne Angabe bleibt alles wie bisher: die Regel greift nur, wo sie dasteht, an bestehenden Deklarationen ändert sich nichts. Das ist zugleich die Falle — eine C-Funktion mit int-Rückgabe, die als int64 deklariert wird, liefert -1 als 4294967295. Genau daran gab std.net.tls Fehlercodes falsch weiter; SSL_read, SSL_write, SSL_connect und Geschwister sind deshalb mit umgestellt.
Mehr als sechs Gleitkommaargumente
Nach der SysV-Konvention gehen die ersten acht Gleitkommawerte in xmm0–xmm7; entsprechend nimmt eine extern fn bis zu acht f64-Argumente ohne Stapel entgegen. Bis lyxc 1.1.3B lag die Schranke bei sechs, weil der FFI-Pfad die Werte aus den GP-Registern in die xmm-Register schob (#1640, abgespalten aus #1486). Grafik-APIs wie OpenGL (glOrtho, glFrustum, Matrixfunktionen) waren damit nicht ansprechbar.
import std.io;
@cap(ui.display)
extern fn f8(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64, g: f64, h: f64): f64 link "./libargsum.so";
fn main(): int64 {
var r: f64 := f8(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
PrintF64(r); // 36.000000
return 0;
}
Ab dem neunten Gleitkommawert geht es über den Stapel; dort meldet der Compiler weiterhin, dass es nicht umgesetzt ist.
Mehr als sechs Argumente
Die ersten sechs Argumente liegen in Registern, alles Weitere auf dem Stapel. Bis lyxc 1.1.1C bekam ein extern fn ab dem siebten Argument Müll: die FFI-Ausrichtung (and rsp, -16) verschob den Stapel unmittelbar nach dem Ablegen der Argumente (#1620). Native Lyx-Aufrufe waren nie betroffen, weil sie ohne diese Ausrichtung auskommen. Behoben; Deklarationen mit sieben und mehr Argumenten sind wieder benutzbar.
float-Schnittstellen sind wieder nutzbar (#1566 behoben). Nachgemessen mit 1.0.21A:
> sqrtf(16) als f32 = 4.000000 > sqrt(16) als f64 = 4.000000 >
Frühere Fassungen dieser Seite rieten, ersatzweise diedouble-Variante der C-Funktion zu nehmen — das ist nicht mehr nötig.
Der gewöhnliche Cast zwischenf32undf64arbeitet ebenfalls korrekt (#1511).
@capabilities([system.exit, system.memory.heap])
import std.io;
import std.alloc;
extern fn memset(dest: int64, c: int32, n: int64): int64 link "libc.so.6";
fn main(): int64 {
var puffer: int64 := alloc(16);
memset(puffer, 65, 16); // mit 'A' füllen
PrintLn(IntToStr(peek8(puffer))); // 65
free(puffer, 16);
return 0;
}
4a. Windows-DLLs (''%%--target=win64 --format=pe%%'')
Eine extern fn mit link „x.dll“ landet seit lyxc 1.1.3E in der Import-Tabelle der erzeugten PE-Datei; Argumente werden nach der Microsoft-x64-Konvention übergeben. Vorher war von einer Windows-Binary aus keine einzige Win32-Funktion erreichbar: der Aufruf endete im page fault (#1671), Argumente kamen falsch an (#1674), und eine zweite DLL erbte den Namenszeiger der ersten (#1676).
unit Main;
import std.io;
@cap(ui.display)
extern fn lstrlenA(s: pchar): int64 link "kernel32.dll";
@cap(ui.display)
extern fn GetSystemMetrics(i: int64): int64 link "user32.dll";
fn main(): int64 {
PrintLn(lstrlenA("abcde")); // 5
PrintLn(GetSystemMetrics(0) > 0); // 1
return 0;
}
lyxc dll2.lyx --target=win64 --format=pe -o dll2.exe
Zwei Punkte dazu:
- Mehrere DLLs in einem Programm sind zulässig — jede Gruppe der Import-Tabelle trägt ihren eigenen Namenszeiger.
- Umgebungsvariablen funktionieren unter Windows:
EnvLookupRawund alles, was darauf aufsetzt (std.datetime,std.os), lesen den Windows-Umgebungsblock statt des SysV-Speicherbilds (#1677).
5. FFI im Safety-Umfeld
| Situation | Empfehlung |
|---|---|
| DAL-A/B-Regelzyklus | Kein FFI — die WCET fremden Codes ist nicht nachweisbar |
| Werkzeuge, Tests, Simulation | FFI zulässig, Capabilities eng fassen |
| Vorhandene C-Bibliothek unvermeidbar | Aufrufe in einer eigenen Unit kapseln und dort per grant beschränken |
Jede extern-Deklaration erscheint im Sicherheits-Audit des Builds. Ein Programm ohne @capabilities erhält keinen seccomp-Filter — siehe Capabilities.
6. Was es nicht gibt
Frühere Fassungen dieser Seite beschrieben mehrere Konstrukte, die der Compiler nicht kennt — jedes davon wird mitunbekanntes Attributabgewiesen:
*@extern— die Deklaration läuft über das Schlüsselwortextern
*@export— Lyx-Funktionen lassen sich derzeit nicht für C exportieren
*@stdcall— keine Wahl der Aufrufkonvention; es gilt die der Zielplattform
*@variadic— variadische C-Funktionen sind nicht deklarierbar
Ebenfalls nicht vorhanden: Callback-Funktionen an C übergeben und dynamisches Nachladen zur Laufzeit (dlopensteht auf der Klasse-3-Liste).
Weiterführende Seiten:
Letzte Aktualisierung: 2026-08-19 — acht Gleitkommaargumente (#1640) und der Abschnitt „Windows-DLLs“ (#1671, #1674, #1676, #1677) ergänzt; beide Beispiele mit lyxc 1.1.3I übersetzt und ausgeführt (das PE unter Wine).
Vorherige Aktualisierung: 2026-08-18 — Breite der Rückgabe (int8/int16/int32 + unsigned, #1607) und die Stapelargumente ab dem siebten (#1620) ergänzt; Beispiel mit lyxc 1.1.2E übersetzt.
Vorherige Aktualisierung: 2026-08-13 — Längenparameter hebt die ≥2-pchar-Sperre seit lyxc 1.0.17K auf (#1180).
Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).
