Runtime
clause_runtime — це бібліотека на C++23 без LLVM. Вона володіє контекстами,
купами процесів, атомами, завантаженими модулями й обліком життєвого циклу та
виконує процеси Erlang на робочих потоках планувальника
(процеси). Усі API внутрішні для проєкту; виклики з боку хоста
мають бути серіалізовані для кожного runtime й не перекриватися з виконанням
програми, чиї робочі потоки синхронізуються між собою (потоки).
Компонування
Скомпонуйте рівно один runtime, зібраний для цілі, через
Clause::generated_program:
add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)
Він приносить архів, заголовки ABI/runtime і C++23, але не LLVM. link_consumer.cpp показує повний життєвий цикл.
Життєвий цикл
Runtime::start(options)→std::expected<std::unique_ptr<Runtime>, Status>. Типові значення: поточна версія ABI й нативна ширина терма,max_atoms2^20 (щонайбільше 2^26; програми задають його через--max-atoms, опції runtime). Кількість контекстів не обмежена.process_heapіprocess_stack— це опції контекстів, створених черезcreate_context();memory_limit_bytes— необов'язкове обмеження на весь runtime, про яке звітуєmemory_bytes(). Типово жодне з них не обмежене.create_context()абоcreate_context(heap_options, stack_options)→ позиченийProcessContext*, стабільний до знищення. Типові значення купи: мінімальна купа в 233 слова (min_heap_words) і без обмеження пам'яті (limit_bytes=UNLIMITED_HEAP_BYTES; заданий бюджет — це кратне слову значення, щонайменше мінімальна купа). Стек теж не обмежений, якщо не заданоStackOptions::limit_words.destroy_context(ctx),shutdown(): shutdown повертаєbusy, поки лишаються контексти; після цього він ідемпотентно успішний, а пізніші виклики повертаютьstopped. Деструктор прибирає контексти, що лишилися.- Ідентичності контекстів і runtime не використовуються повторно; їх
вичерпання дає збій. Ідентичність контексту містить його номер pid
(pid). Вказівник на контекст — це позика, а
не ідентичність.
lifetime()дає слабкий токен, що повідомляєalive() == falseдо демонтажу купи. - Виклики життєвого циклу ніколи не кидають винятків і нічого не виводять. Значення статусу: abi.md.
Порядок завершення: закрити записи планувальника → знищити контексти →
звільнити реєстрації коду → знищити сервіс планувальника → таблицю атомів
останньою. Дескриптори розв’язаних функцій утримують код після завершення
runtime, але слова їхніх слотів атомів не утримують написання. Сховище атомів
виводить глобальні слоти з ужитку до звільнення записів; доступ до написання
через збережений Term нефіксованого атома повертає expired_context.
Пам'ять процесу
Кожен контекст володіє одним ProcessHeap
(process_heap.hpp); його розкладку,
ділянки, розміри, допуск, корені й збирання описано в
runtime-heap.md. API хоста:
allocate(words)повертає обнулене сховище слів.reserveдає резервацію лише з переміщенням (move-only) з явним підтвердженням і автоматичним відкотом. Одна резервація за раз на купу: спершу будуйте дочірні елементи, батьківський резервуйте останнім. Сирі словаallocate()мають лишатися нулями або містити повні об'єкти (розкладка слів).- Нуль, переповнення й вичерпаний бюджет відхиляються до публікації:
out_of_memory(виділення) абоlimit_exceeded(бюджет); згенерований код отримує точний статус (поведінка під час збоїв). add(value)/copy_toкопіюють граф іншого процесу того самого runtime (копіювання між купами).used_wordsрахує виділені слова,capacity_words— блок купи й фрагменти,off_heap_words— буфери поза купою, на які посилається цей процес, кожен один раз (розміри й бюджет).verify()обходить блок купи й кожен фрагмент і перевіряє, що кожен слот терма вказує на початок об'єкта того самого процесу (тести й налагодження; інакшеcorrupt_heap).collect(roots)копіює все досяжне з коренів процесу й кореневих слів хоста в новий блок купи (збирання). Виконується лише в точці безпеки, інакшеunsafe_point(корені й точки безпеки); згенерований код збирає у власних точках безпеки (збирання у згенерованому коді). Хостові Term — однослівні значення без коренів; після збирання їх слід відновлювати з переписаних коренів. Вони не мають перевірок часу життя чи застарілості, і їх не можна читати після збирання або завершення процесу. Фабрики й резервування зберігають власні токени часу життя й надалі повідомляютьexpired_context.
Сервер коду й вбудовані функції
Кожен runtime володіє одним 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});
ModuleRegistryвідображає точні ім'я/арність (≤ 255) на один викликуваний об'єкт, що працює лише зTerm.loadзаморожує й публікує його; дублікати чи збої нічого не публікують.- Реєстрація також індексує прив'язки згенерованих модулів у хеш-таблицях:
дескриптори модулів, record і fun — до їхніх прив'язок і визначень, а слова
атомів
Module:Function/Arity— до фреймів експорту. Тому динамічні виклики,apply/3,fun M:F/Aпід час виконання й пошук дескрипторів займають сталий очікуваний час щодо кількості модулів і експортів. Індекси будуються всередині реєстрації: відхилений модуль не додає записів, і вони змінюються лише під ексклюзивним блокуванням сервера. resolveрозрізняє відсутній модуль і відсутній експорт.ResolvedFunctionзакріплює образ модуля;callперевіряє арність, аргументи й результати та перетворює винятки хоста на збої.- Нативні тіла мають бути синхронними, неблокувальними й не повинні утримувати контекст чи span аргументів.
- Обмежений каталог відомих відкладених BIF (
self/0,length/1,spawn/3,spawn_link/3,send/2,make_ref/0,garbage_collect/0,apply/3(згенерований код натомість викликаєapply/2,3через служби динамічних викликів, fun),tuple_size/1,+/2) повідомляєnot_implemented; інші незареєстровані імена мовчки повертаютьunknown_builtin. Цей шлях хоста не доходить до робочих вбудованих функцій. - Робочі вбудовані функції живуть у
BuiltinRegistryсервера й реєструються під час запуску runtime; згенерований код, динамічні виклики й fun доходять до них через міст вбудованих функцій. CodeServer::export_frameзнаходитьFrameDescriptorекспорту за модулем, атомом функції й арністю;function_frameдодає вбудовані функції для динамічних викликів;external_funінтернує визначення зовнішніх fun, побудованих під час виконання.- Модулі ніколи не вивантажуються;
CodeServer::unloadвідкладено, динамічне завантаження не підтримується.
Постійні терми
Одна таблиця на runtime (memory/persistent_terms), яка належить
Runtime::Impl і доступна через ProcessContext::persistent_terms(),
зберігає терми persistent_term
(вбудовані функції).
- Кожен ключ і значення копіюються у власну область діапазону persistent terms
runtime (
memory/persistent_region): одне резервування адрес (64 GiB адресного простору на 64-бітних хостах, 256 MiB на 32-бітних), сторінки якого виділяються під час виділення областей, тож належність слова до persistent terms визначається одним порівнянням діапазону. Копії спільно використовують лише літерали модулів, тому кожна область незалежна; її binary спільно використовують свої буфери. - Записи — кореневі слова, які знаходять за хешем ключа (вбудовані функції
використовують хеш терма
phash2) і точною рівністю, порівнюючи на місці. - Читання повертає збережене слово на місці, як літеральні persistent terms OTP: будь-який процес може зберігати його; збирання не змінює його, а допуск приймає як літерал. Надсилання іншим процесам, буфери повідомлень і нові persistent terms копіюють слова persistent terms (надсилання обходить повідомлення один раз, до блокування виконавця, тому не може спільно використовувати область, яку тим часом можуть вивести з ужитку).
- Звільнення пам’яті. Область заміненого чи видаленого значення виводиться
з ужитку під новою епохою. Відтоді кожна копія копіює її слова, а кожен
процес, зареєстрований на той момент, копіює збережені об’єкти цієї області
у власну купу під час евакуаційного повного збирання (
Copierз регіоном, кожен об’єкт один раз, буфери binary утримуються до збирання): активний або поставлений у чергу процес — на початку наступного кванта, призупинений — одразу, через put або erase, що вивели область з ужитку (Executor::evacuate_parked, під блокуванням виконавця). Коли всі такі процеси зроблять це або завершаться, область звільняється.info/0рахує лише поточні записи. - Спільне блокування обслуговує читачів на довільній кількості робочих потоків;
put і erase беруть його виключно (копії створюються до цього). CTest
runtime_persistent_termsперевіряє значення, що переживають записувача, читання на місці між збираннями й процесами, замінені та видалені значення, які утримують читачі, одночасних читачів і записувачів та звільнення пам’яті (копії областей, що виводяться з ужитку, евакуацію, звільнені області й облік пам’яті).
Таблиці ETS
Один реєстр на runtime (ets/tables), що належить Runtime::Impl і доступний
через ProcessContext::ets_tables(), зберігає таблиці ets
(вбудовані функції).
- Об’єкти. Кожен збережений об’єкт (і дані спадкоємця) — це
DetachedTerm(memory/detached_term): пласка копія у власному блоці, створена копіюванням графа поза будь-якою купою. Літерали модулів залишаються спільними; постійні терми копіюються, бо їхні області можуть виводитися з ужитку; комірки binary спільно використовують буфери. Блоки ніколи не переміщуються й не належать жодному процесу, тому читачі порівнюють і хешують їх на місці та копіюють у власну купу. Їхні слова враховуються в обліку пам’яті runtime. - Індекси (
ets/index). Таблиціset,bagіduplicate_bagвикористовують хеш-індекс: масив кошиків розміром у степінь двійки, кожен — малий вектор записів (один на ключ, об’єкти в порядку вставлення). Масив подвоюється за понад двох ключів на кошик і зменшується вдвічі за менш ніж одного на вісім кошиків. Ключі знаходять за 64-бітним розсіюванням хешуphash2і точною рівністю. Таблиціordered_setвикористовуютьstd::setз одним вузлом на ключ у порядку термів, де рівні за порівнянням ключі (1,1.0) є одним ключем; заміна об’єкта зберігає його вузол. - Частини й блокування (
ets/table). Хеш-таблиця зwrite_concurrency(trueабоauto) ділить ключі на 64 частини за старшими бітами хешу, кожна — індекс із власнимshared_mutex; інші таблиці мають одну частину. Операції з одним ключем беруть спільне блокування таблиці, а операції з усією таблицею — виключне (списки об’єктів,delete_all_objects/1, фіксація, видалення), тому вставлення списку атомарне й ізольоване, як в OTP. Читання копіює об’єкти назовні під спільним блокуванням їхньої частини. - Обходи й фіксація. Хеш-таблиця обходиться частина за частиною й кошик за
кошиком, як уперед, так і назад;
next/2від відсутнього ключа даєbadarg. Поки таблиця зафіксована (safe_fixtable/2,foldl/3), її індекси не змінюють розміру, видалений ключ лишається як позначка видалення з одним об’єктом для його ключа, а нові ключі додаються в кінець кошика, тому обхід бачить кожен збережений ключ один раз. Останнє зняття фіксації прибирає позначки видалення й змінює розмір. ordered_set переходить від будь-якого ключа до найближчого. - Реєстр. Таблиці за номером ідентифікатора (звичайне посилання з
послідовності програми) та за іменем, а також таблиці, якими кожен процес
володіє або які фіксує. Завершення процесу (
Executor::release_tables, під блокуванням виконавця до сповіщень зв’язкам і моніторам) знімає його фіксації й передає кожну належну йому таблицю живому спадкоємцю, відмінному від нього самого, який отримує{'ETS-TRANSFER', Table, From, HeirData}(без повідомлення для спадкоємця, заданого без даних), або видаляє її. Порядок блокувань: виконавець, реєстр, таблиця; вбудовані функції ніколи не беруть блокування виконавця, утримуючи блокування ETS, аgive_away/3надсилає повідомлення після їх звільнення. - Порції.
tab2list/1і вибірки всіх об’єктів (select/2,select_reverse/2,match/2,match_object/2,select_count/2) обходять таблицю порціями з кількістю об’єктів, яку дозволяє бюджет процесу, після чого передають керування планувальнику (builtins/ets/table_walks.cpp); процес обходу фіксує хеш-таблицю від першої такої передачі до завершення, як це робить OTP, тому її слоти залишаються на місці.delete_all_objects/1іdelete/1одразу очищають або видаляють таблицю, вилучаючи її індекси (Table::empty,Table::destroy), а потім звільняють старі об’єкти порціями (builtins/ets/disposal.cpp,Index::shed); зафіксована хеш-таблиця натомість очищується на місці, зберігаючи позначки видалення. Таблиці, які залишив завершений власник, звільняє робочий потік після зняття блокування виконавця.select_delete/2іselect_replace/2усе ще виконуються цілком. - Лічильники на місці.
update_counter/3,4для ключа, об’єкт якого є власною копією таблиці, а лічильники залишаються малими цілими, змінює збережені поля на місці під блокуванням частини, як в OTP; bignum, об’єкт — літерал модуля або типовий об’єкт ідуть загальним шляхом зі збереженням нової копії. - CTest
executables_ets_concurrencyохоплює лічильники, вставлення й обходи з багатьох процесів на одному й чотирьох робочих потоках, а також завершення власників багатьох таблиць;executables_ets_portionsпоказує виконання інших процесів під час тривалих операцій на одному робочому потоці. - Вибірки (специфікації зіставлення). Вибірка
всієї таблиці читає кожну частину під її блокуваннями; якщо всі голови
зв’язують ключ, вибірка шукає ці ключі. Порційна вибірка обходить
хеш-таблицю слот за слотом (кожен слот під блокуванням його частини), а
ordered_set — за ключами від останнього, тому продовження вказує слот або
ключ.
select_delete/2іselect_replace/2змінюють кожну частину під її власним блокуванням (Index::edit); зафіксована хеш-таблиця тим часом зберігає позначки видалених ключів.
Специфікації зіставлення
runtime/src/match_spec/ компілює й виконує специфікації зіставлення так, як
erl_db_util.c в OTP, для ets і erlang:match_spec_test/3
(вбудовані функції).
- Програми.
compileперетворює специфікацію наProgram: для кожної клаузи — зразок голови, вирази guard і тіла над пронумерованими слотами змінних і змінні, які зв’язує голова ('$$'). Константи згортаються як в OTP ({{...}}і{const, X}у значення; константна map зберігає ключі як записано) й зберігаються як копіїDetachedTerm, тому програма не належить жодному процесу. Два діалекти:table(ets) іtrace(голови — списки аргументів, список із кінцевим'_'зіставляє префікс,'$N'або'_'— увесь список; тіла можуть виконувати дії трасування). - Перевірки. Компілятор застосовує перевірки OTP у порядку OTP, а під час
lint-перевірки (
erlang:match_spec_test/3) видає повідомлення OTP для першої клаузи, яка їх має; операнди викликів і кортежів компілюються від останнього до першого, як в OTP; повідомлення обрізається на 99 байтах, як буфер OTP. Функції guard — з таблиці OTP (match_spec/functions.cpp), з контекстами, у яких кожну можна викликати; вкладеність понад 1000 рівнів даєsystem_limit. - Виконання (
match_spec/run.cpp). Клаузи перевіряються послідовно. Голова зв’язує частини об’єкта на місці (точна рівність для констант і повторних змінних, map містять щонайменше ключі зразка); guard читають їх на місці й мають повертати самеtrue; тіло копіює кожне видане значення в купу викликача. Функція guard, що породжує виняток, робить клаузу невдалою; у тілі вона натомість дає'EXIT', і тіло продовжується, як і дляand,or,andalsoтаorelseнад небулевим термом. Більшість функцій guard — операції сервісу безпосередніх значень над термами (terms/immediate_terms.hpp). - Скомпільовані специфікації — посилання-дескриптори, що утримують програму
(
match_spec/handles.cpp), тому вони переживаютьterm_to_binary/1у межах програми, як magic references в OTP 20+.
Облік планувальника
SchedulerService
(scheduler.hpp) — це запис
життєвого циклу процесів для хоста; він не виконує коду. Програми виконуються
на виконавці (процеси), який його не використовує.
register_process(context)один раз на контекст (того самого runtime); дублікати повертаютьalready_registered.remove_process(id)прибирає запис, що не виконується.begin_dispatch→ running;finish_dispatch→ runnable, waiting або exited (з причиною).set_suspendedперемикає прапорець на записах runnable/waiting. Інші переходи повертаютьinvalid_transition.request_shutdown()закриває реєстрацію, диспетчеризацію й керування призупиненням; інспекція й повернення лишаються доступними для спорожнення.runіexecuteповідомляютьnot_implemented.
Потоки
Робочі потоки планувальника (робочі потоки) виконують процеси на кількох потоках одного runtime. Служби, які вони ділять, синхронізовано; усе інше лишається в межах потоку, що виконує свій процес, або захищене м'ютексом виконавця.
- Атоми:
AtomStorageзахищає map написань і власні записи спільним м’ютексом.existingіsizeвикористовують спільне блокування;internспершу шукає під спільним блокуванням, потім для нового написання бере виключне й перевіряє ще раз, тож конкурентні виклики отримують одне слово. Публікація використовує загальнопроцеснуdetail::atom_table; допуск і читання написань — її неблокувальний індекс за номером (lookupтакож перевіряє власника запису). Фіксовані атоми не потребують блокування. Записи не змінюються, доки під час завершення їхні слоти не буде виведено з ужитку (атоми). - Код:
CodeServerзахищає свої модулі й визначення зовнішніх fun спільним м'ютексом. Пошуки (find_module,resolve,atom_word,record_definition,fun_definition,export_frame,function_frame,owns) беруть його спільно;loadі побудова нового визначення зовнішнього fun беруть його ексклюзивно, тож одночасні реєстрації одного імені публікують один модуль (інші отримуютьduplicate_module), а змагальні викликиexternal_funотримують одне визначення. Реєстр вбудованих функцій заповнюється під час запуску runtime, до виконання будь-якого робочого потоку, і після цього лише читається. - Утримання: модулі ніколи не видаляються протягом життя runtime, а сервер
знищується після всіх контекстів, тому повернуті ним визначення, фрейми й
слоти атомів залишаються чинними для кожного виклику та комірки fun.
Дескриптори
ResolvedFunctionіfind_moduleтакож утримують свій модуль після завершення runtime; їхні слоти атомів зберігають слова, а не утримують написання. - Номери pid і портів:
IdentityNumbersвидає номери під ексклюзивним блокуванням і допускає слова pid і портів під спільним. - Пам'ять: облік на весь runtime (
RuntimeMemory) нараховує й звільняє атомарними операціями; нарахування ніколи не виводить облік за необов'язкове обмеження. - Інші потоки: потік таймерів
(тайм-аути receive), потік вводу-виводу і,
у Windows, потік читання входу
fd(потік вводу-виводу) торкаються стану процесів лише через м'ютекс виконавця. - Компонування: на Linux runtime додає
-pthreadдля своїх потоків; на Windows він компонує Winsock (ws2_32,mswsock) для сокетів.
Стандартний вивід
RuntimeOptions::standard_output
(output.hpp) отримує вивід
erlang:display/1 та io у стандартний вивід (io). Типова
реалізація пише в stdout процесу через C stdio (буферизовано); приймач хоста
повертає false, щоб повідомити про невдалий запис.
erlang:display/1 відображає свій аргумент у стилі display,
пише текст і переведення рядка одним записом і повертає true. Обмеження
відображення й відхилені записи стають статусами інфраструктури
(resource_limit, output_failure, ...) у перевіреному каналі, ніколи не
винятками Erlang.
Запуск програми
CLAUSE_main_v1 (startup.hpp,
runtime/src/startup/) виконує цілу програму для згенерованого main
(виконувані файли): він перевіряє ABI кожного дескриптора,
запускає runtime, реєструє всі модулі до будь-якого коду точки входу, будує
argv у контексті точки входу, виконує точку входу на виконавці й відображає
результат на статус виходу. Звіти йдуть у
stderr після скидання stdout; контекст і runtime демонтуються по порядку на
кожному шляху. CLAUSE_halt_v1 реалізує erlang:halt/0,1 (abort викликає
std::abort).
Відкладені служби
Вони повідомляють один рядок [feature] notimpl (можливості) і
не змінюють стану:
| Межа | Помилка |
|---|---|
TermFactory::function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin для каталогізованої BIF | Status::not_implemented |
Clause