10 pontos por GN⁺ 2025-11-19 | 1 comentários | Compartilhar no WhatsApp
  • safe_c.h é um arquivo de cabeçalho personalizado de 600 linhas que adiciona ao C recursos de segurança e conveniência do C++ e Rust, usado na implementação do grep seguro para threads (cgrep) sem vazamentos de memória
  • Por meio de RAII, ponteiros inteligentes e atributo de limpeza automática (cleanup), automatiza o gerenciamento de recursos sem chamadas manuais de free()
  • Com vetores, views, tipo Result e macros de contrato, realiza com segurança a prevenção de buffer overflow, o tratamento de erros e a validação de pré-condições
  • Com liberação automática de mutex, macros para criação de threads e otimização de predição de desvios, garante segurança mantendo concorrência e desempenho
  • Como resultado, demonstra a possibilidade de escrever código C sem vazamentos nem segfaults com o mesmo desempenho (nível -O2)

Visão geral do safe_c.h

  • safe_c.h é um arquivo de cabeçalho que traz recursos de C++ e Rust para código C
    • Fornece o mesmo comportamento de RAII (limpeza automática) mesmo em compiladores que não suportam o atributo [[cleanup]] do C23 (como GCC 11 e Clang 18)
    • A macro CLEANUP(func) libera recursos automaticamente ao final da função
    • As macros LIKELY() e UNLIKELY() fazem otimização de predição de desvios no caminho quente

Gerenciamento de memória: UniquePtr e SharedPtr

  • UniquePtr é um ponteiro inteligente de propriedade única que chama free() automaticamente ao fim do escopo
    • Ao declarar com a macro AUTO_UNIQUE_PTR(), a memória é liberada automaticamente mesmo em caso de erro ou retorno antecipado
  • SharedPtr é uma estrutura com contagem de referências automática, que destrói o recurso quando a última referência é liberada
    • shared_ptr_init() e shared_ptr_copy() fazem o gerenciamento automático de aumento e redução de referências
    • É usado para gerenciar estruturas compartilhadas com segurança entre threads

Prevenção de buffer overflow: Vector e View

  • A macro DEFINE_VECTOR_TYPE() cria vetores com expansão automática e segurança de tipo
    • Realocação, gerenciamento de capacidade e limpeza (cleanup) são tratados automaticamente
    • Ao declarar com AUTO_TYPED_VECTOR(), o vetor é liberado automaticamente ao fim do escopo
  • StringView e Span são estruturas de referência não proprietária que tratam fatias de strings e arrays sem malloc separado
    • DEFINE_SPAN_TYPE() define um Span específico por tipo
    • Com verificação de limites, garantem acesso seguro a arrays

Tratamento de erros: tipo Result e RAII

  • A estrutura Result é um tipo de retorno que distingue sucesso e falha, semelhante a Result<T, E> do Rust
    • DEFINE_RESULT_TYPE() cria estruturas de resultado por tipo
    • RESULT_IS_OK() e RESULT_UNWRAP_ERROR() permitem um tratamento de erros claro
  • Em combinação com o atributo CLEANUP, os recursos são liberados automaticamente ao encerrar a função
    • A macro AUTO_MEMORY() faz a limpeza automática de memória alocada com malloc

Contratos e strings seguras

  • As macros requires() / ensures() explicitam as pré e pós-condições das funções
    • Em caso de falha, exibem uma mensagem de erro clara
  • safe_strcpy() é uma função de cópia com verificação do tamanho do buffer, prevenindo overflow
    • Em caso de falha, retorna false, permitindo tratamento seguro do erro

Concorrência: desbloqueio automático e macros de thread

  • Uma função de liberação automática de mutex baseada em CLEANUP ajuda a prevenir deadlocks
    • Ao fim do escopo, pthread_mutex_unlock() é chamado automaticamente
  • As macros SPAWN_THREAD() e JOIN_THREAD() simplificam a criação e o join de threads
    • Foram usadas na implementação do pool de threads de processamento de arquivos do cgrep

Otimização de desempenho

  • As macros LIKELY() / UNLIKELY() fornecem predição de desvios no caminho quente
    • Garantem um efeito de otimização em nível de PGO mesmo em builds com -O2
  • Mesmo com os recursos de segurança adicionados, não há perda de desempenho

Conclusão

  • O cgrep usando safe_c.h tem 2.300 linhas de código C e elimina mais de 50 chamadas manuais de free()
  • Mantendo o mesmo assembly e a mesma velocidade de execução, implementa código C seguro, sem vazamentos de memória nem segfaults
  • É um exemplo de combinação entre a simplicidade e a liberdade do C com segurança moderna
  • O autor pretende abordar em um texto posterior por que o cgrep é mais de 2 vezes mais rápido que o ripgrep e usa 20 vezes menos memória
  • O safe_c.h é adequado para projetos novos, embora se mencione que a abordagem baseada em macros pode aumentar a dificuldade de depuração
  • A verificação de exatidão e segurança foi feita com vários analisadores estáticos (GCC analyzer, ASAN, UBSAN, Clang-tidy etc.)

1 comentários

 
GN⁺ 2025-11-19
Comentários do Hacker News
  • Este texto mostra o problema de custo que surge ao implementar abstrações seguras (safe abstraction) em C
    A implementação de ponteiro compartilhado usa mutex do POSIX, então (1) não é independente de plataforma e (2) faz você pagar o overhead de mutex até em thread única
    Ou seja, não é uma ‘zero-cost abstraction’
    O shared_ptr do C++ também tem o mesmo problema, mas o Rust resolve isso separando em dois tipos: Rc e Arc

    • O shared_ptr do C++ usa operações atômicas, não mutex
      É semelhante ao Arc do Rust, e a implementação do blog é simplesmente ineficiente
      Ainda assim, como o C++ não tem um tipo equivalente ao Rc, ainda há custo quando se quer apenas um ponteiro com contagem de referências simples
    • Em ambientes com glibc e libstdc++, se você não linkar com pthreads, o shared_ptr não é thread-safe
      Em runtime ele procura símbolos de pthread e escolhe o caminho atômico ou não atômico
      Acho que seria melhor simplesmente usar atômicos sempre
    • Para mim, o mais importante é fazer o código não dar crash
      Ser multiplataforma, na maioria dos casos, é algo “bom de ter”
      O overhead de mutex incomoda, mas em CPUs modernas está dentro do aceitável
      Sei que Rust é excelente, mas o ecossistema de C é vasto demais para ser substituído por completo
    • Também daria para implementar a contagem de referências com operações atômicas do C11 em vez de mutex
      Nesse caso, não entendo muito bem qual seria a vantagem do mutex
    • Mutex do POSIX já é implementado em várias plataformas, então acho que é até uma API mais genérica
  • Existe um projeto para tornar C seguro em memória com um coletor de lixo chamado FUGC, criado por Fil (aka pizlonator)
    Ele pode ser aplicado ao código existente quase sem modificações e transforma C/C++ em uma linguagem memory-safe
    Veja o post relacionado no HN e o site oficial

    • Foi graças a isso que conheci esse projeto pela primeira vez. Achei uma tentativa muito legal
    • Mas eu não gostaria de aceitar a perda de desempenho de um coletor de lixo
  • Acho que este texto apresenta de forma um pouco equivocada o cerne da segurança de memória
    Liberação automática de variáveis locais ou checagem de limites por si só não bastam
    O verdadeiro problema é o gerenciamento do tempo de vida da memória ao longo do programa inteiro
    Por exemplo, ao retornar um UniquePtr ou copiar um SharedPtr, você não pode esquecer a contagem de referências; e no caso de uma intrusive list, quem gerencia o tempo de vida dos elementos?
    No fim, sinto que a abordagem deste texto não é tão diferente do antigo padrão #define xfree(p)

    • UniquePtr é possível porque dá para retornar a struct por valor
      Mas copiar SharedPtr não faz o incremento da contagem de referências automaticamente
    • Fiquei curioso por que o padrão #define xfree(p) seria ruim
  • Dizem que o C23 introduziu o atributo [[cleanup]], mas na prática isso é uma extensão do GCC, e precisa ser escrito como [[gnu::cleanup()]]
    Veja o código de exemplo

    • Eu estava com dificuldade para encontrar informação sobre isso; no fim, parece que só a sintaxe mudou, enquanto a funcionalidade em si continua sendo uma extensão
  • Havia uma piada do tipo: “C++: vejam o quanto outras linguagens sofrem para imitar sequer parte do meu poder”
    Fico me perguntando por que tentar imitar C++ com macros, mas de qualquer forma é uma tentativa interessante

    • Foi interessante ver o processo de criar um C mais seguro sem colocar todos os recursos do C++
      Mas, quando no fim você já está imitando até recursos do C++17, talvez não fosse melhor simplesmente usar C++
    • Eu quero uma linguagem analisável
      C ainda é fácil de lidar, mas C++ ficou complexo demais para ser abordado sem um frontend
    • C é simples e por isso é uma boa linguagem para hackear
      Ao passar para C++, tudo fica mais complicado com build chain, name mangling, dependência de libstdc++ etc.
    • Este projeto pode permitir só parte dos recursos do C++ e assim impor uma sintaxe restrita
      Já em C++ escrito em estilo C, não existe esse tipo de limitação
    • O fato de fornecedores de CPU embarcada não oferecerem compiladores C++ também é uma restrição prática
  • Não é compatível com tratamento de exceção baseado em setjmp/longjmp
    Em vez disso, dá para integrar com um par de macros de cleanup inspirado em pthread_cleanup_push do POSIX
    Usa-se cleanup_push(fn, type, ptr, init) e cleanup_pop(ptr) para implementar rotinas de limpeza baseadas em pilha
    Essa abordagem tem a vantagem de detectar erros de balanceamento em tempo de compilação

  • Não se deve confundir com o verdadeiro safec.h do safeclib
    Veja os headers do safeclib

    • Fico me perguntando por que alguém quer manter uma implementação do Annex K
      Ela é considerada um fracasso de design por causa do constraint handler global, e a maioria das toolchains não oferece suporte
      Veja também o documento relacionado
  • Se você usar a linguagem Nim, consegue tudo o que safe_c.h oferece
    Nim compila para C e entrega segurança e desempenho ao mesmo tempo
    Ele já oferece por padrão contagem automática de referências baseada em ARC, defer, Option[T], bounds-checking, likely/unlikely e vários outros recursos
    Veja o site oficial, a introdução ao ARC, view types, a documentação de Option e o template likely

  • Se o objetivo desta abordagem é a portabilidade, na prática o mais seguro é ficar no C99
    O compilador C do MSVC é complicado, mas para cross-platform ele é quase indispensável
    Eu também fiz um header parecido, mas não incluí utilitários de cleanup por causa dos problemas de portabilidade

    • Se você fizer as macros gerarem código C++ baseado em destrutores, isso funciona mesmo sem o atributo de cleanup
      Se o código C também compilar como C++, vai funcionar bem
    • Mesmo no Windows, dá para desenvolver tranquilamente com MSYS2 + GCC
      Ele também traz um gerenciador de pacotes
    • Só como referência, o MSVC agora suporta C17
  • No texto faltam links para o código do cgrep, mencionado várias vezes
    Há muitos projetos com esse nome no GitHub, mas a maioria é escrita em outras linguagens

    • Eu também não sei a qual cgrep ele se refere e gostaria de testar por conta própria