Runtime
clause_runtime ist eine LLVM-freie C++23-Bibliothek. Sie besitzt Kontexte,
Prozess-Heaps, Atome, geladene Module und die Verwaltung des Lebenszyklus und
führt Erlang-Prozesse auf Scheduler-Workern aus (Prozesse).
Alle APIs sind projektintern; Host-Aufrufe müssen pro Runtime serialisiert
werden und dürfen sich nicht mit einem Programmlauf überschneiden, dessen
Worker sich untereinander synchronisieren (Threads).
Linken
Genau eine für das Ziel gebaute Runtime über Clause::generated_program
linken:
add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)
Das bringt das Archiv, die ABI-/Runtime-Header und C++23 mit, aber nicht LLVM. link_consumer.cpp zeigt einen vollständigen Lebenszyklus.
Lebenszyklus
Runtime::start(options)→std::expected<std::unique_ptr<Runtime>, Status>. Standardwerte: aktuelle ABI-Version und native Term-Breite,max_atoms2^20 (höchstens 2^26; Programme setzen es mit--max-atoms, Runtime-Optionen). Die Anzahl der Kontexte ist nicht begrenzt.process_heapundprocess_stacksind die Optionen der durchcreate_context()erzeugten Kontexte;memory_limit_bytesist die optionale runtimeweite Grenze, gemeldet durchmemory_bytes(). Alle sind standardmäßig unbegrenzt.create_context()odercreate_context(heap_options, stack_options)→ geliehenerProcessContext*, stabil bis zur Zerstörung. Heap-Standardwerte: minimaler Heap von 233 Wörtern (min_heap_words) und keine Speichergrenze (limit_bytes=UNLIMITED_HEAP_BYTES; ein gesetztes Budget ist ein Vielfaches eines Worts und mindestens der minimale Heap). Auch der Stack ist unbegrenzt, sofernStackOptions::limit_wordsnicht gesetzt ist.destroy_context(ctx),shutdown(): Shutdown gibtbusyzurück, solange Kontexte übrig sind; danach gelingt es idempotent, und spätere Aufrufe gebenstoppedzurück. Der Destruktor räumt verbleibende Kontexte auf.- Kontext- und Runtime-Identitäten werden nicht wiederverwendet; Erschöpfung
führt zu einem Fehler. Die Identität eines Kontexts trägt seine Pid-Nummer
(Pids). Ein Kontext-Zeiger ist eine Leihe,
keine Identität.
lifetime()liefert ein schwaches Token, das vor dem Abbau des Heapsalive() == falsemeldet. - Lebenszyklus-Aufrufe werfen nie und sind stumm. Statuswerte: abi.md.
Abbaureihenfolge: Scheduler-Einträge schließen → Kontexte zerstören → Code-Registrierungen freigeben → Scheduler-Dienst zerstören → Atomtabelle zuletzt. Aufgelöste Funktions-Handles halten Code und Atom-Schreibweisen über den Abbau der Runtime hinaus am Leben, aber nie einen Prozess.
Prozessspeicher
Jeder Kontext besitzt einen ProcessHeap: einen einzigen Heap-Block, der durch
seine erste Allokation erzeugt wird und die Größe
max(min_heap_words, request) hat, plus eine Kette von Heap-Fragmenten im
Besitz desselben Prozesses. Wörter bewegen sich nur, wenn der Host an einem
sicheren Punkt collect() aufruft.
Der Heap folgt dem klassischen ERTS-Design; runtime-heap.md ist sein Vertrag (Layout, Bereiche, Größenbemessung, Zulassung, Wurzeln, Speicherbereinigung).
allocate(words)gibt genullten Wortspeicher zurück.reserveliefert eine nur verschiebbare Reservierung mit explizitem Commit und automatischem Rollback. Eine Reservierung pro Heap zur gleichen Zeit: zuerst die Kinder aufbauen, das Elternobjekt zuletzt reservieren.- Bump-Allokation füllt den Heap-Block; eine Anfrage, die nicht passt, geht an
das neueste Fragment, sonst an ein neues Fragment der Größe
max(min_heap_words, request)(begrenzt durch das verbleibende Budget). Wörter sind nur auf Wortgrenzen ausgerichtet. Ein Rollback setzt das obere Ende des Bereichs zurück, verwirft ein durch die Reservierung erzeugtes Fragment (oder den Heap-Block) und stellt die Buchführung exakt wieder her. - Lehnt null, Überlauf und erschöpftes Budget vor dem
Veröffentlichen ab. Fehler:
out_of_memory(Allokation) oderlimit_exceeded(Budget); erzeugter Code erhält den genauen Status. - Binaries über 64 Bytes liegen in gemeinsamen Puffern außerhalb des Heaps.
Jede Heap-Zelle, die auf einen verweist, hält einen
std::shared_ptrund tritt bei der Veröffentlichung der Off-Heap-Liste des Prozesses bei; der Abbau durchläuft die Liste und gibt diese Referenzen frei (Off-Heap-binaries). add(value)/copy_tokopieren einen Graphen eines anderen Prozesses derselben Runtime, unter Erhalt seiner gemeinsamen Teile und mit gemeinsamer Nutzung von Off-Heap-Puffern; eine gescheiterte Kopie ändert nichts (Kopieren zwischen Heaps).used_wordszählt allokierte Wörter;capacity_wordszählt Heap-Block und Fragmente;off_heap_wordszählt die Puffer, auf die dieser Prozess verweist, jeden einmal. Hintergrund- plus Off-Heap-Wörter teilen sich das optionale Budgetlimit_bytes; eine Speicherbereinigung lässt die Hälfte des Budgets übrig, nachdem die Überlebenden freigeben, sodass seine Erschöpfung bedeutet, dass die lebenden Daten nicht mehr hineinpassen (Fehlerverhalten).- Jedes benutzte Wort lässt sich als Objekt mit führendem Header, als
Cons-Zelle oder als Füllwort lesen
(Wort-Layout); reservierte Wörter beginnen
genullt. Rohe
allocate()-Wörter müssen null bleiben oder vollständige Objekte enthalten.verify()durchläuft den Heap-Block und jedes Fragment und prüft, ob jeder Term-Slot auf einen Objektanfang desselben Prozesses zeigt (Tests und Debugging; sonstcorrupt_heap). collect(roots)kopiert alles, was von den Prozesswurzeln und den Wurzelwörtern des Hosts erreichbar ist, in einen neuen Heap-Block, gibt den alten Block und die Fragmente frei, gibt tote Off-Heap-binaries frei und schreibt die Wurzeln um (Speicherbereinigung). Erzeugter Code bereinigt an Funktionseintritten und Schleifenköpfen von comprehensions, wennwants_collection()(Speicherbereinigung in erzeugtem Code). Sie läuft nur an einem sicheren Punkt (kein erzeugter Code läuft außerhalb einesSafePoint-Bereichs, keine offene Reservierung), sonstunsafe_point; das Wurzelinventar listet, was sie umschreibt; scheitert die Allokation des neuen Blocks, ergibt dasout_of_memory, ohne dass sich etwas ändert.
Code-Server und Builtins
Jede Runtime besitzt einen CodeServer (code_server.hpp,
callable.hpp):
auto functions = std::make_unique<ModuleRegistry>();
auto added = functions->add("identity", 1,
[](ProcessContext &, std::span<const Term> args) -> CallResult<Term> { return args.front(); });
auto loaded = context.code_server().load({"native_demo", CodeImage::linked(), std::move(functions)});
auto fn = context.code_server().resolve({.module = "native_demo", .function = "identity", .arity = 1});
- Eine
ModuleRegistrybildet exakten Namen/Stelligkeit (≤ 255) auf ein aufrufbares Objekt ab, das nur mitTermarbeitet.loadfriert sie ein und veröffentlicht sie; Duplikate oder Fehler veröffentlichen nichts. resolveunterscheidet fehlendes Modul und fehlenden Export.ResolvedFunctionhält das Modul-Image fest;callprüft Stelligkeit, Argumente und Ergebnisse und verwandelt Host-Ausnahmen in Fehler.- Native Rümpfe müssen synchron und nicht blockierend sein und dürfen den Kontext oder den Argument-Span nicht festhalten.
- Ein begrenzter Katalog bekannter zurückgestellter BIFs (
self/0,length/1,spawn/3,spawn_link/3,send/2,make_ref/0,garbage_collect/0,apply/3(erzeugter Code ruftapply/2,3stattdessen über die Dienste für dynamische Aufrufe auf, funs),tuple_size/1,+/2) meldetnot_implemented; andere nicht registrierte Namen geben stummunknown_builtinzurück. Dieser Host-Pfad erreicht die Produktions-Builtins nicht. - Produktions-Builtins liegen in der
BuiltinRegistrydes Servers, beim Start der Runtime registriert; erzeugter Code, dynamische Aufrufe und funs erreichen sie über die Builtin-Brücke. CodeServer::export_framefindet denFrameDescriptoreines Exports nach Modul, Funktionsatom und Stelligkeit;function_frameergänzt die Builtins für dynamische Aufrufe;external_funinterniert die Definitionen externer funs, die zur Laufzeit gebaut werden.CodeServer::unloadist zurückgestellt; dynamisches Laden wird nicht unterstützt.
Scheduler-Buchführung
SchedulerService (scheduler.hpp)
verzeichnet nur den Lebenszyklus von Prozessen; er führt keinen Code aus.
register_process(context)einmal pro Kontext (dieselbe Runtime); Duplikate gebenalready_registeredzurück.remove_process(id)nimmt einen nicht laufenden Eintrag außer Dienst.begin_dispatch→ laufend;finish_dispatch→ lauffähig, wartend oder beendet (mit Grund).set_suspendedschaltet ein Flag an lauffähigen oder wartenden Einträgen um. Andere Übergänge gebeninvalid_transitionzurück.request_shutdown()schließt Registrierung, Dispatch und Suspendierungssteuerung; Inspektion und Rückgaben bleiben zum Leeren verfügbar.runundexecutemeldennot_implemented.
Entwurfsskizzen für Worker und Prozesse liegen in
runtime/design/ und runtime/include/{scheduler,process}.hpp.
Nachrichten sind umgesetzt (Prozesse): Eine Sendung
kopiert die Nachricht in den Heap des Empfängers und hängt sie an seinen
Signal-Eingang an (runtime/include/mailbox.hpp).
Threads
Scheduler-Worker (Plan-Step 56, Worker) führen Prozesse auf mehreren Threads einer Runtime aus. Dienste, die sie gemeinsam nutzen, sind synchronisiert; alles andere bleibt auf den Thread beschränkt, der seinen Prozess ausführt, oder wird durch den Mutex des Executors geschützt.
- Atome (step 54):
AtomStorageschützt beide Indizes mit einem Shared Mutex. Abfragen (lookup,boolean,size) teilen ihn;internsucht unter der geteilten Sperre, und nur eine neue Schreibweise nimmt die exklusive Sperre, prüft erneut und veröffentlicht den Eintrag, sodass konkurrierende Internierungen einer Schreibweise ein einziges Wort erhalten. Wörter bleiben stabil: Ein Eintrag wird vor dem Abbau nie geändert oder entfernt. - Code (step 55):
CodeServerschützt seine Module und die Definitionen externer funs mit einem Shared Mutex. Abfragen (find_module,resolve,atom_word,record_definition,fun_definition,export_frame,function_frame,owns) teilen ihn;loadund der Aufbau einer neuen Definition eines externen fun nehmen ihn exklusiv, sodass nebenläufige Registrierungen eines Namens ein einziges Modul veröffentlichen (die anderen erhaltenduplicate_module) und konkurrierendeexternal_fun-Aufrufe eine einzige Definition erhalten. Die Builtin-Registry wird beim Start der Runtime gefüllt, bevor irgendein Worker läuft, und danach nur gelesen. - Festhalten: Module werden nie entfernt, solange die Runtime lebt (Entladen
ist zurückgestellt), und der Server wird nach jedem Kontext zerstört, sodass
Definitionen, Frames und Atom-Slots, die er zurückgegeben hat, für jeden
Aufruf und jede fun-Zelle gültig bleiben. Handles von
ResolvedFunctionundfind_modulebehalten ihr Modul auch, nachdem die Runtime weg ist. - Pid-Nummern (step 56):
ProcessNumbersvergibt Nummern unter einer exklusiven Sperre und akzeptiert Pid-Wörter unter einer geteilten. - Speicher (step 56): Das runtimeweite Konto (
RuntimeMemory) belastet und entlastet mit atomaren Operationen; eine Belastung bringt das Konto nie über eine optionale Grenze. - Port-I/O (Steps 57C–57F): Die Lese-, Schreib- und Überwachungs-Threads des I/O-Dienstes und der Socket-Thread (Ports) berühren Prozesszustand nur über den Mutex des Executors.
- Linken: Unter Linux fügt die Runtime für ihre Threads
-pthreadhinzu; unter Windows linkt sie Winsock (ws2_32,mswsock) für Sockets.
Standardausgabe
RuntimeOptions::standard_output (output.hpp)
empfängt erlang:display/1 und später Bytes von standard_io. Der Standard
schreibt über C-stdio (gepuffert) auf stdout des Prozesses; eine Host-Senke
gibt false zurück, um einen gescheiterten Schreibvorgang zu melden.
erlang:display/1 stellt sein Argument im Anzeigestil
dar, schreibt den Text und einen Zeilenumbruch in einem Schreibvorgang und gibt
true zurück. Darstellungsgrenzen und abgewiesene Schreibvorgänge werden zu
Infrastruktur-Status (resource_limit, output_failure, ...) im geprüften
Kanal, nie zu Erlang-Ausnahmen.
Programmstart
CLAUSE_main_v1 (startup.hpp,
runtime/src/startup/) führt ein ganzes Programm für das erzeugte main aus:
Es prüft die ABI jedes Deskriptors, startet eine Standard-Runtime, registriert
alle Module vor jeglichem Einstiegscode, baut argv im Einstiegskontext auf,
ruft den Einstiegspunkt auf und bildet das Ergebnis auf den Exit-Status aus
ausführbare Dateien ab. Berichte gehen nach dem
Leeren von stdout an stderr; Kontext und Runtime werden auf jedem Pfad in
Reihenfolge abgebaut. CLAUSE_halt_v1 implementiert erlang:halt/0,1
(abort ruft std::abort auf).
Zurückgestellte Dienste
Diese melden eine Zeile [feature] notimpl (Features) und
ändern keinen Zustand:
| Grenze | Fehler |
|---|---|
TermFactory::port (noch keine Ports, Plan-Step 53), reference(ReferenceIdentity) und function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin auf einem katalogisierten BIF | Status::not_implemented |
Clause