Zum Inhalt

JDL — FFI

Status: Konsolidierungsentwurf, Arbeitsstand Version: 0.2.0 Datum: 2026-09-23 Ersetzt: 05-modulsystem-und-ffi, unversionierte Fassung Geltungsbereich: Anbindung nativer Bibliotheken: Beschreibung, Binding, Service-Oberfläche, nativer Ausführungsvertrag, Callbacks, Autorität und die Grenze zu Ring 0 Nicht hier: Modulsystem und Imports (spec-namespaces-und-module), Scheduler-Mechanik (jdl-vm-scheduler-spec v0.3.0), Handle-Zustand (06-jme-handles-tags §12), Komposition von Blueprints (jdl-blueprint-spec v0.4.0 §4), Resource- und Autoritätsmodell (spec-resource-model), Plugin-Artefakte (spec-plugin-system) Quellen: Vorfassung dieses Dokuments, 00-refinements, 08-intrinsics-vm-api Gruppe 5, jade-fehlermodell, spec-plugin-system §1.6, Entscheidungen aus den Arbeitsgesprächen (Stand 2026-09-23)


0. Über dieses Dokument

0.1 Kennzeichnung

Marke Bedeutung
[N] bestehend normativ belegt, Fundstelle genannt
[E] entschieden, aber noch in keinem anderen Quelldokument ausgeschrieben
[A] aus bestehendem Material ableitbar
[V] Vorschlag, ausgearbeitet, aber nicht entschieden
[O] offen

0.2 Das Modulsystem ist ausgezogen

Die Vorfassung behandelte Modulsystem und FFI gemeinsam. Das Modulsystem steht inzwischen vollständig in spec-namespaces-und-module. Der alte Modulteil widersprach ihr in mehreren Punkten (pub/package, mod.jdl als API-Schicht, Effect[R, E, D]) und entfällt hier ersatzlos.

Der Dateiname ist geblieben, damit Verweise nicht brechen. Ein Umbenennen in spec-ffi wäre naheliegend (§11).

0.3 Syntax

Alle Syntaxbeispiele sind spekulativ und mit // spekulativ markiert. Auf spekulative Formen wird keine Folgerung gestützt.


1. Grundsatz

JDL kennt keine rohen Zeiger [N] 06 JME-1. Native Bibliotheken werden trotzdem angebunden, und zwar über ausdrücklich beschriebene Grenztypen, nie über Zeigerarithmetik im JDL-Code.

Die Anbindung ist in drei Ebenen getrennt [N] Vorfassung §4. Das ist keine Ästhetik, sondern Schutz gegen C-Bibliotheken, die Besitzregeln gern in Kommentaren verstecken:

NativeLayer    was die Bibliothek anbietet: Symbole, Signaturen, Aufrufkonvention
BindingLayer   was das in Jade bedeutet: Besitz, Umwandlung, Fehler, Ausführung, Callbacks
ServiceLayer   was normaler Code sieht: ein Service in der Row

Normaler JDL-Code sieht nur die oberste Ebene. Die unteren beiden sind Beschreibungen, aus denen die FfiEngine Descriptoren macht, und Ring 0 führt diese Descriptoren mechanisch aus (§7).


2. NativeLayer — was die Bibliothek anbietet

2.1 extern [N] Vorfassung §4.1

Ein extern-Block beschreibt Symbole einer Bibliothek. Er enthält nur def-Deklarationen ohne Rumpf.

// spekulativ
extern LibSQLite {
    def sqlite3_open(path: CString, db: Out[SqliteDb]) -> CInt :> CSymbol("sqlite3_open")
    def sqlite3_exec(db: SqliteDb, sql: CString, cb: Callback, userdata: OpaquePtr, err: Out[CString]) -> CInt :> CSymbol("sqlite3_exec")
    def sqlite3_close(db: SqliteDb) -> CInt :> CSymbol("sqlite3_close")
} :> FFILink("sqlite3")
  • Jede Zeile ist ein Symbol der Bibliothek mit seiner C-Signatur in Grenztypen.
  • :> CSymbol(...) bindet den Namen im Binärcode, :> FFILink(...) die Bibliothek.
  • SqliteDb ist ein Handle-Typ: Jade sieht einen undurchsichtigen Griff, nie den Zeiger dahinter.

CSymbol und FFILink sind gewöhnliche Refinements [N] 00-refinements §6 und §8. Seit typefn entfallen ist, sind sie def-Funktionen mit Meta in der Row [E].

2.2 Was ein extern-Block nicht ist [N] Vorfassung §3

  • Er ist keine Service-Abstraktion und wird nie direkt als Vertragsoberfläche benutzt.
  • Er enthält keine Domänenlogik.
  • extern X : implements Y gibt es nicht; implements ist kein Sprachbestandteil.

2.3 Was daraus entsteht [N] Vorfassung §4.1

Ein extern-Block ist Zucker für Blueprints der NativeLayer, eine Bibliothek mit ihren Funktionen:

// spekulativ
type NativeLibrary : blueprint {
    var soname: str
    var abi:    AbiKind = .C
}

type NativeFunction : blueprint {
    var symbol: str
    var input:  [NativeType]
    var output: NativeType
}

Wie alle Blueprints haben sie nur in der Meta-Phase Bedeutung und sind danach konsumiert [E]. Zur Laufzeit gibt es nur die Descriptoren (§7).


3. BindingLayer — was es in Jade bedeutet

3.1 Der Binding-Blueprint

Der BindingLayer sagt, wie rohe ABI-Wirklichkeit in Jade-Semantik übersetzt wird. Die Vorfassung führte dafür einen Blueprint mit sechs festen Listen und darunter rund zehn weitere Themen. Das ist genau der Blueprint, der mit seinem Ökosystem mitwächst.

Diese Fassung trennt Kern und Erweiterung [V]:

// spekulativ
type NativeBinding : blueprint {
    var library:   NativeLibrary
    var functions: [NativeFunction]
    var handles:   [NativeHandleSpec]
    var ownership: [OwnershipRule]
} :> ExtensibleVia(FfiContribution)
  • library und functions kommen aus der NativeLayer.
  • handles beschreibt den Ausführungsvertrag jedes Handle-Typs (§4.1).
  • ownership sagt, wem ein Handle oder Speicherblock gehört und wie er freigegeben wird.
  • ExtensibleVia(FfiContribution) öffnet die Andockstelle für wiederkehrende Konventionen (§8).

3.2 Was Kern bleibt und warum [V]

Besitzregeln und der Ausführungsvertrag der Handles stehen immer ausdrücklich im Binding und kommen nie aus einer Component. An ihnen hängt, ob Speicher doppelt freigegeben wird und ob Ring 0 einen Aufruf auf dem richtigen Thread ausführt. Solche Zusagen sollen an genau einer Stelle stehen, die der Autor des Bindings selbst geschrieben hat.

[O] Ob Besitzkonventionen ganzer Ökosysteme (etwa GLib) später als Component angeboten werden dürfen, etwa mit ausdrücklicher Bestätigung im Binding, ist offen.

3.3 Was der BindingLayer modelliert [N] Vorfassung §4.2

out- und inout-Parameter, Zeiger, die leer sein dürfen, geliehene und besessene Handles, undurchsichtige Handles, Fehler über Rückgabecodes oder errno, Callbacks mit Benutzerdaten, Thread-Bindung, blockierende Aufrufe, optionale oder versionierte Symbole, eigene Allokatorregeln.

Thread-Bindung und blockierende Aufrufe hatten in der Vorfassung keinen Platz in den Descriptoren. Sie haben ihn jetzt (§4, §7).


4. Der native Ausführungsvertrag [E]

Dieser Abschnitt legt fest, was im Descriptor steht. Wie Ring 0 es umsetzt, steht in jdl-vm-scheduler-spec §1.3 und §3.7 bis §3.14; welcher Zustand dafür am Handle geführt wird, in 06-jme-handles-tags §12.

4.1 Pro Handle-Typ

access:    shared | exclusive
affinity:  any | stable

access = exclusive heißt: nie zwei native Aufrufe gleichzeitig auf demselben Handle. SQLite verlangt das für eine Verbindung.

affinity = stable heißt: alle Aufrufe auf dem Handle laufen auf demselben Betriebssystem-Thread. OpenGL-Kontexte und viele GUI-Toolkits verlangen das.

Die beiden Achsen sind unabhängig. Ein Handle kann beides sein. Bei stable ist exclusive mechanisch bereits erfüllt, weil eine Lane Aufrufe nacheinander ausführt (Scheduler §3.9).

4.2 Pro Aufruf

execution:        direct | blocking
affinityBinding:  none | create(result) | inherit(result, from)

execution = direct ist die Zusage des Bindings, dass der Aufruf kurz ist. Er läuft dann direkt auf dem Worker, ohne dass das Reduction-Budget zählt. Stimmt die Zusage nicht, leidet die Fairness, nicht die Korrektheit.

execution = blocking heißt: Der Aufruf darf seinen Thread lange blockieren und darf deshalb keinen Worker blockieren. Bei affinity = any läuft er im NativeBlockingPool, bei stable auf der Lane des Handles. blocking überstimmt stable nie.

affinityBinding betrifft Aufrufe, die ein Handle mit affinity = stable erzeugen:

create(result)          das Ergebnis bekommt eine neue AffinityDomain
inherit(result, from)   das Ergebnis erbt die Domain des Handles im Argument from

create löst das Henne-Ei-Problem: Die Domain und ihre Lane entstehen vor dem Aufruf, der erzeugende Aufruf läuft schon dort, und das Handle wird danach an die Domain gebunden.

OpenGLContext     create   → neue Domain A
Texture(context)  inherit  → Domain A
Shader(context)   inherit  → Domain A

Ein Aufruf, der ein stable-Handle erzeugt, ohne create oder inherit anzugeben, ist ein Fehler im Binding und wird in der Meta-Phase abgewiesen [A].

4.3 Beispiel SQLite

SqliteDb         access = exclusive, affinity = any
sqlite3_open     execution = blocking
sqlite3_exec     execution = blocking
sqlite3_close    execution = blocking

SQLite verlangt keinen festen Thread, nur dass eine Verbindung nicht gleichzeitig benutzt wird. Die Aufrufe greifen auf Dateien zu und können warten; deshalb blocking, und sie laufen im Pool statt auf einem Worker.

4.4 Freigabe ist ein nativer Aufruf [E]

Die Freigabefunktion aus einer Besitzregel, etwa sqlite3_close, ist selbst ein nativer Aufruf und unterliegt demselben Vertrag wie jeder andere Aufruf auf dem Handle. Bei stable läuft sie auf der Lane der Domain.

Das gilt auch auf dem Trap-Pfad. Dort läuft nach dem Fehlermodell kein JDL-Code (F-8). Die Freigabefunktion ist kein JDL-Code, sondern ein im Descriptor festgehaltener nativer Aufruf, den die Runtime mechanisch ausführen darf (06 §12.6).

4.5 Abbruch [E]

Wird eine Task abgebrochen, während ihr nativer Aufruf läuft, läuft der Aufruf weiter und behält, was er braucht, bis er zurückkehrt (Scheduler §3.12). Jade kann einen laufenden C-Aufruf nicht zurückholen.

[O] Bietet eine Bibliothek selbst eine Abbruchfunktion, kann das Binding einen Abbruchpfad beschreiben. Die Form dieser Beschreibung ist offen.


5. Callbacks [E]

Die Vorfassung führte Callback-Brücken als offen. Mit dem Ausführungsvertrag haben sie eine feste Form.

5.1 Grundregel

Ein nativer Callback führt nie Jade-Code direkt auf dem Thread aus, der ihn aufruft. Er wird zu einer Jade-Task, die der Scheduler auf einem gewöhnlichen Worker ausführt (Scheduler §3.13). Das gilt für Lanes, für den Pool und für fremde Threads, die eine Bibliothek selbst startet.

5.2 Der Callback-Vertrag

delivery:   async | sync

async: Die Callback-Task wird eingereiht, und der Callback kehrt sofort zu C zurück.

sync: Die Callback-Task wird eingereiht, und der native Thread wartet auf ihr Ergebnis, bevor er weitermacht. Der NativeEntryToken des ursprünglichen Aufrufs wird weitergegeben.

5.3 Asynchrone Callbacks kopieren ihre Argumente

Bei async läuft die Jade-Task erst, nachdem C den Callback schon verlassen hat. Zeigt ein Argument auf Daten der Bibliothek, sind sie dann womöglich nicht mehr gültig.

Deshalb gilt für asynchrone Callbacks:

  • Alle Argumente werden im Moment des Aufrufs vollständig kopiert.
  • Geliehene native Zeiger sind als Argument unzulässig.
  • Ein Argument, das auf einen Speicherblock zeigt, braucht eine Kopierregel im Descriptor.
  • Ein Jade-Handle als Argument ist nur zulässig, wenn seine Lebensdauer über den Callback hinaus ausdrücklich gehalten wird.

Ein asynchroner Callback, der gegen diese Regeln verstößt, wird in der Meta-Phase abgewiesen.

5.4 Synchrone Callbacks brauchen einen Ersatzwert [V]

Endet eine synchrone Callback-Task nicht mit Success, sondern mit Failure, Cancelled oder Trap, wartet der native Thread trotzdem auf einen Rückgabewert für C.

Vorschlag: Der Callback-Vertrag jedes synchronen Callbacks mit Rückgabewert nennt einen Ersatzwert, den C in diesem Fall bekommt. Fehlt er, ist das Binding ungültig. Viele C-APIs haben dafür ohnehin einen Wert, etwa „abbrechen“ oder einen Fehlercode.

Ein Trap in der Callback-Task folgt davon unabhängig dem Fehlermodell. Der Ersatzwert sorgt nur dafür, dass die native Seite nicht hängen bleibt.

5.5 Wiedereintritt [E]

Ein synchroner Callback darf keine native Ausführung in einer AffinityDomain anfordern, die derselbe Eintrittspfad gerade belegt. Ebenso trappt der Wiedereintritt in ein exklusives Handle mit demselben Token (Scheduler §3.10, §3.11).

[O] Ein ausdrücklicher Modus, der Wiedereintritt erlaubt, kann später folgen, wenn ein realer Anwendungsfall ihn verlangt.

5.6 Unter welcher Engine ein Callback läuft

Synchrone Callbacks laufen unter der Engine-Konfiguration der Task, die den nativen Aufruf gemacht hat. Asynchrone unter der, die bei der Registrierung des Callbacks festgehalten wurde (Scheduler §3.13).

5.7 Lebensdauer

Ein erreichbarer Callback hält die Ressourcen, die er braucht, am Leben. Solange eine Bibliothek einen Callback aufrufen kann, darf sein Code nicht freigegeben werden [N] spec-plugin-system §1.6 und Hold-Regeln.


6. Autorität [A]

Eine native Bibliothek zu laden und aufzurufen, ist Autorität. Sie wird wie jede andere Autorität über einen Resource-Träger ausgeübt, den der Context bindet, nicht über ein Flag.

Ein Plugin darf keine undeklarierte native Bibliothek zur Laufzeit über Suchpfade entdecken [N] spec-plugin-system P-8. Native Abhängigkeiten gehören zur Artefakt- oder Abhängigkeitsbeschreibung.

[O] Die konkrete Form der Resource (NativeHost, NativeLibrary oder beides) ist offen. spec-plugin-system §1.6 verweist dafür ausdrücklich auf dieses Dokument.


7. Die Grenze zu Ring 0

7.1 Die JadeValue-Bridge führt aus, sie entscheidet nicht [N] Vorfassung §4.4

Die Bridge in Ring 0 (Zig) führt die FFI-Descriptoren mechanisch aus: Bibliothek laden, Symbol suchen, aufrufen, Werte umwandeln, Handles festhalten, Traps an der Grenze. Sie entscheidet nicht, was ein SQLite-Fehler bedeutet oder wem ein Zeiger gehört.

Neu ist, dass sie nach dem Ausführungsvertrag verteilt: direkt auf dem Worker, im Pool oder auf einer Lane.

7.2 Der Grundwortschatz

Ring 0 versteht genau diese Descriptor-Arten:

NativeLibraryDescriptor    Bibliothek, Aufrufkonvention
NativeHandleDescriptor     access, affinity                          neu
FfiCallDescriptor          Symbol, Signatur, execution, affinityBinding   erweitert
MarshalDescriptor          Umwandlung von Werten
OwnershipDescriptor        Besitz und Freigabefunktion
ErrorMappingDescriptor     Rückgabecode oder errno zu Fehler
CallbackDescriptor         delivery, Kopierregeln, Ersatzwert             erweitert

Alles, was ein Binding ausdrückt, muss sich in diese Arten übersetzen lassen. Components (§8) können den Wortschatz nicht erweitern. Egal wie viele Konventionen dazukommen, Ring 0 sieht immer dieselben Arten.

7.3 Was erst beim Laden geprüft wird

Die Meta-Phase darf keine Dateien lesen. Ob ein Symbol in der Bibliothek wirklich existiert oder die Version passt, prüft deshalb erst die Bridge beim Laden. Ein fehlendes Symbol ist ein Ladefehler.


8. Konventionen als Components [V]

Viele C-Bibliotheken folgen denselben Mustern: Fehler über Rückgabecodes, Fehler über errno, Zeichenketten mit Nullbyte am Ende. Diese Muster lassen sich einmal als Component schreiben und an jedes Binding andocken (jdl-blueprint-spec §4):

// spekulativ
type SqliteBinding : NativeBinding & ReturnCodes & {
    library   = LibSQLite
    functions = [LibSQLite.sqlite3_open, LibSQLite.sqlite3_exec, LibSQLite.sqlite3_close]
    handles   = [NativeHandleSpec { type = SqliteDb, access = .Exclusive, affinity = .Any }]
    ownership = [OwnershipRule { handle = SqliteDb, release = LibSQLite.sqlite3_close }]
    returnCodes.success = 0
}
  • Kern: Bibliothek, Funktionen, Handle-Vertrag, Besitz. Alles ausdrücklich.
  • ReturnCodes dockt als Konvention an: „0 ist Erfolg, alles andere ein Fehler“.

Der Provider von ReturnCodes sieht nur seine eigenen Werte, nicht die Funktionsliste. Er beantragt deshalb eine allgemeine Regel („für jede Funktion mit ganzzahligem Rückgabewert gilt …“), und die FfiEngine wendet sie auf die Funktionen an. Fehlerabbildungen sind pro Funktion von der Art „Einer“: Zwei widersprüchliche Konventionen für dieselbe Funktion sind ein Konflikt beim Übersetzen.

Nur Werte, keine Haken. Die FfiEngine nimmt von Components nur Regeln an, keine Funktionen. Eine eigene Umwandlungsfunktion an der ABI-Grenze wäre fremder Code genau dort, wo Ring 0 mechanisch und vorhersagbar bleiben soll. Was sich nicht als Regel beschreiben lässt, gehört in den ServiceLayer.

Anträge von Components, die den Scheduler oder Speicher betreffen, etwa blocking oder eigene Allokatorregeln, werden dem Benutzer angezeigt, wie bei Context-Components.


9. ServiceLayer — was normaler Code sieht

Normaler Code sieht einen Service, nie ein natives Symbol [N] Vorfassung §4.3.

// spekulativ
type Db : service {
    def query(sql: str) ->{Throw[DbError]} RowSet
}

type SqliteHandler : struct { db: SqliteDb }

provide Db for SqliteHandler {
    def query(self, sql: str) ->{Throw[DbError]} RowSet = ...
}
  • type Db : service ist der Vertrag. service ist ein eigener TypeKind [E].
  • ->{Throw[DbError]} ersetzt den früheren Result-Rückgabetyp; Fehler laufen über die Row.
  • provide Db for SqliteHandler bindet den Vertrag an die Implementierung, die das Binding benutzt und SQLite-Fehler in DbError übersetzt.
  • = ... ist der Platzhalter für einen noch nicht geschriebenen Rumpf.

Code, der die Datenbank braucht, schreibt {Db} in seine Row. Der Context bindet Db [N] spec-namespaces-und-module §8.4: Services werden nicht importiert.

Die Row führt nie ein natives Symbol als Service. Eine Row wie ->{LibSQLite} ist unzulässig.

[O] Ob provide ein Schlüsselwort bleibt, ist offen. Die Form der Service-Zuordnung hängt daran.


10. Grenztypen [N] Vorfassung §4.6

Erlaubt an der Grenze:

  • primitive Skalare,
  • Zeigertypen nur in extern-Signaturen, nie im normalen Code,
  • undurchsichtige Handles,
  • ABI-kompatible Darstellungen von Structs und Enums,
  • ausdrücklich beschriebene Blueprints der Native- und Binding-Ebene.

Nicht erlaubt:

  • Closures mit Mitnahmen,
  • Werte, deren Speicherregion die Grenze nicht überleben darf,
  • lokal geteilte Typen über unzulässige Übergänge,
  • generische Signaturen ohne eindeutige ABI,
  • Rows, die native Symbole als Service führen.

11. Offene Punkte

# Punkt Abschnitt
1 Besitzkonventionen als Component §3.2
2 Form eines Abbruchpfads für Bibliotheken mit eigener Abbruchfunktion §4.5
3 Ersatzwert synchroner Callbacks (Vorschlag) §5.4
4 Modus für erlaubten Wiedereintritt §5.5
5 Resource-Form für native Autorität §6
6 provide als Schlüsselwort §9
7 Aufrufkonventionen und repr: C im Detail Vorfassung §6
8 Umbenennung der Datei in spec-ffi §0.2
9 Komposition insgesamt (Kapitel 8 hängt an jdl-blueprint-spec §4) §8
10 Name der Engine: dieses Dokument sagt FfiEngine, 12-design-constitution §8.2 FfiBindingEngine §1

12. Normative Invarianten

FFI-1   Service-Zuordnung nur über provide Service for Handler.
FFI-2   implements ist kein Sprachbestandteil.
FFI-3   extern-Blöcke sind keine Service-Abstraktion und enthalten keine Domänenlogik.
FFI-4   FFI-Einstieg benutzt def in extern-Blöcken.
FFI-5   Symbolbindung über :> CSymbol(...), Bibliotheksbindung über :> FFILink(...).
FFI-6   extern wird zu Blueprints der NativeLayer, aus denen die FfiEngine Descriptoren erzeugt.
FFI-7   Rows führen Services, nie native Symbole.
FFI-8   Die JadeValue-Bridge in Ring 0 führt Descriptoren mechanisch aus und entscheidet keine Semantik.
FFI-9   Jeder native Handle-Typ trägt access und affinity; jeder Aufruf execution und,
        falls er ein stable-Handle erzeugt, affinityBinding.
FFI-10  blocking überstimmt stable nie.
FFI-11  Besitzregeln und Handle-Vertrag stehen ausdrücklich im Binding, nicht in einer Component.
FFI-12  Die Freigabefunktion eines Handles unterliegt dessen Ausführungsvertrag, auch auf dem Trap-Pfad.
FFI-13  Ein nativer Callback führt Jade-Code nur als Scheduler-Task aus.
FFI-14  Asynchrone Callbacks kopieren ihre Argumente vollständig; geliehene Zeiger sind unzulässig.
FFI-15  Components im BindingLayer liefern nur Regeln, keine Funktionen.
FFI-16  Alles, was ein Binding ausdrückt, lässt sich in die festen Descriptor-Arten aus §7.2 übersetzen.

13. Was gegenüber der Vorfassung wegfällt oder sich ändert

Vorfassung Jetzt
Modulsystem §2 entfällt, steht in spec-namespaces-und-module
pub, package, mod.jdl als API-Schicht entfällt, widersprach der Namespace-Spec
Effect[R, E, D] Effect Row mit Services
Result[RowSet, DbError] im Service ->{Throw[DbError]}
service Db { ... } type Db : service { ... }
Bridge „in D“ Ring 0 in Zig
Thread-Bindung und blockierende Aufrufe nur als Liste nativer Ausführungsvertrag (§4)
Callback-Brücken offen Callback-Vertrag (§5)
sechs Descriptor-Arten sieben, zwei erweitert (§7.2)
Invariante „Effects referenzieren Services“ FFI-7

Änderungsprotokoll

0.2.0 (2026-09-23) — Neuaufbau als reine FFI-Spec. Modulteil entfernt. Syntax und Begriffe auf den aktuellen Stand gebracht (type ... : service, Effect Rows, Throw, Zig). Neu: nativer Ausführungsvertrag mit access, affinity, execution und affinityBinding; Freigabe als nativer Aufruf auch auf dem Trap-Pfad; Callback-Vertrag mit async/sync, Kopierregel und Ersatzwert; Autorität über Resources; Konventionen als Components ohne Haken; Grundwortschatz von Ring 0 um NativeHandleDescriptor ergänzt. Invarianten FFI-1 bis FFI-16.