- safe_c.h es un archivo de encabezado personalizado de 600 líneas que agrega a C funciones de seguridad y comodidad de C++ y Rust, y se usa en una implementación de grep seguro para hilos y sin fugas de memoria (cgrep)
- Mediante RAII, punteros inteligentes y el atributo de limpieza automática (
cleanup), automatiza la gestión de recursos sin llamadas manuales a free()
- Con vectores, vistas, tipos
Result y macros de contrato, permite manejar de forma segura desbordamientos de búfer, tratamiento de errores y verificación de precondiciones
- Con liberación automática de mutex, macros para crear hilos y optimización de predicción de ramas, mantiene la concurrencia y el rendimiento sin sacrificar seguridad
- Como resultado, demuestra la posibilidad de escribir código C sin fugas ni segfaults con el mismo rendimiento (a nivel de
-O2)
Resumen de safe_c.h
- safe_c.h es un archivo de encabezado que lleva funciones de C++ y Rust al código C
- Ofrece el mismo comportamiento de RAII (limpieza automática) incluso en compiladores que no soportan el atributo
[[cleanup]] de C23 (como GCC 11 y Clang 18)
- La macro
CLEANUP(func) libera recursos automáticamente al salir de la función
- Las macros
LIKELY() y UNLIKELY() permiten optimizar la predicción de ramas en la ruta caliente
Gestión de memoria: UniquePtr y SharedPtr
- UniquePtr es un puntero inteligente de propiedad única que llama automáticamente a
free() al salir del alcance
- Al declararlo con la macro
AUTO_UNIQUE_PTR(), la memoria se libera automáticamente incluso si ocurre un error o hay un retorno anticipado
- SharedPtr es una estructura con conteo de referencias automático que destruye el recurso cuando se libera la última referencia
shared_ptr_init() y shared_ptr_copy() gestionan automáticamente el aumento y la disminución de referencias
- Se usa para administrar estructuras compartidas de forma segura entre hilos
Prevención de desbordamientos de búfer: Vector y View
- La macro DEFINE_VECTOR_TYPE() crea vectores autoexpandibles con seguridad de tipos
- Gestiona automáticamente la reasignación, la capacidad y la limpieza (
cleanup)
- Al declararlos con
AUTO_TYPED_VECTOR(), se liberan automáticamente al salir del alcance
- StringView y Span son estructuras de referencia no propietaria para trabajar con slices de cadenas y arreglos sin necesidad de
malloc adicional
DEFINE_SPAN_TYPE() define un Span para cada tipo
- Incluyen verificación de límites para garantizar acceso seguro a arreglos
Manejo de errores: tipo Result y RAII
- La estructura Result es un tipo de retorno que distingue éxito y fallo, similar a
Result<T, E> de Rust
DEFINE_RESULT_TYPE() crea estructuras de resultado por tipo
RESULT_IS_OK() y RESULT_UNWRAP_ERROR() permiten un manejo de errores claro
- Combinado con el atributo
CLEANUP, libera recursos automáticamente al terminar la función
- La macro
AUTO_MEMORY() limpia automáticamente la memoria asignada con malloc
Contratos y cadenas seguras
- Las macros requires() / ensures() permiten declarar precondiciones y postcondiciones de las funciones
- Si fallan, muestran mensajes de error claros
- safe_strcpy() es una función de copia con verificación del tamaño del búfer, lo que evita desbordamientos
- Si falla, devuelve
false, permitiendo un manejo seguro del error
Concurrencia: desbloqueo automático y macros de hilos
- Una función de desbloqueo automático de mutex basada en
CLEANUP ayuda a prevenir deadlocks
- Al salir del alcance, llama automáticamente a
pthread_mutex_unlock()
- Las macros
SPAWN_THREAD() y JOIN_THREAD() simplifican la creación y unión de hilos
- Se usan en la implementación del pool de hilos para procesar archivos en cgrep
Optimización de rendimiento
- Las macros
LIKELY() / UNLIKELY() ofrecen predicción de ramas en la ruta caliente
- Consiguen efectos de optimización similares a PGO incluso en compilaciones con
-O2
- Aunque se agregan funciones de seguridad, no hay pérdida de rendimiento
Conclusión
- cgrep, construido con safe_c.h, tiene 2,300 líneas de código C y elimina más de 50 llamadas manuales a
free()
- Mantiene el mismo ensamblado y velocidad de ejecución mientras logra código C seguro, sin fugas de memoria ni segfaults
- Es un ejemplo de cómo combinar seguridad moderna manteniendo la simplicidad y libertad de C
- El autor planea explicar en una publicación posterior por qué cgrep es más de 2 veces más rápido que ripgrep y usa 20 veces menos memoria
- Se menciona que safe_c.h es adecuado para proyectos nuevos y que, al estar basado en macros, puede aumentar la dificultad de depuración
- La corrección y seguridad se verificaron con varios analizadores estáticos (GCC analyzer, ASAN, UBSAN, Clang-tidy, etc.)
1 comentarios
Comentarios en Hacker News
Este artículo muestra el problema del costo que surge al implementar abstracciones seguras (safe abstractions) en C
La implementación de punteros compartidos usa un mutex de POSIX, así que (1) no es independiente de la plataforma y (2) incluso en un solo hilo se paga el overhead del mutex
Es decir, no es una ‘zero-cost abstraction’
El
shared_ptrde C++ también tiene el mismo problema, pero Rust lo resuelve distinguiendo entre dos tipos: Rc y Arcshared_ptrde C++ no usa mutex sino operaciones atómicasEs similar a
Arcde Rust, y la implementación del blog simplemente es ineficienteAun así, en C++ no existe un tipo equivalente a
Rc, así que si quieres un puntero con conteo de referencias simple, igual hay un costopthreads,shared_ptrno es thread-safeEn tiempo de ejecución busca símbolos de pthread para elegir entre la ruta atómica o no atómica
Yo pensaría que sería mejor usar siempre atómicos
La compatibilidad multiplataforma, en la mayoría de los casos, es más bien ‘algo bueno de tener’
El overhead del mutex molesta, pero en CPUs modernas está dentro de un nivel tolerable
Sé que Rust es excelente, pero el ecosistema de C es tan grande que es difícil reemplazarlo por completo
En ese caso no me queda claro cuál sería la ventaja de usar mutex
Hay un proyecto para hacer C memory-safe con un garbage collector llamado FUGC, creado por Fil (aka pizlonator)
Se puede aplicar al código existente casi sin cambios y convierte C/C++ en un lenguaje memory-safe
Consulta el post relacionado en HN y el sitio oficial
Creo que este artículo expresa de forma algo equivocada el núcleo de la memory safety
La liberación automática de variables locales o los bounds checks no son suficientes
El verdadero problema es la gestión de la vida útil de la memoria en todo el programa
Por ejemplo, si al devolver un
UniquePtro copiar unSharedPtrno se olvida actualizar el conteo de referencias, o quién gestiona la vida útil de los elementos de una intrusive listAl final, siento que el enfoque de este artículo no es tan distinto del viejo patrón
#define xfree(p)UniquePtrsí es posible porque se puede devolver una struct por valorPero al copiar un
SharedPtrno se incrementa automáticamente el conteo de referencias#define xfree(p)es maloSe dice que C23 introdujo el atributo
[[cleanup]], pero en realidad es una extensión de GCC y hay que escribirlo como[[gnu::cleanup()]]Consulta este código de ejemplo
Había un chiste: “C++: miren cuánto sufren otros lenguajes para imitar aunque sea una parte de mi poder”
Me da curiosidad por qué querrían imitar C++ con macros, pero de cualquier modo es un intento interesante
Aunque al final, viendo que hasta se imitan funciones de C++17, me pregunto si no sería mejor simplemente usar C++
C sigue siendo fácil de manejar, pero C++ es tan complejo que es difícil acercarse a él sin un frontend
Al pasar a C++ todo se complica con la build chain, el name mangling, la dependencia de libstdc++, etc.
En cambio, si usas C++ con estilo C no existe esa clase de restricción
No es compatible con manejo de excepciones basado en
setjmp/longjmpEn cambio, se puede integrar con un par de macros de cleanup inspiradas en
pthread_cleanup_pushde POSIXUsa
cleanup_push(fn, type, ptr, init)ycleanup_pop(ptr)para implementar rutinas de limpieza basadas en stackEste enfoque tiene la ventaja de detectar errores de balance en tiempo de compilación
No debe confundirse con el verdadero
safec.hde safeclibConsulta el header de safeclib
Se considera un fracaso de diseño por el constraint handler global y la mayoría de las toolchains no lo soportan
Consulta este documento relacionado
Si usas el lenguaje Nim, puedes obtener todas las funciones que ofrece
safe_c.hNim compila a C y ofrece seguridad y rendimiento al mismo tiempo
Incluye de forma nativa conteo automático de referencias basado en ARC,
defer,Option[T], bounds-checking,likely/unlikelyy varias funciones másConsulta el sitio oficial, la introducción a ARC, view types, la documentación de Option y la plantilla likely
Si este enfoque apunta a la portabilidad, en la práctica lo más seguro es quedarse en C99
El compilador de C de MSVC es problemático, pero para multiplataforma es casi indispensable
Yo también hice un header parecido, pero por problemas de portabilidad no incluí utilidades de cleanup
Si el código C también compila como C++, funciona bien
También incluye un package manager
Falta un enlace al código de cgrep, que se menciona varias veces en el artículo
En GitHub hay muchos proyectos con ese mismo nombre, pero la mayoría están escritos en otros lenguajes
cgrepse refiere, y me gustaría probarlo directamente