diff --git a/Makefile b/Makefile index b4a2bc6..6d4cf2e 100644 --- a/Makefile +++ b/Makefile @@ -4,17 +4,17 @@ CPPFLAGS = -Iinclude all: build/hello -build/escpos.o: src/escpos.c include/escpos.h +build/%.o: src/%.c include/escpos.h mkdir -p build/ - $(CC) $(CFLAGS) $(CPPFLAGS) -c src/escpos.c -o build/escpos.o + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ build/hello.o: examples/hello.c include/escpos.h mkdir -p build/ - $(CC) $(CFLAGS) $(CPPFLAGS) -c examples/hello.c -o build/hello.o + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ -build/hello: build/hello.o build/escpos.o +build/hello: build/hello.o build/escpos.o build/buffer.o mkdir -p build/ - $(CC) build/hello.o build/escpos.o -o build/hello + $(CC) $^ -o $@ clean: rm -rf build diff --git a/PROGRESO.md b/PROGRESO.md index 3454c36..ba23c9b 100644 --- a/PROGRESO.md +++ b/PROGRESO.md @@ -2,8 +2,8 @@ ## Estado actual -- **Fase 1** — Estructura del proyecto. -- Paso actual: Makefile. Hecho: reglas con dependencias correctas, variables CC/CFLAGS/CPPFLAGS, clean, .PHONY, cero warnings con gcc y clang. Siguiente: .gitignore y carpeta build/ en un clon limpio; después reglas patrón. +- **Fase 2** — Buffer dinámico. (Fase 1 cerrada en Fedora; falta probar en Windows.) +- Paso actual: 2.2, escpos_buffer_append_byte con realloc y duplicado de capacidad. - Entorno activo: Fedora 42 nativo (gcc 15.2, clang 20, make 4.4, gdb 17, valgrind 3.26). @@ -20,13 +20,20 @@ `static` para encapsular, `nm` y errores del enlazador. - Cadenas terminadas en `'\0'`, `const char *`. - Memoria virtual de un proceso, modo usuario y modo núcleo. +- Make: reglas, dependencias por fechas, variables, regla patrón con $@ $< $^, .PHONY. +- Pila y montón, malloc/free, NULL, propiedad de punteros, tipo opaco, typedef, `->`, uint8_t/size_t, Valgrind. ## Decisiones de diseño - Prefijo `escpos_` para toda la API pública. +- `escpos_buffer` es un tipo opaco: declarado en `escpos.h`, definido en `src/buffer.c`. Campos `uint8_t *data`, `size_t len`, `size_t cap`. +- Estilo: nombres en `snake_case` y llave de apertura en línea aparte (estilo Allman) en funciones, structs y bloques. Aplicarlo a todo el proyecto, también a `escpos.c`. +- Lo generado va en `build/`, que se crea en las recetas con `mkdir -p` y no se versiona. ## Pendiente / puntos débiles -- Hay binarios (`.o`, `.exe`) versionados en git. Falta `.gitignore`. +- `.gitignore` creado. Siguen versionados `emulator/escpos-emu.exe` y `goruntime/goruntime.exe`. - Ficheros sin salto de línea final: corregido; editor configurado. +- Make: costó entender que va hacia atrás desde el objetivo, y que una regla patrón es una plantilla. Repasar con la sección de docs/06-make.md. +- Punteros: le cuesta saber cuándo hace falta `*` y cuándo no (también le pasaba en Go). Reforzar con ejemplos de su propio código. - Impresoras físicas: sin registrar marca y modelo. diff --git a/docs/02-pointers-and-strings.md b/docs/02-pointers-and-strings.md index d1edd2b..5b839a3 100644 --- a/docs/02-pointers-and-strings.md +++ b/docs/02-pointers-and-strings.md @@ -17,6 +17,20 @@ Los literales como `"1.0.0"` viven en **memoria de solo lectura**. Si intentas escribir en ellos, el comportamiento es indefinido; en la práctica, el programa se cierra. +## `'A'` frente a `"A"` + +| Literal | Qué es | Tipo | Ocupa | +|---|---|---|---| +| `'A'` (comillas simples) | Un **carácter**: el número 65 | `int` (cabe en un `uint8_t`) | 1 byte guardado en un `uint8_t` | +| `"A"` (comillas dobles) | Una **cadena**: los bytes `'A'` y `'\0'` en memoria de solo lectura | `char *`: un puntero a esos bytes | 2 bytes, más el puntero | + +**En Go** es la misma diferencia que entre `'A'` (un `rune`) y `"A"` (un +`string`). + +Pasar `"A"` donde se espera un byte es pasar **una dirección** donde se +espera **un número**. gcc lo detecta: `makes integer from pointer without a +cast` o `differ in signedness`. + ## `const char *` Una función que "devuelve una cadena" en realidad devuelve **la dirección de @@ -44,6 +58,87 @@ Si `p` apunta a `"1.0.0"`, `*p` es solo el `'1'`. **En Go**: es lo mismo que `var p *T` frente a `*p`. +### Regla práctica: ¿quiero la dirección o la cosa? + +Un puntero es un papel con la dirección de una casa: + +- `buffer` es **el papel**, la dirección. +- `*buffer` es **la casa**, el struct que está en esa dirección. +- `buffer->len` es "ve a la casa y coge `len`". La flecha ya va a la casa, + así que no lleva `*`. Equivale a `(*buffer).len`. +- `&x` es lo contrario de `*`: "dame la dirección de `x`". + +Antes de escribir, pregúntate: **¿esta función o esta operación necesita la +dirección o la cosa?** + +Aplicado a `buffer.c`: + +| Código | ¿`*`? | Por qué | +|---|---|---| +| `escpos_buffer *buffer = ...` | Sí | Declaración: dice que `buffer` **es** un puntero | +| `malloc(sizeof *buffer)` | Sí | Quiero el tamaño **de la casa** (el struct, 24 bytes). `sizeof buffer` sería el tamaño **del papel** (el puntero, 8 bytes) | +| `if (buffer == NULL)` | No | Compruebo si el papel tiene una dirección válida | +| `buffer->data = NULL` | No | `->` ya va a la casa | +| `return buffer;` | No | Devuelvo el papel; la función devuelve `escpos_buffer *` | +| `free(buffer)` | No | `free` necesita la dirección del bloque | + +**En Go** las reglas son las mismas, pero Go oculta casi todos los `*`: +`p.X` desreferencia solo, y los métodos con receptor puntero también. Por eso en +Go casi solo ves `*` en los tipos (`*Buffer`) y rara vez en expresiones +(`*p = v`). En C la diferencia entre papel y casa siempre se escribe. + +## Dónde va el `*`: siempre con el tipo + +Los espacios alrededor del `*` dan igual. Estas tres líneas son idénticas: + +```c +escpos_buffer* escpos_buffer_new(void); +escpos_buffer * escpos_buffer_new(void); +escpos_buffer *escpos_buffer_new(void); +``` + +En todas, el tipo de retorno es `escpos_buffer *` ("puntero a +`escpos_buffer`"). El `*` **no** es de la función, es del tipo que devuelve. +En Go se lee igual: `func New() *Buffer`. + +La costumbre en C es pegarlo al nombre por esta trampa: + +```c +int *a, b; /* a es int *, pero b es int, NO un puntero */ +``` + +En una declaración múltiple, el `*` solo afecta al nombre que tiene al lado. +Pegarlo al nombre lo deja a la vista. Mejor aún: una variable por línea. + +## Nombres de parámetros en las declaraciones + +En una **declaración** (el prototipo de la cabecera), el nombre del parámetro +es opcional: `void escpos_buffer_free(escpos_buffer *);` es válido. El +compilador solo necesita los tipos. + +En la **definición** (en el `.c`) sí hace falta, porque el cuerpo lo usa. + +Aun así, conviene ponerlo también en la cabecera: es documentación. Con +`void copy(char *, const char *)` no sabes cuál es el destino; +con `void copy(char *dst, const char *src)` sí. + +**En Go** pasa lo mismo en los tipos función y en las interfaces: +`func(int) error` es válido sin nombres. + +## Paso por valor: un struct entero frente a un puntero + +C copia siempre los argumentos, igual que Go. Si un parámetro es +`escpos_buffer` (sin `*`), la función recibiría **una copia del struct +entero**. Eso tiene dos problemas: + +1. Con un tipo opaco ni siquiera compila: el struct está incompleto fuera de + su `.c` y no se sabe cuánto copiar. +2. Liberar una copia no sirve de nada: el bloque que reservó `malloc` es el + original. + +Por eso las funciones de un tipo opaco reciben siempre un **puntero**. Es lo +mismo que los métodos con receptor puntero de Go (`func (b *Buffer) Free()`). + ## `printf` y el formato El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena diff --git a/docs/06-make.md b/docs/06-make.md index cb262a7..dbfdf8c 100644 --- a/docs/06-make.md +++ b/docs/06-make.md @@ -26,6 +26,19 @@ objetivo: prerrequisitos - **Prerrequisitos**: los ficheros de los que depende. - **Receta**: los comandos de shell que producen el objetivo. +Es como una receta de cocina: *plato: ingredientes* y, debajo, los pasos. + +```make +build/hello: build/hello.o build/escpos.o + $(CC) build/hello.o build/escpos.o -o build/hello +``` + +| Parte | Qué es | Pregunta que responde | +|---|---|---| +| `build/hello` (antes de `:`) | **Objetivo**: el fichero que sale. No es el nombre de un comando. | ¿Qué quiero tener? | +| `build/hello.o build/escpos.o` (después de `:`) | **Prerrequisitos**: los ficheros que necesita. | ¿Qué hace falta antes? | +| línea con tabulador | **Receta**: el comando. | ¿Cómo se fabrica? | + Make rehace un objetivo si no existe o si **algún prerrequisito tiene una fecha de modificación más reciente** que él. No usa hashes como Go: solo compara fechas (`mtime`). Por eso un `touch` basta para forzar una recompilación. @@ -54,8 +67,33 @@ build/escpos.o: src/escpos.c include/escpos.h **En Go** es la diferencia entre llamar a `go build` paquete por paquete desde un script y dejar que `go build` resuelva los imports. -Make recorre el grafo desde el objetivo que le pides (o el primero del fichero) -hacia sus prerrequisitos, en profundidad. +### Make va hacia atrás + +Make no ejecuta el Makefile de arriba abajo como un script. Empieza por el +objetivo que le pides (o el primero del fichero si no pides ninguno) y va +tirando de lo que necesita: + +``` +all + └─ build/hello (enlazar) + ├─ build/hello.o ← examples/hello.c + escpos.h + └─ build/escpos.o ← regla patrón: src/escpos.c + escpos.h +``` + +1. Le pides `all`, que necesita `build/hello`. +2. `build/hello` necesita `build/hello.o` y `build/escpos.o`. +3. Para cada prerrequisito busca una regla que lo fabrique y repite lo mismo, + hasta llegar a ficheros que ya existen (`examples/hello.c`, `src/escpos.c`). +4. Después construye de las hojas hacia arriba. Antes de cada receta compara + fechas: si el objetivo existe y es más nuevo que todos sus prerrequisitos, + no lo rehace. + +El orden de las reglas en el fichero da igual. Solo importa que `all` sea la +primera, porque es la que se construye por defecto. + +Nadie le dice a Make "compila `escpos.o`": llega ahí porque `build/hello` lo +pide. Si un objetivo no aparece como prerrequisito de nada, Make no lo +construye. ## Piezas que vas a necesitar @@ -103,6 +141,45 @@ cambies compilador o flags: `make clean` y construir de cero. **En Go** los flags y la versión del compilador forman parte del hash de la caché, así que `go build` sí recompila lo necesario. +### Regla patrón = función con parámetros + +Una regla patrón es como una función de Go, y `$@` y `$<` son sus parámetros: + +```make +build/%.o: src/%.c + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ +``` + +```go +// Equivalente mental en Go +func buildObject(target, source string) { // target = $@, source = $< + run("gcc -std=c17 ... -c " + source + " -o " + target) +} +``` + +Cuando Make necesita `build/escpos.o`: + +1. Busca una regla para `build/escpos.o`. No hay ninguna escrita a mano. +2. Prueba la regla patrón `build/%.o`. Encaja, con `%` = `escpos`. +3. Calcula el prerrequisito: `src/%.c` → `src/escpos.c`. +4. "Llama a la función": `$@` = `build/escpos.o`, `$<` = `src/escpos.c`. +5. Sustituye las variables, **imprime el comando ya sustituido** y lo ejecuta. + +Por eso en la salida de `make` nunca se ve `$<`: Make imprime el comando +después de sustituir, igual que `fmt.Printf` imprime los valores y no `%s`. + +Otro ejemplo, para `build/buffer.o`: + +| | Valor | Ojo | +|---|---|---| +| `%` | `buffer` | Solo la parte que varía: sin carpeta ni extensión. | +| `$@` | `build/buffer.o` | El objetivo **completo**, con su carpeta. | +| `$<` | `src/buffer.c` | El prerrequisito **completo**, con su carpeta. | + +**La regla patrón es una plantilla, no compila "todo lo que hay en `src`".** +Make solo la usa cuando algún objetivo pide un `build/ALGO.o`. Si creas +`src/buffer.c` y nadie pide `build/buffer.o`, no se compila. + ## El problema de las cabeceras Si `escpos.c` incluye `escpos.h` y cambias solo el `.h`, make no lo sabe: diff --git a/docs/07-dynamic-memory.md b/docs/07-dynamic-memory.md new file mode 100644 index 0000000..0027ab2 --- /dev/null +++ b/docs/07-dynamic-memory.md @@ -0,0 +1,329 @@ +# Memoria dinámica: `malloc` y `free` + +## Pila y montón + +Un proceso tiene dos sitios principales donde guardar datos mientras se +ejecuta: + +| | Pila (*stack*) | Montón (*heap*) | +|---|---|---| +| Qué va ahí | Variables locales de una función | Lo que pides con `malloc` | +| Cuándo se libera | Sola, al salir de la función | Cuando llamas a `free` | +| Tamaño | Fijo y pequeño (unos 8 MB en Linux) | Grande, crece bajo demanda | +| Coste | Casi gratis | Llamada a función. A veces, llamada al sistema | + +El problema de la pila: al salir de la función, sus variables dejan de existir. +Si devuelves la dirección de una variable local, el que llama recibe un puntero +a memoria que ya no es suya (*dangling pointer*). Leerlo es **comportamiento +indefinido**: puede parecer que funciona, dar basura o colgar el programa. + +### Dónde están físicamente + +En la **misma RAM**. La pila no está en un chip especial ni el montón en +otro. La diferencia es de organización, no física: + +- Cada proceso ve un **espacio de direcciones virtual** propio (ver + [03-process-memory.md](03-process-memory.md)). Dentro de ese espacio, el + sistema operativo coloca cada zona en un rango de direcciones distinto. +- La MMU traduce cada página virtual (4 KB) a un **marco físico cualquiera** de + la RAM. Páginas contiguas de la pila pueden estar en marcos dispersos, e + incluso en disco (*swap*) si falta memoria. + +Disposición típica en Linux x86-64, de direcciones bajas a altas: + +``` +0x0000... código del programa (.text) r-x + datos globales (.data, .bss) rw- + [heap] ↓ crece hacia arriba rw- (malloc, brk) + ... librerías y mmap ... (malloc grandes) + [stack] ↑ crece hacia abajo rw- (8 MB máx., ulimit -s) +0x7fff... +``` + +Se ve en vivo: `cat /proc/self/maps` muestra el mapa del propio `cat`, con +las líneas `[heap]` y `[stack]`. Para un proceso en marcha: +`cat /proc//maps`. + +**`[heap]` no es todo el montón.** La línea `[heap]` del mapa es solo la +zona clásica que `malloc` hace crecer con la llamada al sistema `brk`. Los +bloques grandes (más de 128 KB en glibc) los pide `malloc` con `mmap`, y +aparecen como zonas sin nombre. Chromium, V8 y el runtime de Go usan sus +propios gestores sobre `mmap` y pueden no tener `[heap]` en absoluto. +Comprobado con el proceso principal de VS Code: tiene `[stack]` pero no +`[heap]`. + +### Memoria virtual frente a memoria real + +`/proc//status` muestra dos cifras: + +- `VmSize`: direcciones virtuales reservadas. Reservar direcciones no gasta + RAM. +- `VmRSS` (*Resident Set Size*): páginas que están de verdad en RAM porque + alguien ha escrito en ellas. Es lo que cuenta para el OOM killer y para el + límite de memoria de un contenedor. + +Ejemplo real con el proceso principal de VS Code: + +``` +VmSize: 1519018808 kB ~1,4 TB +VmRSS: 293276 kB ~286 MB +``` + +V8 reserva un rango enorme de direcciones (su *sandbox*) para que los objetos +de JavaScript no puedan apuntar fuera de él. En x86-64 cada proceso tiene +128 TB de espacio virtual, así que 1,4 TB es un 1 %. + +**Por qué la pila es tan rápida** si está en la misma RAM: + +- Reservar en la pila es restar un número a un registro (`rsp`, el *stack + pointer*): una instrucción. Al salir de la función se suma de vuelta. +- Esas páginas se usan constantemente, así que están en la caché de la CPU. +- `malloc` en cambio tiene que buscar un hueco libre en sus listas, y a veces + pedir más memoria al núcleo con una llamada al sistema (`brk` o `mmap`). + +**En Go**: la pila de cada goroutine **no** es la pila del sistema operativo. +El runtime la reserva en su propio montón: empieza pequeña (unos KB) y la +copia a un bloque mayor cuando se queda corta. Por eso puedes tener millones de +goroutines y en C no puedes tener millones de hilos. + +## Qué hacía Go por mí + +En Go puedes escribir esto sin problema: + +```go +func newBuffer() *Buffer { + b := Buffer{} + return &b +} +``` + +El compilador hace *escape analysis*: ve que `b` sobrevive a la función y la +coloca en el montón en lugar de la pila. Luego el recolector de basura (GC) la +libera cuando nadie la referencia. Se ve con `go build -gcflags=-m` +(`moved to heap: b`). + +En C nada de eso existe. **Tú decides** dónde va cada dato y **tú lo liberas**. + +## `malloc` y `free` + +Se declaran en ``. + +- `malloc(n)` reserva `n` bytes en el montón y devuelve un puntero al primero. + - El contenido **no está inicializado**: es basura. En Go, `new` y `make` + ponen todo a cero; aquí no. + - Si no hay memoria, devuelve `NULL`. **Hay que comprobarlo siempre.** + - Devuelve `void *`, un puntero "a cualquier cosa". En C se convierte solo al + tipo de puntero que lo recibe, sin *cast*. +- `free(p)` devuelve esa memoria. + - Después, `p` sigue apuntando a la misma dirección, pero ya no es tuya. Usarla + (*use after free*) es comportamiento indefinido. + - Liberar dos veces lo mismo (*double free*) también lo es. + - `free(NULL)` no hace nada. Es seguro. +- `calloc(count, size)` es como `malloc`, pero pone a cero la memoria. + +Forma idiomática de reservar un `struct`: + +```c +struct escpos_buffer *buf = malloc(sizeof *buf); +``` + +`sizeof *buf` es "el tamaño de aquello a lo que apunta `buf`". Se calcula al +compilar, no lee nada en ejecución. Así, si cambia el tipo de `buf`, el tamaño +cambia solo. + +## `realloc`: cambiar el tamaño de un bloque + +`realloc(p, n)` cambia el tamaño del bloque `p` a `n` bytes y devuelve un +puntero al bloque redimensionado: + +- Si hay sitio justo detrás, lo agranda **en el mismo sitio** y devuelve la + misma dirección. +- Si no, reserva un bloque nuevo, **copia** el contenido, libera el viejo y + devuelve la dirección **nueva**. Cualquier otro puntero al bloque viejo queda + colgando. +- `realloc(NULL, n)` equivale a `malloc(n)`. Por eso un buffer vacío puede + empezar con `data = NULL`. +- Si falla, devuelve `NULL` y **el bloque viejo sigue intacto y sigue siendo + tuyo**. + +**Qué bloque se redimensiona.** En un buffer hay dos bloques: + +``` +buffer ──▶ [ data | len | cap ] struct: 24 bytes fijos, no crece nunca + │ + └──▶ [ A A A A ... ] bytes: este es el que crece +``` + +Se hace `realloc` de `buffer->data`, nunca de `buffer`. Hacer `realloc` del +struct puede moverlo a otra dirección y liberar el viejo, y entonces el +puntero `buffer` que tiene el que llama queda colgando. + +El error clásico: + +```c +p = realloc(p, n); /* MAL: si falla, p pasa a NULL y el bloque viejo se pierde (fuga) */ +``` + +Lo correcto es guardar el resultado en una variable temporal, comprobarlo y +solo entonces asignarlo. + +**En Go** es exactamente lo que hace `append` por dentro. Por eso hay que +escribir `s = append(s, x)`: si el array subyacente se movió, el slice viejo +apunta al sitio antiguo. + +### Estrategia de crecimiento + +Si el buffer creciera de byte en byte, cada byte nuevo podría provocar una +copia de todo lo anterior: añadir `n` bytes costaría del orden de `n²`. La +solución es **duplicar la capacidad** cada vez que se llena. Así hay pocas +copias, y el coste medio de añadir un byte es constante (*coste amortizado +O(1)*). + +`append` en Go duplica hasta 256 elementos y a partir de ahí crece más despacio +(unos 1,25×). + +## Fugas de memoria + +Si pierdes el último puntero a un bloque sin hacer `free`, ese bloque queda +reservado hasta que acabe el proceso: es una **fuga** (*memory leak*). En un +programa que dura poco no se nota. En un servicio que imprime tiques todo el +día, la memoria crece hasta que el sistema lo mata. + +**En Go** el GC lo evita. Las fugas que hay en Go son de otro tipo: goroutines +bloqueadas o referencias que guardas sin querer. + +### Qué pasa cuando se acaba la memoria + +**Linux: *overcommit* y OOM killer.** + +- `malloc` casi nunca devuelve `NULL` en Linux. El núcleo solo reserva + **direcciones virtuales**, y promete más memoria de la que tiene + (*overcommit*). La RAM física se asigna página a página, **la primera vez + que escribes** en ella, mediante un fallo de página. +- Si llega un momento en que alguien escribe y no queda RAM ni *swap*, el núcleo + no puede cumplir la promesa. Entonces actúa el **OOM killer** (*Out Of + Memory*): elige el proceso con mayor `oom_score`, que es básicamente el que + más memoria usa, y le envía `SIGKILL`. +- `SIGKILL` no se puede capturar ni ignorar. El proceso muere en el acto, sin + `defer`, sin cerrar ficheros y sin dejar un error en su propio log. +- Queda rastro en el log del núcleo: `journalctl -k | grep -i "out of memory"` + (`Killed process 1234 (hello) ...`). +- Fedora tiene además `systemd-oomd`, que actúa antes, cuando detecta presión + de memoria sostenida, y mata grupos de procesos enteros. +- **Contenedores**: un límite de memoria de Docker o Kubernetes es un *cgroup*. + Al superarlo actúa el mismo OOM killer, pero solo dentro del contenedor. Es + el `OOMKilled` de Kubernetes (código de salida 137 = 128 + 9, la señal + `SIGKILL`). + +**Windows** no tiene OOM killer. Lleva la cuenta de la memoria prometida +(*commit charge*) frente a RAM + fichero de paginación. Si se supera, `malloc` +**sí devuelve `NULL`**. + +**Android** tiene `lmkd` (*low memory killer*), que mata primero las apps en +segundo plano. + +Por eso hay que comprobar `NULL` aunque en Linux casi nunca salga: en Windows +sí ocurre, también con límites como `ulimit -v`, y con peticiones absurdas +(por ejemplo, un tamaño calculado mal que da varios exabytes). + +**En Go** pasa lo mismo. Si el runtime no consigue memoria, el programa muere +con `fatal error: runtime: out of memory`, que no se puede recuperar con +`recover`. Y si lo mata el OOM killer, ni siquiera eso. `GOMEMLIMIT` le dice al +GC que trabaje más cuando se acerca a un límite, para no llegar ahí. + +## Propiedad (*ownership*) + +Como no hay GC, cada puntero tiene un **dueño**: el código responsable de +liberarlo. La API tiene que dejarlo claro. La convención del proyecto es +documentar en la cabecera, en cada función, quién reserva y quién libera. + +El patrón típico es una pareja de funciones: + +- `x_new()` reserva y devuelve el puntero. El que llama pasa a ser el dueño. +- `x_free(p)` libera. Después de llamarla, el que llama no debe usar `p`. + +Es parecido a `defer f.Close()` en Go: un recurso que tienes que devolver tú. +La diferencia es que en C también la memoria es un recurso que se devuelve a +mano. + +**El dueño es quien tiene la obligación de liberar.** No es quien reservó la +memoria, sino quien debe acabar llamando a `free`. Esa obligación puede pasar +de una función a otra, y por eso hay que escribirla en la cabecera: el tipo +`escpos_buffer *` no dice nada sobre quién libera. + +Dos funciones de la API que devuelven punteros con dueños distintos: + +| Función | Quién reserva | Dueño | Qué debe hacer el que llama | +|---|---|---|---| +| `escpos_buffer_new()` | la librería, con `malloc` | **el que llama** (se le traspasa) | llamar a `escpos_buffer_free` una vez, y no usar el puntero después | +| `escpos_version()` | nadie: es un literal de cadena en memoria de solo lectura | **la librería** (vive mientras vive el programa) | solo leerlo. Si hace `free`, el comportamiento es indefinido (normalmente el programa aborta) | + +Mirando solo las firmas, ambas "devuelven un puntero". El comentario es lo que +dice qué hacer con cada una. + +**Con cgo** (fase 12) importa todavía más: el GC de Go no ve la memoria de C. +El código Go que llame a `escpos_buffer_new` tendrá que hacer +`defer C.escpos_buffer_free(b)`, igual que con un fichero. + +## Valgrind + +`valgrind` ejecuta tu programa en una CPU simulada y vigila cada acceso a +memoria. Detecta fugas, lecturas de memoria sin inicializar, *use after free* +y accesos fuera de un bloque. El programa va unas 20-50 veces más lento. + +``` +valgrind --leak-check=full ./build/hello +``` + +Al final del informe: + +- `All heap blocks were freed -- no leaks are possible`: todo bien. +- `definitely lost: N bytes in M blocks`: fuga. Con `-g`, Valgrind indica + la línea del `malloc` que nunca se liberó. + +Solo funciona en Linux. Por eso la parte portable se prueba en Fedora. + +### Cómo leer un informe de fuga + +Informe real, quitando el `escpos_buffer_free` de `hello.c`: + +``` +HEAP SUMMARY: + in use at exit: 24 bytes in 1 blocks + total heap usage: 2 allocs, 1 frees, 4,120 bytes allocated + +24 bytes in 1 blocks are definitely lost in loss record 1 of 1 + at 0x483FB26: malloc (vg_replace_malloc.c:447) + by 0x4004DD: escpos_buffer_new (buffer.c:16) + by 0x4004A7: main (hello.c:10) +``` + +- **`2 allocs`**: una es nuestra (24 bytes: el struct). La otra (4.096 bytes) + es el búfer interno que crea `printf` para la salida estándar, que la + librería de C libera al terminar. Valgrind vigila todo el proceso, no solo + tu código. +- **La pila de llamadas se lee de abajo arriba**: `main` (línea 10 de + `hello.c`) llamó a `escpos_buffer_new`, que en la línea 16 de `buffer.c` + llamó a `malloc`. Los ficheros y las líneas salen gracias a `-g`. +- Valgrind dice **dónde se reservó** el bloque perdido, no dónde faltó el + `free`. Eso lo deduces tú: ¿quién era el dueño de ese puntero? + +**Cómo sabe Valgrind que es una fuga.** Al terminar el programa, recorre toda +la memoria que todavía es accesible (pila, variables globales, registros y los +demás bloques del montón) buscando algún valor que sea la dirección del bloque. +Si no encuentra ninguno, ningún código podría ya hacer `free` de ese bloque: la +fuga es **segura**, no una sospecha. Es la fase de marcado del GC de Go: el GC +libera lo que no alcanza, y Valgrind lo denuncia. + +En el ejemplo, el único puntero era la variable local `buffer` de `main`, que +estaba en la pila. Cuando `main` terminó, esa variable desapareció y con ella +la última referencia al bloque. + +Tipos de pérdida en `LEAK SUMMARY`: + +| Tipo | Significado | +|---|---| +| `definitely lost` | Ningún puntero apunta ya al bloque. Fuga segura | +| `indirectly lost` | Solo se llega al bloque desde otro bloque perdido. Por ejemplo, el `data` de un buffer cuyo struct se perdió | +| `possibly lost` | Solo hay punteros al interior del bloque, no a su inicio | +| `still reachable` | Al salir aún había un puntero (por ejemplo, una variable global). No es una fuga, pero nadie lo liberó | diff --git a/docs/08-structs.md b/docs/08-structs.md new file mode 100644 index 0000000..49ccf91 --- /dev/null +++ b/docs/08-structs.md @@ -0,0 +1,201 @@ +# Structs + +## Definir un struct + +Definir es dar los campos. Es el equivalente a `type Point struct { ... }` de +Go: + +```c +struct point { + int x; + int y; +}; /* ← punto y coma obligatorio */ +``` + +Diferencias con Go: + +| Go | C | +|---|---| +| `X int` (nombre, tipo) | `int x;` (tipo, nombre) | +| Sin `;` | Cada campo acaba en `;`, y la llave final también: `};` | +| El nombre del tipo es `Point` | El nombre del tipo es `struct point`: la palabra `struct` forma parte del nombre | +| Mayúscula = exportado | No hay campos privados: o ves la definición entera o no ves nada | + +## Declarar un struct (tipo incompleto) + +Declarar es decir "existe un struct con este nombre" sin dar sus campos: + +```c +struct point; +``` + +Hasta que aparezca la definición, `struct point` es un **tipo incompleto**. El +compilador no conoce su tamaño ni sus campos, así que: + +| Con un tipo incompleto | ¿Se puede? | +|---|---| +| Declarar un puntero: `struct point *p;` | Sí. Todos los punteros a struct miden lo mismo | +| Pasar o devolver punteros en funciones | Sí | +| Crear una variable: `struct point p;` | No: no sabe cuántos bytes reservar | +| `sizeof(struct point)` | No | +| Acceder a un campo: `p->x` | No: no sabe que existe `x` | + +Esto es justo lo que hace funcionar el **tipo opaco**: la cabecera pública +declara, el `.c` define. El código cliente solo maneja punteros y llama a +funciones de la librería. Dentro del `.c`, donde está la definición, el tipo +está completo y se usan los campos con normalidad. + +Es parecido a devolver una interfaz en Go para ocultar el tipo concreto, pero +sin coste de llamada indirecta: es solo un puntero. + +### Struct público frente a struct opaco + +Definir el struct completo en la cabecera es perfectamente válido. Es una +decisión de diseño: + +| | Struct definido en el `.h` (público) | Struct opaco | +|---|---|---| +| Crear uno | En la pila, sin `malloc`: `struct point p;` | Solo con la función `_new` de la librería (montón) | +| Campos | Cualquiera puede leerlos **y cambiarlos** | Solo a través de funciones | +| Invariantes (`len <= cap`) | El cliente puede romperlas: `b.len = 9999` y luego un desbordamiento | La librería las garantiza | +| Cambiar los campos | Rompe a quien ya compiló contra la versión anterior (el tamaño y los desplazamientos van dentro de su `.o`) | Libre: el cliente nunca supo cómo era | +| Coste | Acceso directo, sin llamadas | Una llamada a función por cada acceso | + +Se usa **público** para datos simples sin reglas internas, que el usuario +rellena: `struct tm` (fechas) o `struct sockaddr_in` (direcciones de red, fase +4). Se usa **opaco** para objetos con estado e invariantes: `FILE` en la +práctica, o nuestro `escpos_buffer`. + +**En Go** es la diferencia entre `type Point struct { X, Y int }` (campos +exportados) y `type Buffer struct { data []byte }` (campos sin exportar, con +constructor y métodos). La diferencia es que en Go el tamaño del struct no +importa entre paquetes, porque todo se recompila junto. + +Para este proyecto el handle opaco es obligatorio: los bindings de cgo +(fase 12) necesitan una API que no cambie aunque cambien las tripas. + +## `typedef`: un alias para no escribir `struct` + +```c +typedef struct point point; +``` + +Se lee así: "`point` es otro nombre para `struct point`". A partir de ahí, +`point *p` equivale a `struct point *p`. Puede ir antes de la definición: sirve +igual para un tipo incompleto. Es lo más parecido a cómo nombra Go los tipos. + +Es habitual usar el mismo nombre para el struct y para el alias. No chocan, +porque C guarda los nombres de struct en un espacio de nombres aparte. + +La sintaxis es `typedef ;`. El nombre nuevo va +**al final**, como en la declaración de una variable. Error típico: escribir +`typedef struct escpos_buffer;` sin el nombre nuevo. gcc solo avisa +(`useless storage class specifier in empty declaration`): declara el struct +pero no crea ningún alias. + +**En Go**, el alias equivalente es `type Nuevo = Existente`, con el orden al +revés. + +## Tipos que necesitan un `#include` + +En Go, `byte`, `int` o `uint8` existen siempre. En C solo vienen de serie los +tipos básicos (`char`, `int`, `long`...). Los de tamaño fijo y los de tamaños +son `typedef` declarados en cabeceras estándar: + +| Tipo | Cabecera | Equivalente Go | +|---|---|---| +| `uint8_t`, `int32_t`, `uint64_t`... | `` | `uint8`, `int32`, `uint64` | +| `size_t` (tamaños y longitudes) | `` (también la traen ``, ``, ``) | el `int` de `len()`, pero sin signo | +| `bool`, `true`, `false` | `` | `bool` | +| `NULL` | `` (y las mismas que `size_t`) | `nil` | + +Sin el `#include`: `error: unknown type name 'uint8_t'`. El editor (IntelliSense +de VS Code) lo muestra como `identifier "uint8_t" is undefined`. + +### `uint8_t` + +Entero **sin signo** (*u*nsigned) de **exactamente 8 bits**: valores de 0 a +255. Es el `byte`/`uint8` de Go. El sufijo `_t` es la convención para los +nombres de tipos creados con `typedef`. + +¿Por qué no `char`, que también mide un byte? Porque el estándar no fija si +`char` tiene signo. En x86 lo tiene (de -128 a 127), y en ARM, el procesador +de casi todos los Android, no. Un byte `0xE9` (la `é` en algunas páginas de +códigos) valdría -23 en tu PC y 233 en el móvil. `uint8_t` es 0-255 en +todas partes. + +### `size_t` + +Entero **sin signo** pensado para **tamaños en bytes y número de elementos**: + +- Es lo que devuelve `sizeof` y lo que recibe `malloc`. +- Mide lo mismo que un puntero: 64 bits en x86-64 y 32 bits en un ARM de 32 + bits. Así siempre cabe el tamaño de cualquier bloque que puedas direccionar. +- En `printf` se imprime con `%zu`. + +**En Go** su papel lo hace `int`, que es lo que devuelven `len()` y `cap()`. +Pero `int` tiene signo y `size_t` no. + +Trampa de los tipos sin signo: no hay negativos. `size_t n = 0; n - 1` **no +da -1**: da la vuelta al valor máximo (18446744073709551615 en 64 bits). Un +bucle `for (size_t i = n - 1; i >= 0; i--)` no termina nunca, porque +`i >= 0` siempre es verdad. Go, con `int`, no tiene este problema. + +### Qué tipo usar + +Se elige por **lo que representa el valor**, no por ahorrar memoria: + +| Representa | Tipo | Go | +|---|---|---| +| Un número cualquiera, un contador o un código de resultado (puede ser negativo: `-1`) | `int` | `int` | +| Un tamaño, una longitud o un índice en memoria | `size_t` | `int` (`len`, `cap`) | +| Un byte de datos crudos (lo que se envía a la impresora) | `uint8_t` | `byte` | +| Un valor con anchura fija por protocolo o formato (2 bytes de un comando, 4 de una cabecera) | `uint16_t`, `uint32_t`... | `uint16`, `uint32` | +| Sí o no | `bool` (``) | `bool` | +| Texto | `char` / `char *` | `string` | + +Usar un tipo pequeño para una variable local o un valor de retorno **no ahorra +nada**. El valor viaja en un registro de 64 bits igualmente, y en las +operaciones C lo convierte a `int` (*promoción entera*). Solo complica las +cosas. + +**Ejemplo real de fallo**: una función que devuelve `uint8_t` y hace +`return -1;`. Un `uint8_t` no tiene negativos, así que `-1` se convierte en +`255`. Después, `if (err == -1)` compara `255` con `-1`: **siempre es falso**, y +el error no se detecta nunca. gcc con los flags del proyecto no avisa. Clang +sí (`comparison of constant -1 with expression of type 'uint8_t' is always +false`), y gcc también lo hace con `-Wconversion`. + +Cada fichero incluye lo que usa él mismo. No hay que fiarse de que otra +cabecera lo traiga de rebote. + +## Acceder a los campos: `.` y `->` + +| Tienes | C | Go | +|---|---|---| +| Un valor: `struct point p` | `p.x` | `p.X` | +| Un puntero: `struct point *p` | `p->x` | `p.X` (Go desreferencia solo) | + +`p->x` es una abreviatura de `(*p).x`: "sigue el puntero y coge el campo `x`". +Si usas `.` con un puntero o `->` con un valor, el compilador da error. + +## Inicializar + +```c +struct point a = { .x = 1, .y = 2 }; /* inicializadores designados */ +struct point b = { 0 }; /* todo a cero */ +``` + +Los campos que no nombras quedan a cero. Como en un literal de Go +(`Point{X: 1}`). + +Pero esto solo vale al **crear una variable**. Un struct reservado con +`malloc` es basura. Hay que rellenar cada campo a mano con `->`, o usar +`calloc`, que lo pone todo a cero. + +## Dónde va cada cosa en el proyecto + +| Fichero | Contenido | Quién lo ve | +|---|---|---| +| `include/escpos.h` | `typedef struct escpos_buffer escpos_buffer;` (declaración + alias) | Todo el mundo | +| `src/buffer.c` | `struct escpos_buffer { ... };` (definición) | Solo `buffer.c` | diff --git a/docs/09-debugging.md b/docs/09-debugging.md new file mode 100644 index 0000000..1e4d2d6 --- /dev/null +++ b/docs/09-debugging.md @@ -0,0 +1,85 @@ +# Depurar: *segfault* y gdb + +## Qué es un *segmentation fault* + +El programa accede a una dirección que no tiene mapeada en su espacio +virtual, o la usa sin permiso (por ejemplo, escribe en memoria de solo +lectura). La MMU no encuentra la traducción, el procesador avisa al núcleo, y +el núcleo envía al proceso la señal `SIGSEGV`, que por defecto lo mata (ver +[03-process-memory.md](03-process-memory.md)). + +- La shell lo muestra como `Segmentation fault (core dumped)`. +- El código de salida es **139** = 128 + 11 (`SIGSEGV` es la señal 11). Es la + misma cuenta que el 137 de `SIGKILL` en el OOM killer. +- El caso más típico es escribir o leer a través de un puntero `NULL`. La + página de la dirección 0 nunca se mapea, precisamente para que este error se + detecte siempre. + +Ojo: no todo acceso indebido da *segfault*. Si te sales de un bloque pero +caes en memoria que sí es del proceso, no pasa nada visible: corrompes datos +en silencio. Para eso están Valgrind y AddressSanitizer. + +**En Go**: el mismo fallo da `panic: runtime error: invalid memory address or +nil pointer dereference`, con la traza de la pila. Go captura el `SIGSEGV` y lo +convierte en un `panic`. En C no hay traza: hay que pedírsela a gdb. + +## `free(): invalid pointer` y *Aborted* + +glibc comprueba algunas cosas al hacer `free`. Si el puntero no es el inicio de +un bloque que haya dado `malloc`, `calloc` o `realloc`, imprime +`free(): invalid pointer` y llama a `abort()`. + +- La shell muestra `Aborted (core dumped)`, con código **134** = 128 + 6 + (`SIGABRT`). +- Causas típicas: hacer `free` de la dirección de una variable local (`&x`), + de un literal (`"hola"`), de un puntero al **interior** de un bloque, o de un + bloque ya liberado (*double free*). + +**Para depurarlo, mejor Valgrind que gdb.** gdb te dice dónde se cae (en el +`free`), pero el error está antes, donde el puntero recibió un valor malo. +Valgrind te dice **de dónde viene** la dirección: + +``` +Invalid free() / delete / delete[] / realloc() + at free + by escpos_buffer_free (buffer.c:36) + by main (hello.c:22) + Address 0x1ffefff544 is on thread 1's stack +``` + +"Is on thread 1's stack": la dirección es de **la pila**, así que nunca la dio +`malloc`. Después busca cada línea que asigna un valor a ese puntero +(`grep -n "data =" src/*.c`) y mira cuál le da una dirección de la pila. + +### Método general + +1. Lee el mensaje. Valgrind casi siempre dice qué pasó (`Invalid write`, + `Invalid free`), dónde y de qué tipo es la dirección (pila, N bytes después + de un bloque, bloque ya liberado...). +2. El síntoma (donde se cae) no suele ser la causa. Pregúntate de dónde salió + el valor malo. +3. Busca todas las asignaciones de esa variable y descarta una a una. + +## gdb: lo mínimo + +Hace falta compilar con `-g`. El Makefile ya lo hace. + +``` +gdb ./build/hello +``` + +| Comando de gdb | Qué hace | +|---|---| +| `run` (`r`) | Ejecuta el programa. Si se cuelga, gdb se para en la línea exacta | +| `bt` (*backtrace*) | Muestra la pila de llamadas: quién llamó a quién hasta llegar aquí | +| `print expr` (`p`) | Muestra el valor de una variable o expresión: `p buffer->data` | +| `break fichero.c:línea` (`b`) | Pone un punto de parada | +| `next` (`n`) | Ejecuta la línea actual sin entrar en las funciones | +| `step` (`s`) | Ejecuta la línea actual entrando en las funciones | +| `continue` (`c`) | Sigue hasta el siguiente punto de parada | +| `quit` (`q`) | Sale | + +Al arrancar, Fedora pregunta si quieres descargar símbolos de depuración del +sistema (*debuginfod*). Para nuestro código no hacen falta: responde `n`. + +**En Go** el equivalente es Delve (`dlv debug`), con comandos casi iguales. diff --git a/docs/README.md b/docs/README.md index b2ca6e2..c2fe925 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,9 @@ Ordenados por tema, en el orden en que se ven en el proyecto. para compilar, cabeceras e `#include`, encapsulación y tipos opacos, *include guards*, flags, qué ve quien recibe los binarios, `nm` y errores del enlazador. 2. [Punteros y cadenas](02-pointers-and-strings.md): cadenas terminadas en - `'\0'`, `const char *`, los dos significados de `*` y el formato de `printf`. + `'\0'`, `const char *`, los dos significados de `*`, dónde va el `*`, + nombres de parámetros, paso por valor frente a puntero y el formato de + `printf`. 3. [La memoria de un proceso](03-process-memory.md): memoria virtual, fallos de página, modo usuario y modo núcleo, llamadas al sistema, cómo se lee la memoria de otro proceso, y por qué el peligro real está en la tuya. @@ -21,6 +23,16 @@ Ordenados por tema, en el orden en que se ven en el proyecto. 6. [Make](06-make.md): reglas, grafo de dependencias por fechas, variables automáticas, reglas patrón, dependencias de cabeceras con `-MMD -MP` y detección de Windows/Linux. +7. [Memoria dinámica](07-dynamic-memory.md): pila y montón, *escape + analysis* de Go, `malloc`/`free`/`calloc`, `sizeof *p`, `realloc` y + estrategia de crecimiento, fugas, propiedad de + los punteros y Valgrind. +8. [Structs](08-structs.md): definir frente a declarar, tipos incompletos y + tipo opaco, struct público frente a opaco, `typedef`, `uint8_t` y + `size_t`, qué tipo usar en cada caso, `.` frente a `->` e inicializadores designados. +9. [Depurar](09-debugging.md): qué es un *segfault*, código de salida 139, + `NULL` y la página 0, `free(): invalid pointer` (código 134), método para + depurar con Valgrind y los comandos mínimos de gdb. [Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por tarea. diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index 9157843..cb6d601 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -32,6 +32,30 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. | `tail -c 5 fichero \| xxd` | Muestra los últimos bytes: sirve para ver si el fichero acaba en `0a` (`\n`). | | `echo $?` | Código de salida del último comando: `0` es éxito. | +## Memoria + +| Comando | Qué hace | +|---|---| +| `valgrind ./build/hello` | Sin `--leak-check`: errores de acceso (`Invalid write`, `Invalid free`) y de dónde viene cada dirección. | +| `valgrind --leak-check=full ./build/hello` | Ejecuta vigilando la memoria: fugas, *use after free*, lecturas sin inicializar. Solo Linux. | +| `cat /proc/self/maps` | Mapa de memoria virtual del proceso: código, `[heap]`, `[stack]`, librerías. Con `` en vez de `self`, el de otro proceso. | +| `journalctl -k \| grep -i "out of memory"` | Ver si el OOM killer del núcleo ha matado algún proceso. | +| `pgrep -a nombre` | Lista los procesos cuyo nombre contiene `nombre`, con su PID y su línea de comandos. | +| `pgrep -o nombre` | PID del proceso más antiguo con ese nombre (normalmente el principal). | +| `grep -E 'VmSize\|VmRSS' /proc//status` | Memoria virtual reservada (`VmSize`) frente a RAM física en uso (`VmRSS`). | +| `grep -E 'heap\|stack' /proc//maps` | Rangos de direcciones del montón y de la pila de un proceso. | +| `pmap -x ` | Mapa de memoria con tamaño virtual y residente de cada zona. | +| `ulimit -s` | Tamaño máximo de la pila en KB (8192 = 8 MB). | +| `go build -gcflags=-m` | (Go) Muestra qué variables escapan al montón. | + +## Depurar + +| Comando | Qué hace | +|---|---| +| `gdb ./build/hello` | Abre el programa en el depurador (hace falta `-g`). | +| `run` / `bt` / `print x` | Dentro de gdb: ejecutar, ver la pila de llamadas, ver una variable. | +| `break f.c:42` / `next` / `step` / `continue` | Dentro de gdb: punto de parada, siguiente línea, entrar en la función, seguir. | + ## Solo en Windows (MSYS2) | Comando | Qué hace | @@ -61,6 +85,13 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. | `missing separator` | Make | La receta empieza por espacios en vez de tabulador. | | `'x' is up to date` | Make | No es un error: el objetivo es más nuevo que todos sus prerrequisitos. Si `x` es una acción (`clean`), falta en `.PHONY`. | | `no newline at end of file` | Clang | El fichero no termina en `\n`. | +| `unknown type name 'uint8_t'` / `identifier ... is undefined` | Compilador / VS Code | Falta el `#include` que declara ese tipo (``, ``...). | +| `makes integer from pointer without a cast` | Compilador | Se pasa una dirección (puntero) donde se espera un número. Por ejemplo, `"A"` en vez de `'A'`. | +| `pointer targets ... differ in signedness` | Compilador | Se mezclan `char *` y `uint8_t *` (`unsigned char *`). | +| `Segmentation fault (core dumped)` (código 139) | Núcleo | Acceso a una dirección no mapeada; casi siempre un puntero `NULL`. Usa gdb. | +| `free(): invalid pointer` / `Aborted` (código 134) | glibc | `free` de algo que no dio `malloc`: dirección de la pila (`&x`), literal, interior de un bloque o *double free*. Usa Valgrind. | +| `comparison of constant -1 with expression of type 'uint8_t' is always false` | Clang / VS Code | Comparas un tipo sin signo con un negativo: nunca puede ser igual. Revisa el tipo. | +| `useless storage class specifier in empty declaration` | Compilador | Un `typedef` sin nombre nuevo al final. | ## Go equivalente diff --git a/examples/hello.c b/examples/hello.c index bb29426..9a598a8 100644 --- a/examples/hello.c +++ b/examples/hello.c @@ -6,6 +6,23 @@ int main(void) { const char *version = escpos_version(); printf("%s\n", version); + + escpos_buffer *buffer = escpos_buffer_new(); + if (buffer == NULL) + { + return 1; + } + + for (int i = 0; i < 100; i++) + { + int err = escpos_buffer_append_byte(buffer, 'A'); + if (err == -1) + { + return 1; + } + } + + escpos_buffer_free(buffer); return 0; } diff --git a/include/escpos.h b/include/escpos.h index 643a28a..8f04363 100644 --- a/include/escpos.h +++ b/include/escpos.h @@ -1,6 +1,30 @@ #ifndef ESCPOS_H - #define ESCPOS_H +#define ESCPOS_H + #include const char *escpos_version(void); + + typedef struct escpos_buffer escpos_buffer; + + /* + * Crea un buffer vacío. Devuelve un puntero al buffer, o NULL si no hay + * memoria. + * El que llama es el dueño y debe liberarlo con escpos_buffer_free. + * Por ejemplo: si main.c llama a escpos_buffer_new, main.c es EL DUEÑO. + */ + escpos_buffer *escpos_buffer_new(void); + + /* + * Libera el buffer y toda su memoria. Si recibe NULL no hace nada. + * Propiedad: después de llamarla, el puntero ya no es válido. + */ + void escpos_buffer_free(escpos_buffer *buffer); + + /* + * Añade un byte al final. Devuelve 0 o -1 si no hay memoria; en ese caso + * el buffer queda intacto. + */ + int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte); + #endif diff --git a/src/buffer.c b/src/buffer.c new file mode 100644 index 0000000..25f3172 --- /dev/null +++ b/src/buffer.c @@ -0,0 +1,68 @@ +#include "escpos.h" +#include +#include +#include + + +struct escpos_buffer +{ + uint8_t *data; + size_t len; + size_t cap; +}; + +escpos_buffer *escpos_buffer_new(void) +{ + escpos_buffer *buffer = malloc(sizeof *buffer); + if (buffer == NULL) + { + return NULL; + } + + buffer->data = NULL; + buffer->len = 0; + buffer->cap = 0; + + return buffer; +} + +void escpos_buffer_free(escpos_buffer *buffer) +{ + if (buffer == NULL) + { + return; + } + + free(buffer->data); + free(buffer); +} + +int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte) +{ + if (buffer->len == buffer->cap) + { + size_t new_cap = 0; + if (buffer->cap == 0) + { + new_cap = 64; + } else + { + new_cap = buffer->cap * 2; + }; + + uint8_t *tmp = realloc(buffer->data, new_cap); + if (tmp == NULL) + { + return -1; + } + + buffer->data = tmp; + buffer->cap = new_cap; + + } + + buffer->data[buffer->len] = byte; + buffer->len++; + + return 0; +} diff --git a/src/escpos.c b/src/escpos.c index 8ed6099..e105074 100644 --- a/src/escpos.c +++ b/src/escpos.c @@ -1,5 +1,6 @@ #include "escpos.h" -const char *escpos_version(void) { +const char *escpos_version(void) +{ return "1.0.0"; }