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, але ніколи не процес.
Пам'ять процесу
Кожен контекст володіє одним ProcessHeap: єдиним блоком купи, створеним
його першим виділенням і розміром max(min_heap_words, request), плюс
ланцюгом фрагментів купи, що належать тому самому процесу. Слова
переміщуються лише тоді, коли хост викликає collect() у безпечній точці.
Купа відповідає класичному дизайну ERTS; runtime-heap.md — це її контракт (розкладка, ділянки, розміри, допуск, корені, збирання).
allocate(words)повертає обнулене сховище слів.reserveдає резервацію лише з переміщенням (move-only) з явним підтвердженням і автоматичним відкотом. Одна резервація за раз на купу: спершу будуйте дочірні елементи, батьківський резервуйте останнім.- Виділення зсувом вказівника (bump allocation) заповнює блок купи; запит,
що не вміщується, йде в найновіший фрагмент, інакше в новий фрагмент
розміром
max(min_heap_words, request)(обмежений бюджетом, що лишився). Слова лише вирівняні за словом. Відкат скидає верхівку ділянки, відкидає фрагмент (чи блок купи), створений резервацією, і точно відновлює облік. - Відхиляє нуль, переповнення й вичерпаний бюджет до публікації. Помилки:
out_of_memory(виділення) абоlimit_exceeded(бюджет); згенерований код отримує точний статус. - Binary, довші за 64 байти, живуть у спільних буферах поза купою. Кожна
комірка купи, що посилається на такий буфер, тримає
std::shared_ptrі під час публікації додається до списку процесу поза купою (off-heap); демонтаж обходить список і відкидає ці посилання (binary поза купою). add(value)/copy_toкопіюють граф іншого процесу того самого runtime, зберігаючи його спільні частини й спільно використовуючи буфери поза купою; невдале копіювання нічого не змінює (копіювання між купами).used_wordsрахує виділені слова;capacity_wordsрахує блок купи й фрагменти;off_heap_wordsрахує буфери, на які посилається цей процес, кожен один раз. Опорні слова плюс слова поза купою ділять необов'язковий бюджетlimit_bytes; збирання залишає половину бюджету, що лишається після звільнення вцілілих, тож його вичерпання означає, що живі дані більше не вміщуються (поведінка під час збоїв).- Кожне використане слово розбирається як об'єкт із заголовком на початку,
cons-комірка або заповнювач (розкладка слів);
зарезервовані слова починаються обнуленими. Сирі слова
allocate()мають лишатися нулями або містити повні об'єкти.verify()обходить блок купи й кожен фрагмент і перевіряє, що кожен слот терма вказує на початок об'єкта того самого процесу (тести й налагодження; інакшеcorrupt_heap). collect(roots)копіює все, що досяжне з коренів процесу та кореневих слів хоста, у новий блок купи, звільняє старий блок і фрагменти, звільняє мертві binary поза купою й переписує корені (збирання). Згенерований код збирає сміття на входах у функції й у головах циклів comprehension, колиwants_collection()(збирання в згенерованому коді). Воно виконується лише в безпечній точці (жоден згенерований код не виконується поза областюSafePoint, немає відкритої резервації), інакшеunsafe_point; перелік коренів називає, що воно переписує; невдача виділити новий блок — цеout_of_memoryбез жодних змін.
Сервер коду й вбудовані функції
Кожен 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заморожує й публікує його; дублікати чи збої нічого не публікують.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відкладено; динамічне завантаження не підтримується.
Облік планувальника
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/design/ і runtime/include/{scheduler,process}.hpp.
Повідомлення реалізовано (процеси): надсилання
копіює повідомлення в купу отримувача й додає його до його вхідної черги
сигналів (runtime/include/mailbox.hpp).
Потоки
Робочі потоки планувальника (крок плану 56, робочі потоки) виконують процеси на кількох потоках одного runtime. Служби, які вони ділять, синхронізовано; усе інше лишається в межах потоку, що виконує свій процес, або захищене м'ютексом виконавця.
- Атоми (крок 54):
AtomStorageзахищає обидва індекси спільним м'ютексом. Пошуки (lookup,boolean,size) беруть його спільно;internшукає під спільним блокуванням, і лише новий запис бере ексклюзивне блокування, перевіряє знову й публікує елемент, тож змагальні виклики intern для одного запису отримують одне слово. Слова лишаються стабільними: елемент ніколи не змінюється й не видаляється до демонтажу. - Код (крок 55):
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 (крок 56):
ProcessNumbersвидає номери під ексклюзивним блокуванням і приймає слова pid під спільним. - Пам'ять (крок 56): облік на весь runtime (
RuntimeMemory) нараховує й звільняє атомарними операціями; нарахування ніколи не виводить облік за необов'язкове обмеження. - Ввід-вивід портів (кроки 57C–57F): потоки читання, запису й спостереження служби вводу-виводу та потік сокетів (порти) торкаються стану процесів лише через м'ютекс виконавця.
- Компонування: на Linux runtime додає
-pthreadдля своїх потоків; на Windows він компонує Winsock (ws2_32,mswsock) для сокетів.
Стандартний вивід
RuntimeOptions::standard_output (output.hpp)
отримує байти erlang:display/1, а пізніше standard_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::port (портів ще немає, крок плану 53), reference(ReferenceIdentity) і function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin для каталогізованої BIF | Status::not_implemented |
Clause