Inhaltsverzeichnis

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<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
}


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;
}

Generics & Traits — Statischer Dispatch als Alternative zu Vererbung
Memory Management — Heap-Allokation und Lebensdauer