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 in std/ und data/ 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)
 
Die link-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 still 0 — 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_get immer „“, 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:

  1. 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.
  2. Die Annotation gehört unmittelbar vor die Deklaration, nicht an den Unit-Kopf. Auf Modulebene wirkt sie für diesen Zweck nicht.
  3. 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

 
In std.*-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])

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 über int64-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 xmm0xmm7; 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 die double-Variante der C-Funktion zu nehmen — das ist nicht mehr nötig.

Der gewöhnliche Cast zwischen f32 und f64 arbeitet 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: EnvLookupRaw und 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 mit unbekanntes Attribut abgewiesen:

* @extern — die Deklaration läuft über das Schlüsselwort extern
* @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 (dlopen steht 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).