====== 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'' | Verlinkt gegen ''lib.so'' / ''lib.a'' | | ''-L'' | 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 #include // 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'' | → [[lyx_-_programmiersprache:abi-calling-conventions|ABI & Calling Conventions]]