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