Runtime
clause_runtime é uma biblioteca C++23 sem LLVM. Detém os contextos, os heaps
dos processos, os átomos, os módulos carregados e o registo do ciclo de vida, e
executa processos Erlang em workers do escalonador (processos).
Todas as APIs são internas ao projeto; as chamadas do anfitrião têm de ser
serializadas por runtime e não podem sobrepor-se a uma execução do programa,
cujos workers se sincronizam entre si (threads).
Ligação
Ligar exatamente um runtime compilado para o alvo através de
Clause::generated_program:
add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)
Traz o arquivo, os cabeçalhos de ABI/runtime e o C++23, mas não o LLVM. link_consumer.cpp mostra um ciclo de vida completo.
Ciclo de vida
Runtime::start(options)→std::expected<std::unique_ptr<Runtime>, Status>. Valores por omissão: versão atual da ABI e largura nativa dos termos,max_atoms2^20 (no máximo 2^26; os programas definem-no com--max-atoms, opções do runtime). O número de contextos não é limitado.process_heapeprocess_stacksão as opções dos contextos criados porcreate_context();memory_limit_bytesé o limite global do runtime opcional, indicado pormemory_bytes(). Por omissão, nenhum tem limite.create_context()oucreate_context(heap_options, stack_options)→ProcessContext*emprestado, estável até ser destruído. Valores por omissão do heap: heap mínimo de 233 palavras (min_heap_words) e sem limite de memória (limit_bytes=UNLIMITED_HEAP_BYTES; um orçamento definido é um múltiplo da palavra pelo menos igual ao heap mínimo). A pilha também não tem limite, a menos queStackOptions::limit_wordsesteja definido.destroy_context(ctx),shutdown(): shutdown devolvebusyenquanto houver contextos; depois disso tem sucesso de forma idempotente e as chamadas seguintes devolvemstopped. O destrutor limpa os contextos restantes.- As identidades de contextos e de runtimes não são reutilizadas; o seu
esgotamento é uma falha. A identidade de um contexto inclui o seu número de
pid (pids). Um ponteiro de contexto é um
empréstimo, não uma identidade.
lifetime()dá um token fraco que indicaalive() == falseantes da desmontagem do heap. - As chamadas do ciclo de vida nunca lançam exceções e são silenciosas. Valores de estado: abi.md.
Ordem de desmontagem: fechar os registos do escalonador → destruir os contextos → libertar os registos de código → destruir o serviço do escalonador → tabela de átomos por último. Os handles de funções resolvidas mantêm vivos o código e as grafias dos átomos após a desmontagem do runtime, mas nunca um processo.
Memória dos processos
Cada contexto detém um ProcessHeap: um único bloco de heap criado pela sua
primeira alocação, com tamanho max(min_heap_words, request), mais uma cadeia
de fragmentos de heap pertencentes ao mesmo processo. As palavras só se movem
quando o anfitrião chama collect() num ponto seguro.
O heap segue o desenho clássico do ERTS; runtime-heap.md é o seu contrato (disposição, áreas, dimensionamento, admissão, raízes, recolha).
allocate(words)devolve armazenamento de palavras a zero.reservedá uma reserva só movível, com commit explícito e rollback automático. Uma reserva de cada vez por heap: construir primeiro os filhos e reservar o pai por último.- A alocação por incremento de ponteiro (bump allocation) preenche o bloco de
heap; um pedido que não cabe vai para o fragmento mais recente ou, caso
contrário, para um novo fragmento de tamanho
max(min_heap_words, request)(limitado pelo orçamento restante). As palavras só são alinhadas à palavra. O rollback repõe o topo da área, descarta um fragmento (ou o bloco de heap) criado pela reserva e restaura a contabilidade com exatidão. - Rejeita zero, overflow e orçamento esgotado antes de publicar. Erros:
out_of_memory(alocação) oulimit_exceeded(orçamento); o código gerado recebe o estado exato. - Os binaries com mais de 64 bytes residem em buffers partilhados fora do heap.
Cada célula do heap que se refere a um deles contém um
std::shared_ptre junta-se à lista off-heap do processo quando é publicada; a desmontagem percorre a lista e liberta essas referências (binaries off-heap). add(value)/copy_tocopiam um grafo de outro processo do mesmo runtime, mantendo a sua partilha e partilhando os buffers off-heap; uma cópia falhada não altera nada (cópia entre heaps).used_wordsconta as palavras alocadas;capacity_wordsconta o bloco de heap e os fragmentos;off_heap_wordsconta os buffers que este processo referencia, cada um uma vez. As palavras de suporte e as off-heap partilham o orçamento opcionallimit_bytes; uma recolha mantém livre metade do orçamento que resta após os sobreviventes, pelo que esgotá-lo significa que os dados vivos já não cabem (comportamento em caso de falha).- Cada palavra usada interpreta-se como um objeto encabeçado por um header,
uma célula cons ou preenchimento
(disposição das palavras); as palavras
reservadas começam a zero. As palavras de
allocate()em bruto têm de ficar a zero ou conter objetos completos.verify()percorre o bloco de heap e todos os fragmentos e verifica que cada slot de termo aponta para o início de um objeto do mesmo processo (testes e depuração; caso contráriocorrupt_heap). collect(roots)copia tudo o que é alcançável a partir das raízes do processo e das palavras-raiz do anfitrião para um novo bloco de heap, liberta o bloco antigo e os fragmentos, liberta os binaries off-heap mortos e reescreve as raízes (recolha). O código gerado faz a recolha nas entradas de funções e nas cabeças dos ciclos das comprehensions quandowants_collection()(recolha no código gerado). Só corre num ponto seguro (nenhum código gerado a correr fora de um âmbitoSafePoint, nenhuma reserva aberta), caso contráriounsafe_point; o inventário de raízes lista o que reescreve; uma falha na alocação do novo bloco éout_of_memory, sem nada alterado.
Servidor de código e builtins
Cada runtime detém um 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});
- Um
ModuleRegistryassocia um nome/aridade exato (≤ 255) a um callable só comTerm.loadcongela-o e publica-o; duplicados ou falhas não publicam nada. resolvedistingue entre módulo em falta e export em falta.ResolvedFunctionfixa a imagem do módulo;callverifica a aridade, os argumentos e os resultados, e converte exceções do anfitrião em falhas.- Os corpos nativos têm de ser síncronos e não bloqueantes, e não podem reter o contexto nem o span de argumentos.
- Um catálogo limitado de BIFs diferidos conhecidos (
self/0,length/1,spawn/3,spawn_link/3,send/2,make_ref/0,garbage_collect/0,apply/3(o código gerado chama antesapply/2,3através dos serviços de chamada dinâmica, funs),tuple_size/1,+/2) indicanot_implemented; os outros nomes não registados devolvemunknown_builtinsilenciosamente. Este caminho do anfitrião não chega aos builtins de produção. - Os builtins de produção residem no
BuiltinRegistrydo servidor, registados no arranque do runtime; o código gerado, as chamadas dinâmicas e os funs chegam a eles através da ponte de builtins. CodeServer::export_frameencontra oFrameDescriptorde um export pelo módulo, pelo átomo da função e pela aridade;function_frameacrescenta os builtins para chamadas dinâmicas;external_funinterna as definições de funs externos construídos em tempo de execução.CodeServer::unloadestá adiado; o carregamento dinâmico não é suportado.
Registo do escalonador
SchedulerService (scheduler.hpp)
regista apenas o ciclo de vida dos processos; não executa código.
register_process(context)uma vez por contexto (mesmo runtime); os duplicados devolvemalready_registered.remove_process(id)retira um registo que não esteja em execução.begin_dispatch→ em execução;finish_dispatch→ executável, em espera ou terminado (com razão).set_suspendedalterna um indicador nos registos executáveis/em espera. As outras transições devolveminvalid_transition.request_shutdown()fecha o registo, o despacho e o controlo de suspensão; a inspeção e as devoluções continuam disponíveis para o esvaziamento.runeexecuteindicamnot_implemented.
Os esboços de desenho dos workers e dos processos encontram-se em
runtime/design/ e runtime/include/{scheduler,process}.hpp.
As mensagens estão implementadas (processos): um envio
copia a mensagem para o heap do recetor e acrescenta-a à sua caixa de entrada
de sinais (runtime/include/mailbox.hpp).
Threads
Os workers do escalonador (step 56 do plano, workers) executam processos em várias threads de um runtime. Os serviços que partilham são sincronizados; tudo o resto fica confinado à thread que executa o seu processo ou protegido pelo mutex do executor.
- Átomos (step 54):
AtomStorageprotege ambos os índices com um mutex partilhado. As consultas (lookup,boolean,size) partilham-no;internconsulta sob o bloqueio partilhado e só uma grafia nova obtém o bloqueio exclusivo, verifica de novo e publica a entrada, pelo que interns concorrentes da mesma grafia obtêm uma única palavra. As palavras mantêm-se estáveis: uma entrada nunca é alterada nem removida antes da desmontagem. - Código (step 55):
CodeServerprotege os seus módulos e as definições de funs externos com um mutex partilhado. As consultas (find_module,resolve,atom_word,record_definition,fun_definition,export_frame,function_frame,owns) partilham-no;loade a construção de uma nova definição de fun externo obtêm-no em exclusivo, pelo que registos concorrentes do mesmo nome publicam um único módulo (os outros recebemduplicate_module) e chamadas concorrentes aexternal_funobtêm uma única definição. O registo de builtins é preenchido no arranque do runtime, antes de qualquer worker correr, e depois só é lido. - Fixações: os módulos nunca são removidos enquanto o runtime vive (o
descarregamento está adiado) e o servidor é destruído depois de todos os
contextos, pelo que as definições, os frames e os slots de átomos que
devolveu permanecem válidas para todas as invocações e todas as células de
fun. Os handles de
ResolvedFunctionefind_moduletambém mantêm o seu módulo depois de o runtime desaparecer. - Números de pid (step 56):
ProcessNumbersemite números sob um bloqueio exclusivo e admite palavras de pid sob um bloqueio partilhado. - Memória (step 56): a conta global do runtime (
RuntimeMemory) debita e liberta com operações atómicas; um débito nunca leva a conta além de um limite opcional. - E/S de portas (steps 57C–57F): as threads de leitura, de escrita e de vigilância do serviço de E/S e a thread de sockets (portas) só tocam no estado dos processos através do mutex do executor.
- Ligação: no Linux, o runtime acrescenta
-pthreadpara as suas threads; no Windows, liga o Winsock (ws2_32,mswsock) para os sockets.
Saída padrão
RuntimeOptions::standard_output (output.hpp)
recebe os bytes de erlang:display/1 e, mais tarde, de standard_io. Por
omissão, escreve no stdout do processo através do stdio de C (com buffer); um
destino do anfitrião devolve false para indicar uma escrita falhada.
erlang:display/1 apresenta o seu argumento no
estilo de display, escreve o texto e uma mudança de linha
numa só escrita e devolve true. Os limites de apresentação e as escritas
rejeitadas tornam-se estados de infraestrutura (resource_limit,
output_failure, ...) no canal verificado, nunca exceções Erlang.
Arranque do programa
CLAUSE_main_v1 (startup.hpp,
runtime/src/startup/) executa um programa inteiro para o main gerado:
verifica a ABI de cada descritor, inicia um runtime por omissão, regista todos
os módulos antes de qualquer código de entrada, constrói o argv no contexto de
entrada, chama a entrada e converte o resultado no código de saída dos
executáveis. Os relatórios vão para o stderr
depois de o stdout ser esvaziado; o contexto e o runtime são desmontados por
ordem em todos os caminhos. CLAUSE_halt_v1 implementa erlang:halt/0,1
(abort chama std::abort).
Serviços adiados
Estes emitem uma linha [feature] notimpl (funcionalidades) e
não alteram nenhum estado:
| Fronteira | Erro |
|---|---|
TermFactory::port (ainda sem portas, step 53 do plano), reference(ReferenceIdentity) e function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin num BIF catalogado | Status::not_implemented |
Clause