Lyx – Foreign Function Interface (FFI)

Das Foreign Function Interface (FFI) verbindet Lyx mit Code, der in C geschrieben wurde — und umgekehrt. Lyx kann C-Funktionen aufrufen (@extern) und eigene Funktionen für C exportieren (@export). Die Grundlage ist die C ABI der jeweiligen Zielplattform: System V AMD64 auf Linux/macOS, Microsoft x64 auf Windows, AAPCS64 auf ARM64.

Philosophie: Lyx-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 oder Lyx in ein bestehendes C-Projekt eingebettet werden soll.

1. Externe Funktionen deklarieren (@extern)

@extern teilt dem Compiler mit, dass eine Funktion nicht in Lyx implementiert ist. Der Linker sucht sie beim Build in den angegebenen Bibliotheken.

// Deklaration — kein Funktionskörper, nur Signatur
@extern
fn strlen(s: pchar): int64;

@extern
fn memcpy(dst: int64, src: int64, n: int64): int64;

@extern
fn malloc(size: int64): int64;

@extern
fn free(ptr: int64): void;

fn main(): int64 {
    var msg: pchar := "Hallo Welt";
    var len: int64 := strlen(msg);
    PrintInt(len);    // 10
    PrintStr("\n");
    return 0;
}

Kompilieren mit Verlinkung gegen libc:

# libc wird auf Linux/macOS automatisch verlinkt
lyxc main.lyx -o main

# Explizit — andere Bibliotheken
lyxc main.lyx -lm -o main          # libm (math)
lyxc main.lyx -lssl -lcrypto -o main  # OpenSSL
lyxc main.lyx -L/usr/local/lib -lfoo -o main  # Benutzerdefinierter Pfad

Flag Bedeutung
-l<name> Verlinkt gegen lib<name>.so / lib<name>.a
-L<pfad> Fügt Suchpfad für Bibliotheken hinzu
–static Statische Verlinkung (kein .so zur Laufzeit)

2. Typ-Mapping: Lyx ↔ C

Die Typen müssen bit-genau übereinstimmen. Lyx verwendet durchgängig explizite Größen — kein int, das je nach Plattform 32 oder 64 Bit groß ist.

Lyx C Bits Hinweis
int8 int8_t / char 8 Vorzeichenbehaftet
int16 int16_t / short 16 Vorzeichenbehaftet
int32 int32_t / int 32 Vorzeichenbehaftet
int64 int64_t / long 64 Vorzeichenbehaftet
uint8 uint8_t / unsigned char 8 Vorzeichenlos
uint16 uint16_t / unsigned short 16 Vorzeichenlos
uint32 uint32_t / unsigned int 32 Vorzeichenlos
uint64 uint64_t / unsigned long 64 Vorzeichenlos
f32 float 32 IEEE 754
f64 double 64 IEEE 754
bool _Bool / int 8/32 Vorsicht: C bool ist oft int
pchar char * 64 (Ptr) Nullterminierter String
int64 void * / T * 64 (Ptr) Generischer Zeiger — als int64 behandeln

Zeiger in Lyx: Es gibt keinen eigenen Zeigertyp außer pchar. Alle anderen Zeiger — auf Structs, Arrays, void — werden als int64 übergeben und empfangen. Der Wert ist die numerische Adresse im Speicher.

@extern
fn qsort(base: int64, nmemb: int64, size: int64, compare: int64): void;

@extern
fn bsearch(key: int64, base: int64, nmemb: int64, size: int64, compare: int64): int64;

// C: int compare(const void *a, const void *b)
fn CompareInt64(a: int64, b: int64): int32 {
    var va: int64 := a as int64;   // Zeiger auf int64 → Wert lesen
    var vb: int64 := b as int64;
    if (va < vb) { return -1i32; }
    if (va > vb) { return  1i32; }
    return 0i32;
}

fn main(): int64 {
    var arr: int64[5] := [3, 1, 4, 1, 5];
    qsort(arr as int64, 5, 8, CompareInt64 as int64);
    // arr ist jetzt: [1, 1, 3, 4, 5]
    return 0;
}


3. Variadic Funktionen

C-Funktionen mit variabler Argumentanzahl (printf, sprintf etc.) werden mit @variadic deklariert. Die festen Parameter werden normal angegeben, Lyx übergibt weitere Argumente über den Stack entsprechend der C-Calling-Convention.

@extern
@variadic
fn printf(format: pchar): int32;

@extern
@variadic
fn sprintf(buf: int64, format: pchar): int32;

@extern
@variadic
fn snprintf(buf: int64, size: int64, format: pchar): int32;

fn main(): int64 {
    printf("Hallo %s, du bist %d Jahre alt\n", "Welt", 42);

    var buf: uint8[128];
    snprintf(buf as int64, 128, "Wert: %.2f", 3.14159);
    PrintStr(buf as pchar);
    PrintStr("\n");

    return 0;
}

Achtung: Der Compiler kann Argumenttypen variadic Aufrufe nicht prüfen. Typ-Fehler (z.B. int32 wo int64 erwartet wird) führen zu undefiniertem Verhalten. Für typsichere Ausgabe sind PrintStr, PrintInt und PrintF64 vorzuziehen.

4. Structs und Memory-Layout (@packed)

Lyx und C fügen standardmäßig Padding-Bytes zwischen Struct-Felder ein, um Alignment-Anforderungen zu erfüllen. Das führt dazu, dass sizeof(struct) in C und die Struct-Größe in Lyx übereinstimmen — aber nur, wenn beide dieselben Alignment-Regeln verwenden.

Bei Hardware-Protokollen, Netzwerk-Frames und C-Bibliotheken, die exakte Layouts erwarten, wird @packed benötigt.

Ohne @packed — Padding entsteht

// Ohne @packed: Compiler fügt 3 Bytes Padding nach 'id' ein
type SensorHeader = struct {
    id:       uint8;    // 1 Byte
                        // 3 Bytes Padding (Compiler ergänzt automatisch)
    value:    uint32;   // 4 Bytes
    checksum: uint16;   // 2 Bytes
                        // 2 Bytes Padding
    // Gesamtgröße: 12 Bytes (nicht 7!)
};

Mit @packed — exaktes Layout

@packed
type SensorHeader = struct {
    id:       uint8;    // 1 Byte — direkt gefolgt von:
    value:    uint32;   // 4 Bytes
    checksum: uint16;   // 2 Bytes
    // Gesamtgröße: 7 Bytes — kein Padding
};

@packed
type EthernetFrame = struct {
    dst_mac:  uint8[6];  // 6 Bytes
    src_mac:  uint8[6];  // 6 Bytes
    ethertype: uint16;   // 2 Bytes
    payload:  uint8[46]; // 46 Bytes Minimum
    // Gesamtgröße: 60 Bytes — exakt wie in IEEE 802.3
};

fn ProcessFrame(raw: int64): void {
    var frame: EthernetFrame := (raw as int64) as EthernetFrame;
    PrintStr("EtherType: ");
    PrintInt(frame.ethertype as int64);
    PrintStr("\n");
}

C-Struct aus einer Bibliothek spiegeln

// C-Original:
// struct tm {
//     int tm_sec;   int tm_min;  int tm_hour;
//     int tm_mday;  int tm_mon;  int tm_year;
//     int tm_wday;  int tm_yday; int tm_isdst;
// };

@packed
type CTm = struct {
    tm_sec:   int32;
    tm_min:   int32;
    tm_hour:  int32;
    tm_mday:  int32;
    tm_mon:   int32;
    tm_year:  int32;
    tm_wday:  int32;
    tm_yday:  int32;
    tm_isdst: int32;
};

@extern
fn localtime(timer: int64): int64;   // gibt *struct tm zurück

fn PrintCurrentTime(): void {
    var t: int64 := 0;
    // time(&t) — Adresse von t übergeben
    var tm_ptr: int64 := localtime(t as int64);
    var tm: CTm := tm_ptr as CTm;
    PrintInt(tm.tm_hour as int64); PrintStr(":");
    PrintInt(tm.tm_min  as int64); PrintStr(":");
    PrintInt(tm.tm_sec  as int64); PrintStr("\n");
}


5. Arrays und Zeiger an C übergeben

C-Funktionen erwarten Zeiger — in Lyx wird der Array-Name mit as int64 in eine Adresse umgewandelt.

@extern
fn memset(ptr: int64, value: int32, size: int64): int64;

@extern
fn memcmp(a: int64, b: int64, n: int64): int32;

fn main(): int64 {
    var buf: uint8[256];

    // Buffer auf 0 setzen
    memset(buf as int64, 0i32, 256);

    // Zwei Puffer vergleichen
    var buf2: uint8[256];
    var diff: int32 := memcmp(buf as int64, buf2 as int64, 256);
    if (diff == 0i32) {
        PrintStr("Puffer sind identisch\n");
    }

    return 0;
}

Mehrdimensionale Arrays werden zeilenweise im Speicher abgelegt (Row-Major). An C wird der Zeiger auf das erste Element übergeben — die Dimensionen müssen separat als Parameter mitgegeben werden, da C keine Längeninformation im Zeiger trägt.

@extern
fn SomeMatrixLib_Multiply(a: int64, b: int64, c: int64, rows: int32, cols: int32): void;

fn main(): int64 {
    var a: f64[4] := [1.0, 2.0, 3.0, 4.0];   // 2×2 Matrix
    var b: f64[4] := [5.0, 6.0, 7.0, 8.0];
    var c: f64[4];

    SomeMatrixLib_Multiply(a as int64, b as int64, c as int64, 2i32, 2i32);
    return 0;
}


6. Strings zwischen Lyx und C

pchar in Lyx ist direkt ein char * in C — nullterminiert, kompatibel. Wichtig ist die Frage des Speicher-Eigentums: Wer hat den Speicher allokiert, und wer muss ihn freigeben?

Lyx-String an C übergeben

String-Literale in Lyx liegen im Read-only-Datensegment — kein Freigeben nötig:

@extern
fn puts(s: pchar): int32;

@extern
fn strlen(s: pchar): int64;

fn main(): int64 {
    puts("Hallo aus Lyx");              // String-Literal — kein dispose
    PrintInt(strlen("Test"));           // 4
    PrintStr("\n");
    return 0;
}

C gibt Zeiger zurück — Besitz klären

@extern
fn getenv(name: pchar): pchar;   // C besitzt den Speicher — nie dispose!

@extern
fn strdup(s: pchar): pchar;      // C allokiert neuen Speicher — free() nötig!

@extern
fn free(ptr: int64): void;

fn main(): int64 {
    // getenv: statisch verwaltet von der C-Laufzeit — nicht freigeben
    var path: pchar := getenv("PATH");
    if (path != 0 as pchar) {
        PrintStr(path);
        PrintStr("\n");
        // KEIN dispose — der Zeiger gehört der C-Laufzeit
    }

    // strdup: malloc-Speicher — muss mit free() freigegeben werden
    var copy: pchar := strdup("Hallo");
    PrintStr(copy);
    PrintStr("\n");
    free(copy as int64);   // Pflicht — sonst Memory Leak

    return 0;
}

Lyx-String auf dem Heap für C

Wenn C den String modifizieren können soll, braucht Lyx einen beschreibbaren Puffer:

@extern
fn getcwd(buf: int64, size: int64): int64;

fn main(): int64 {
    var buf: uint8[1024];
    var result: int64 := getcwd(buf as int64, 1024);
    if (result != 0) {
        PrintStr(buf as pchar);
        PrintStr("\n");
    }
    return 0;
}


7. Callback-Funktionen

Lyx kann Funktionszeiger an C übergeben. Damit kann C Lyx-Code zurückrufen — z.B. bei Timern, Event-Loops oder Sortieralgorithmen.

Funktionszeiger-Typ definieren

// Typ für den Callback — muss exakt der C-Erwartung entsprechen
type CompareFunc = fn(a: int64, b: int64): int32;
type SignalHandler = fn(signum: int32): void;
type TimerCallback = fn(elapsed_ms: int64): void;

@extern
fn qsort(base: int64, n: int64, size: int64, cmp: CompareFunc): void;

@extern
fn signal(signum: int32, handler: SignalHandler): int64;

Callback implementieren und übergeben

fn CompareDescending(a: int64, b: int64): int32 {
    var va: int64 := (a as int64);
    var vb: int64 := (b as int64);
    if (va > vb) { return -1i32; }
    if (va < vb) { return  1i32; }
    return 0i32;
}

fn OnSigInt(signum: int32): void {
    PrintStr("Programm wird beendet...\n");
    // Cleanup-Logik hier
}

con SIGINT: int32 := 2i32;

fn main(): int64 {
    // Signal-Handler registrieren
    signal(SIGINT, OnSigInt);

    // Array absteigend sortieren
    var data: int64[6] := [3, 1, 4, 1, 5, 9];
    qsort(data as int64, 6, 8, CompareDescending);

    var i: int64 := 0;
    while (i < 6) limit(6) {
        PrintInt(data[i]);
        PrintStr(" ");
        i := i + 1;
    }
    PrintStr("\n");
    // Ausgabe: 9 5 4 3 1 1

    return 0;
}

Calling Convention für Callbacks

Lyx-Callbacks müssen mit der Calling Convention der Zielplattform übereinstimmen — was automatisch der Fall ist, da Lyx die Standard-C-ABI verwendet. Auf Windows muss bei WinAPI-Callbacks @stdcall gesetzt werden:

// Windows: WinAPI erwartet __stdcall (Aufgerufener räumt Stack auf)
@stdcall
fn WndProc(hwnd: int64, msg: uint32, wparam: int64, lparam: int64): int64 {
    if (msg == 0x0002u32) {   // WM_DESTROY
        PostQuitMessage(0i32);
    }
    return DefWindowProcA(hwnd, msg, wparam, lparam);
}


8. Lyx-Funktionen für C exportieren (@export)

Mit @export wird eine Lyx-Funktion in der Symboltabelle des erzeugten Binaries sichtbar — C-Code kann sie dann per Deklaration direkt aufrufen.

// lyx_module.lyx — wird als shared library compiliert

@export
pub fn LyxAdd(a: int64, b: int64): int64 {
    return a + b;
}

@export
pub fn LyxProcessBuffer(data: int64, len: int64): int64 {
    var i: int64 := 0;
    var sum: int64 := 0;
    while (i < len) limit(65536) {
        sum := sum + (data + i * 8) as int64;
        i := i + 1;
    }
    return sum;
}

Kompilieren als Shared Library:

lyxc lyx_module.lyx --shared -o liblyx_module.so

C-seitige Nutzung:

// main.c
#include <stdint.h>
#include <stdio.h>

// Deklaration der exportierten Lyx-Funktionen
extern int64_t LyxAdd(int64_t a, int64_t b);
extern int64_t LyxProcessBuffer(void *data, int64_t len);

int main() {
    printf("LyxAdd(3, 4) = %ld\n", LyxAdd(3, 4));
    return 0;
}

gcc main.c -L. -llyx_module -o main
./main
# LyxAdd(3, 4) = 7


9. Plattform-spezifische APIs

POSIX (Linux / macOS)

@extern
fn open(path: pchar, flags: int32, mode: int32): int32;

@extern
fn read(fd: int32, buf: int64, count: int64): int64;

@extern
fn write(fd: int32, buf: int64, count: int64): int64;

@extern
fn close(fd: int32): int32;

con O_RDONLY: int32 := 0i32;
con O_WRONLY: int32 := 1i32;
con O_CREAT:  int32 := 64i32;

fn ReadFileC(path: pchar): void {
    var fd: int32 := open(path, O_RDONLY, 0i32);
    if (fd < 0i32) {
        PrintStr("Fehler beim Öffnen\n");
        return;
    }

    var buf: uint8[4096];
    var n: int64 := read(fd, buf as int64, 4096);
    if (n > 0) {
        write(1i32, buf as int64, n);   // fd 1 = stdout
    }

    close(fd);
}

Windows API (Win32)

@extern
fn GetLastError(): uint32;

@extern
fn CreateFileA(
    path:              pchar,
    access:            uint32,
    share:             uint32,
    security:          int64,
    creation:          uint32,
    flags:             uint32,
    template:          int64
): int64;

@extern
fn ReadFile(
    handle:    int64,
    buf:       int64,
    to_read:   uint32,
    read_out:  int64,
    overlapped: int64
): int32;

@extern
fn CloseHandle(handle: int64): int32;

con GENERIC_READ:    uint32 := 0x80000000u32;
con OPEN_EXISTING:   uint32 := 3u32;
con INVALID_HANDLE:  int64  := -1;

fn ReadFileWin(path: pchar): void {
    var handle: int64 := CreateFileA(path, GENERIC_READ, 0u32, 0, OPEN_EXISTING, 0u32, 0);
    if (handle == INVALID_HANDLE) {
        PrintStr("Fehler: ");
        PrintInt(GetLastError() as int64);
        PrintStr("\n");
        return;
    }

    var buf: uint8[4096];
    var bytes_read: uint32 := 0u32;
    ReadFile(handle, buf as int64, 4096u32, bytes_read as int64, 0);

    PrintStr(buf as pchar);
    CloseHandle(handle);
}


10. FFI im Safety-Umfeld (DO-178C)

Direkte @extern-Aufrufe sind in @flight_crit-Funktionen verboten — der Compiler gibt einen Fehler aus. Der Grund: Lyx kann nicht prüfen, was innerhalb der C-Bibliothek passiert. Speicherverletzungen, nicht-deterministische Laufzeit, interne Allokationen — alles ist möglich.

Die Lösung ist das Wrapper-Pattern: Eine Lyx-Funktion kapselt den @extern-Aufruf, validiert Ein- und Ausgaben und ist selbst testbar und prüfbar.

import std.result;

// ── Roher C-Aufruf — nicht direkt in Safety-Code verwenden ─────────────────

@extern
fn c_read_adc(channel: int32): int32;

// ── Validierter Wrapper — dies ist der Safety-Boundary ─────────────────────

con ADC_CHANNELS: int32 := 8i32;
con ADC_MAX_RAW:  int32 := 4095i32;   // 12-Bit ADC

fn ReadADC(channel: int32): ResultInt64 {
    // Eingabe validieren — bevor der C-Code aufgerufen wird
    if (channel < 0i32 || channel >= ADC_CHANNELS) {
        return ErrInt64(ERR_INVALID_INPUT);
    }

    var raw: int32 := c_read_adc(channel);

    // Ausgabe validieren — nach dem C-Aufruf
    if (raw < 0i32 || raw > ADC_MAX_RAW) {
        return ErrInt64(ERR_IO);
    }

    return OkInt64(raw as int64);
}

// ── Safety-Code ruft nur noch den Wrapper auf ──────────────────────────────

@flight_crit
@stack_limit(256)
fn SampleSensor(channel: int32): ResultInt64 {
    // Kein @extern hier — nur der validierte Wrapper
    return ReadADC(channel);
}

Regel Begründung
Kein @extern in @flight_crit-Funktionen C-Code ist nicht von Lyx verifizierbar
Eingaben vor dem Aufruf prüfen Pufferüberläufe, ungültige Indizes in C verhindern
Ausgaben nach dem Aufruf prüfen C kann Fehlerwerte ohne Signal zurückgeben
Wrapper ist reine Lyx-Funktion Testbar, statisch analysierbar, MC/DC-fähig
Wrapper im Call-Graph sichtbar –call-graph zeigt die Safety-Boundary explizit

Wrapper im Call-Graph nachweisen

# Call-Graph zeigt: SampleSensor → ReadADC → c_read_adc (extern)
# Die Safety-Boundary ist damit für DO-178C-Auditoren sichtbar
lyxc flight_system.lyx --call-graph -o evidence/call_graph.dot


Zusammenfassung

Szenario Mechanismus
C-Funktion aufrufen @extern fn Name(…): Typ;
Variadic (printf etc.) @extern @variadic fn Name(fmt: pchar): Typ;
Exaktes Struct-Layout @packed type Name = struct { … }
Array an C arr as int64 (Zeiger auf erstes Element)
String an C pchar direkt — ist bereits char *
Funktionszeiger Typ definieren: type F = fn(…): Typ, übergeben als Wert
Windows-Callbacks @stdcall vor der Funktion
Lyx für C exportieren @export pub fn Name(…) + –shared
Safety-Wrapper @extern-Wrapper ohne @flight_crit, dann validierter Lyx-Wrapper mit @flight_crit

ABI & Calling Conventions