====== 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]]