Skip to content
mobius1qwe edited this page Nov 9, 2026 · 7 revisions

Internos: Utilitários 🚧

Home > Guia de contribuição > Internos: Utilitários

Para quem mexe no código: stream, compressão, criptografia, hash, JSON, storages e tokens. Aqui fica o porquê de cada parte ser como é, e o que não pode mudar sem quebrar algo. O código traz só a descrição curta de cada coisa.

Conteúdo da versão 2.0, ainda em desenvolvimento (branch 2.0).

Storages e o TBufDataset

O TBufDataset do FPC procura no próprio log de mudanças a cada Post, e um registro carregado entra nesse log: carregar um storage num TBufDataset era quadrático (40 mil registros, 2,5 s; 100 mil, 17 s). Todo storage chama RALStoragePosted depois de cada Post - que esvazia o log a cada mil registros - e o TRALStorageLink chama RALStorageLoaded no fim, porque o que um storage traz é dado, não mudança a aplicar. No Delphi as duas não fazem nada.

JSON, BSON, erros e bibliotecas (src/common) 🚧

  • Um JSON só. RALJson é parser e gravador do RAL nos dois compiladores: o mesmo texto sai igual em toda plataforma, e nenhum pacote depende de fpJSON ou de System.JSON. Números que vão para o fio continuam saindo pelo TRALJSONTextWriter e pelo RALJSONNumberText (forma curta com .), não pelo ToJSON do valor.
  • BSON canônico. TRALBSON.Parse devolve, por padrão, os tipos do BSON que o JSON não tem como JSON estendido ($numberLong, datas): é o que deixa uma ida e volta BSON → JSON → BSON sem perder o tipo. O storage BSON lê os dois formatos de data (o antigo, em segundos, e o de milissegundos).
  • Código antes da mensagem. ERALException guarda o ErrorCode e põe [RAL <código>] na frente da mensagem traduzida (RALErrorCodes liga código e texto nos três idiomas). Uma exceção que precisa descender da exceção de outra biblioteca fica fora disso: ERALfpConnectionClosed é um EHTTPClient, porque o SendUrl do fpHTTP a testa antes da EHTTPClient genérica; ela leva o texto puro e quem a pega monta a mensagem com a URL.
  • Carregar biblioteca. TRALDynLibs é um carregamento por objeto, com TryBind deixando nil o símbolo que falta: quem chama decide sem o que não vive. O RALTLS tem por cima dele o carregador por lista de nomes (libssl-3-x64.dll, libssl.so.3, ...): o primeiro que carrega vale, e os ponteiros resolvidos voltam a nil quando a biblioteca é liberada.

Texto em bytes (RALTypes)

StringToBytesUTF8 é uma cópia direta, sem TEncoding: StringRAL já é UTF-8. TEncoding.UTF8.GetBytes passa por UTF-16 e volta (duas alocações) e troca cada sequência inválida por U+FFFD: os bytes A3 9A 4F C2 00 7E FF 10 voltariam EF BF BD EF BF BD 4F EF BF BD 00 7E EF BF BD 10. Uma chave derivada é feita de bytes assim (ver HMACAsDigest em RALHashBase).

Tamanhos de buffer e de bloco (RALConsts)

  • DEFAULTBUFFERSTREAMSIZE (64 KB) é o teto do buffer de trabalho de toda transformação (zlib, zstd, brotli, AES, hashes, base64, hex, multipart): o buffer é Min(tamanho, 64 KB). Um buffer do tamanho do corpo seria uma cópia a mais em cada transformação, e o gzip roda na mesma velocidade com 16 KB e com 4 MB. Com ele, todo caminho em pedaços roda em corpos acima de 64 KB, então um defeito de pedaço (base64 com = no meio, hash sobrescrevendo um bloco parcial) aparece nos testes. O valor tem de ser múltiplo de 64 (o bloco dos hashes); o 3 e o 4 do base64 ele resolve sozinho.
  • DEFAULTCHUNKABOVE (8 MB): um bloco contíguo até 8 MB, lista de blocos acima. Medido no Delphi 13 Win64 com FastMM: um TMemoryStream enchido em pedaços de 64 KB reserva 2 MB além de um corpo de 4 MB, 7 MB além de 8 MB, 18 MB além de 16 MB (366 MB além de 512 MB), e enche 2 a 3 vezes mais devagar que a lista, que fica abaixo de 1% de sobra em todo tamanho. Em 8 MB o crescimento já custa o que juntar os blocos custa aos motores que precisam de memória contígua (mORMot2, QUIC).
  • DEFAULTCHUNKSIZE é 1 MB menos 1 KB: o cabeçalho do gerenciador de memória empurra uma potência de dois exata para o próximo grânulo de 64 KB. Blocos de 1 MB reservam 6,4% a mais que o corpo, e de 256 KB, 25%; de 1023 KB e 255 KB, 0,4%. O tamanho do bloco não muda a velocidade do gzip entre 64 KB e 64 MB.

Compressão (src/compress) 🚧

  • A classe de cada formato fica guardada no registro. RegisterCompress recebe a classe e CompressClasses guarda o ponteiro: a escolha do formato (GetBestCompress, várias vezes por pedido) não passa por GetClass, que trava o registro global de classes do RTL. CompressDefs existe só para a lista por nome do design.
  • ctDeflate é deflate cru nos dois compiladores, que é o que o FPC grava; o Delphi usa windowBits -15. Na leitura, StartsWithZlibHeader olha os dois primeiros bytes: o "deflate" do HTTP é zlib (RFC 9110 8.4.1.2), e o RAL e alguns servidores mandam cru. A leitura de gzip no FPC pula o cabeçalho e confere o trailer (CRC e tamanho) à mão, porque a zlib do FPC não lê o enquadramento gzip; a janela passada a ela é uma fatia do stream de quem chama, que pode ser a vista somente leitura do buffer de um motor.
  • Um corpo vazio sai comprimido mesmo assim: um gzip, um zlib ou um deflate de nada ainda é um stream (20, 8 e 2 bytes), e um decodificador estrito recusa um corpo de zero bytes debaixo de um Content-Encoding. O Decompress aceita o vazio como vazio.
  • Janela e memLevel pelo tamanho do corpo (ZLibSizes), com os pedaços na pilha: uma resposta pequena comprime sem alocar os 64 KB que o TCompressionStream alocaria, e blocos desse tamanho o gerenciador de memória serve debaixo de uma trava só.
  • O brotli é conduzido direto, um codificador para o corpo inteiro: o TBrotliCompressionStream do pascal_brotli perde 1 KB por stream no Flush e cria um codificador por Write, que é a coisa mais cara que o brotli faz (e pedaços comprimidos à parte comprimem pior). A saída é um stream brotli padrão.
  • O zstd e o brotli se desregistram no finalization: o RALCompress guarda o ponteiro da classe, e o pacote pode ser descarregado pela IDE sem ele.

Criptografia (src/crypto) 🚧

  • AES-CBC com IV aleatório por mensagem e HMAC-SHA256: IV (16), texto cifrado com PKCS#7, MAC (32) sobre IV e texto cifrado. As quatro formas de passar o corpo pela cifra trabalham com dois buffers de DEFAULTBUFFERSTREAMSIZE e assinam com o HMAC incremental, sem guardar uma segunda cópia da mensagem; o decifrar escreve cada pedaço em cima do texto cifrado de onde ele saiu, porque a leitura anda sempre à frente da escrita.
  • Sem threads. O CBC não se paraleliza na cifragem (cada bloco precisa do anterior), e um corpo pequeno pagaria a criação das threads.
  • A chave derivada (rkdPBKDF2) fica em cache por texto de chave: uma cifra é criada para cada corpo, e as 100 mil rodadas são caras de propósito. Ela é calculada fora da trava; duas threads que encontram a mesma chave nova ao mesmo tempo calculam as duas, o que custa tempo e mais nada. Sem derivação (rkdNone, o padrão), a chave curta é completada com zeros até o tamanho do AES.
  • Os hashes desligam overflow e range check na unit inteira: a aritmética é módulo 2^32 / 2^64 por definição, e um build Debug com {$Q+} levantaria EIntOverflow em todo bloco. Um pedaço que não completa o bloco soma ao que o bloco já tem (HashUpdate): 16 + 16 bytes têm de dar o mesmo resumo que 32 de uma vez.
  • JWT: só um objeto é cabeçalho (um array, um texto ou um número deixam o cabeçalho como estava, como texto que não interpreta); um texto que não é token não deixa nada do token anterior; uma claim gravada duas vezes é substituída, porque a lista é ordenada pela linha chave=valor inteira e uma chave repetida é JSON inválido; as datas (exp, iat, nbf) passam por RALDateTimeToGMT antes de virar tempo Unix.
  • Basic: uma credencial que não é base64 é uma credencial errada (401), não uma exceção do decodificador (500).
  • RandomBytes usa a fonte criptográfica do sistema (RtlGenRandom no Windows, /dev/urandom com o handle guardado nos outros): nonce, id de token e IV do AES não podem ser adivinháveis.
  • RALOpenSSL e RALCriptoOpenSSL ficam em modo Delphi no FPC: as funções do OpenSSL são variáveis procedurais, e no ObjFPC EVP_sha256 sem parênteses devolveria a variável em vez de chamar a função. Antes do OpenSSL 1.1.0 toda operação passa por uma trava (BeginOperation), porque a biblioteca não é thread-safe sozinha.
  • O certificado do RALX509 segue a receita do certificado de desenvolvimento do ASP.NET Core: folha (cA FALSE), digitalSignature + keyEncipherment, serverAuth, nomes e endereços no subjectAltName e os identificadores de chave. Um autoassinado com cA TRUE é recusado pelo Firefox como certificado de servidor, e um sem subjectAltName por todo navegador atual. O PKCS#12 é o que o OpenSSL 3 grava (PBES2 com PBKDF2-HMAC-SHA256 e AES-256-CBC, MAC HMAC-SHA256), que o SChannel lê a partir do Windows 10 1709.

Streams, texto e o que vem do fio (src/common, src/storage) 🚧

  • TRALChunkedStream guarda o corpo em blocos de BlockSize: acrescentar nunca move o que já está lá, e com ReleaseOnRead cada bloco lido volta na hora, então uma transformação segura entrada e saída ao custo de um bloco. O Read enche o pedido inteiro atravessando blocos, porque o RAL lê um tamanho com um Read só.
  • Toda leitura de tamanho fixo confere o que leu (ReadExact do TRALBinaryWriter e do TRALStorageBIN): esses valores vêm da rede, e uma leitura curta deixaria o valor com o lixo da pilha. Um byte que vira enumerado é conferido antes do cast (ReadEnum, RALIsFieldTypeOrdinal), e uma contagem que o resto do stream não comporta é recusada antes de alocar.
  • O memo viaja como texto UTF-16 little-endian no BIN, seja qual for a codificação do campo (ANSI no ftMemo do Delphi, UTF-8 no do FPC), porque o leitor o carrega num ftWideMemo. No JSON e no CSV o memo é o texto.
  • Os leitores de storage devolvem os campos somente leitura e reativam os controles num finally: um registro que falha no meio não pode deixar o dataset aberto para escrita.
  • CSV: o stream é lido byte a byte (um Char tem dois bytes no Delphi), o separador é o de FormatOptions, aspas dentro de um valor saem dobradas (RFC 4180) e o BOM é escrito como três bytes - um array dinâmico passado a um parâmetro const sem tipo é o ponteiro dele.
  • Multipart: o delimitador é -- mais o boundary e nada mais (RFC 2046), como o Content-Type o anuncia; o parser do libmicrohttpd (Sagui) ancora o delimitador no início da linha e descarta o corpo que difere. O boundary é aleatório, não do relógio: um boundary previsível deixa um valor forjado encerrar o multipart. Um boundary entre aspas (boundary="...", o que o HttpClient do .NET manda) perde as aspas.
  • TRALMIMEType procura a extensão num hash (endereçamento aberto, potência de dois, no máximo meio cheio), porque GetMIMEType roda para cada arquivo servido, e a lista é ordenada por AnsiCompareText, que no Windows ignora hífen: uma busca binária byte a byte não acharia uma extensão com hífen.
  • TRALStringListSafe.IsEmpty lê sem a trava: Count é um campo, e a resposta já está velha no instante em que volta, com trava ou sem; quem chama a usa como dica. As listas de segurança, vazias na maioria dos servidores, são perguntadas em todo pedido.
  • FixRoute devolve o mesmo texto quando o caminho já está certo (os motores e a busca de rota perguntam os dois) e junta barras repetidas numa passada só: linear no tamanho, seja quantas barras a URI tiver.

A rede própria (RALNetPackage, src/net) 🚧

Socket, TLS, WebSocket e proxy que o RAL usa sem motor (o MQTT hoje; e-mail, balanceador e clientes WebSocket depois). As restrições:

  • Um código só para todo sistema. Só as declarações de onde vem cada função (ws2_32 ou a libc) e um punhado de constantes mudam, todas num bloco no começo da implementação de RALSocket. Esperar é sempre poll (WSAPoll no Windows): o socket é não bloqueante por baixo, e Connect/WaitFor esperam com prazo.
  • Um relógio só (RALSocketNow, milissegundos que só andam para frente): os prazos dos sockets e os do protocolo de cima (o RALMQTTNow devolve o mesmo) se comparam.
  • A OpenSSL é carregada em execução, 1.1.1 ou 3.x, para que o pacote não dependa de nada na compilação; o TLS é um canal de camada (TRALLayerChannel) que entra por cima de um canal aberto, e é assim que o STARTTLS funciona.
  • A rede não conhece o protocolo de cima. Ela levanta ERALSocketError, ERALTLSError e ERALWebSocketError (com o CloseCode que a falha pede); quem está em cima traduz (o broker MQTT manda o DISCONNECT com o motivo certo). O subprotocolo do WebSocket é uma propriedade (Protocol), não uma constante.
  • Valores de enumeração publicados mantêm o nome antigo (mtv12, mpkHTTP): os formulários gravam o nome do valor.
  • O laço de eventos é da rede, o protocolo é do dono (TRALSocketLoop, em RALSocketLoop): uma thread aceita nos sockets que o dono escuta, N threads de poll ficam cada uma com uma fatia das conexões (a nova vai para a que tem menos), e um relógio chama o dono a cada TimerInterval. Só a thread dona lê e escreve o canal de uma conexão; as outras enfileiram e chamam Wake, que manda um datagrama para a própria thread e encerra a espera. O dono desce de TRALSocketLoopConnection e dá o protocolo em Serve (uma passada: ler, responder, prazos), HandleError, FlushOutput, HasOutput e Stopping (as últimas palavras quando o servidor para). O broker MQTT é o primeiro dono.
  • O laço não decide o que esperar: a conexão diz (PollEvents). O padrão é o que o canal quer mais RALPollOut com saída pendente; um dono devolve menos para parar de ler (contrapressão) ou só RALPollOut enquanto um connect termina. Returned diz o que o poll trouxe naquela passada, para Serve não ler à toa. Attach entrega uma conexão à mesma thread de outra: duas que conversam (os dois lados de um proxy) nunca precisam de trava. Thread.Context guarda um objeto do dono por thread.
  • Connect sem esperar (ConnectStart + PendingError) para quem está num laço: o socket fica gravável quando termina e o SO_ERROR diz se deu. No Windows, para um endereço de loopback, o SYN não é repetido (SIO_TCP_INITIAL_RTO): sem isso uma porta fechada só falha depois de ~2 s, porque o Windows insiste antes de aceitar o RST.
  • Retomada de sessão TLS do cliente. Os contextos são um por conexão, então as sessões ficam fora deles, em RALTLSSessions, pela chave de RALTLSSessionKey: o nome conferido, o host e a porta, e o certificado do cliente - uma sessão só volta para o mesmo servidor e a mesma identidade. Ela chega pelo retorno de chamada de sessão nova, porque no TLS 1.3 o ticket vem depois do handshake, com a primeira leitura. A regra do certificado roda de novo numa conexão retomada (a sessão guarda o certificado do servidor).
  • Um certificado por nome no servidor (AddCertificate): cada nome ganha um contexto filho, e o retorno de chamada de SNI troca o contexto da conexão (SSL_set_SSL_CTX). O filho responde o ALPN do pai e copia a confiança dos certificados de cliente (a verificação usa a loja do contexto em vigor). StrictServerName recusa com o alerta unrecognized_name.
  • Ler o ClientHello sem abrir o TLS (RALTLSPeekClientHello, sem OpenSSL; RALTLSPeekServerName com MSG_PEEK): um handshake pode vir partido em vários registros, e o que foi espiado continua no socket para a OpenSSL.
  • Fim sem close_notify é fim. A OpenSSL 1.1.1 avisa isso como SSL_ERROR_SYSCALL (lido como fechamento); a 3.x, como erro. O contexto liga SSL_OP_IGNORE_UNEXPECTED_EOF só na 3.x (o valor 0x80 era outra coisa antes), e os protocolos de cima delimitam as próprias mensagens.
  • O SCRAM é cripto pura (RALSCRAM, em src/crypto, no pacote-base): nenhum transporte; o MQTT (e depois SMTP, IMAP) leva as mensagens do seu jeito e traduz ERALSCRAMError no erro dele. Um usuário desconhecido percorre os mesmos passos com valores inventados: a resposta não conta quem existe.

O balanceador dedicado (RALBalancer) 🚧

  • Os dois lados de uma conexão são duas conexões do laço, na mesma thread (Attach): um lê direto no buffer de saída do outro, sem trava e sem cópia intermediária. Quem não esvazia o próprio buffer faz o outro parar de ler.
  • As rotas que as threads leem são uma cópia (TRALBalancerRouting), trocada inteira por Reload e guardada até o balanceador parar: um pedido em curso continua com a regra e o upstream que pegou. Pelo mesmo motivo os upstreams que saem de uso não são liberados pelo balanceador.
  • As conexões mantidas com os servidores de trás são da thread (um TRALBalancerPool em Thread.Context): o lado do cliente só pode usar uma conexão da sua thread. Uma ociosa que fica legível foi fechada pelo outro lado e sai.
  • O enquadramento é lido, não refeito: o chunked passa como veio e só é seguido até o fim, para saber onde a mensagem termina; bytes depois do fim de um pedido são o próximo pedido (pipelining) e voltam para a fila do cliente. Por isso as regras de contrabando são estritas: o balanceador e o servidor de trás têm de concordar sobre onde cada pedido acaba.
  • Uma resposta de erro fecha devagar (o lingering close do NGINX): o que o cliente ainda manda depois é lido e jogado fora por até 2 s. Fechar com bytes não lidos faz o sistema mandar RST, e o cliente perde a resposta (no Windows sempre).
  • No Windows, escutar em todos os endereços numa porta que outro programa tem só em 127.0.0.1 dá certo (no Linux, não): o teste de porta ocupada usa uma escuta em todos os endereços.

Clone this wiki locally