====== Lyx – Low-Level: Pointer & Inlining ======
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.
===== 1. Pointer – Grundlagen =====
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).
==== Pointer-Operatoren ====
^ 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).
==== Lesen und Schreiben über einen Pointer ====
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.
===== 2. Pointer auf Structs =====
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.
===== 3. Mehrfach-Pointer (Pointer auf Pointer) =====
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.
===== 4. nil – Der Null-Pointer =====
''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'').
==== Nullable Pointer-Typen ====
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);
===== 5. Pointer ↔ int64 – Typ-Casts =====
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 von ''unsafe''-Blöcken sind sie nur für MMIO-Adressen (''@volatile'') und nachgewiesene FFI-Szenarien gedacht.
===== 6. @volatile – Memory-Mapped I/O (MMIO) =====
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.
==== GPIO-Beispiel (ESP32 / RISC-V) ====
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
}
==== UART Status-Register ====
@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.
===== 7. pchar – String als Zeiger =====
''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 |
===== 8. unsafe – Pointer-Arithmetik =====
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;
}
==== Was ist in unsafe erlaubt? ====
^ 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) |
==== Typisches Muster: Byte-Buffer ====
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)'' erzeugt ''unsafe'' standardmäßig einen **Compiler-Fehler**, sofern keine explizite Ausnahme mit ''@allow_unsafe'' dokumentiert ist.
===== 9. Pointer in Safety-Code =====
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 |
==== Empfohlenes Muster für @flight_crit ====
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;
}
===== 10. @inline – Inlining erzwingen =====
''@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).
==== Wann sinnvoll? ====
* Kleine mathematische Hilfsfunktionen (''clamp'', ''min'', ''max'', Bit-Operationen)
* Getter/Setter für Felder (Zugriff mit Null-Overhead)
* Heiße Pfade in Schleifen, die weniger als ~10 Instruktionen erzeugen
@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;
}
==== Trade-off: Geschwindigkeit vs. Code-Größe ====
^ 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. ''@inline'' **erzwingt** Inlining unabhängig vom Energy-Level.
===== 11. @no_opt – Optimierung deaktivieren =====
''@no_opt'' verhindert jede Optimierung für eine einzelne Funktion. Das ist notwendig, wenn:
* Timing exakt eingehalten werden muss (Busy-Wait-Schleifen)
* Der Debugger den Code 1:1 nachvollziehen muss
* Eine Hardware-Sequenz in genau dieser Reihenfolge ablaufen muss
@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.
===== 12. @parallel – SIMD-Vektorisierung =====
''@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. ''src'' und ''dst'' zeigen auf überlappende Bereiche), warnt ''--lint'' und verzichtet auf Vektorisierung.
===== 13. @section – ELF-Sektion steuern =====
''@section'' platziert den Maschinencode einer Funktion oder die Daten einer Variablen in einer benannten ELF-Sektion. Das ist entscheidend für:
* **Embedded-Systeme**: Code in schnellem SRAM statt langsamem Flash
* **Bootloader**: Init-Code in einer separaten Sektion (''".boot"'')
* **Interrupt-Handler**: ISR-Code muss im direkt erreichbaren Speicher liegen
@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.
===== 14. Vollständiges Beispiel: Hardware-Treiber mit Pointer & Pragmas =====
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;
}
===== 15. Zusammenfassung: Wann was nutzen? =====
^ 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:**
* [[lyx_-_programmiersprache:memory-management|Memory Management – Stack, Heap, std.alloc]]
* [[lyx_-_programmiersprache:ffi|FFI – C-Interoperabilität & @extern]]
* [[lyx_-_programmiersprache:das-energy-aware-programmiermodell|Energy-Aware Programmiermodell]]
* [[lyx_-_programmiersprache:aerospace-safety|Aerospace & Safety – @flight_crit & DO-178C]]
* [[lyx_-_programmiersprache:attributes-pragmas|Attribute & Pragmas – vollständige @-Referenz]]