====== Lyx – Objektorientierte Programmierung ====== Lyx unterscheidet bewusst zwischen zwei Arten von benutzerdefinierten Typen: **Structs** und **Klassen**. Diese Trennung ist kein Stilmittel — sie macht die Performance-Implikationen im Code sichtbar. Ein Struct liegt auf dem Stack, ein Klassenobjekt auf dem Heap. Ein Struct-Aufruf ist ein direkter Funktionsaufruf, ein virtueller Methodenaufruf geht durch eine V-Table. Beides ist in Lyx explizit und damit für den Entwickler kontrollierbar. ^ Eigenschaft ^ Struct ^ Klasse ^ | Speicherort | Stack (Copy-by-Value) | Heap (Reference-Type) | | Vererbung | Nein | Ja (''extends'') | | Virtuelle Methoden | Nein | Ja (''virtual'' / ''override'') | | Allokation | Automatisch (Scope) | Explizit (''new'' / ''dispose'') | | Polymorphismus | Nein | Ja (V-Table) | | WCET-Berechenbarkeit | Exakt | Eingeschränkt (V-Table-Dispatch) | | Einsatz | Sensordaten, Vektoren, Pakete | Treiber, Komponenten, Protokoll-Handler | ---- ===== 1. Structs ===== Structs sind stackbasierte, leichtgewichtige Datencontainer. Zuweisung kopiert den Inhalt vollständig (Copy-by-Value) — es gibt keinen versteckten Heap-Zugriff. ==== Definition und Felder ==== type Point = struct { x: int64; y: int64; }; type SensorReading = struct { sensor_id: int64; temperature: f64; pressure: f64; timestamp: int64; valid: bool; }; ==== Methoden im Struct ==== Structs können Methoden besitzen. ''self'' referenziert die aktuelle Instanz. Da Structs Value-Types sind, muss ''var self'' verwendet werden, wenn eine Methode Felder verändert: type Point = struct { x: int64; y: int64; // Lesende Methode — self wird nicht verändert fn LengthSquared(): int64 { return self.x * self.x + self.y * self.y; } fn DistanceTo(other: Point): f64 { var dx: int64 := other.x - self.x; var dy: int64 := other.y - self.y; return math.Sqrt((dx * dx + dy * dy) as f64); } // Mutierende Methode — verändert self fn Translate(dx: int64, dy: int64): void { self.x := self.x + dx; self.y := self.y + dy; } fn ToString(): pchar { return StrFormat("(%d, %d)", self.x, self.y); } }; fn main(): int64 { var p1: Point := Point { x: 3, y: 4 }; var p2: Point := Point { x: 0, y: 0 }; PrintStr("Länge²: "); PrintInt(p1.LengthSquared()); // 25 PrintStr("\n"); PrintStr("Abstand: "); PrintF64(p1.DistanceTo(p2)); // 5.0 PrintStr("\n"); p1.Translate(1, 1); PrintStr(p1.ToString()); // (4, 5) PrintStr("\n"); // Copy-by-Value: p3 ist eine unabhängige Kopie von p1 var p3: Point := p1; p3.Translate(10, 10); // p1 ist unverändert PrintStr(p1.ToString()); // (4, 5) PrintStr("\n"); return 0; } ==== Structs in Safety-Code ==== Structs sind die bevorzugte Datenstruktur in ''@flight_crit''-Funktionen: deterministisch, keine Allokation, WCET-exakt berechenbar. type FlightState = struct { altitude: f64; airspeed: f64; heading: f64; bank: f64; pitch: f64; valid: bool; }; type ControlCommand = struct { throttle: int64; aileron: int64; elevator: int64; }; @flight_crit @stack_limit(512) fn ComputeCommand(state: FlightState): ControlCommand { if (!state.valid) { return ControlCommand { throttle: 50, aileron: 0, elevator: 0 }; } return ControlCommand { throttle: ClampI(50 + (250 - state.airspeed as int64) / 5, 0, 100), aileron: ClampI(-(state.bank as int64), -45, 45), elevator: ClampI((10000 - state.altitude as int64) / 50, -30, 30) }; } ---- ===== 2. Klassen ===== Klassen sind Reference-Types: Sie werden mit ''new'' auf dem Heap angelegt und mit ''dispose'' freigegeben. Eine Klasse unterstützt Vererbung und virtuelle Methoden. ==== Definition ==== type Logger = class { var log_fd: int64; var prefix: pchar; var count: int64; // Konstruktor — wird von new aufgerufen fn Create(fd: int64, pfx: pchar): void { self.log_fd := fd; self.prefix := pfx; self.count := 0; } // Destruktor — wird von dispose aufgerufen fn Destroy(): void { if (self.log_fd > 2) { SysClose(self.log_fd); } } pub fn Log(msg: pchar): void { self.count := self.count + 1; PrintStr(self.prefix); PrintStr(": "); PrintStr(msg); PrintStr("\n"); } pub fn Count(): int64 { return self.count; } }; fn main(): int64 { var logger: int64 := new Logger(1, "[APP]"); (logger as Logger).Log("Programm gestartet"); (logger as Logger).Log("Verbindung hergestellt"); PrintStr("Log-Einträge: "); PrintInt((logger as Logger).Count()); PrintStr("\n"); dispose logger; // ruft Destroy() auf und gibt Heap-Speicher frei return 0; } ==== Sichtbarkeit ==== Ohne explizite Angabe sind Felder und Methoden **unit-privat** — außerhalb der Unit nicht sichtbar. ''pub'' macht sie öffentlich. type Database = class { pub var host: pchar; // öffentlich — andere Units können lesen/schreiben pub var port: int64; var socket_fd: int64; // privat — nur innerhalb dieser Unit zugänglich var connected: bool; pub fn Connect(): bool { self.socket_fd := OpenTCPSocket(self.host, self.port); self.connected := self.socket_fd > 0; return self.connected; } pub fn IsConnected(): bool { return self.connected; } // Privat — nur intern genutzt fn ResetState(): void { self.socket_fd := -1; self.connected := false; } pub fn Disconnect(): void { if (self.connected) { SysClose(self.socket_fd); self.ResetState(); } } }; ---- ===== 3. Vererbung ===== Eine Klasse erbt Felder und Methoden ihrer Elternklasse mit ''extends''. In Lyx gibt es einfache Vererbung (eine Elternklasse). Mehrere Interfaces werden über Traits modelliert. // Basisklasse type Shape = class { var color: int64; // RGB als int64 fn Create(c: int64): void { self.color := c; } virtual fn Area(): f64 { return 0.0; } virtual fn Perimeter(): f64 { return 0.0; } pub fn Describe(): void { PrintStr("Farbe: "); PrintInt(self.color); PrintStr(", Fläche: "); PrintF64(self.Area()); PrintStr(" cm²\n"); } }; // Abgeleitete Klasse type Circle = class extends Shape { var radius: f64; fn Create(c: int64, r: f64): void { super.Create(c); // Eltern-Konstruktor aufrufen self.radius := r; } override fn Area(): f64 { return math.PI * self.radius * self.radius; } override fn Perimeter(): f64 { return 2.0 * math.PI * self.radius; } }; type Rectangle = class extends Shape { var width: f64; var height: f64; fn Create(c: int64, w: f64, h: f64): void { super.Create(c); self.width := w; self.height := h; } override fn Area(): f64 { return self.width * self.height; } override fn Perimeter(): f64 { return 2.0 * (self.width + self.height); } }; fn main(): int64 { var circle: int64 := new Circle(0xFF0000, 5.0); var rect: int64 := new Rectangle(0x00FF00, 4.0, 6.0); (circle as Circle).Describe(); (rect as Rectangle).Describe(); dispose circle; dispose rect; return 0; } ==== super — Elternmethoden aufrufen ==== ''super.MethodName()'' ruft die Implementierung der Elternklasse auf. Das ist besonders im Konstruktor und beim partiellen Überschreiben nützlich: type TimestampedLogger = class extends Logger { var start_time: int64; fn Create(fd: int64, pfx: pchar): void { super.Create(fd, pfx); // Logger.Create aufrufen self.start_time := SysTimeMs(); } override pub fn Log(msg: pchar): void { var elapsed: int64 := SysTimeMs() - self.start_time; PrintStr("["); PrintInt(elapsed); PrintStr("ms] "); super.Log(msg); // Logger.Log aufrufen } }; ---- ===== 4. Virtuelle Methoden und Polymorphismus ===== Virtuelle Methoden ermöglichen Polymorphismus: Verschiedene Klassen können dieselbe Basisklasse implementieren und austauschbar verwendet werden. Der Aufruf wird zur Laufzeit über eine **V-Table** aufgelöst. type Sensor = class { var id: int64; fn Create(sensor_id: int64): void { self.id := sensor_id; } virtual fn Read(): f64 { return 0.0; } virtual fn IsReady(): bool { return true; } virtual fn Name(): pchar { return "Sensor"; } }; type PressureSensor = class extends Sensor { var port: int64; fn Create(sid: int64, hw_port: int64): void { super.Create(sid); self.port := hw_port; } override fn Read(): f64 { var raw: int64 := MemRead32(self.port); return (raw as f64) * 0.1; // Rohwert → hPa } override fn Name(): pchar { return "PressureSensor"; } }; type TemperatureSensor = class extends Sensor { var port: int64; fn Create(sid: int64, hw_port: int64): void { super.Create(sid); self.port := hw_port; } override fn Read(): f64 { var raw: int64 := MemRead32(self.port); return (raw as f64) * 0.0625 - 40.0; // Rohwert → °C } override fn Name(): pchar { return "TemperatureSensor"; } }; // Diese Funktion arbeitet mit jedem Sensor — Polymorphismus fn LogSensor(sensor: int64): void { var s: Sensor := sensor as Sensor; if (!s.IsReady()) { PrintStr("Sensor nicht bereit\n"); return; } PrintStr(s.Name()); PrintStr(" → "); PrintF64(s.Read()); PrintStr("\n"); } fn main(): int64 { var pressure: int64 := new PressureSensor(1, 0x40020000); var temperature: int64 := new TemperatureSensor(2, 0x40020004); // Beide Aufrufe gehen durch LogSensor — V-Table entscheidet welche Read() LogSensor(pressure); LogSensor(temperature); dispose pressure; dispose temperature; return 0; } ==== Wie die V-Table funktioniert ==== Jede Klasse mit virtuellen Methoden bekommt eine **Virtual Table (V-Table)**: ein Array von Funktionszeigern, einem pro virtueller Methode. Jede Instanz trägt einen Zeiger auf die V-Table ihrer Klasse. Speicherlayout einer Sensor-Instanz: ┌──────────────────────────────────────────┐ │ vtable_ptr ──→ PressureSensor V-Table │ │ [0] Read → PressureSensor::Read │ │ [1] IsReady → Sensor::IsReady │ │ [2] Name → PressureSensor::Name │ │ id: int64 │ │ port: int64 │ └──────────────────────────────────────────┘ Ein virtueller Aufruf ''s.Read()'' wird zu: lade vtable_ptr → springe zu vtable[0]. Das ist ein **indirekter Sprung** — minimal mehr Overhead als ein direkter Call, aber der WCET-Analysator kann den Zielbereich nicht statisch einengen. ---- ===== 5. Abstrakte Methoden ===== Eine abstrakte Methode hat keinen Körper und **muss** in der abgeleiteten Klasse überschrieben werden. Der Compiler gibt einen Fehler aus, wenn eine abstrakte Klasse direkt instanziiert wird. type Transport = class { var name: pchar; fn Create(n: pchar): void { self.name := n; } // Abstrakt — muss implementiert werden virtual fn Send(data: int64, len: int64): int64; virtual fn Recv(buf: int64, max_len: int64): int64; virtual fn IsConnected(): bool; // Konkrete Methode — nutzt die abstrakten pub fn SendString(msg: pchar): bool { if (!self.IsConnected()) { return false; } var len: int64 := StrLen(msg); return self.Send(msg as int64, len) == len; } }; type TCPTransport = class extends Transport { var fd: int64; fn Create(host: pchar, port: int64): void { super.Create("TCP"); self.fd := OpenTCPSocket(host, port); } override fn Send(data: int64, len: int64): int64 { return SysWrite(self.fd, data, len); } override fn Recv(buf: int64, max_len: int64): int64 { return SysRead(self.fd, buf, max_len); } override fn IsConnected(): bool { return self.fd > 0; } }; type UARTTransport = class extends Transport { var port: int64; fn Create(hw_port: int64): void { super.Create("UART"); self.port := hw_port; } override fn Send(data: int64, len: int64): int64 { return UARTWrite(self.port, data, len); } override fn Recv(buf: int64, max_len: int64): int64 { return UARTRead(self.port, buf, max_len); } override fn IsConnected(): bool { return UARTIsReady(self.port); } }; ---- ===== 6. Laufzeit-Typprüfung: is und as ===== ''is'' prüft zur Laufzeit, ob ein Objekt eine bestimmte Klasse oder eine ihrer Unterklassen ist. ''as'' führt den Cast durch — gibt es einen Typ-Mismatch, löst ''as'' einen ''panic()'' aus. fn ProcessSensor(sensor: int64): void { var s: Sensor := sensor as Sensor; // is: sicherer Typ-Check if (sensor is PressureSensor) { var ps: PressureSensor := sensor as PressureSensor; PrintStr("Druck: "); PrintF64(ps.Read()); PrintStr(" hPa\n"); } else if (sensor is TemperatureSensor) { var ts: TemperatureSensor := sensor as TemperatureSensor; PrintStr("Temperatur: "); PrintF64(ts.Read()); PrintStr(" °C\n"); } else { // Generischer Fallback PrintStr(s.Name()); PrintStr(": "); PrintF64(s.Read()); PrintStr("\n"); } } ''as'' ohne vorangehendes ''is'' ist nur sicher, wenn der Typ durch den Programmfluss garantiert ist: // Sicher: Der Typ kommt aus einer bekannten Quelle var known_pressure: int64 := new PressureSensor(1, 0x4000); var ps: PressureSensor := known_pressure as PressureSensor; // kein Panic möglich // Unsicher — nur mit vorherigem is: fn Cast(unknown: int64): void { if (unknown is PressureSensor) { var ps: PressureSensor := unknown as PressureSensor; // OK } // var ps: PressureSensor := unknown as PressureSensor; // Panic wenn kein PressureSensor } ---- ===== 7. Komposition statt Vererbung ===== Tiefe Vererbungshierarchien erschweren die WCET-Analyse und machen Code schwerer testbar. Die bevorzugte Alternative in Lyx ist **Komposition**: Eine Klasse enthält andere Klassen als Felder. // ── Schlechtes Muster: tiefe Hierarchie ─────────────────────────────────── type Base = class { ... }; type Middle = class extends Base { ... }; type Concrete = class extends Middle { ... }; // V-Table mit 3 Ebenen // ── Besseres Muster: Komposition ────────────────────────────────────────── type DataBuffer = struct { data: int64; len: int64; cap: int64; }; type Framer = struct { header_size: int64; fn WrapData(buf: DataBuffer): DataBuffer { ... } }; type SerialPort = class { var baud: int64; var fd: int64; var framer: Framer; // Komposition — kein extends nötig var buffer: DataBuffer; fn Create(device: pchar, baud_rate: int64): void { self.fd := OpenSerial(device); self.baud := baud_rate; self.framer := Framer { header_size: 4 }; self.buffer := DataBuffer { data: new uint8[4096], len: 0, cap: 4096 }; } pub fn Send(data: int64, len: int64): void { var framed: DataBuffer := self.framer.WrapData( DataBuffer { data: data, len: len, cap: len } ); SysWrite(self.fd, framed.data, framed.len); } }; ---- ===== 8. OOP im Safety-Umfeld ===== Klassen mit virtuellen Methoden sind in ''@flight_crit''-Funktionen eingeschränkt nutzbar. Virtuelle Aufrufe erzeugen indirekte Sprünge — ihre WCET lässt sich nicht exakt berechnen, wenn der Typ zur Compile-Zeit nicht bekannt ist. ^ Situation ^ Empfehlung ^ | Daten im Regelzyklus | Struct (Stack, deterministisch) | | Algorithmen mit festem Typ | Direkte Klasseninstanz — kein virtueller Call | | Polymorphismus im Regelzyklus | Vermeiden — oder Typ zur Compile-Zeit fixieren | | Polymorphismus in Initialisierung | Unbedenklich — kein WCET-Budget | | Tiefe Vererbung (>2 Ebenen) | Komposition bevorzugen | | Instanziierung mit new | Nur in Initialisierungsphase | | Typ-Cast mit as | Nur nach vorangehendem ''is''-Check | ==== Statischer Dispatch als Alternative ==== Wo Polymorphismus gewünscht, aber V-Table-Dispatch verboten ist, bieten Generics + Traits die Alternative — sie werden durch Monomorphisierung statisch aufgelöst: // Statt: virtual fn Read(): f64 (V-Table, dynamisch) // Besser: Generic + Trait (monomorphisiert, statisch) trait SensorHW { fn Read(): f64; fn IsReady(): bool; } @flight_crit @stack_limit(128) fn SampleSensor(var hw: S): f64 { if (!hw.IsReady()) { return -1.0; } return hw.Read(); // Kein V-Table-Sprung — der Compiler kennt den Typ zur Compile-Zeit } ---- ===== 9. Vollständiges Beispiel: Plugin-System ===== Ein Protokoll-Handler-System, das Polymorphismus in der Initialisierung nutzt und im Regelzyklus auf direkten Dispatch umschaltet: unit ProtocolStack; // ── Basis-Handler ───────────────────────────────────────────────────────────── type ProtocolHandler = class { var name: pchar; var enabled: bool; fn Create(n: pchar): void { self.name := n; self.enabled := true; } virtual fn HandlePacket(data: int64, len: int64): bool; virtual fn GetStats(out_rx: int64, out_tx: int64): void; pub fn IsEnabled(): bool { return self.enabled; } pub fn Enable(): void { self.enabled := true; } pub fn Disable(): void { self.enabled := false; } }; // ── Konkrete Handler ────────────────────────────────────────────────────────── type HTTPHandler = class extends ProtocolHandler { var rx_count: int64; var tx_count: int64; fn Create(): void { super.Create("HTTP/1.1"); self.rx_count := 0; self.tx_count := 0; } override fn HandlePacket(data: int64, len: int64): bool { if (!self.enabled) { return false; } // HTTP-Parsing... self.rx_count := self.rx_count + 1; return true; } override fn GetStats(out_rx: int64, out_tx: int64): void { (out_rx as int64) := self.rx_count; (out_tx as int64) := self.tx_count; } }; type MQTTHandler = class extends ProtocolHandler { var rx_count: int64; var tx_count: int64; fn Create(): void { super.Create("MQTT"); self.rx_count := 0; self.tx_count := 0; } override fn HandlePacket(data: int64, len: int64): bool { if (!self.enabled) { return false; } // MQTT-Parsing... self.rx_count := self.rx_count + 1; return true; } override fn GetStats(out_rx: int64, out_tx: int64): void { (out_rx as int64) := self.rx_count; (out_tx as int64) := self.tx_count; } }; // ── Router — nutzt Polymorphismus zur Dispatch-Zeit ─────────────────────────── con MAX_HANDLERS: int64 := 8; type Router = class { var handlers: int64[8]; // Array von Klassen-Zeigern var count: int64; fn Create(): void { self.count := 0; } pub fn Register(handler: int64): bool { if (self.count >= MAX_HANDLERS) { return false; } self.handlers[self.count] := handler; self.count := self.count + 1; return true; } pub fn Dispatch(data: int64, len: int64): int64 { var handled: int64 := 0; var i: int64 := 0; while (i < self.count) limit(MAX_HANDLERS) { var h: ProtocolHandler := self.handlers[i] as ProtocolHandler; if (h.IsEnabled() && h.HandlePacket(data, len)) { handled := handled + 1; } i := i + 1; } return handled; } pub fn Destroy(): void { var i: int64 := 0; while (i < self.count) limit(MAX_HANDLERS) { dispose self.handlers[i]; i := i + 1; } } }; fn main(): int64 { // Initialisierungsphase — new ist erlaubt var router: int64 := new Router(); var http: int64 := new HTTPHandler(); var mqtt: int64 := new MQTTHandler(); (router as Router).Register(http); (router as Router).Register(mqtt); // Regelzyklus — kein new, nur Dispatch var buf: uint8[1500]; var n: int64 := ReceivePacket(buf as int64, 1500); if (n > 0) { var handled: int64 := (router as Router).Dispatch(buf as int64, n); PrintStr("Handler aktiv: "); PrintInt(handled); PrintStr("\n"); } // Aufräumen (router as Router).Destroy(); dispose router; return 0; } → [[lyx_-_programmiersprache:generics-traits|Generics & Traits — Statischer Dispatch als Alternative zu Vererbung]]\\ → [[lyx_-_programmiersprache:memory-management|Memory Management — Heap-Allokation und Lebensdauer]]