- 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
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_ptrdo C++ também tem o mesmo problema, mas o Rust resolve isso separando em dois tipos:RceArcshared_ptrdo C++ usa operações atômicas, não mutexÉ semelhante ao
Arcdo Rust, e a implementação do blog é simplesmente ineficienteAinda 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 simplesshared_ptrnão é thread-safeEm 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
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
Nesse caso, não entendo muito bem qual seria a vantagem do mutex
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
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
UniquePtrou copiar umSharedPtr, 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 valorMas copiar
SharedPtrnão faz o incremento da contagem de referências automaticamente#define xfree(p)seria ruimDizem 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
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
Mas, quando no fim você já está imitando até recursos do C++17, talvez não fosse melhor simplesmente usar C++
C ainda é fácil de lidar, mas C++ ficou complexo demais para ser abordado sem um frontend
Ao passar para C++, tudo fica mais complicado com build chain, name mangling, dependência de
libstdc++etc.Já em C++ escrito em estilo C, não existe esse tipo de limitação
Não é compatível com tratamento de exceção baseado em
setjmp/longjmpEm vez disso, dá para integrar com um par de macros de cleanup inspirado em
pthread_cleanup_pushdo POSIXUsa-se
cleanup_push(fn, type, ptr, init)ecleanup_pop(ptr)para implementar rotinas de limpeza baseadas em pilhaEssa abordagem tem a vantagem de detectar erros de balanceamento em tempo de compilação
Não se deve confundir com o verdadeiro
safec.hdo safeclibVeja os headers do safeclib
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.hofereceNim 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/unlikelye vários outros recursosVeja 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 o código C também compilar como C++, vai funcionar bem
Ele também traz um gerenciador de pacotes
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
cgrepele se refere e gostaria de testar por conta própria