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 |
Structs sind stackbasierte, leichtgewichtige Datencontainer. Zuweisung kopiert den Inhalt vollständig (Copy-by-Value) — es gibt keinen versteckten Heap-Zugriff.
type Point = struct {
x: int64;
y: int64;
};
type SensorReading = struct {
sensor_id: int64;
temperature: f64;
pressure: f64;
timestamp: int64;
valid: bool;
};
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 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)
};
}
Klassen sind Reference-Types: Sie werden mit new auf dem Heap angelegt und mit dispose freigegeben. Eine Klasse unterstützt Vererbung und virtuelle Methoden.
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;
}
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();
}
}
};
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.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
}
};
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;
}
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.
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);
}
};
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
}
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);
}
};
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 |
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<S: SensorHW>(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
}
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;
}
→ Generics & Traits — Statischer Dispatch als Alternative zu Vererbung
→ Memory Management — Heap-Allokation und Lebensdauer