Cedro es una extensión del lenguaje C que funciona como pre-procesador con nueve prestaciones:
x@ f(), g(y);
→ f(x); g(x, y);
(obras relacionadas).auto ...
o defer ...
(obras relacionadas).break etiqueta;
(obras relacionadas).ristra[inicio..fin]
(obras relacionadas).#define { ... #define }
.#foreach { ... #foreach }
(obras relacionadas).#embed "..."
(obras relacionadas).12'34
| 12_34
→ 1234
, 0b1010
→ 0xA
).Para activarlo, el fichero fuente debe contener esta línea:
#pragma Cedro 1.0
Si no, el fichero se copia directamente a la salida.
Esa línea puede contener ciertas opciones, por ejemplo
#pragma Cedro 1.0 defer
para activar el uso de
defer
en vez de auto
.
El código fuente (licencia Apache 2.0) se encuentra en la biblioteca.
Para compilarlo, véase Compilar.cedro
sólo usa las funciones estándar C, cedrocc
y cedro-new
requieren POSIX.
Uso: cedro [opciones] <fichero.c>…
cedro new <nombre> # Ejecuta: cedro-new <nombre>
Para leer desde stdin, se pone - en vez de <fichero.c>.
El resultado va a stdout, se puede compilar sin fichero intermedio:
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:
--insert-line-directives
Sólo se modifica el código tras la línea `#pragma Cedro 1.0`,
que puede incluir ciertas opciones: `#pragma Cedro 1.0 defer`
--apply-macros Aplica las macros: pespunte, diferido, etc. (implícito)
--no-apply-macros No aplica las macros.
--escape-ucn Encapsula los caracteres no-ASCII en identificadores.
--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)
--embed-as-string=<límite> Usa cadenas literales en vez de octetos
para ficheros menores que <límite>.
Valor implícito: 0
--c99 Produce código fuente para C99. (implícito)
Elimina los separadores de dígitos («'» | «_»),
convierte literales binarios en hexadecimales («0b1010» → «0xA»),
y expande `#embed`.
No es un traductor entre distintas versiones de C.
--c89 Produce código fuente para C89/C90. Por ahora es lo mismo que --c99.
--c11 Produce código fuente para C11. Por ahora es lo mismo que --c99.
--c17 Produce código fuente para C17. Por ahora es lo mismo que --c99.
--c23 Produce código fuente para C23.
Por ejemplo, mantiene los separadores de dígitos («'» | «_» → «'»),
los literales binarios («0b1010» → «0b1010»),
y no expande las directivas `#embed`.
--print-markers Imprime los marcadores.
--no-print-markers No imprime los marcadores. (implícito)
--benchmark Realiza una medición de rendimiento.
--validate=ref.c Compara el resultado con el fichero «ref.c» dado.
No aplica las macros: para comparar el resultado de aplicar Cedro
a un fichero, pase la salida a través de esta opción, por ejemplo:
`cedro fichero.c | cedro - --validate=ref.c`
--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 make doc
que necesita tener
Doxygen instalado.
Compilar
#compileLo más sencillo es cc -o bin/cedro src/cedro.c
, pero es más conveniente usar make
:
$ make help
Objetivos disponibles:
release: compilación optimizada, comprobaciones desactivadas, NDEBUG definido.
→ bin/cedro*
debug: compilación para diagnóstico, comprobaciones activadas.
→ bin/cedro*-debug
static: lo mismo que release, sólo que con enlace estático.
→ bin/cedro*-static
doc: construye la documentación con Doxygen https://www.doxygen.org
→ doc/api/index.html
test: construye tanto release como debug, y dispara la batería de pruebas.
check: aplica varias herramientas de análisis estático:
sparse: https://sparse.docs.kernel.org/
valgrind: https://valgrind.org/
gcc -fanalyzer:
https://gcc.gnu.org/onlinedocs/gcc/Static-Analyzer-Options.html
clean: elimina el directorio bin/, y limpia también dentro de doc/.
cedrocc
#cedroccEl 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 - -x none» mas los otros argumentos.
cedrocc -o fichero fichero.c
cedro fichero.c | cc -x c - -o fichero
Las opciones se pasan tal cual al compilador, excepto las que
empiecen con «--cedro:…» que corresponden a opciones de cedro,
por ejemplo «--cedro:escape-ucn» es como «cedro --escape-ucn».
La siguiente opción es implícita:
--cedro:insert-line-directives
Algunas opciones de GCC activan opciones de Cedro automáticamente:
--std=c90|c89|iso9899:1990|iso8999:199409 → --cedro:c90
--std=c99|c9x|iso9899:1999|iso9899:199x → --cedro:c99
--std=c11|c1x|iso9899:2011 → --cedro:c11
--std=c17|c18|iso9899:2017|iso9899:2018 → --cedro:c17
--std=c2x → --cedro:c23
Además, para cada `#include`, si encuentra el fichero lo lee y
si encuentra `#pragma Cedro 1.0` lo procesa e inserta el resultado
en lugar del `#include`.
Se puede especificar el compilador, p.ej. `gcc`:
CEDRO_CC='gcc -x c - -x none' cedrocc …
Para depuración, esto escribe el código que iría entubado a `cc`,
en `stdout`:
CEDRO_CC='' cedrocc …
Si recibes un mensaje de error como «embedding a directive within macro arguments is not portable» (GCC) o «embedding a directive within macro arguments has undefined behavior» (clang), significa que usas Cedro con --insert-line-directives
dentro de los parámetros de una macro. Puedes bien expandir el código dado a la macro manualmente, o evitar el --insert-line-directives
reemplazando cedrocc -o fichero fichero.c
con cedro fichero.c | cc -x c - -o fichero
.
cedrocc
hace otra cosa además de cedro fichero.c | cc -x c - -o fichero
:
para cada #include
, si encuentra el fichero busca #pragma Cedro 1.0
y si lo encuentra (-I ...
), procesa el fichero e inserta el resultado en lugar del #include
. El motivo es poder compilar de una tacada programas que usen Cedro en varios ficheros, en vez de tener que transformar cada uno en ficheros temporales para compilarlos después.
cedro-new
#cedro-newHay 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 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.
Al producir el borrador, se reemplazan ciertos patrones en
{#year}
: el año corriente.{#Author}
: nombre y dirección de correo electrónico
reportados por
git config user.name
y … user.email
si están disponibles, y si no
«Your Name Here <email@example.com>».{#Template}
: el nombre de proyecto,
p.ej. «Cedro».{#template}
: el nombre de programa,
p.ej. «cedro».{#TEMPLATE}
: el nombre de programa en mayúsculas,
p.ej. «CEDRO».La plantilla incluye varios borradores de proyectos, que se pueden activar en el
Herramienta de línea de comandos («CLI»)
que identifica el tipo de cada argumento,
y usa un árbol btree
para contar cuántas veces se repite cada uno.
Se construye si no se modifica el
Aplicación gráfica con nanovg.
Descarga (con curl
) y
compila automáticamente nanovg, GLFW, y GLEW.
include Makefile.nanovg.mk
MAIN=src/main.nanovg.c
Servidor HTTP/1.1 usando libuv.
Descarga (con curl
) y
compila automáticamente libuv.
include Makefile.libuv.mk
MAIN=src/main.libuv.c
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.
Nota: el símbolo @
no se reconoce
cuando se escribe \u0040
,
pero se convierte en @
en la salida.
Esto sirve para encapsularlo al encadenar Cedro con otro
pre-procesador que lo use.
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 una de las piezas
[
,
++
, --
, .
, ->
,
=
, +=
, -=
, *=
, /=
, %=
, <<=
, >>=
, &=
, ^=
, |=
,
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; |
Se pueden usar expresiones complejas como prefijos poniéndolas
a la izquierda del @
y dejando los puntos suspensivos
sin prefijo ni sufijo:
// ngx_http_sqlite_module.c#L802
(*chain->last)->buf->@ ...
pos = u_str,
last = u_str + ns.len,
memory = 1; |
// ngx_http_sqlite_module.c#L802
(*chain->last)->buf->pos = u_str;
(*chain->last)->buf->last = u_str + ns.len;
(*chain->last)->buf->memory = 1; |
La parte de objeto se puede omitir, lo que sirve por ejemplo para añadir prefijos o sufijos a enumeraciones:
typedef enum {
@PIEZA_... ESPACIO, PALABRA, NÚMERO
} TipoDePieza;
|
typedef enum {
PIEZA_ESPACIO, PIEZA_PALABRA, PIEZA_NÚMERO
} TipoDePieza;
|
// http://docs.libuv.org/en/v1.x/guide/threads.html#core-thread-operations
// `liebre` y `tortuga` son funciones.
int main() {
int lon_pista = 10;
@uv_thread_...
t id_liebre,
t id_tortuga,
create(&id_liebre, liebre, &lon_pista),
create(&id_tortuga, tortuga, &lon_pista),
join(&id_liebre),
join(&id_tortuga);
return 0;
}
|
// http://docs.libuv.org/en/v1.x/guide/threads.html#core-thread-operations
// `liebre` y `tortuga` son funciones.
int main() {
int lon_pista = 10;
uv_thread_t id_liebre;
uv_thread_t id_tortuga;
uv_thread_create(&id_liebre, liebre, &lon_pista);
uv_thread_create(&id_tortuga, tortuga, &lon_pista);
uv_thread_join(&id_liebre);
uv_thread_join(&id_tortuga);
return 0;
}
|
función(a, @prefijo_... b, c) |
función(a, prefijo_b, prefijo_c) |
La parte de los segmentos también se puede omitir para añadir bien un prefijo o un sufijo a un identificador:
Next(lector) @xmlTextReader...; |
xmlTextReaderNext(lector); |
get(&vector, índice) @..._Byte_vec; |
get_Byte_vec(&vector, índice); |
función(a, b @prefijo_..., c) |
función(a, prefijo_b, c) |
Es un operador asociativo por la izquierda:
objeto @ f(a) @ g(b); |
g(f(objeto, a), b); |
x @ uno() @ dos() @ tres() @ cuatro(); |
cuatro(tres(dos(uno(x)))); |
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
además del compilador C.
Clojure también tiene una macro llamada doto
que funciona
de manera parecida,
por ejemplo para hacer f₁(x); f₂(x); f₃(x);
:
Magma | doto | macro doto | doto(x) { f₁(); f₂(); f₃(); } |
---|---|---|---|
Clojure | doto | macro doto | (doto x f₁ f₂ f₃) |
Cedro | @ | macro pespunte | x @ f₁(), f₂(), f₃() |
Los lenguajes funcionales suelen tener un operador similar
sin la capacidad de hilvanar un mismo valor
a través de varias funciones.
Por ejemplo, el equivalente de f₃(f₂(f₁(x)))
:
Shell | | | operador tubería | echo x | f₁ | f₂ | f₃ |
---|---|---|---|
Haskell | & | operador aplicación inversa | x & f₁ & f₂ & f₃ |
OCaml | |> | operador aplicación inversa | x |> f₁ |> f₂ |> f₃ |
Elixir | |> | operador tubería | x |> f₁ |> f₂ |> f₃ |
Clojure | -> | macro hilvanado | (-> x f₁ f₂ f₃) |
Cedro | @ | macro pespunte | x @ f₁() @ f₂() @ f₃() |
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.
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
anterior al C23
porque es implícita y se puede reemplazar con signed
ya que
tiene el mismo efecto.
También es posible usar defer
en vez de auto
añadiendo la clave «defer» al «pragma»:
#pragma Cedro 1.0 defer
.
#pragma Cedro 1.0
…
char* texto = malloc(cuenta + 1);
if (!texto) return ENOMEM;
auto free(texto);
…
if (nombre_de_fichero) {
FILE* fichero = fopen(nombre_de_fichero, "w");
if (!fichero) return errno;
auto fclose(fichero);
fwrite(texto, sizeof(char), cuenta, fichero);
… |
#pragma Cedro 1.0 defer
…
char* texto = malloc(cuenta + 1);
if (!texto) return ENOMEM;
defer free(texto);
…
if (nombre_de_fichero) {
FILE* fichero = fopen(nombre_de_fichero, "w");
if (!fichero) return errno;
defer fclose(fichero);
fwrite(texto, sizeof(char), cuenta, fichero);
… |
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;
}
texto[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
:
$ 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:21s,
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].
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;
}
|
Convierte break etiqueta;
o continue etiqueta;
en goto etiqueta;
. En C sólo es posible salir de un bucle cada vez al usar break
, lo que también es un problema cuando la interrupción viene de un bloque switch
.
#include <stdio.h>
#include <stdlib.h>
#pragma Cedro 1.0
int main(int argc, char* argv[])
{
int x = 0, y = 0;
for (x = 0; siguiente_x: x < 100; ++x) {
for (y = 0; y < 100; ++y) {
switch (x + y) {
case 157:
break encontrada_descomposición_del_número;
case 11:
x = 37;
fprintf(stderr, "Saltamos de x=11 a x=%d\n", x);
--x;
continue siguiente_x;
}
}
} encontrada_descomposición_del_número:
if (x < 100 || y < 100) {
fprintf(stderr, "Encontrada %d = %d + %d\n",
x + y, x, y);
}
return 0;
}
|
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int x = 0, y = 0;
for (x = 0; x < 100; ++x) {
for (y = 0; y < 100; ++y) {
switch (x + y) {
case 157:
goto encontrada_descomposición_del_número;
case 11:
x = 37;
fprintf(stderr, "Saltamos de x=11 a x=%d\n", x);
--x;
goto siguiente_x;
}
}
siguiente_x:;
} encontrada_descomposición_del_número:
if (x < 100 || y < 100) {
fprintf(stderr, "Encontrada %d = %d + %d\n",
x + y, x, y);
}
return 0;
}
|
La diferencia entre break …
, continue …
, y goto …
está en las restricciones:
break etiqueta
sólo permite saltos hacia adelante, y la etiqueta debe ir justo tras el fin del bucle.continue etiqueta
sólo saltos hacia atrás, y la etiqueta debe ir en la condición [cond-expression] del bucle: for (i = 0; etiqueta: i < 10; ++i)
, while (etiqueta: i < 10)
.goto etiqueta
no tiene restricciones.Es parte de la macro de devolución diferida de recursos:
#include <stdio.h>
#include <stdlib.h>
#pragma Cedro 1.0
int main(int argc, char* argv[])
{
int x = 0, y = 0;
char *nivel1 = malloc(1);
auto free(nivel1);
for (x = 0; siguiente_x: x < 100; ++x) {
char *nivel2 = malloc(2);
auto free(nivel2);
for (y = 0; y < 100; ++y) {
char *nivel3 = malloc(3);
auto free(nivel3);
switch (x + y) {
case 157:
break encontrada_descomposición_del_número;
case 11:
x = 37;
fprintf(stderr, "Saltamos de x=11 a x=%d\n", x);
--x;
continue siguiente_x;
}
}
} encontrada_descomposición_del_número:
if (x < 100 || y < 100) {
fprintf(stderr, "Encontrada %d = %d + %d\n",
x + y, x, y);
}
return 0;
}
|
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int x = 0, y = 0;
char *nivel1 = malloc(1);
for (x = 0; x < 100; ++x) {
char *nivel2 = malloc(2);
for (y = 0; y < 100; ++y) {
char *nivel3 = malloc(3);
switch (x + y) {
case 157:
free(nivel3);
free(nivel2);
goto encontrada_descomposición_del_número;
case 11:
x = 37;
fprintf(stderr, "Saltamos de x=11 a x=%d\n", x);
--x;
free(nivel3);
goto siguiente_x;
}
free(nivel3);
}
siguiente_x:;
free(nivel2);
} encontrada_descomposición_del_número:
if (x < 100 || y < 100) {
fprintf(stderr, "Encontrada %d = %d + %d\n",
x + y, x, y);
}
free(nivel1);
return 0;
}
|
Usando goto
en general no se puede garantizar que los recursos vayan a devolverse correctamente, pero con las restricciones al usar break …
y continue …
sí que funciona.
$ bin/cedrocc test/defer-label-break.c -std=c99 -pedantic-errors -Wall -fanalyzer -o /tmp/find-number-decomposition
$ valgrind --leak-check=yes /tmp/find-number-decomposition
…
Saltamos de x=11 a x=37
Encontrada 157 = 58 + 99
==1077==
==1077== HEAP SUMMARY:
==1077== in use at exit: 0 bytes in 0 blocks
==1077== total heap usage: 2,236 allocs, 2,236 frees, 6,683 bytes allocated
==1077==
==1077== All heap blocks were freed -- no leaks are possible
==1077==
==1077== For lists of detected and suppressed errors, rerun with: -s
==1077== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) |
El lenguaje de programación BLISS 11 fue el primero que introdujo etiquetas para su palabra clave leave
(análogo al break
del C) alrededor de 1974, y luego otros lenguajes como Java, Javascript, y Go hicieron lo mismo con continue
y break
.
Convierte ristra[inicio..fin]
en &ristra[inicio], &ristra[fin]
. El valor ristra/puntero ristra
puede ser simplemente un identificador o en general una expresión que, al evaluarse dos veces, no debe tener efecto secundario alguno, tal como las macros del preprocesador C estándar.
Podemos usarlo para mejorar el ejemplo de vectores de la sección de macros de bucles:
añade(&palabras, animales[0..2]);
añade(&palabras, plantas[0..3]);
añade(&palabras, animales[2..4]);
|
añade(&palabras, &animales[0], &animales[2]);
añade(&palabras, &plantas[0], &plantas[3]);
añade(&palabras, &animales[2], &animales[4]);
|
El fin de la porción puede llevar un signo positivo para indicar que
es una posición relativa al inicio de la porción:
ristra[inicio..+fin]
se convierte en
&ristra[inicio], &ristra[inicio+fin]
.
En este caso, la advertencia sobre doble ejecución de efectos secundarios se aplica también a
inicio
además de a ristra
.
añade(&palabras, animales[0..+2]);
añade(&palabras, plantas[0..3]);
añade(&palabras, animales[2..+2]);
|
añade(&palabras, &animales[0], &animales[0+2]);
añade(&palabras, &plantas[0], &plantas[3]);
añade(&palabras, &animales[2], &animales[2+2]);
|
Si la ristra se compone de más de una pieza, se envuelve con paréntesis para asegurar que sea correcto.
añade(&palabras, (uint8_t*)animales[0..+2]);
añade(&palabras, (uint8_t*)plantas[0..3]);
añade(&palabras, (uint8_t*)animales[2..+2]);
|
añade(&palabras, &((uint8_t*)animales)[0], &((uint8_t*)animales)[0+2]);
añade(&palabras, &((uint8_t*)plantas)[0], &((uint8_t*)plantas)[3]);
añade(&palabras, &((uint8_t*)animales)[2], &((uint8_t*)animales)[2+2]);
|
Se puede usar en inicializadores, en cuyo caso las llaves pueden omitirse:
#include <stdio.h>
#pragma Cedro 1.0
typedef struct {
const char* a;
const char* b;
} char_slice_t;
const char* texto = "uno dos tres";
/** Extrae "dos" de `texto`. */
int main(void)
{
char_slice_t porción = texto[4..+3];
const char* cursor;
for (cursor = porción.a; cursor != porción.b; ++cursor) {
putc(*cursor, stderr);
}
putc('\n', stderr);
}
| #include <stdio.h>
typedef struct {
const char* a;
const char* b;
} char_slice_t;
const char* texto = "uno dos tres";
/** Extrae "dos" de `texto`. */
int main(void)
{
char_slice_t porción = { &texto[4], &texto[4+3] };
const char* cursor;
for (cursor = porción.a; cursor != porción.b; ++cursor) {
putc(*cursor, stderr);
}
putc('\n', stderr);
}
|
Este ejemplo completo muestra un letrero con texto deslizante:
#ifdef _WIN32
#include <windows.h>
// Véase https://stackoverflow.com/a/3930716/
int sleep_ms(int ms) { Sleep(ms); return 0; }
#else
#define _XOPEN_SOURCE 500
#include <unistd.h> // Desfasado pero simple:
int sleep_ms(int ms) { return usleep(ms*1000); }
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#pragma Cedro 1.0
typedef char utf8_char[5]; // Como cadena C.
void
imprime_porción(const utf8_char* inicio,
const utf8_char* final)
{
while (inicio < final) fputs(*inicio++, stdout);
}
int
main(int argc, char* argv[])
{
int tamaño_letrero = 8;
if (argc < 2) {
fprintf(stderr, "Uso: letrero <texto>\n",
tamaño_letrero);
exit(1);
}
const char separador[] = " *** ";
size_t lon_octetos = strlen(argv[1]);
size_t lon_separador = strlen(separador);
char* m = malloc(lon_octetos + lon_separador);
auto free(m);
memcpy(m, argv[1], lon_octetos);
memcpy(m + lon_octetos, separador, lon_separador);
lon_octetos += lon_separador;
// Extrae cada carácter codificado como UTF-8,
// que necesita hasta 4 octetos + 1 terminador.
utf8_char* mensaje =
malloc(sizeof(utf8_char) * lon_octetos);
auto free(mensaje);
size_t lon = 0;
for (size_t final = 0; final < lon_octetos;) {
const char b = m[final];
size_t u;
if (0xF0 == (b & 0xF8)) u = 4;
else if (0xE0 == (b & 0xF0)) u = 3;
else if (0xC0 == (b & 0xE0)) u = 2;
else u = 1;
if (final + u > lon_octetos) break;
memcpy(&mensaje[lon], &m[final], u);
mensaje[final][u] = '\0';
final += u;
++lon;
}
if (lon < 2) {
fprintf(stderr, "texto de mensaje demasiado corto.\n");
exit(2);
} else if (lon < tamaño_letrero) {
tamaño_letrero = lon - 1;
}
for (;;) {
for (int i = 0; i < lon; ++i) {
int resto = i + tamaño_letrero > lon?
i + tamaño_letrero - lon: 0;
int visible = tamaño_letrero - resto;
imprime_porción(mensaje[i .. +visible]);
imprime_porción(mensaje[0 .. resto]);
putc('\r', stdout);
fflush(stdout);
sleep_ms(300);
}
}
return 0;
}
| #ifdef _WIN32
#include <windows.h>
// Véase: https://stackoverflow.com/a/3930716/
int sleep_ms(int ms) { Sleep(ms); return 0; }
#else
#define _XOPEN_SOURCE 500
#include <unistd.h> // Desfasado pero simple:
int sleep_ms(int ms) { return usleep(ms*1000); }
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef char utf8_char[5]; // Como cadena C.
void
imprime_porción(const utf8_char* inicio,
const utf8_char* final)
{
while (inicio < final) fputs(*inicio++, stdout);
}
int
main(int argc, char* argv[])
{
int tamaño_letrero = 8;
if (argc < 2) {
fprintf(stderr, "Uso: letrero <texto>\n",
tamaño_letrero);
exit(1);
}
const char separador[] = " *** ";
size_t lon_octetos = strlen(argv[1]);
size_t lon_separador = strlen(separador);
char* m = malloc(lon_octetos + lon_separador);
memcpy(m, argv[1], lon_octetos);
memcpy(m + lon_octetos, separador, lon_separador);
lon_octetos += lon_separador;
// Extrae cada carácter codificado como UTF-8,
// que necesita hasta 4 octetos + 1 terminador.
utf8_char* mensaje =
malloc(sizeof(utf8_char) * lon_octetos);
size_t lon = 0;
for (size_t final = 0; final < lon_octetos;) {
const char b = m[final];
size_t u;
if (0xF0 == (b & 0xF8)) u = 4;
else if (0xE0 == (b & 0xF0)) u = 3;
else if (0xC0 == (b & 0xE0)) u = 2;
else u = 1;
if (final + u > lon_octetos) break;
memcpy(&mensaje[lon], &m[final], u);
mensaje[final][u] = '\0';
final += u;
++lon;
}
if (lon < 2) {
fprintf(stderr, "texto de mensaje demasiado corto.\n");
exit(2);
} else if (lon < tamaño_letrero) {
tamaño_letrero = lon - 1;
}
for (;;) {
for (int i = 0; i < lon; ++i) {
int resto = i + tamaño_letrero > lon?
i + tamaño_letrero - lon: 0;
int visible = tamaño_letrero - resto;
imprime_porción(&mensaje[i], &mensaje[i+visible]);
imprime_porción(&mensaje[0], &mensaje[resto]);
putc('\r', stdout);
fflush(stdout);
sleep_ms(300);
}
}
free(mensaje);
free(m);
return 0;
}
|
La notación [a..b]
para porciones de ristras se
definió por primera vez en Algol 68
donde era una alternativa a la notación primaria [a:b]
, y
ambas han sido adoptadas por otros lenguajes desde entonces.
La forma [a..b]
se usa en Ada,
Perl, D, y
Rust, por ejemplo.
Formatea una macro multi-línea en una sola 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)
/// Versión de f() para el tipo A.
f_##A(B, C) /// Sin punto y coma «;» al final.
#define }
int main(void) {
int x = 1, y = 2;
macro(int, x, y);
// → f_int(x, y);
}
|
#define macro(A, B, C) \
/** Versión de f() para el tipo A. */ \
f_##A(B, C) /** Sin punto y coma «;» al final. */ \
/* End #define */
int main(void) {
int x = 1, y = 2;
macro(int, x, y);
// → f_int(x, y);
}
|
En casos como este
(«function-like macros»),
al no haber punto y coma tras f_##A(B, C)
herramientas tales como editores de texto
(p.ej. Emacs)
sangran [«indent»] el código incorrectamente.
La solución es dejarlo ahí para el editor,
y añadirlo también tras
#define }
como #define };
lo que indica a Cedro que lo elimine de la definición.
#define { macro(A, B, C)
/// Versión de f() para el tipo A.
f_##A(B, C); /// Punto y coma «;» eliminado por Cedro.
#define };
int main(void) {
int x = 1, y = 2;
macro(int, x, y);
// → f_int(x, y);
}
|
#define macro(A, B, C) \
/** Versión de f() para el tipo A. */ \
f_##A(B, C) /** Punto y coma «;» eliminado por Cedro. */ \
/* End #define */
int main(void) {
int x = 1, y = 2;
macro(int, x, y);
// → f_int(x, y);
}
|
Las directrices de preprocesador no se permiten
dentro de macros, de manera que no se puede 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 }
.
Repite las líneas entre
#foreach { ...
y #foreach }
sustituyendo las variables dadas.
Esas líneas pueden contener definiciones de macro (#define
)
pero las variables del bucle no se expandirán dentro de ellas.
Como en #define
,
el ##
sirve para
juntar piezas
de forma que si por ejemplo
T
es float
,
Vec_##T
produce Vec_float
.
Dentro del bucle,
cualquier operador que siga a un #
(p.ej. #,
) se omite en la última vuelta.
typedef enum {
#foreach { V {ESPACIO, NÚMERO, \
PALABRA_CLAVE, IDENTIFICADOR, OPERADOR}
T_##V#,
#foreach }
} TipoDePieza;
|
typedef enum {
T_ESPACIO,
T_NÚMERO,
T_PALABRA_CLAVE,
T_IDENTIFICADOR,
T_OPERADOR
} TipoDePieza;
|
Si una variable lleva el prefijo #
,
el resultado es una cadena con su contenido:
si T
es float
,
char* name = #T;
produce
char* name = "float";
.
Los bucles se pueden anidar, y la lista de valores puede salir de una variable definida en un bucle exterior.
#foreach { VALORES {{ESPACIO, NÚMERO, \
PALABRA_CLAVE, IDENTIFICADOR, OPERADOR}}
typedef enum {
#foreach { V VALORES
T_##V#,
#foreach }
} TipoDePieza;
const char* const TipoDePieza_CADENA[] = {
#foreach { V VALORES
#V#,
#foreach }
};
#foreach }
|
typedef enum {
T_ESPACIO,
T_NÚMERO,
T_PALABRA_CLAVE,
T_IDENTIFICADOR,
T_OPERADOR
} TipoDePieza;
const char* const TipoDePieza_CADENA[] = {
"ESPACIO",
"NÚMERO",
"PALABRA_CLAVE",
"IDENTIFICADOR",
"OPERADOR"
};
|
Es posible iterar múltiples variables en paralelo usando tuplas de variables y valores, que deben tener el mismo número de elementos:
#foreach { {TIPO, PREFIJO, VALORES} { \
{ TipoDePieza, T_, {ESPACIO, NÚMERO, \
PALABRA_CLAVE, IDENTIFICADOR, OPERADOR} }, \
{ ConfDePúa, M_, {ENTRADA, SALIDA} } \
}
typedef enum {
#foreach { V VALORES
PREFIJO##V#,
#foreach }
} TIPO;
const char* const TIPO##_CADENA[] = {
#foreach { V VALORES
#V#,
#foreach }
};
#foreach }
|
typedef enum {
T_ESPACIO,
T_NÚMERO,
T_PALABRA_CLAVE,
T_IDENTIFICADOR,
T_OPERADOR
} TipoDePieza;
const char* const TipoDePieza_CADENA[] = {
"ESPACIO",
"NÚMERO",
"PALABRA_CLAVE",
"IDENTIFICADOR",
"OPERADOR"
};
typedef enum {
M_ENTRADA,
M_SALIDA
} ConfDePúa;
const char* const ConfDePúa_CADENA[] = {
"ENTRADA",
"SALIDA"
};
|
Este ejemplo itera sobre una lista de campos
para construir una struct
y la correspondiente función imprime_...()
.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#pragma Cedro 1.0
void imprime_double(double n, FILE* salida)
{
fprintf(salida, "%f", n);
}
typedef uint32_t Color_ARGB;
void imprime_Color_ARGB(Color_ARGB c, FILE* salida)
{
if ((c & 0xFF000000) == 0xFF000000) {
fprintf(salida, "#%.6X", c & 0x00FFFFFF);
} else {
fprintf(salida, "#%.8X", c);
}
}
#foreach { CAMPOS {{ \
{ double, x, /** Posición X. */ }, \
{ double, y, /** Posición Y. */ }, \
{ Color_ARGB, color, /** Color 32 bits. */ } \
}}
typedef struct Punto {
#foreach { {TIPO, NOMBRE, COMENTARIO} CAMPOS
TIPO NOMBRE; COMENTARIO
#foreach }
} Punto;
void imprime_Punto(Punto punto, FILE* salida)
{
#foreach { {TIPO, NOMBRE, COMENTARIO} CAMPOS
imprime_##TIPO(punto.NOMBRE, salida); putc('\n', salida);
#foreach }
}
#foreach }
int main(void)
{
Punto punto = { .x = 12.3, .y = 4.56, .color = 0xFFa3f193 };
imprime_Punto(punto, stderr);
}
| #include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
void imprime_double(double n, FILE* salida)
{
fprintf(salida, "%f", n);
}
typedef uint32_t Color_ARGB;
void imprime_Color_ARGB(Color_ARGB c, FILE* salida)
{
if ((c & 0xFF000000) == 0xFF000000) {
fprintf(salida, "#%.6X", c & 0x00FFFFFF);
} else {
fprintf(salida, "#%.8X", c);
}
}
typedef struct Punto {
double x; /** Posición X. */
double y; /** Posición Y. */
Color_ARGB color; /** Color 32 bits. */
} Punto;
void imprime_Punto(Punto punto, FILE* salida)
{
imprime_double(punto.x, salida); putc('\n', salida);
imprime_double(punto.y, salida); putc('\n', salida);
imprime_Color_ARGB(punto.color, salida); putc('\n', salida);
}
int main(void)
{
Punto punto = { .x = 12.3, .y = 4.56, .color = 0xFFa3f193 };
imprime_Punto(punto, stderr);
}
|
Este ejemplo completo
define variantes para los tipos
float
, str
, y cstr
de una ristra/vector de capacidad variable
con una función llamada añade_Vec_##T()
para cada uno.
Luego usa el
_Generic
del C11 para definir una macro/función
añade()
pseudo-polimórfica.
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h> // Para memcpy().
typedef struct str { uint8_t* start; uint8_t* end; } str;
typedef char* cstr; // Nombres de tipo deben ser palabras.
#pragma Cedro 1.0
#foreach { LISTA_DE_TIPOS {{float, str, cstr}}
#foreach { T LISTA_DE_TIPOS
/** Tipo Vector (ristra extensible). */
typedef struct {
T* _;
size_t len;
size_t capacity;
} Vec_##T;
/** Añade una porción a un vector de elementos de este tipo. */
bool
añade_Vec_##T(Vec_##T *v, const T *start, const T *end)
{
const size_t to_add = (size_t)(end - start);
if (v->len + to_add > v->capacity) {
const size_t new_capacity = v->len +
(to_add < v->len? v->len: to_add);
T * const new_start =
realloc(v->_, new_capacity * sizeof(T));
if (!new_start) return false;
v->_ = new_start;
v->capacity = new_capacity;
}
memcpy(v->_ + v->len, start, to_add * sizeof(T));
v->len += to_add;
return true;
}
#foreach }
#foreach { DEFINE {#define} // Evita juntar las líneas.
DEFINE añade(VEC, START, END) _Generic((VEC), \
#foreach { T LISTA_DE_TIPOS
Vec_##T*: añade_Vec_##T#, \
#foreach }
)(VEC, START, END)
#foreach }
#foreach }
#include <stdio.h>
int main(void)
{
Vec_cstr palabras = {0};
cstr animales[] = { "caballo", "gato", "pollo", "perro" };
cstr plantas [] = { "rábano", "trigo", "tomate" };
añade(&palabras, &animales[0], &animales[2]);
añade(&palabras, &plantas[0], &plantas[3]);
añade(&palabras, &animales[2], &animales[4]);
for (cstr *p = palabras._, *fin = palabras._ + palabras.len;
p != fin; ++p) {
fprintf(stderr, "Palabra: \"%s\"\n", *p);
}
return 0;
}
|
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h> // Para memcpy().
typedef struct str { uint8_t* start; uint8_t* end; } str;
typedef char* cstr; // Nombres de tipo deben ser palabras.
/** Tipo Vector (ristra extensible). */
typedef struct {
float* _;
size_t len;
size_t capacity;
} Vec_float;
/** Añade una porción a un vector de elementos de este tipo. */
bool
añade_Vec_float(Vec_float *v, const float *start, const float *end)
{
const size_t to_add = (size_t)(end - start);
if (v->len + to_add > v->capacity) {
const size_t new_capacity = v->len +
(to_add < v->len? v->len: to_add);
float * const new_start =
realloc(v->_, new_capacity * sizeof(float));
if (!new_start) return false;
v->_ = new_start;
v->capacity = new_capacity;
}
memcpy(v->_ + v->len, start, to_add * sizeof(float));
v->len += to_add;
return true;
}
/** Tipo Vector (ristra extensible). */
typedef struct {
str* _;
size_t len;
size_t capacity;
} Vec_str;
/** Añade una porción a un vector de elementos de este tipo. */
bool
añade_Vec_str(Vec_str *v, const str *start, const str *end)
{
const size_t to_add = (size_t)(end - start);
if (v->len + to_add > v->capacity) {
const size_t new_capacity = v->len +
(to_add < v->len? v->len: to_add);
str * const new_start =
realloc(v->_, new_capacity * sizeof(str));
if (!new_start) return false;
v->_ = new_start;
v->capacity = new_capacity;
}
memcpy(v->_ + v->len, start, to_add * sizeof(str));
v->len += to_add;
return true;
}
/** Tipo Vector (ristra extensible). */
typedef struct {
cstr* _;
size_t len;
size_t capacity;
} Vec_cstr;
/** Añade una porción a un vector de elementos de este tipo. */
bool
añade_Vec_cstr(Vec_cstr *v, const cstr *start, const cstr *end)
{
const size_t to_add = (size_t)(end - start);
if (v->len + to_add > v->capacity) {
const size_t new_capacity = v->len +
(to_add < v->len? v->len: to_add);
cstr * const new_start =
realloc(v->_, new_capacity * sizeof(cstr));
if (!new_start) return false;
v->_ = new_start;
v->capacity = new_capacity;
}
memcpy(v->_ + v->len, start, to_add * sizeof(cstr));
v->len += to_add;
return true;
}
#define añade(VEC, START, END) _Generic((VEC), \
Vec_float*: añade_Vec_float, \
Vec_str*: añade_Vec_str, \
Vec_cstr*: añade_Vec_cstr \
)(VEC, START, END)
#include <stdio.h>
int main(void)
{
Vec_cstr palabras = {0};
cstr animales[] = { "caballo", "gato", "pollo", "perro" };
cstr plantas [] = { "rábano", "trigo", "tomate" };
añade(&palabras, &animales[0], &animales[2]);
añade(&palabras, &plantas[0], &plantas[3]);
añade(&palabras, &animales[2], &animales[4]);
for (cstr *p = palabras._, *fin = palabras._ + palabras.len;
p != fin; ++p) {
fprintf(stderr, "Palabra: \"%s\"\n", *p);
}
return 0;
}
|
Esto puede hacerse sin Cedro con la llamadas «X Macros».
The X macro technique was used extensively in the operating system and utilities for the DECsystem-10 as early as 1968, and probably dates back further to PDP-1 and TX-0 programmers at MIT.
Randy Meyers en «The New C: X Macros», Dr.Dobb’s 2001-05-01
Cedro | X macro |
---|---|
typedef enum {
#foreach { V { \
ESPACIO, NÚMERO, \
PALABRA_CLAVE, IDENTIFICADOR, OPERADOR \
}
T_##V#,
#foreach }
} TipoDePieza;
|
#define LISTA_DE_VARIABLES \
X(ESPACIO), X(NÚMERO), \
X(PALABRA_CLAVE), X(IDENTIFICADOR), X(OPERADOR)
typedef enum {
#define X(V) T_##V
LISTA_DE_VARIABLES
#undef X
} TipoDePieza;
#undef LISTA_DE_VARIABLES
|
#foreach { VALORES {{ \
ESPACIO, NÚMERO, \
PALABRA_CLAVE, IDENTIFICADOR, OPERADOR \
}}
typedef enum {
#foreach { V VALORES
T_##V#,
#foreach }
} TipoDePieza;
const char* const TipoDePieza_CADENA[] = {
#foreach { V VALORES
#V#,
#foreach }
};
#foreach }
|
#define LIST_OF_VARIABLES \
X(ESPACIO), X(NÚMERO), \
X(PALABRA_CLAVE), X(IDENTIFICADOR), X(OPERADOR)
typedef enum {
#define X(V) T_##V
LISTA_DE_VARIABLES
#undef X
} TipoDePieza;
const char* const TipoDePieza_CADENA[] = {
#define X(V) #V
LISTA_DE_VARIABLES
#undef X
};
#undef LISTA_DE_VARIABLES
|
Inserta un fichero en forma de ristra de octetos, o de literal de cadena como se describe más adelante.
El nombre de fichero es relativo al fichero C incluyente.
#include <stdint.h>
#pragma Cedro 1.0
const uint8_t imagen[] = {
#embed "../images/cedro-32x32.png"
};
|
#include <stdint.h>
const uint8_t imagen[] = {
/* cedro-32x32.png */
0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,…
0x00,0x00,0x00,0x20,0x00,0x00,0x00,…
⋮
};
|
La forma Nota: al usar | |
#include <stdint.h>
#pragma Cedro 1.0
const uint8_t imagen
#include {images/cedro-32x32.png}
;
Esta forma está obsoleta, y se va a eliminar.
|
#include <stdint.h>
const uint8_t imagen
[1559] = { /* cedro-32x32.png */
0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,…
0x00,0x00,0x00,0x20,0x00,0x00,0x00,…
⋮
};
|
#embed
es más flexible porque permite añadir octetos
antes o después del fichero insertado,
y también combinar varios ficheros.
#pragma Cedro 1.0
const char const sombreador_de_fragmento[] = {
#embed "shader.frag.glsl"
, 0x00 // Cadena terminada con zero.
};
|
const char const sombreador_de_fragmento[] = {
/* shader.frag.glsl */
0x23,0x76,0x65,0x72,0x73,0x69,0x6F,0x6E,…
0x65,0x63,0x69,0x73,0x69,0x6F,0x6E,0x20,…
⋮
, 0x00 // Cadena terminada con zero.
};
|
#pragma Cedro 1.0
const char const dos_líneas[] = {
#embed "línea-de-texto-1.txt"
, '\n',
#embed "línea-de-texto-2.txt"
, 0x00
};
|
const char const dos_líneas[] = {
0x46,0x69,0x72,0x73,0x74,0x20,0x6C,0x69,0x6E,0x65,0x2E
, '\n',
0x53,0x65,0x63,0x6F,0x6E,0x64,0x20,0x6C,0x69,0x6E,0x65,0x2E
, 0x00
};
|
En vez de insertar literales de octeto uno por uno,
se pueden poner todos de una vez en un literal de cadena
con la opción --embed-as-string=<límite>
,
aunque esta variante tiene ciertas limitaciones
dependiendo del compilador C que se vaya a usar con el resultado.
Por ejemplo, el compilador C de Microsoft tiene un límite de 2048 octetos por cada literal de cadena, con un máximo de 65535 tras concatenar las cadenas que aparezcan juntas. (Véase «Maximum String Length»)
La norma ANSI/ISO C requiere que todos los compiladores acepten al menos 509 octetos en total tras la concatenación en C89, y 4096 en C99/C11/C17, pero otros compiladores como GCC y clang permiten cadenas muchísimo mayores.
Por eso es necesario especificar un límite para el tamaño: si el fichero sobrepasa ese límite, el resultado será una ristra de octetos en vez de una cadena.
En una prueba informal con /usr/bin/time -v
,
tomando los valores de la ejecución más rápida
con un fichero de 8 MB,
en una CPU IBM Power9 con los ficheros en disco RAM,
la generación de código llevó un 26% menos tiempo
que al usar octetos hexadecimales,
y la compilación con GCC fue 28 veces más rápida
usando 10 veces menos memoria.
Con clang la compilación fue 72 veces más rápida
que con octetos,
usando 7 veces menos memoria.
Generar código | Compilar con GCC 11 | Compilar con clang 12 | ||||
---|---|---|---|---|---|---|
cedro | 0.23 s | 1.56 MB | 27.66 s | 1328.13 MB | 30.44 s | 984.59 MB |
cedro --embed-as-string=… | 0.17 s | 1.87 MB | 0.99 s | 125.32 MB | 0.42 s | 144.95 MB |
bin2c | 0.03 s | 1.37 MB | 0.90 s | 108.70 MB | 0.52 s | 145.73 MB |
En comparación con
bin2c
,
la generación de código llevó cinco veces más tiempo,
y la compilación es muy similar
(±100ms, el resultado de bin2c compila más rápido en GCC,
el de Cedro en clang)
porque el formato es
casi el mismo.
#pragma Cedro 1.0
const char const two lines[] = {
#embed "text-line-1.txt"
, '\n',
#embed "text-line-2.txt"
, 0x00
};
|
--embed-as-string=30
const char const dos_líneas[25] =
/* línea-de-texto-1.txt */
"First line.""\n"
/* línea-de-texto-2.txt */
"Second line.";
|
Nótese cómo Cedro se da cuenta de que el último octeto es cero y lo elimina, porque al haber una cadena justo antes, el compilador añadirá el terminador cero automáticamente.
En el siguiente ejemplo, al no haber un octeto cero tras
la línea |
#pragma Cedro 1.0
const char const sombreador_de_fragmento[] = {
#embed "shader.frag.glsl"
, 0x00 // Cadena terminada con zero.
};
|
--embed-as-string=170
const char const sombreador_de_fragmento[164] =
/* shader.frag.glsl */
"#version 140\n"
"\n"
"precision highp float; // needed only for version 1.30\n"
"\n"
"in vec3 ex_Color;\n"
"out vec4 out_Color;\n"
"\n"
"void main(void)\n"
"{\n"
"\tout_Color = vec4(ex_Color,1.0);\n"
"}\n";
|
#include <stdint.h>
#pragma Cedro 1.0
const uint8_t imagen[] = {
#embed "../images/cedro-32x32.png"
};
|
--embed-as-string=1600
#include <stdint.h>
const uint8_t imagen[1559] = /* cedro-32x32.png */
"\211PNG\r\n"
"\032\n"
"\000\000\000\rIHDR\000\000\000 \000\000\000 \b\002…"
⋮
…";
|
#include <stdint.h>
#pragma Cedro 1.0
const uint8_t imagen
#include {images/cedro-32x32.png}
;
Esta forma está obsoleta, y se va a eliminar.
|
--embed-as-string=1600
#include <stdint.h>
const uint8_t imagen
[1559] = /* cedro-32x32.png */
"\211PNG\r\n"
"\032\n"
"\000\000\000\rIHDR\000\000\000 \000\000\000 \b\002…"
⋮
…";
|
Insertar directamente el código en el programa es muy conveniente
pero va a enlentecer la compilación.
La manera de reducir el problema,
aparte de usar --embed-as-string=<límite>
,
es compilar esta parte por separado
como se puede ver en
#include <stdint.h>
#include <stdlib.h>
#pragma Cedro 1.0
const uint8_t imagen[] = {
#embed "../images/cedro-32x32.png"
};
const size_t sizeof_imagen = sizeof(imagen); |
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
extern const uint8_t imagen[];
extern const size_t sizeof_imagen;
int main(void)
{
unsigned int suma = 0;
for (size_t i = 0; i < sizeof_imagen; ++i) {
suma += imagen[i];
}
fprintf(stderr,
"La suma (módulo UINT_MAX=%u) de los bytes de la imagen es %u.\n",
UINT_MAX, suma);
} |
cedrocc -c -o assets.o assets.c -std=c99
cedrocc -c -o main.o main.c -std=c99
cc -o programa main.o assets.o |
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.
La idea de producir literales de cadena en vez de ristras de octetos me la dió este comentario:
the way that you’ve lowered them is absolutely the worst case for compiler performance. The compiler needs to create a literal constant object for every byte and an array object wrapping them. If you lower them as C string literals with escapes, you will generate code that compiles much faster. For example, the cedro-32x32.png example lowered as “\x89\x50\x4E\x47\0D\x0A\x1A…” will be faster and use less memory in the C compiler.
David Chisnall en Lobsters, 2021-08-12
I did not realize that, you are right of course! I know there are limits to the size of string literals, but maybe that does not apply if you split them. I’ll have to check that out.
EDIT: I’ve just found out that bin2c (which I knew existed but haven’t used) does work in the way you describe, with strings instead of byte arrays: https://github.com/adobe/bin2c#comparison-to-other-tools It does mention the string literal size limit. I suspect you know, but for others reading this: the C standard defines some sizes that all compilers must support as a minimum, and one of them is the string literal maximum size. Compilers are free to allow bigger tokens when parsing.
I’m concerned that it would be a problem, because as I hinted above my use case includes compiling on old platforms with outdated C compilers (sometimes for fun, others because my customers need that) so it is important that cedro does not fail any more than strictly necessary when running on unusual machines.
Thinking about it, I could use strings when under the length limit, but those would be the cases where the performance difference would be small. I’ll keep things like this for now, but thanks to you I’ll take these aspects into account. EDIT END.
Alberto González Palomo en Lobsters, 2021-08-12
Un ejemplo de ese método es el
bin2c de Adobe
publicado en 2020 (no es el mismo que el
bin2c of 2012
que produce literales de octeto como xxd
),
y aunque no he mirado su código fuente,
Cedro sigue
lo especificado en su documentación
excepto los finales de línea, donde Cedro parte el literal de cadena
de la misma forma que suele hacerse a mano
y además limita el tamaño de cada cadena individual a 500 octetos
con la esperanza de que funcione en compiladores antiguos.
Más referencias (en inglés) con información sobre otros métodos:
Permite usar separadores de dígitos ('
o _
)
y literales binarios (0b…
).
A partir de C23, el separador apóstrofe y los literales binarios
son estándar.
Si tu compilador acepta C23, puedes usar la opción
--c23
para dejarlos como están.
Yo prefiero el subrayado, pero el comité del C23 no pudo usarlo por compatibilidad con el C++, que no pudo usarlo porque choca con los sufijos definibles que ya usaban el subrayado:
The syntax of digit separators is ripe for bikeshed discussions about what character to use as the separator token. The most common suggestions from the community are to use underscore (
_
), a single quote ('
), or whitespace as these seem like they would not produce any lexical ambiguities. However, two of these suggestions turn out to have usability issues in practice.[...] Use of an underscore character is also problematic, but only for C++. C++ has the ability for users to define custom literal suffixes [WG21 N2765], and these suffixes are required to be named with a legal C++ identifier, which includes the underscore character. Using an underscore as a digit separator then becomes ambiguous for a construct like
Aaaron Ballman en N2606 “Digit Separators”.0x1234_a
(does the_a
require a lookup for a user-defined literal suffix or is it part of the hexadecimal constant?).
Varios compiladores, por ejemplo GCC a partir de v4.3, permiten literales binarios como extensión del lenguage C.
123'45'67 |
1234567 |
123_45_67 |
1234567 |
123'45_67 |
1234567 |
0b10100110 |
0xA6 |
0b_1010_0110 |
0xA6 |
0b'1010_0110 |
0xA6 |