Cedro atlántico azul Cedro Español, English Sentido-Labs.com

Cedro es una extensión del lenguaje C que funciona como pre-procesador con cuatro prestaciones:

  1. El operador pespunte @backstitch» en inglés].
  2. Devolución diferida de recursosdefer»].
  3. Macros de bloque.
  4. Inclusión binaria.

Para activarlo, el fichero fuente debe contener esta línea: #pragma Cedro 1.0
Si no, el fichero se copia directamente a la salida.

Uso: cedro [opciones] fichero.c [fichero2.c … ] El resultado va a stdout, puede usarse sin fichero intermedio así: cedro fichero.c | cc -x c - -o fichero Es lo que hace el programa cedrocc: cedrocc -o fichero fichero.c Con cedrocc, las siguientes opciones son implícitas: --discard-comments --insert-line-directives --apply-macros Aplica las macros: pespunte, diferido, etc. (implícito) --escape-ucn Encapsula los caracteres no-ASCII en identificadores. --no-apply-macros No aplica las macros. --no-escape-ucn No encapsula caracteres en identificadores. (implícito) --discard-comments Descarta los comentarios. --discard-space Descarta los espacios en blanco. --no-discard-comments No descarta los comentarios. (implícito) --no-discard-space No descarta los espacios. (implícito) --insert-line-directives Inserta directivas #line. --no-insert-line-directives No inserta directivas #line. (implícito) --print-markers Imprime los marcadores. --no-print-markers No imprime los marcadores. (implícito) --enable-core-dump Activa el volcado de memoria al estrellarse. --no-enable-core-dump No activa el volcado de memoria al estrellarse. (implícito) --benchmark Realiza una medición de rendimiento. --version Muestra la versión: 1.0 El «pragma» correspondiente es: #pragma Cedro 1.0

La opción --escape-ucn encapsula los caracteres Unicode® fuera del intervalo ASCII, cuando forman parte de un identificador, como nombres de caracteres universales C99 («C99 standard», página 65, «6.4.3 Universal character names»), lo que puede servir para compiladores más antiguos sin capacidad UTF-8 como GCC antes de la versión 10.

Para la documentación (en inglés) de la API, véase doc/api/index.html tras ejecutar make doc que necesita tener Doxygen instalado.

El segundo ejecutable, cedrocc, permite usar Cedro como si fuera parte del compilador C.

Uso: cedrocc [opciones] <fichero.c> [ fichero2.o … ] Ejecuta Cedro en el primer nombre de fichero que acabe en «.c», y compila el resultado con «cc -x c -» mas los otros argumentos. cedrocc -o fichero fichero.c cedro fichero.c | cc -x c - -o fichero Se puede especificar el compilador, p.ej. gcc: CEDRO_CC='gcc -x c -' cedrocc … Para depuración, esto escribe el código que iría entubado a cc, en stdout: CEDRO_CC='' cedrocc …

Hay un tercer ejecutable, cedro-new, que produce un borrador de programa de manera similar a cargo new en Rust. cedro new … en realidad ejecuta cedro-new …. El contenido se produce a partir de la plantilla en el directorio template/, que se incluye en el ejecutable cedro-new al compilarlo.

Uso: cedro-new [opciones] <nombre> Crea un directorio llamado <nombre>/ con la plantilla. -h, --help Muestra este mensaje. -i, --interactive Pregunta por los nombres de programa y proyecto. Si no, se eligen a partir del nombre del directorio.

Operador pespunte: @ #backstitch-operator

Hilvana un valor a través de una secuencia de llamadas de función, como primer parámetro para cada una.

Es una versión explícita de lo que hacen otros lenguajes de programación para implementar funciones miembro, y el resultado es un patrón habitual en bibliotecas en C.

objeto @ f(a), g(b); f(objeto, a); g(objeto, b);
&objeto @ f(a), g(b); f(&objeto, a); g(&objeto, b);
objeto.casilla @ f(a), g(b); f(objeto.casilla, a); g(objeto.casilla, b);
int x = (objeto @ f(a), g(b)); int x = (f(objeto, a), g(objeto, b));

Esto es el operador coma del C, lo mismo que

f(objeto, a); int x = g(objeto, b);
objeto @prefijo_... f(a), g(b); prefijo_f(objeto, a); prefijo_g(objeto, b);
objeto @..._sufijo f(a), g(b); f_sufijo(objeto, a); g_sufijo(objeto, b);
contexto_gráfico @nvg... BeginPath(), Rect(100,100, 120,30), Circle(120,120, 5), PathWinding(NVG_HOLE), FillColor(nvgRGBA(255,192,0,255)), Fill();
nvgBeginPath(contexto_gráfico); nvgRect(contexto_gráfico, 100,100, 120,30); nvgCircle(contexto_gráfico, 120,120, 5); nvgPathWinding(contexto_gráfico, NVG_HOLE); nvgFillColor(contexto_gráfico, nvgRGBA(255,192,0,255)); nvgFill(contexto_gráfico);

Para cada segmento separado por comas, si empieza con uno de los tókens[”, “++”, “--”, “.”, “->”, “=”, “+=”, “-=”, “*=”, “/=”, “%=”, “<<=”, “>>=”, “&=”, “^=”, “|=”, o si no hay nada que parezca una llamada de función, el punto de inserción es el comienzo del segmento:

ristra_de_números @ [3]=44, [2]=11; ristra_de_números[3]=44; ristra_de_números[2]=11;
*ristra_de_números++ = @ 1, 2; *ristra_de_números++ = 1; *ristra_de_números++ = 2;
punto_central_de_figura @ .x=44, .y=11; punto_central_de_figura.x=44; punto_central_de_figura.y=11;

La parte de objeto se puede omitir, lo que sirve por ejemplo para añadir prefijos o sufijos a enumeraciones:

typedef enum { @TOKEN_... ESPACIO, PALABRA, NUMERO } TipoDeToken; typedef enum { TOKEN_ESPACIO, TOKEN_PALABRA, TOKEN_NUMERO } TipoDeToken;

Nota: el símbolo @ no se reconoce cuando se escribe \u0040, pero se convierte en @ en la salida. Esto se puede usar para encapsularlo al encadenar Cedro con otro pre-procesador que lo use.

Buscando realizaciones anteriores de esta idea he encontrado magma (2014), donde se llama doto. Es una macro para el pre-procesador cmacro que tiene el inconveniente de necesitar el compilador Common Lisp SBCL.

Los lenguajes funcionales suelen tener un operador similar aunque hilvana el resultado de la primera función como primer parámetro de la siguiente etc. en vez del mismo valor para todas las funciones. Por ejemplo, el equivalente de f₃(f₂(f₁(x))):

Ada 2005 introdujo una prestación llamada notación prefija [«prefixed-view notation»] que es más parecida al C++ ya que la función exacta que se ejecuta no se puede determinar sin conocer qué métodos están implementados para el tipo de objeto.

Devolución diferida de recursos: #deferred-resource-release

Mueve el código de devolución de una variable al final de su alcance, incluídos los puntos de salida break, continue, goto, return.

En C, los recursos deben devolverse al sistema explícitamente una vez no son necesarios, lo que generalmente ocurre bastante lejos de la parte donde se reservaron. Al pasar el tiempo y acumularse cambios en el programa, es fácil olvidar devolverlos en todos los casos o intentar devolver un recurso dos veces.

Otros lenguages de programación tienen mecanismos para devolución automática de recursos: C++ por ejemplo, usa funciones llamadas destructores que se ejecutan de manera implícita al salir del alcance de una variable.

El lenguaje Go introdujo una notación explícita llamada «defer» que pega mejor con el estilo del C. La primera diferencia es que en Go, todas las devoluciones ocurren al salir de la función, mientras que con Cedro las devoluciones ocurren al salir de cada bloque, como hacen los destructores en C++.

Hay más diferencias, como por ejemplo que en Go se puede usar para modificar el valor de retorno de la función, y que Cedro ni siquiera intenta tratar con longjmp(), exit(), thrd_exit() etc. porque sólo podría aplicar las acciones diferidas en la función actual, no en otras functiones que llamaran a ésta. Véase “A defer mechanism for C” (artículo académico publicado como PDF en la conferencia SAC’21) para una implementación a nivel de compilador que efectivamente trata con el longjmp() y con el desenrollado de la pila [«stack unwinding»].

En Cedro, la función de devolución se marca con la palabra clave C auto que no se necesita en código estándar C porque es implícita. Si quiere usar Cedro con código estándar C que ya usa auto, puede primero reemplazarla con signed ya que tiene el mismo efecto.

En este ejemplo, hay un depósito de texto y un fichero que deben ser devueltos al sistema:

#include <stdio.h> #include <stdlib.h> #include <errno.h> #pragma Cedro 1.0 int repite_letra(char letra, size_t cuenta, char* nombre_de_fichero) { char* texto = malloc(cuenta + 1); if (!texto) return ENOMEM; auto free(texto); for (size_t i = 0; i < cuenta; ++i) { texto[i] = letra; } texto[cuenta] = 0; if (nombre_de_fichero) { FILE* fichero = fopen(nombre_de_fichero, "w"); if (!fichero) return errno; auto fclose(fichero); fwrite(texto, sizeof(char), cuenta, fichero); fputc('\n', file); } printf("Repetido %lu veces: %s\n", cuenta, texto); return 0; } int main(void) { return repite_letra('A', 6, "aaaaaa.txt"); }

#include <stdio.h> #include <stdlib.h> #include <errno.h> int repite_letra(char letra, size_t cuenta, char* nombre_de_fichero) { char* texto = malloc(cuenta + 1); if (!texto) return ENOMEM; for (size_t i = 0; i < cuenta; ++i) { texto[i] = letra; } text[cuenta] = 0; if (nombre_de_fichero) { FILE* fichero = fopen(nombre_de_fichero, "w"); if (!fichero) { free(texto); return errno; } fwrite(texto, sizeof(char), cuenta, fichero); fputc('\n', file); fclose(fichero); } printf("Repetido %lu veces: %s\n", cuenta, texto); free(texto); return 0; } int main(void) { return repite_letra('A', 6, "aaaaaa.txt"); }

Compilación con GCC or clang, a la izquierda ejecutando explícitamente el compilador, y a la derecha usando cedrocc que tiene el mismo efecto:

$ cedro repite.c | cc -o repite -x c - $ ./repite Repeated 6 times: AAAAAA $ cat aaaaaa.txt AAAAAA $ valgrind --leak-check=yes ./repeat … ==8795== HEAP SUMMARY: ==8795== in use at exit: 0 bytes in 0 blocks ==8795== total heap usage: 4 allocs, 4 frees, 5,599 bytes allocated ==8795== ==8795== All heap blocks were freed -- no leaks are possible $ cedrocc -o repite repite.c $ ./repite Repeated 6 times: AAAAAA $ cat aaaaaa.txt AAAAAA $ valgrind --leak-check=yes ./repeat … ==8795== HEAP SUMMARY: ==8795== in use at exit: 0 bytes in 0 blocks ==8795== total heap usage: 4 allocs, 4 frees, 5,599 bytes allocated ==8795== ==8795== All heap blocks were freed -- no leaks are possible

En este ejemplo adaptado de «Proposal for C2x, WG14 ​n2542, Defer Mechanism for C» pág. 40, los recursos devueltos son bloqueos giratorios [«spin locks»]: (la diferencia por supuesto es que en este caso las llamadas a spin_unlock() no se ejecutan tras el «panic»)

/* Adapted from example in n2542.pdf#40 */ #pragma Cedro 1.0 int f1(void) { puts("g called"); if (bad1()) { return 1; } spin_lock(&lock1); auto spin_unlock(&lock1); if (bad2()) { return 1; } spin_lock(&lock2); auto spin_unlock(&lock2); if (bad()) { return 1; } /* Access data protected by the spinlock then force a panic */ completed += 1; unforced(completed); return 0; } /* Adapted from example in n2542.pdf#40 */ int f1(void) { puts("g called"); if (bad1()) { return 1; } spin_lock(&lock1); if (bad2()) { spin_unlock(&lock1); return 1; } spin_lock(&lock2); if (bad()) { spin_unlock(&lock2); spin_unlock(&lock1); return 1; } /* Access data protected by the spinlock then force a panic */ completed += 1; unforced(completed); spin_unlock(&lock2); spin_unlock(&lock1); return 0; }

Andrew Kelley comparó la gestión de recursos entre C y su lenguaje de programación Zig en una presentación de 2019 titulada “The Road to Zig 1.0" a los 29:21″, y aquí he re-creado su ejemplo en C usando Cedro para producir la función tal cual la mostró, excepto que Cedro no sabe que el bucle for al final nunca termina así que añade devoluciones innecesarias de recursos tras él.

// Example retrofitted from C example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s #pragma Cedro 1.0 int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_destroy(soundio); int err; if ((err = soundio_connect(soundio))) { fprintf(stderr, "unable to connect: %s\n", soundio_strerror(err)); return 1; } soundio_flush_events(soundio); int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { fprintf(stderr, "No output device\n"); return 1; } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_device_unref(device); struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_outstream_destroy(outstream); outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { fprintf(stderr, "unable to open device: %s" "\n", soundio_strerror(err)); return 1; } if ((err = soundio_outstream_start(outstream))) { fprintf(stderr, "unable to start device: %s\n", soundio_strerror(err)); return 1; } for (;;) soundio_wait_events(soundio); } // Example retrofitted from C example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { fprintf(stderr, "out of memory\n"); return 1; } int err; if ((err = soundio_connect(soundio))) { fprintf(stderr, "unable to connect: %s\n", soundio_strerror(err)); soundio_destroy(soundio); return 1; } soundio_flush_events(soundio); int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { fprintf(stderr, "No output device\n"); soundio_destroy(soundio); return 1; } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { fprintf(stderr, "out of memory\n"); soundio_destroy(soundio); return 1; } struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { fprintf(stderr, "out of memory\n"); soundio_device_unref(device); soundio_destroy(soundio); return 1; } outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { fprintf(stderr, "unable to open device: %s" "\n", soundio_strerror(err)); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return 1; } if ((err = soundio_outstream_start(outstream))) { fprintf(stderr, "unable to start device: %s\n", soundio_strerror(err)); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return 1; } for (;;) soundio_wait_events(soundio); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); }

Sin embargo, su ejemplo en Zig tuvo la ventaja injusta de devolver códigos de error en vez de imprimir los mensajes lo cual ocupa más espacio. Lo siguiente es una función en C que se ajusta más a la versión en Zig:

// Example retrofitted from Zig example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s #pragma Cedro 1.0 int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { return SoundIoErrorNoMem; } auto soundio_destroy(soundio); int err; if ((err = soundio_connect(soundio))) return err; soundio_flush_events(soundio); const int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) return SoundIoErrorNoSuchDevice; const struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) return SoundIoErrorNoMem; auto soundio_device_unref(device); const struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) return SoundIoErrorNoMem; auto soundio_outstream_destroy(outstream); outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) return err; if ((err = soundio_outstream_start(outstream))) return err; while (true) soundio_wait_events(soundio); } // Example retrofitted from Zig example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { return SoundIoErrorNoMem; } int err; if ((err = soundio_connect(soundio))) { soundio_destroy(soundio); return err; } soundio_flush_events(soundio); const int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { soundio_destroy(soundio); return SoundIoErrorNoSuchDevice; } const struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { soundio_destroy(soundio); return SoundIoErrorNoMem; } const struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { soundio_device_unref(device); soundio_destroy(soundio); return SoundIoErrorNoMem; } outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return err; } if ((err = soundio_outstream_start(outstream))) { soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return err; } while (true) soundio_wait_events(soundio); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); }

La versión con Cedro se acerca mucho más, pero su argumento se mantiene porque la versión en C puro necesita mucho código repetido y es más frágil. Y por supuesto Zig tiene muchas otras prestaciones estupendas.

Aparte de la ya mencionada «A defer mechanism for C», hay macros que usan un bucle for como for (reserva e inicialización; condición; devolución) { acciones } [1] u otras técnicas [2].

[1] “P99 Scope-bound resource management with for-statements” del mismo autor (2010), “Would it be possible to create a scoped_lock implementation in C?” (2016), ”C compatible scoped locks“ (2021), “Modern C and What We Can Learn From It - Luca Sas [ ACCU 2021 ] 00:17:18”, 2021
[2] “Would it be possible to create a scoped_lock implementation in C?” (2016), “libdefer: Go-style defer for C” (2016), “A Defer statement for C” (2020), “Go-like defer for C that works with most optimization flag combinations under GCC/Clang” (2021)

Compiladores como GCC y clang tienen características no estandarizadas para hacerlo, como el atributo de variables __cleanup__ (en inglés).

Cedro no tiene la limitación de que el código diferido tenga que ser una función: puede ser un bloque de código, con o sin condicionales, lo que permite por ejemplo emular el errdefer de Zig realizando acciones diferentes en caso de error:

char* reserva_bloque(size_t n, char** err_p) { char* resultado = malloc(n); auto if (*err_p) { free(resultado); resultado = NULL; } if (n > 10) { *err_p = "n es demasiado grande"; } return resultado; } char* reserva_bloque(size_t n, char** err_p) { char* resultado = malloc(n); if (n > 10) { *err_p = "n es demasiado grande"; } if (*err_p) { free(resultado); resultado = NULL; } return resultado; }

Macros de bloque: #block-macros

Formatea una macro multi-línea en una sóla línea.

Las macros en C deben escribirse todo en una línea, pero a veces hay que partirlas en varias pseudo-líneas y se hace tedioso y propenso a errores el mantener todos los escapes de nueva línea (“\”).

Añadiendo llaves (“{” o “}”) justo tras #define podemos hacer que Cedro se encargue de eso por nosotros:

#define { macro(A, B, C) f_##A(B, C); /// Versión especializada de f() para el tipo A. #define } #define macro(A, B, C) \ f_##A(B, C); /** Versión especializada de f() para el tipo A. */ \ /* End #define */

Aún así, las directrices de preprocesador no se permiten dentro de macros, de manera que no se pueden usar #if, #include, etc.

Nota: la directiva debe empezar exactamente con «#define {» o «#define }», ni más ni menos espacio entre «#define» y la llave «{» o «}».

Inclusión binaria: #binary-include

Inserta un fichero en forma de ristra de octetos.

#include <stdint.h> const uint8_t imagen #include {images/cedro-32x32.png} ; #include <stdint.h> const uint8_t imagen [1480] = { /* cedro-32x32.png */ 0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,… 0x00,0x00,0x00,0x20,0x00,0x00,0x00,… ⋮ };

El nombre de fichero es relativo al fichero C incluyente.

Nota: la directiva debe empezar exactamente con «#include {», ni más ni menos espacio entre «#include» y la llave «{».

Esta característica es una vieja idea y hay varias implementaciones anteriores, por ejemplo xxd (como xxd -i, manual en inglés) que usé hace muchos años y la tiene desde 1994.

Más recientemente, la macro include_bytes!() me ha sido muy útil en mis programas en Rust.