Systemprogrammierung – Treiber, Kernel-Module, Echtzeit-Regler, Hardware-Abstraktionen – erfordert direkten Zugriff auf Speicheradressen und präzise Kontrolle über den generierten Maschinencode. Lyx bietet dafür ein vollständiges Pointer-Modell nach Pascal-Vorbild sowie mehrere Performance-Pragmas (@inline, @no_opt, @parallel, @section), die das Backend gezielt steuern.
Sicherheits-Garantien wie Range-Checks und @flight_crit-Regeln bleiben dabei erhalten: Pointer-Arithmetik ist explizit in unsafe-Blöcke eingekapselt, und in zertifizierungspflichtigen Modulen erzeugt ungesicherter Pointer-Einsatz einen Compiler-Fehler.
Ein Pointer speichert die Adresse einer anderen Variable im Speicher. Intern wird jeder Pointer als vorzeichenloser int64 (8 Byte) dargestellt – auf allen unterstützten 64-Bit-Plattformen (x86_64, ARM64, RISC-V64).
| Operator | Bedeutung | Beispiel |
|---|---|---|
^T | Pointer-Typ auf T | var p: ^int64 |
^x | Adresse von x (Address-of) | ^counter |
p^ | Wert an Adresse p (Dereferenz) | p^ := 42 |
nil | Null-Pointer (kein gültiges Ziel) | var p: ^int64 := nil |
Syntax-Merkregel:
Der Hut^zeigt vor dem Typ auf den Typ (^int64= Pointer auf int64) und vor einer Variable auf deren Adresse (^x= Adresse von x). Nach dem Pointer-Namen dereferenziert er (p^= Wert an p).
unit demo;
import std.io;
fn main(): int64 {
var x: int64 := 10;
var p: ^int64 := ^x; // p zeigt auf x
PrintInt(p^); // Ausgabe: 10 (lesen)
p^ := 99; // schreiben über den Pointer
PrintInt(x); // Ausgabe: 99 (x wurde geändert)
return 0;
}
p^ und x benennen denselben Speicherplatz: Eine Zuweisung über p^ verändert x direkt.
Struct-Felder sind über einen Pointer mit . erreichbar – Lyx erfordert keine gesonderte Pfeil-Syntax (→). Der Dereferenz-Operator wirkt auf den gesamten Struct-Wert.
unit structs;
import std.io;
type Vec2 = struct {
x: f64;
y: f64;
};
fn Scale(p: ^Vec2, factor: f64) {
p^.x := p^.x * factor;
p^.y := p^.y * factor;
}
fn main(): int64 {
var v := Vec2 { x: 3.0, y: 4.0 };
Scale(^v, 2.0);
PrintFloat(v.x); // 6.0
PrintFloat(v.y); // 8.0
return 0;
}
Das Übergeben eines Struct-Pointers (^Vec2) ist die Standard-Methode, um große Strukturen ohne Kopier-Overhead zu modifizieren.
Pointer können auf andere Pointer zeigen. Der Typ ergibt sich durch Verschachtelung mit Klammern.
var x: int64 := 42;
var p: ^int64 := ^x; // Pointer auf int64
var pp: ^(^int64) := ^p; // Pointer auf Pointer auf int64
PrintInt(p^); // 42
PrintInt(pp^^); // 42 (doppelte Dereferenz)
pp^^ := 99; // schreibt 99 direkt in x
PrintInt(x); // 99
Mehrfach-Pointer werden in der Praxis für Output-Parameter von Funktionen oder für dynamische Treiber-Dispatch-Tabellen verwendet.
nil ist der Null-Pointer: ein Pointer ohne gültiges Ziel. Der Versuch, nil^ zu lesen oder zu schreiben, führt zu einem Laufzeit-Fehler (Segmentation Fault oder explizitem panic).
fn TryRead(p: ^int64): int64 {
if (p = nil) {
return -1; // Fehlerfall explizit behandeln
}
return p^;
}
nil-Check-Pflicht:
Der Compiler warnt (–lint), wenn ein Pointer ohne vorherigen nil-Check dereferenziert wird und nicht aus einem Kontext stammt, der nil ausschließt (z. B.^lokaleVariable).
Mit dem ?-Suffix wird ein Pointer explizit als nullable deklariert. In Kombination mit dem Null-Coalescing-Operator ?? lassen sich Fallback-Werte definieren:
var name: pchar? := GetOptionalName(); // darf nil sein
var display: pchar := name ?? "Unbekannt";
PrintStr(display);
Da alle Pointer intern als int64 gespeichert sind, ist der explizite Cast in beide Richtungen möglich. Das ist für MMIO-Adressen und FFI-Aufrufe notwendig.
var addr: int64 := 0x40020000;
var reg: ^uint32 := addr as ^uint32; // Integer → Pointer
var p: ^uint8 := get_buffer();
var raw: int64 := p as int64; // Pointer → Integer (für Arithmetik)
Achtung:
as-Casts zwischen Pointer und Integer umgehen die Typsicherheit vollständig. Außerhalb vonunsafe-Blöcken sind sie nur für MMIO-Adressen (@volatile) und nachgewiesene FFI-Szenarien gedacht.
Auf Mikrocontrollern (ESP32, STM32, RISC-V) sind Hardware-Register direkt in den Adressraum eingeblendet. Ohne @volatile kann der Compiler Zugriffe auf dieselbe Adresse wegoptimieren, weil er keine Seiteneffekte erkennt.
unit gpio;
@volatile
var GPIO_OUT: ^uint32 := 0x3FF44004 as ^uint32; // Setze GPIOs HIGH
@volatile
var GPIO_OUT_W1TC: ^uint32 := 0x3FF4400C as ^uint32; // Lösche GPIOs (Write-1-to-Clear)
fn ToggleLed(pin: uint32) {
GPIO_OUT^ := 1u32 << pin; // LED an
// … Wartezeit …
GPIO_OUT_W1TC^ := 1u32 << pin; // LED aus
}
@volatile
var UART_SR: ^uint8 := 0x40013800 as ^uint8;
con UART_TXE: uint8 := 0x80u8; // Transmit Empty Bit
fn WaitTxReady() {
while (UART_SR^ & UART_TXE = 0) { } // Busy-Wait – darf nicht wegoptimiert werden
}
@volatile garantiert, dass jeder Lese- und Schreibzugriff exakt in der programmierten Reihenfolge im Maschinencode erscheint.
pchar ist ein Alias für ^uint8 (char* in C) und repräsentiert einen nullterminierten ASCII-String. String-Literale in Lyx haben den Typ pchar.
let msg: pchar := "Hallo Lyx";
PrintStr(msg); // gibt "Hallo Lyx" aus
// Einzelne Zeichen lesen:
var first: uint8 := msg^; // 'H' = 72
var second: uint8 := (msg + 1)^; // 'a' = 97 (unsafe!)
Das Lesen einzelner Zeichen per Offset-Arithmetik erfordert einen unsafe-Block (→ Abschnitt 8). Für sicheres String-Handling stehen std.string und std.io zur Verfügung.
| Situation | Empfehlung |
|---|---|
| Read-only Ausgabe | pchar-Literal direkt an PrintStr / FFI übergeben |
| Zeichen lesen | std.string.CharAt(s, i) statt manueller Arithmetik |
| String manipulieren | std.string-Funktionen; pchar nur für FFI-Grenzen |
| Ownership übergeben | Kommentar, ob Aufrufer oder Aufgerufener free() trägt |
Pointer-Arithmetik (Offset-Addition/Subtraktion) ist in Lyx standardmäßig verboten. Innerhalb eines unsafe-Blocks hebt der Compiler diese Einschränkung auf und erlaubt skalierte Adress-Berechnung.
unit buffer_ops;
import std.io;
fn ReadWord(base: ^uint8, offset: int64): uint32 {
var result: uint32;
unsafe {
var ptr: ^uint8 := base + offset; // Pointer-Arithmetik
var p32: ^uint32 := ptr as ^uint32; // Reinterpret-Cast
result := p32^; // Lese 4 Bytes
}
return result;
}
| Operation | Beschreibung |
|---|---|
ptr + n / ptr - n | Addition/Subtraktion um n Bytes (skaliert nach Elementgröße) |
ptr as ^OtherType | Reinterpret-Cast: gleiche Adresse, anderer Typ |
(ptr)^ | Dereferenz eines berechneten Zeigers |
addr as ^T | Integer → Pointer (MMIO, FFI) |
fn CopyBytes(dst: ^uint8, src: ^uint8, count: int64) {
var i: int64 := 0;
while (i < count) {
unsafe {
(dst + i)^ := (src + i)^;
}
i++;
}
}
Sicherheits-Warnung:
unsafe-Blöcke erscheinen im Compiler-Report (–call-graph) und in der Provenance-Ausgabe (–provenance). In Modulen mit@dal(A)oder@dal(B)erzeugtunsafestandardmäßig einen Compiler-Fehler, sofern keine explizite Ausnahme mit@allow_unsafedokumentiert ist.
Lyx unterscheidet klar zwischen zulässigen und verbotenen Pointer-Operationen in sicherheitskritischen Modulen.
| Operation | @flight_crit | @dal(A) | @dal(B/C) |
|---|---|---|---|
Pointer auf Stack-Variable (^x) | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt |
Pointer als Funktionsparameter (^T) | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt |
@volatile MMIO-Zugriff | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt |
nil-Pointer (mit Prüfung) | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt |
new / dispose (Heap) | ❌ Verboten | ❌ Verboten | ⚠️ Warnung |
unsafe Pointer-Arithmetik | ❌ Compiler-Fehler | ❌ Compiler-Fehler | ⚠️ Warnung |
as-Cast Pointer ↔ int64 | ⚠️ Warnung | ❌ Compiler-Fehler | ⚠️ Warnung |
Alle Puffer und Arbeitsspeicher werden in der Init-Phase als Stack-Arrays angelegt und als Pointer weitergereicht. Innerhalb des Regel-Zyklus entstehen keine neuen Pointer und keine Arithmetik.
@flight_crit
@dal(B)
@stack_limit(4096)
unit sensor_ctrl;
fn main(): int64 {
// Init-Phase: Puffer als Stack-Array
var samples: [256]f64;
var buf_ptr: ^f64 := ^samples[0]; // Pointer auf erstes Element – erlaubt
// Regel-Zyklus: nur sicherer Zugriff
while (true) {
var val := ReadSensor();
buf_ptr^ := val; // Schreiben via Pointer – erlaubt
buf_ptr^ := ProcessSample(buf_ptr^);
}
return 0;
}
@inline weist den Compiler an, den Funktionsaufruf durch den Funktionskörper zu ersetzen. Damit entfällt der Aufruf-Overhead (Register-Sicherung, Stack-Frame-Aufbau, Branch).
clamp, min, max, Bit-Operationen)
@inline
fn clamp(val: int64, lo: int64, hi: int64): int64 {
if (val < lo) { return lo; }
if (val > hi) { return hi; }
return val;
}
@inline
fn abs_f64(x: f64): f64 {
if (x < 0.0) { return -x; }
return x;
}
fn main(): int64 {
var speed: int64 := clamp(ReadSensor(), 0, 300); // kein Funktionsaufruf im Asm
return 0;
}
| Aspekt | Ohne @inline | Mit @inline |
|---|---|---|
| Aufruf-Overhead | Ja (push/pop, call/ret) | Nein |
| Code-Größe | Klein (eine Kopie) | Wächst pro Aufrufstelle |
| Instruction-Cache | Gut (dichte Loop) | Schlechter bei vielen Aufrufstellen |
| Stack-Tiefe | Tiefer (Stack-Frame) | Flacher |
Energy-Level-Interaktion:
Das Energy-Aware-Backend inlinet bei Level 4–5 automatisch kleine Funktionen auch ohne@inline. Bei Level 1–2 wird Inlining bewusst reduziert, um den Instruction-Cache zu schonen.@inlineerzwingt Inlining unabhängig vom Energy-Level.
@no_opt verhindert jede Optimierung für eine einzelne Funktion. Das ist notwendig, wenn:
@no_opt
fn WaitMicros(us: int64) {
var i: int64 := 0;
while (i < us * 100) { // Schleife bleibt erhalten – kein "dead loop" Entfernen
i++;
}
}
@no_opt
fn SendByte(b: uint8) {
while (TX_SR^ & 0x80u8 = 0) { } // Warte auf "TX empty"
TX_DATA^ := b; // Sende Byte
}
Im Gegensatz zu @volatile (das nur Lade-/Store-Reihenfolge schützt) verhindert @no_opt auch das Umordnen, Fusionieren oder Entfernen beliebiger Operationen.
@parallel signalisiert dem Compiler, dass eine Schleife oder ein Array-Block keine Datenabhängigkeiten zwischen den Iterationen besitzt und für SIMD-Vektorisierung geeignet ist (AVX2 auf x86_64, NEON auf ARM64).
unit dsp;
fn NormalizeSignal(samples: [1024]f32, gain: f32) {
@parallel
for i := 0 to 1023 do {
samples[i] := samples[i] * gain;
}
}
fn DotProduct(a: [512]f64, b: [512]f64): f64 {
var sum: f64 := 0.0;
@parallel
for i := 0 to 511 do {
sum := sum + a[i] * b[i];
}
return sum;
}
| Plattform | SIMD-Erweiterung | Vektorgröße |
|---|---|---|
| x86_64 (AVX2) | 256-Bit-Register | 8× f32 oder 4× f64 |
| ARM64 (NEON) | 128-Bit-Register | 4× f32 oder 2× f64 |
| RISC-V (V-Extension) | Variabel (>=128 Bit) | Abhängig vom Hart |
Voraussetzung:
Der Compiler prüft, ob Schreibzugriffe innerhalb der Schleife unabhängig voneinander sind. Falls Aliasing möglich ist (z. B.srcunddstzeigen auf überlappende Bereiche), warnt–lintund verzichtet auf Vektorisierung.
@section platziert den Maschinencode einer Funktion oder die Daten einer Variablen in einer benannten ELF-Sektion. Das ist entscheidend für:
„.boot“)
@section(".fast_ram")
fn IsrUart() {
// Wird in SRAM geladen – kein Flash-Read-Wait-State
var byte := UART_DATA^;
RingBufferPush(byte);
}
@section(".slow_mem")
fn PrintDiagnostics() {
// Seltener Aufruf – darf im langsamen Flash liegen
PrintStr("Diagnostics OK");
}
@section(".boot")
fn SystemInit() {
ClockSetup();
MemoryInit();
}
Die Sektions-Namen müssen im Linker-Script (*.ld) definiert sein und einer physikalischen Speicherregion zugeordnet werden.
Ein UART-Treiber kombiniert alle beschriebenen Mechanismen:
unit uart_driver;
import std.io;
// ── Hardware-Register (MMIO) ──────────────────────────────────────────────────
@volatile var UART_DR: ^uint8 := 0x40013800 as ^uint8; // Data Register
@volatile var UART_SR: ^uint32 := 0x40013804 as ^uint32; // Status Register
@volatile var UART_BRR: ^uint32 := 0x40013808 as ^uint32; // Baud Rate
con UART_SR_TXE: uint32 := 0x0080u32; // Transmit Empty
con UART_SR_RXNE: uint32 := 0x0020u32; // Receive Not Empty
// ── Inline-Hilfsfunktionen ────────────────────────────────────────────────────
@inline
fn TxReady(): bool {
return UART_SR^ & UART_SR_TXE != 0;
}
@inline
fn RxReady(): bool {
return UART_SR^ & UART_SR_RXNE != 0;
}
// ── Busy-Wait (darf nicht wegoptimiert werden) ────────────────────────────────
@no_opt
fn WaitTx() {
while (!TxReady()) { }
}
// ── Byte senden ───────────────────────────────────────────────────────────────
@section(".fast_ram")
@flight_crit
fn SendByte(b: uint8) {
WaitTx();
UART_DR^ := b;
}
// ── String senden ─────────────────────────────────────────────────────────────
fn SendStr(s: pchar) {
var p: ^uint8 := s as ^uint8;
while (p^ != 0u8) {
SendByte(p^);
unsafe { p := p + 1; }
}
}
// ── Buffer empfangen ─────────────────────────────────────────────────────────
fn RecvBytes(dst: ^uint8, count: int64): int64 {
var received: int64 := 0;
while (received < count) {
if (RxReady()) {
unsafe {
(dst + received)^ := UART_DR^ as uint8;
}
received++;
}
}
return received;
}
fn main(): int64 {
UART_BRR^ := 0x683u32; // 9600 Baud @ 16 MHz
SendStr("UART bereit\n");
return 0;
}
| Feature | Einsatzgebiet | Risiko / Einschränkung |
|---|---|---|
^T Stack-Pointer | Struct-Parameter, Output-Werte, Treiber-Register | nil-Check notwendig |
@volatile | Hardware-Register (MMIO), Shared Memory Multicore | Keines – verhindert Fehloptimierung |
nil-Check | Pointer aus externen Quellen, optionale Werte | Pflicht vor Dereferenz |
unsafe + Arithmetik | Byte-Buffer, pchar-Iteration, FFI-Wrapper | Verboten in @dal(A/B) |
pchar | String-Literale, C-FFI-Strings | Keine Bounds-Checks – std.string bevorzugen |
@inline | Kleine Helferfunktionen, heiße Schleifen | Code-Größe steigt |
@no_opt | Busy-Wait, Hardware-Sequenzen, Timing-kritisch | Kein Optimierungs-Benefit |
@parallel | DSP/ML-Schleifen ohne Datenabhängigkeit | Aliasing-Check notwendig |
@section | ISR, Bootloader, SRAM-Placement | Linker-Script muss passen |
Weiterführende Seiten: