10 puntos por GN⁺ 2025-11-19 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2025-11-19
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_ptr de C++ también tiene el mismo problema, pero Rust lo resuelve distinguiendo entre dos tipos: Rc y Arc

    • El shared_ptr de C++ no usa mutex sino operaciones atómicas
      Es similar a Arc de Rust, y la implementación del blog simplemente es ineficiente
      Aun así, en C++ no existe un tipo equivalente a Rc, así que si quieres un puntero con conteo de referencias simple, igual hay un costo
    • En entornos con glibc y libstdc++, si no enlazas pthreads, shared_ptr no es thread-safe
      En 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
    • Siento que es mucho más importante hacer que el código no se caiga
      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
    • También se podría implementar el conteo de referencias con operaciones atómicas de C11 en lugar de mutex
      En ese caso no me queda claro cuál sería la ventaja de usar mutex
    • Los mutex de POSIX ya están implementados en varias plataformas, así que de hecho me parecen una API más general
  • 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

    • Gracias a esto conocí este proyecto por primera vez. Me parece un intento realmente genial
    • Pero no quisiera asumir la pérdida de rendimiento de un garbage collector
  • 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 UniquePtr o copiar un SharedPtr no se olvida actualizar el conteo de referencias, o quién gestiona la vida útil de los elementos de una intrusive list
    Al final, siento que el enfoque de este artículo no es tan distinto del viejo patrón #define xfree(p)

    • UniquePtr sí es posible porque se puede devolver una struct por valor
      Pero al copiar un SharedPtr no se incrementa automáticamente el conteo de referencias
    • Me da curiosidad por qué el patrón #define xfree(p) es malo
  • Se 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

    • Me costó encontrar información relacionada, pero al final parece que solo cambió la sintaxis y la funcionalidad en sí sigue siendo una extensión
  • 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

    • Fue interesante ver el proceso de crear un C más seguro sin meter todas las funciones de C++
      Aunque al final, viendo que hasta se imitan funciones de C++17, me pregunto si no sería mejor simplemente usar C++
    • Yo quiero un lenguaje parseable
      C sigue siendo fácil de manejar, pero C++ es tan complejo que es difícil acercarse a él sin un frontend
    • C es simple, así que es un buen lenguaje para hackear
      Al pasar a C++ todo se complica con la build chain, el name mangling, la dependencia de libstdc++, etc.
    • Este proyecto puede permitir solo algunas funciones de C++ para imponer una sintaxis restringida
      En cambio, si usas C++ con estilo C no existe esa clase de restricción
    • También es una limitación real que los vendors de CPU embebidos no ofrezcan compiladores de C++
  • No es compatible con manejo de excepciones basado en setjmp/longjmp
    En cambio, se puede integrar con un par de macros de cleanup inspiradas en pthread_cleanup_push de POSIX
    Usa cleanup_push(fn, type, ptr, init) y cleanup_pop(ptr) para implementar rutinas de limpieza basadas en stack
    Este enfoque tiene la ventaja de detectar errores de balance en tiempo de compilación

  • No debe confundirse con el verdadero safec.h de safeclib
    Consulta el header de safeclib

    • Me pregunto por qué alguien querría mantener una implementación de Annex K
      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.h
    Nim 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/unlikely y varias funciones más
    Consulta 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 haces que las macros generen código C++ (basado en destructores), se puede lograr incluso sin el atributo cleanup
      Si el código C también compila como C++, funciona bien
    • Incluso en Windows se puede desarrollar sin problema con MSYS2 + GCC
      También incluye un package manager
    • Como referencia, MSVC ahora ya soporta C17
  • 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

    • Yo tampoco sé a cuál cgrep se refiere, y me gustaría probarlo directamente