Backends

Ein Backend ist die einzige Stelle mit Kontakt zum Betriebssystem. Es erfüllt den Vertrag IVegaBackend aus Vega/Backend.lyx — 21 Methoden, mehr nicht.

Vorhandene Backends

Backend Unit Plattform Stand
TX11Backend Vega/Backend/X11.lyx Linux, X11 vollständig
TWin32Backend Vega/Backend/Win32.lyx Windows vollständig
TNullBackend Vega/Backend/Null.lyx keine für Tests

Das Null-Backend ist kein Notbehelf: es macht die 67 Linux-Testprogramme möglich, die ohne Fenstersystem laufen. Ereignisse werden hineingereicht (PushEvent), Fenstergrößen und Monitore sind frei einstellbar.

Auswahl nach Zielplattform

Was der Compiler hergibt

Symbol –target=linux –target=win64
TARGET_OS 1 3

Dazu OS_LINUX = 1, OS_MACOS = 2, OS_WINDOWS = 3.

Es gibt keine bedingte Übersetzung — kein –define, kein @if, kein ifdef.

Warum TARGET_OS allein nicht reicht

Die naheliegende Fabrikfunktion

pub fn NewPlatformBackend(): IVegaBackend {
  if (TARGET_OS == OS_WINDOWS) { return new TWin32Backend() as IVegaBackend; }
  return new TX11Backend() as IVegaBackend;
}

übersetzt fehlerfrei und ist trotzdem unbrauchbar. Beide Zweige werden gebunden, und extern-Deklarationen sind harte Abhängigkeiten, auch wenn der Code sie nie erreicht:

$ ./programm          # ELF, Win32-Backend im toten Zweig
error while loading shared libraries: kernel32.dll: cannot open shared object file

$ wine programm.exe   # PE, X11-Backend im toten Zweig
wine: failed to open "programm.exe": c0000135

Beide starten gar nicht erst. Die Entscheidung muss also vor das Binden.

Die Lösung: der Include-Pfad

Die Unit Vega.BackendAuto gibt es zweimal:

platform/linux/Vega/BackendAuto.lyx   -> TX11Backend
platform/win64/Vega/BackendAuto.lyx   -> TWin32Backend

Beide bieten dasselbe:

pub fn NewPlatformBackend(): IVegaBackend;
pub fn PlatformBackendName(): pchar;      // "X11" bzw. "Win32"
pub fn PlatformBackendMatches(): bool;    // prueft gegen TARGET_OS

lyxc app.lyx -I . -I platform/linux
lyxc app.lyx -I . -I platform/win64 --target=win64 --format=pe

Dasselbe Verfahren benutzt Lazarus mit seinen plattformabhängigen Verzeichnissen. Eine dritte Plattform braucht damit nur ein weiteres Verzeichnis und keine Zeile im gemeinsamen Code.

<WRAP center round important 90%> Die letzte -I-Angabe gewinnt, nicht die erste. Der Plattformpfad gehört hinter -I .. </WRAP>

Wozu TARGET_OS dann doch gut ist

PlatformBackendMatches() vergleicht es mit der Erwartung der Unit und fängt den häufigsten Baufehler ab: den Plattformpfad stehenlassen und für das andere Ziel bauen. Ohne die Prüfung entstünde eine .exe, die libX11 verlangt — und die Meldung beim Start hat nichts mit der Ursache zu tun.

Was ausdrücklich so bleibt

TApplication nimmt das Backend weiterhin als Argument. Das ist keine Nachlässigkeit, sondern die Voraussetzung dafür, dass die Tests ohne Fenstersystem laufen. NewPlatformBackend() ist das Angebot obendrauf, kein Ersatz.

Auch Vega/Backend.lyx und Vega/Backend/ bleiben frei von Auswahlcode — dort steht nur ein Verweis hierher.

Das Windows-Backend im Besonderen

Die Aufrufschicht

lyxc kann extern fn auf win64 nur mit vier Argumenten (Compilerbefund #1674). CreateWindowExA braucht zwölf, SetDIBitsToDevice zwölf, SetWindowPos sieben.

Vega/Backend/WinCall.lyx löst das mit einem Trampolin: knapp hundert Byte Maschinencode, zur Laufzeit über VirtualAlloc (vier Argumente, geht direkt) in eine ausführbare Seite geschrieben. Es legt die Argumente in die Microsoft-Register und auf den Stapel und springt.

Dazu ein zweites, kleineres Trampolin für die Gegenrichtung: Windows ruft die Fensterprozedur nach seiner Konvention, Lyx erwartet seine eigene.

<WRAP center round alert 90%> Das Trampolin erzwingt die Stapelausrichtung mit and rsp, -16 statt sie vom Aufrufer zu erben. Ohne diese eine Zeile läuft derselbe Aufruf aus main richtig und aus einem Konstruktor heraus in einen page fault. Ein Aufruf über einen Lyx-Funktionszeiger benutzt außerdem weiterhin die SysV-Register — das Ziel steht in rdi, nicht in rcx. </WRAP>

Eigenheiten von Win32

  • Die Zeichenfläche muss umgerechnet werden. Windows nimmt

Außenmaße; ohne AdjustWindowRect ist jedes Fenster um Rahmen und

  Titelleiste zu klein.
* **''SetDIBitsToDevice'' statt ''StretchDIBits''** — letzteres hat
  dreizehn Argumente, eines mehr als der Argumentblock fasst. Gestreckt
  wird ohnehin nicht.
* **''biHeight'' negativ** heißt „von oben nach unten" — genau so liegt
  Vegas Puffer. Ohne das Minus stünde das Bild auf dem Kopf.
* **''WM_SETCURSOR'' muss behandelt werden.** Windows setzt den Zeiger
  bei jeder Bewegung zurück; ohne Behandlung zeigt ein Eingabefeld nie
  seinen Textcursor.
* **Mauskoordinaten in ''lParam'' sind vorzeichenbehaftete 16 Bit.**
  Ohne Umrechnung wird aus x = −3 die Zahl 65533.
* **''WM_MOUSEWHEEL'' liefert Bildschirmkoordinaten** — als einzige
  Mausnachricht. Ohne ''ScreenToClient'' weiß der Dispatcher nicht, über
  welchem Control gedreht wurde.

Stand unter Wine

Von 65 dort gebauten Testprogrammen laufen 54 byteweise identisch zur Linux-Fassung. Die übrigen hängen an zwei Compilerbefunden und nicht an Vega:

Befund Wirkung
lyxc #1678 Objekte anlegen und wieder freigeben bricht nach wenigen Durchläufen ab
lyxc #1677 std.env stürzt auf win64 ab (EnvBase unterstellt das Linux-Speicherbild)

Ein neues Backend schreiben

  1. Vega/Backend/<Name>.lyx anlegen, IVegaBackend erfüllen.
  2. Ereignisse in TEvent übersetzen — die Ereignisarten stehen in

Vega/Events.lyx.

  1. platform/<ziel>/Vega/BackendAuto.lyx anlegen.
  2. build.sh und run-tests-*.sh um das Ziel erweitern.

Der plattformunabhängige Teil von Vega läuft dann unverändert — das ist gemessen, nicht behauptet: Windows brauchte keine einzige Änderung an Controls, Zeichnen, Schrift oder Layout.