Runtime
clause_runtime är ett LLVM-fritt C++23-bibliotek. Det äger kontexter,
processheapar, atomer, laddade moduler och livscykelbokföring, och kör
Erlang-processer på schemaläggarens arbetstrådar (processer).
Alla API:er är interna för projektet; värdanrop måste serialiseras per runtime
och får inte överlappa en programkörning, vars arbetstrådar synkroniserar
sinsemellan (trådar).
Länkning
Länka exakt en runtime byggd för målet genom
Clause::generated_program:
add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)
Det tar med arkivet, ABI-/runtime-headers och C++23, men inte LLVM. link_consumer.cpp visar en fullständig livscykel.
Livscykel
Runtime::start(options)→std::expected<std::unique_ptr<Runtime>, Status>. Standardvärden: aktuell ABI-version och native-termbredd,max_atoms2^20 (högst 2^26; program sätter det med--max-atoms, runtime-alternativ). Antalet kontexter är inte begränsat.process_heapochprocess_stackär alternativen för kontexter som skapas avcreate_context();memory_limit_bytesär den valfria gränsen för hela runtimen, som rapporteras avmemory_bytes(). Alla saknar tak som standard.create_context()ellercreate_context(heap_options, stack_options)→ lånadProcessContext*, stabil tills den förstörs. Standardvärden för heapen: minimiheap på 233 ord (min_heap_words) och inget minnestak (limit_bytes=UNLIMITED_HEAP_BYTES; en satt budget är en multipel av ordstorleken som är minst minimiheapen). Stacken saknar också tak om inteStackOptions::limit_wordsär satt.destroy_context(ctx),shutdown(): shutdown returnerarbusymedan kontexter finns kvar; därefter lyckas den idempotent och senare anrop returnerarstopped. Destruktorn städar upp återstående kontexter.- Identiteter för kontexter och runtimes återanvänds inte; när de tar slut
blir det fel. En kontexts identitet bär dess pid-nummer
(pids). En kontextpekare är ett lån, inte
en identitet.
lifetime()ger en svag token som rapporteraralive() == falseinnan heapen rivs. - Livscykelanrop kastar aldrig undantag och är tysta. Statusvärden: abi.md.
Nedrivningsordning: stäng schemaläggarposter → förstör kontexter → släpp
kodregistreringar → förstör schemaläggartjänsten → atomtabellen sist.
Upplösta funktionshandtag håller kod vid liv efter runtimens nedrivning, men
deras atomplatsord fäster inga stavningar. Atomlagringen tar sina globala platser
ur bruk innan poster frigörs; stavningsåtkomst genom en överlevande icke-fast
atom-Term rapporterar expired_context.
Processminne
Varje kontext äger en ProcessHeap
(process_heap.hpp); dess layout,
områden, dimensionering, admission, rötter och skräpsamling specificeras i
runtime-heap.md. Värd-API:et:
allocate(words)returnerar nollställt ordutrymme.reserveger en reservation som endast kan flyttas, med explicit bekräftelse (commit) och automatisk återställning (rollback). En reservation åt gången per heap: bygg barnen först och reservera föräldern sist. Råaallocate()-ord måste förbli noll eller innehålla kompletta objekt (ordlayout).- Noll, överflöd och uttömd budget avvisas innan något publiceras:
out_of_memory(allokering) ellerlimit_exceeded(budget); genererad kod får den exakta statusen (felbeteende). add(value)/copy_tokopierar en graf från en annan process i samma runtime (kopiering mellan heapar).used_wordsräknar allokerade ord,capacity_wordsheapblocket och fragmenten,off_heap_wordsde buffertar utanför heapen som denna process refererar till, var och en en gång (dimensionering och budget).verify()går igenom heapblocket och varje fragment och kontrollerar att varje termplats pekar på början av ett objekt i samma process (tester och felsökning; annarscorrupt_heap).collect(roots)kopierar allt som kan nås från processrötterna och värdens rotord till ett nytt heapblock (skräpsamling). Den körs endast vid en safepoint, annarsunsafe_point(rötter och safepoints); genererad kod samlar skräp vid sina egna safepoints (skräpsamling i genererad kod). Värd-Terms är orotade enordsvärden; bygg om dem från omskrivna rötter efter skräpsamling. De saknar livstids- och inaktualitetskontroller och får inte läsas efter skräpsamling eller processens slut. Fabriker och reservationer behåller egna livstidstoken och rapporterar ännuexpired_context.
Kodserver och inbyggda funktioner
Varje runtime äger en 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});
- En
ModuleRegistryavbildar exakt namn/aritet (≤ 255) på ett anropbart objekt där allt ärTerm.loadfryser och publicerar det; dubbletter eller fel publicerar ingenting. - Registreringen indexerar också genererade modulers bindningar i
hashtabeller: modul-, record- och fun-deskriptorer till deras bindningar och
definitioner, och atomord för
Module:Function/Aritytill exportramar. Dynamiska anrop,apply/3,fun M:F/Aunder körning och uppslag av deskriptorer tar därför konstant förväntad tid i antalet moduler och exporter. Indexen byggs inuti registreringen: en avvisad modul lägger inte till någon post, och de ändras endast under serverns exklusiva lås. resolveskiljer mellan saknad modul och saknad export.ResolvedFunctionfäster modulavbildningen;callkontrollerar aritet, argument och resultat och gör om värdundantag till fel.- Native-kroppar måste vara synkrona, icke-blockerande och får inte behålla kontexten eller argumentspannet.
- En begränsad katalog över kända uppskjutna BIF:ar (
self/0,length/1,spawn/3,spawn_link/3,send/2,make_ref/0,garbage_collect/0,apply/3(genererad kod anropar i ställetapply/2,3genom tjänsterna för dynamiska anrop, funs),tuple_size/1,+/2) rapporterarnot_implemented; andra oregistrerade namn returnerarunknown_builtintyst. Denna värdväg når inte produktionens inbyggda funktioner. - Produktionens inbyggda funktioner finns i serverns
BuiltinRegistry, registrerade vid runtimens uppstart; genererad kod, dynamiska anrop och funs når dem genom bryggan för inbyggda funktioner. CodeServer::export_framehittarFrameDescriptorför en export utifrån modul, funktionsatom och aritet;function_framelägger till de inbyggda funktionerna för dynamiska anrop;external_funinternerar definitionerna av externa funs som byggs under körning.- Moduler laddas aldrig ur;
CodeServer::unloadär uppskjuten och dynamisk laddning stöds inte.
Persistenta termer
En tabell per runtime (memory/persistent_terms), som ägs av Runtime::Impl
och nås genom ProcessContext::persistent_terms(), håller termerna för
persistent_term (inbyggda funktioner).
- Varje nyckel och värde kopieras till ett eget område i runtimens intervall
för persistenta termer (
memory/persistent_region): en adressreservation (64 GiB adressrymd på 64-bitarsvärdar, 256 MiB på 32-bitarsvärdar) vars sidor tilldelas när områden allokeras, så att det bara krävs en intervalljämförelse för att avgöra om ett ord betecknar en persistent term. Kopior delar endast modulliteraler, så varje område är fristående; dess binaries delar buffertar. - Poster är rotord som hittas via en hash av nyckeln (de inbyggda funktionerna
använder termhashen från
phash2) och exakt likhet, jämförd på plats. - En läsning returnerar det lagrade ordet på plats, som OTP:s literala persistenta termer: varje process får hålla det; skräpsamlingar lämnar det orört och admissionen godtar det som en literal. Sändningar till andra processer, meddelandebuffertar och nya persistenta termer kopierar persistenta ord (en sändning går igenom sitt meddelande en gång, före exekverarlåset, så att den inte kan dela ett område som kan tas ur bruk under tiden).
- Återvinning. Ett ersatt eller raderat värdes område tas ur bruk under en
ny epok. Därefter kopierar varje kopia dess ord, och varje process som var
registrerad då kopierar de objekt den håller från området till sin egen heap
vid en evakuerande fullständig skräpsamling (
Copiermed regionen, varje objekt en gång, binary-buffertar hålls före samlingen): en körande eller köad process i början av nästa tidskvantum, en parkerad direkt, genom den put eller erase som tog området ur bruk (Executor::evacuate_parked, under exekverarlåset). När varje sådan process gjort detta eller avslutats frigörs området.info/0räknar bara aktuella poster. - Ett delat lås betjänar läsare på valfritt antal arbetare; put och erase tar
det exklusivt (kopior görs innan). CTest
runtime_persistent_termstäcker värden som överlever sin skrivare, läsningar på plats över skräpsamlingar och mellan processer, ersatta och raderade värden som läsare håller, samtidiga läsare och skrivare samt återvinning (kopior från områden som tas ur bruk, evakuering, frigjorda områden och minnesdebiteringar).
ETS-tabeller
Ett register per runtime (ets/tables), som ägs av Runtime::Impl och nås
genom ProcessContext::ets_tables(), håller tabellerna för ets
(inbyggda funktioner).
- Objekt. Varje lagrat objekt (och en arvtagares data) är en
DetachedTerm(memory/detached_term): en platt kopia i ett eget block, skapad av grafkopieringen, utanför alla heapar. Modulliteraler förblir delade; persistenta termer kopieras, eftersom deras områden kan tas ur bruk; binary-celler delar sina buffertar. Block flyttas aldrig och tillhör ingen process, så läsare jämför och hashar dem på plats och kopierar dem till sin egen heap. Deras ord debiteras runtimens minneskonto. - Index (
ets/index). Tabeller av typenset,bagochduplicate_baganvänder ett hashindex: en array av hinkar vars antal är en tvåpotens, var och en en liten vektor av poster (en per nyckel, med objekt i insättningsordning), som fördubblas över två nycklar per hink och halveras under en på åtta. Nycklar hittas genom 64-bitars spridning av derasphash2-hash och exakt likhet. Tabeller av typenordered_setanvänder enstd::setmed en nod per nyckel i termordning, där nycklar som jämförs lika (1,1.0) är en nyckel; när ett objekt ersätts behålls dess nod. - Delar och lås (
ets/table). En hashtabell medwrite_concurrency(trueellerauto) delar sina nycklar i 64 delar efter hashens högsta bitar, var och en ett index med en egenshared_mutex; alla andra tabeller har en del. Tabellåset delas av operationer på enstaka nycklar och tas exklusivt av operationer på hela tabellen (listor av objekt,delete_all_objects/1, fixering, borttagning), så att en listinsättning är atomär och isolerad som i OTP. Läsningar kopierar ut objekt medan deras del är låst med ett delat lås. - Genomgångar och fixering. En hashtabell gås igenom del för del och hink
för hink, bakåt liksom framåt;
next/2från en nyckel den inte innehåller gerbadarg. Medan tabellen är fixerad (safe_fixtable/2,foldl/3) ändrar dess index aldrig storlek, en borttagen nyckel stannar som en borttagningsmarkör som håller ett objekt för sin nyckel, och nya nycklar läggs sist i sin hink, så att en genomgång ser varje kvarvarande nyckel en gång. När den sista fixeringen släpps tas markörerna bort och storleken justeras. En ordered_set går från valfri nyckel till den närmaste. - Register. Tabeller efter identitetsnummer (en vanlig referens från
programmets sekvens) och namn, samt de tabeller varje process äger eller
fixerar. När en process avslutas (
Executor::release_tables, under exekverarlåset innan länkar och monitorer signaleras) släpps dess fixeringar och varje tabell den ägde överförs till en levande arvtagare som inte är den själv, vilken får{'ETS-TRANSFER', Table, From, HeirData}(inget för en arvtagare satt utan data), eller tas bort. Låsordning: exekverarens, registrets, tabellens; inbyggda funktioner tar aldrig exekverarlåset medan de håller ett ETS-lås, ochgive_away/3skickar sitt meddelande efter att de släppts. - Portioner.
tab2list/1och urval av varje objekt (select/2,select_reverse/2,match/2,match_object/2,select_count/2) går igenom tabellen med så många objekt per portion som processens budget tillåter och lämnar sedan över körningen (builtins/ets/table_walks.cpp); en hashtabell fixeras av processen som går igenom den från dess första överlämning till slutet, som OTP fixerar den, så att dess platser ligger kvar.delete_all_objects/1ochdelete/1tömmer eller tar bort tabellen direkt genom att ta ut dess index (Table::empty,Table::destroy) och frigör sedan de gamla objekten i portioner (builtins/ets/disposal.cpp,Index::shed); en fixerad hashtabell töms i stället på plats och behåller sina borttagningsmarkörer. Tabeller som en avslutad ägare lämnar frigörs av arbetstråden efter att den släppt exekverarlåset.select_delete/2ochselect_replace/2körs fortfarande i ett stycke. - Räknare på plats.
update_counter/3,4på en nyckel vars objekt är tabellens egen kopia och vars räknare förblir små heltal ändrar de lagrade fälten där de finns, under delens lås, som OTP gör; ett bignum, ett objekt som är en modulliteral eller ett standardobjekt tar den allmänna vägen, som lagrar en ny kopia. - CTest
executables_ets_concurrencytäcker räknare, insättningar och genomgångar från många processer på en och fyra arbetstrådar samt avslut av ägare till många tabeller;executables_ets_portionsvisar andra processer som körs under långa operationer på en arbetstråd. - Urval (matchningsspecifikationer). Ett urval
över hela tabellen läser varje del under dess lås; ett vars huvuden alla
binder nyckeln slår upp nycklarna. Ett portionsvis urval går igenom en
hashtabell plats för plats (varje plats under sin dels lås) och en
ordered_set efter nycklar från den senaste, så att en fortsättning anger en
plats eller en nyckel.
select_delete/2ochselect_replace/2ändrar varje del under dess eget lås (Index::edit); en fixerad hashtabell behåller under tiden borttagningsmarkörerna för borttagna nycklar.
Matchningsspecifikationer
runtime/src/match_spec/ kompilerar och kör matchningsspecifikationer som
OTP:s erl_db_util.c gör, för ets och erlang:match_spec_test/3
(inbyggda funktioner).
- Program.
compilegör om en specifikation till ettProgram: per klausul ett huvudmönster, guard- och kroppsuttryck över numrerade variabelplatser, samt variablerna som huvudet binder ('$$'). Konstanter beräknas som i OTP ({{...}}och{const, X}till värden; en konstant map behåller sina nycklar som de är skrivna) och behålls somDetachedTerm-kopior, så att ett program inte tillhör någon process. Två dialekter:table(ets) ochtrace(huvuden är argumentlistor, en lista som slutar med'_'matchar ett prefix,'$N'eller'_'hela listan; kroppar får köra spårningsåtgärder). - Kontroller. Kompilatorn tillämpar OTP:s kontroller i OTP:s ordning och
ger vid lintning (
erlang:match_spec_test/3) OTP:s meddelanden för den första klausul som har några, med operander till anrop och tupler kompilerade från sista till första som i OTP; ett meddelande kapas vid 99 byte som OTP:s buffert gör. Guard-funktionerna är OTP:s tabell (match_spec/functions.cpp), med de kontexter där var och en får anropas; nästling över 1000 nivåer gersystem_limit. - Körning (
match_spec/run.cpp). Klausuler prövas i ordning. Ett huvud binder delar av objektet på plats (exakt likhet för konstanter och upprepade variabler, maps som innehåller minst mönstrets nycklar); guards läser dem på plats och måste ge exakttrue; en kropp kopierar varje värde den ger till anroparens heap. En guard-funktion som kastar ett undantag får klausulen att misslyckas; i en kropp ger den'EXIT'i dess ställe och kroppen fortsätter, liksomand,or,andalsoochorelsemed en term som inte är ett booleskt värde. De flesta guard-funktioner är den omedelbara tjänstens termoperationer (terms/immediate_terms.hpp). - Kompilerade specifikationer är handtagsreferenser som håller programmet
(
match_spec/handles.cpp), så att de överleverterm_to_binary/1inom programmet som magiska referenser i OTP 20+ gör.
Schemaläggarens bokföring
SchedulerService
(scheduler.hpp) är en
värdvänd post över processernas livscykel; den kör ingen kod. Program körs på
exekutorn (processer), som inte använder den.
register_process(context)en gång per kontext (samma runtime); dubbletter returneraralready_registered.remove_process(id)pensionerar en post som inte körs.begin_dispatch→ körs;finish_dispatch→ körbar, väntande eller avslutad (med orsak).set_suspendedväxlar en flagga på körbara/väntande poster. Andra övergångar returnerarinvalid_transition.request_shutdown()stänger registrering, utdelning och suspensionsstyrning; inspektion och returer förblir tillgängliga för dränering.runochexecuterapporterarnot_implemented.
Trådar
Schemaläggarens arbetstrådar (arbetstrådar) kör processer på flera trådar i en runtime. Tjänster som de delar är synkroniserade; allt annat förblir begränsat till tråden som kör dess process eller skyddas av exekutorns mutex.
- Atomer:
AtomStorageskyddar sin stavnings-map och ägda poster med ett delat mutex.existingochsizedelar det;internslår först upp under det delade låset, tar sedan det exklusiva låset för en ny stavning och kontrollerar igen, så samtidiga interneringar får ett ord. Publicering använder processens gemensammadetail::atom_table; admission och stavningsläsningar använder dess låsfria nummerindex (lookupkontrollerar även postens ägare). Fasta atomer behöver inget lås. Poster förblir oförändrade tills nedrivningen tar deras platser ur bruk (atomer). - Kod:
CodeServerskyddar sina moduler och definitioner av externa funs med en delad mutex. Uppslagningar (find_module,resolve,atom_word,record_definition,fun_definition,export_frame,function_frame,owns) delar den;loadoch byggandet av en ny definition av en extern fun tar den exklusivt, så att samtidiga registreringar av samma namn publicerar en modul (de andra fårduplicate_module) och kapplöpandeexternal_fun-anrop får en definition. Registret för inbyggda funktioner fylls vid runtimens uppstart, innan någon arbetstråd körs, och läses endast därefter. - Fästen: moduler tas aldrig bort medan runtimen lever och servern förstörs
efter varje kontext, så definitioner, ramar och atomplatser som den
returnerat förblir giltiga för varje anrop och varje fun-cell. Handtag från
ResolvedFunctionochfind_modulebehåller också sin modul efter att runtimen är borta; deras atomplatser behåller ord, inte fästen för stavningar. - Pid- och portnummer:
IdentityNumbersutfärdar nummer under ett exklusivt lås och godtar pid- och portord under ett delat. - Minne: redovisningen för hela runtimen (
RuntimeMemory) debiterar och frigör med atomära operationer; en debitering tar aldrig redovisningen förbi en valfri gräns. - Andra trådar: timertråden (timeouts i receive),
I/O-tråden och, på Windows, lästråden för en
fd-indata (I/O-tråd) rör processtillstånd endast genom exekutorns mutex. - Länkning: på Linux lägger runtimen till
-pthreadför sina trådar; på Windows länkar den Winsock (ws2_32,mswsock) för socketar.
Standard-utdata
RuntimeOptions::standard_output
(output.hpp) tar emot
erlang:display/1 och io-utdata till standard-utdata (io).
Standardimplementationen skriver till processens stdout genom C:s stdio
(buffrat); en värdmottagare returnerar false för att rapportera en
misslyckad skrivning.
erlang:display/1 återger sitt argument i visningsstil,
skriver texten och en radbrytning i en enda skrivning och returnerar true.
Återgivningsgränser och avvisade skrivningar blir infrastrukturstatusar
(resource_limit, output_failure, ...) i den kontrollerade kanalen, aldrig
Erlang-undantag.
Programuppstart
CLAUSE_main_v1 (startup.hpp,
runtime/src/startup/) kör ett helt program åt den genererade main
(körbara filer): den kontrollerar varje deskriptors ABI,
startar en runtime, registrerar alla moduler före all ingångskod, bygger argv i
ingångskontexten, kör ingångspunkten på exekutorn och översätter resultatet
till avslutsstatusen. Rapporter går till stderr
efter att stdout har tömts; kontexten och runtimen rivs i ordning på varje väg.
CLAUSE_halt_v1 implementerar erlang:halt/0,1 (abort anropar
std::abort).
Uppskjutna tjänster
Dessa rapporterar en rad [feature] notimpl (funktioner) och
ändrar inget tillstånd:
| Gräns | Fel |
|---|---|
TermFactory::function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin på en katalogiserad BIF | Status::not_implemented |
Clause