diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..adfe7dc --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,21 @@ +{ + "version": 4, + "configurations": [ + { + "name": "Linux", + "includePath": ["${workspaceFolder}/include"], + "compilerPath": "/usr/bin/gcc", + "compilerArgs": ["-std=c17", "-Wall", "-Wextra", "-Wpedantic"], + "cStandard": "c17", + "intelliSenseMode": "linux-gcc-x64" + }, + { + "name": "Win32", + "includePath": ["${workspaceFolder}/include"], + "compilerPath": "C:/msys64/ucrt64/bin/gcc.exe", + "compilerArgs": ["-std=c17", "-Wall", "-Wextra", "-Wpedantic"], + "cStandard": "c17", + "intelliSenseMode": "windows-gcc-x64" + } + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..78e47fd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Depurar hello (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/hello", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "preLaunchTask": "make", + "setupCommands": [ + { + "description": "Formato legible para structs y arrays", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "No descargar símbolos de depuración del sistema", + "text": "set debuginfod enabled off", + "ignoreFailures": true + } + ], + "windows": { + "program": "${workspaceFolder}/build/hello.exe", + "miDebuggerPath": "C:/msys64/ucrt64/bin/gdb.exe" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1cc56f0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "files.associations": { + "*.h": "c" + }, + "[c]": { + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.formatOnSave": false + }, + "[makefile]": { + "editor.insertSpaces": false, + "editor.detectIndentation": false + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..a2901dc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "make", + "type": "shell", + "command": "make", + "group": { "kind": "build", "isDefault": true }, + "problemMatcher": ["$gcc"] + }, + { + "label": "make clean", + "type": "shell", + "command": "make clean", + "problemMatcher": [] + }, + { + "label": "make (clang)", + "type": "shell", + "command": "make clean && make CC=clang", + "problemMatcher": ["$gcc"] + }, + { + "label": "valgrind", + "type": "shell", + "command": "valgrind --leak-check=full ./build/hello", + "dependsOn": "make", + "problemMatcher": [] + } + ] +} diff --git a/Makefile b/Makefile index 6d4cf2e..63c8ff6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CC = gcc -CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -g +CFLAGS = -std=c17 -Werror -Wall -Wextra -Wpedantic -g CPPFLAGS = -Iinclude all: build/hello @@ -19,5 +19,11 @@ build/hello: build/hello.o build/escpos.o build/buffer.o clean: rm -rf build +valgrind: build/hello + valgrind --leak-check=full build/hello + +run: build/hello + build/hello + .PHONY: all clean diff --git a/PROGRESO.md b/PROGRESO.md index ba23c9b..1ed4534 100644 --- a/PROGRESO.md +++ b/PROGRESO.md @@ -2,8 +2,10 @@ ## Estado actual -- **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. +- **Fase 2**: buffer dinámico. La Fase 1 está cerrada en Fedora, pero falta + probarla en Windows. +- Paso actual: 2.3b, `escpos_buffer_append` (varios bytes: puntero `const` + + cantidad + `memcpy`). El 2.3a ya está hecho: `buffer_reserve`, `static`, `t` en `nm`. - Entorno activo: Fedora 42 nativo (gcc 15.2, clang 20, make 4.4, gdb 17, valgrind 3.26). @@ -13,27 +15,73 @@ la usa. Compila y enlaza a mano en Windows y en Fedora. - `samples/hello_world.bin`: ESC @, texto, LF, ESC d 5, GS V 0. - Emulador ESC/POS en Go (`emulator/`) para probar sin gastar papel. +- Makefile con regla patrón, `clean` y `.PHONY`. Compila desde un clon limpio y + sin avisos con gcc y con clang. +- 2.1: `escpos_buffer_new` / `escpos_buffer_free` con tipo opaco. Valgrind + limpio, y fuga provocada a propósito para leer el informe. +- 2.2: `escpos_buffer_append_byte` con `realloc`: 64 bytes al principio, luego + el doble. Comprobado con 100 bytes (crece de 64 a 128). Valgrind limpio. + Sin el `free`, Valgrind da 24 `definitely` + 128 `indirectly`. +- 2.3a: crecimiento extraído a `static int buffer_reserve(buffer, min_cap)`: + arranca en 64 o en `cap` y dobla hasta que cabe; un único `realloc`. ## Conceptos de C aprendidos - Etapas de compilación, unidades de traducción, cabeceras, *include guards*, `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. +- Cadenas terminadas en `'\0'`, `const char *`, `'A'` frente a `"A"`. +- Memoria virtual de un proceso, modo usuario y modo núcleo. `VmSize` frente a + `VmRSS`, *overcommit* y OOM killer. +- Make: reglas, dependencias por fechas, variables, regla patrón con + `$@ $< $^`, `.PHONY`. +- Pila y montón, `malloc`/`free`/`realloc`, `NULL`, propiedad de punteros, + tipo opaco, `typedef`, `->`, `uint8_t`/`size_t`, qué tipo usar. +- Depuración: *segfault* (139), `free(): invalid pointer` (134), gdb básico, + y leer errores e informes de fugas en Valgrind. + +## Forma de trabajar + +- Desde el 2026-10-02: sin *vibecoding*. Enunciado y pruebas, predicción antes + de compilar, y pistas por niveles solo si las pide. Ante un error, primero su + diagnóstico. ## 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. +- `escpos_buffer` es un tipo opaco: declarado en `escpos.h` y definido en + `src/buffer.c`. Campos `uint8_t *data`, `size_t len` y `size_t cap`. +- Crecimiento del buffer: 64 bytes al principio y luego el doble. +- Por ahora, las funciones que pueden fallar devuelven `int`: `0` si va bien y + `-1` si no hay memoria. Pendiente de cambiar a códigos de error propios. +- Estilo: nombres en `snake_case` y llave de apertura en línea aparte (estilo + Allman) en funciones, structs y bloques. +- Lo generado va en `build/`, que se crea en las recetas con `mkdir -p` y no se + versiona. ## Pendiente / puntos débiles -- `.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. +- Fase 1: probar el Makefile en Windows (MSYS2), con el sufijo `.exe`. +- `.gitignore` creado. Siguen versionados `emulator/escpos-emu.exe` y + `goruntime/goruntime.exe`. +- Make: le costó entender que va hacia atrás desde el objetivo y que una regla + patrón es una plantilla. Repasar con `docs/06-make.md`. +- Punteros: le cuesta saber cuándo hace falta `*` y cuándo no, y distinguir la + dirección del bloque (`data`) de una casilla (`data[i]`). Le funciona la + analogía de papel y casa. Reforzar con ejemplos de su propio código. +- Tipos: usó `uint8_t` para un código de retorno. Repasar la tabla "Qué tipo + usar" de `docs/08-structs.md`. +- Valorar añadir `-Wconversion` a `CFLAGS`: detecta conversiones con pérdida + que gcc no avisa por defecto. +- Bucles: en `buffer_reserve` calculaba cada vuelta a partir de un valor que no + cambiaba (`buffer->cap`) o que empezaba en 0, y eso daba bucles infinitos. + Lo resolvió siguiendo los valores vuelta a vuelta. Seguir pidiéndole trazas + a mano. - Impresoras físicas: sin registrar marca y modelo. +- Confunde puntero colgante ("se pierde") con fuga. Tampoco tenía claro por qué + el slice viejo es seguro en Go tras `append` (el GC no libera el array viejo, + no es por la copia). Repasar con la tabla de docs/07-dynamic-memory.md. + +## Otros + +- `goescpos/`: equivalente en Go idiomático (`bytes.Buffer`) de lo hecho hasta + ahora, para comparar. diff --git a/docs/02-pointers-and-strings.md b/docs/02-pointers-and-strings.md index 5b839a3..4ceb2bc 100644 --- a/docs/02-pointers-and-strings.md +++ b/docs/02-pointers-and-strings.md @@ -139,6 +139,63 @@ entero**. Eso tiene dos problemas: 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()`). +## Un puntero no sabe cuántos elementos hay + +Un slice de Go lleva dentro tres cosas: puntero, `len` y `cap`. Un puntero de +C lleva **solo la dirección del primer elemento**. No hay forma de preguntarle +cuántos hay detrás. + +Por eso las funciones de C que reciben un bloque de datos piden siempre **dos +parámetros**: el puntero y la cantidad. + +```c +int escpos_buffer_append(escpos_buffer *buffer, const uint8_t *bytes, size_t count); +``` + +En Go sería un solo parámetro: `func (b *Buffer) Append(bytes []byte)`. + +Si el que llama pasa un `count` mayor que lo que hay de verdad, la función lee +fuera del bloque. El compilador no puede detectarlo. Es la fuente de los +desbordamientos de memoria más famosos. + +Las cadenas son la excepción: su longitud se deduce porque acaban en `'\0'`. +Pero los datos binarios (un comando ESC/POS, una imagen) pueden contener +bytes `0x00` en medio, así que necesitan su cantidad aparte. + +## `const` en parámetros puntero: una promesa + +```c +const uint8_t *bytes +``` + +Se lee "puntero a `uint8_t` que **no voy a modificar**". Es un contrato con el +que llama: le garantizas que sus datos salen intactos. Si dentro de la función +intentas escribir `bytes[0] = 0;`, el compilador da error. + +- Pasar un `uint8_t *` normal donde se pide `const uint8_t *` está permitido: + solo añades una restricción. +- Al revés da aviso: estarías quitando una promesa. + +**En Go** no existe. Un slice que recibes lo puedes modificar, y el que llama no +tiene ninguna garantía. + +## Arrays + +```c +uint8_t init[] = { 0x1B, 0x40 }; /* ESC @ */ +``` + +- Un array de tamaño fijo. Si no pones el tamaño entre corchetes, el + compilador lo cuenta a partir de los valores. +- `sizeof init` da el tamaño **en bytes** del array completo (aquí 2). Solo + funciona con el array en su ámbito, no con un puntero. +- Al pasar un array a una función, se convierte en un **puntero a su primer + elemento** (*decay*). La función recibe la dirección, no una copia, y pierde + el tamaño. Por eso hay que pasar `sizeof init` aparte. + +**En Go** `[2]byte{0x1B, 0x40}` es un array con su longitud dentro del tipo. +Se pasa por copia, y para pasarlo sin copiar se usa un slice: `init[:]`. + ## `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 dbfdf8c..fcaccbd 100644 --- a/docs/06-make.md +++ b/docs/06-make.md @@ -130,6 +130,19 @@ Make. Por eso allí todo va en `.PHONY` y la comparación de fechas no se usa nunca. En C es al revés: Make **es** el sistema de compilación, y solo las acciones (`all`, `clean`) son phony. +### Los avisos solo salen al compilar + +Un *warning* aparece **solo cuando ese `.c` se compila**. Como el `.o` sí se +genera, el siguiente `make` lo ve al día, no recompila, y el aviso no vuelve a +salir. Es fácil no verlo nunca. Hay tres formas de evitarlo: + +- `make clean && make` para verlo todo otra vez. +- `touch src/buffer.c && make` para recompilar solo ese fichero. +- Añadir `-Werror` a `CFLAGS`: convierte los avisos en errores. Así no se + genera el `.o` y el aviso sale en cada `make` hasta que lo arreglas. + +**En Go** no pasa: lo que en C son avisos importantes, en Go son errores. + ### Variables desde la línea de comandos `make CC=clang` sobrescribe la variable `CC` del Makefile solo para esa diff --git a/docs/07-dynamic-memory.md b/docs/07-dynamic-memory.md index 0027ab2..e77aab5 100644 --- a/docs/07-dynamic-memory.md +++ b/docs/07-dynamic-memory.md @@ -171,6 +171,37 @@ solo entonces asignarlo. escribir `s = append(s, x)`: si el array subyacente se movió, el slice viejo apunta al sitio antiguo. +**Colgante no es lo mismo que perdido.** Tras un `realloc` que mueve el bloque, +el puntero viejo no cambia: sigue con la dirección antigua, pero esa memoria ya +está liberada. Es un puntero colgante, y usarlo es comportamiento indefinido. +"Perdido" es lo contrario, una fuga: memoria reservada sin ningún puntero. + +| | C (`realloc`) | Go (`append`) | +|---|---|---| +| ¿Copia al bloque nuevo? | Sí | Sí | +| ¿Libera el bloque viejo? | Sí, en el acto | No; el GC lo mantiene vivo mientras algo lo referencie | +| Puntero/slice viejo | Colgante: comportamiento indefinido | Válido pero desconectado: ya no ve los cambios | +| Síntoma del fallo | Cuelgue o corrupción del montón | Datos incorrectos, sin ningún error | + +```go +a := make([]byte, 3) // len 3, cap 3: lleno +b := a // b comparte el array con a +a = append(a, 'X') // no cabe: nuevo array +a[0] = 'Z' +fmt.Println(b[0]) // 0: b sigue en el array viejo +``` + +Si `a` hubiera tenido capacidad libre, `append` no movería nada y `b[0]` valdría +`'Z'`. El resultado depende de `cap`. + +**Qué hace `bytes.Buffer` por debajo.** Es el mismo algoritmo que +`escpos_buffer_append_byte`: 64 bytes iniciales (`smallBufferSize`) y duplicar +al llenarse. Diferencias: nunca crece en el sitio (siempre reserva y copia), +pone la memoria a cero (`malloc` no), redondea la capacidad al tamaño de clase +del asignador de Go, comprueba los índices y, si falla, hace `panic` en vez de +devolver un error. El struct `bytes.Buffer` puede vivir en la pila si no +escapa; solo los datos van al montón. + ### Estrategia de crecimiento Si el buffer creciera de byte en byte, cada byte nuevo podría provocar una @@ -182,6 +213,63 @@ O(1)*). `append` en Go duplica hasta 256 elementos y a partir de ahí crece más despacio (unos 1,25×). +## `memcpy`: copiar bloques de bytes + +`memcpy(dst, src, n)` (en ``) copia `n` bytes desde la dirección +`src` a la dirección `dst`. Su firma: + +```c +void *memcpy(void *dst, const void *src, size_t n); +``` + +| Parte | Qué significa | +|---|---| +| `void *dst` | Dirección de destino. `void *` es "puntero a cualquier cosa": acepta un `uint8_t *`, un `int *` o un `struct x *` sin *cast* | +| `const void *src` | Dirección de origen. El `const` es la promesa: `memcpy` no modifica tus datos | +| `size_t n` | Cuántos **bytes** copiar. Se pasa aparte porque un puntero no sabe cuántos hay detrás | +| devuelve `void *` | El mismo `dst`. Casi nunca se usa | + +Fíjate en que su firma resuelve el mismo problema que cualquier función que +recibe un bloque de datos: puntero, cantidad y `const` en lo que solo se lee. + +`n` va en **bytes**, no en elementos. Con `uint8_t` coincide. Para copiar 10 +`int`, `n` es `10 * sizeof(int)`, o sea, 40. + +Comparado con el `copy(dst, src)` de Go: + +- No sabe nada de tamaños: copia exactamente `n` bytes, quepan o no. Si en + `dst` no hay `n` bytes reservados, escribe fuera del bloque. +- Los dos bloques **no pueden solaparse**. Si se solapan, se usa `memmove`. +- Go copia `min(len(dst), len(src))` y devuelve cuántos copió: nunca se sale. + `memcpy` no puede comprobar nada porque no conoce los tamaños. + +**Por qué usarlo en vez de un bucle:** glibc implementa `memcpy` en ensamblador +optimizado, con instrucciones que copian 16 o 32 bytes de una vez (SIMD). A +veces gcc incluso convierte un bucle de copia en una llamada a `memcpy`. Y +además dice lo que haces en una sola línea. + +La documentación de cualquier función de la librería de C se consulta con +`man 3 memcpy`. El 3 es la sección de funciones de librería; la 2 es la de +llamadas al sistema. + +Para copiar al final del buffer hace falta la dirección de la casilla `len`: + +```c +&buffer->data[buffer->len] /* "la dirección de la casilla len" */ +buffer->data + buffer->len /* lo mismo: aritmética de punteros */ +``` + +Sumar un número a un puntero avanza ese número de **elementos** (no de bytes; +aquí coincide porque cada elemento es un `uint8_t`). + +### Desbordamiento al calcular tamaños + +`len + count` o `cap * 2` pueden pasar del máximo de `size_t` y dar la vuelta +a un número pequeño. Entonces `realloc` reserva poco y `memcpy` escribe fuera. +Con datos que vienen de fuera (por ejemplo, desde Go mediante cgo) hay que +comprobarlo antes de sumar: `if (count > SIZE_MAX - len)`. `SIZE_MAX` está en +``. + ## Fugas de memoria Si pierdes el último puntero a un bloque sin hacer `free`, ese bloque queda @@ -265,6 +353,57 @@ dice qué hacer con cada una. El código Go que llame a `escpos_buffer_new` tendrá que hacer `defer C.escpos_buffer_free(b)`, igual que con un fichero. +## Liberar en los caminos de error: C no tiene `defer` + +En Go, `defer b.Close()` se ejecuta salga la función por donde salga. En C no +existe nada así: cada `return` tiene que dejar liberado todo lo que se creó +antes. Con varios recursos, repetir los `free` delante de cada `return` es +largo y es fácil olvidarse de alguno. + +El patrón clásico de C es **un único punto de limpieza al final**, al que se +salta con `goto`: + +```c +int run(void) +{ + int result = 1; /* se supone fallo hasta llegar al final */ + thing *a = NULL; /* todo a NULL desde el principio */ + thing *b = NULL; + + a = thing_new(); + if (a == NULL) { goto cleanup; } + + b = thing_new(); + if (b == NULL) { goto cleanup; } + + /* ... trabajo ... */ + + result = 0; /* todo ha ido bien */ + +cleanup: /* etiqueta: destino del goto */ + thing_free(b); /* seguro aunque sea NULL */ + thing_free(a); + return result; +} +``` + +Por qué funciona: + +- Todos los punteros empiezan en `NULL`, y la función de liberar acepta `NULL` + sin hacer nada. Por eso `escpos_buffer_free` se diseñó así. Da igual en qué + punto falle: la limpieza libera lo que exista e ignora lo demás. +- Hay un solo `return`. Al añadir un recurso nuevo, se añade su `free` en un + solo sitio. +- Se libera en orden inverso al de creación, como hace `defer` (LIFO). + +`goto` tiene mala fama por el código "espagueti", pero este uso, saltar +**hacia delante** a la limpieza, es idiomático. El núcleo de Linux lo usa por +todas partes. + +gcc y clang tienen una extensión, `__attribute__((cleanup(f)))`, que se +parece a `defer`. No es C estándar y no funciona en MSVC, así que en este +proyecto no se usa. + ## Valgrind `valgrind` ejecuta tu programa en una CPU simulada y vigila cada acceso a diff --git a/docs/09-debugging.md b/docs/09-debugging.md index 1e4d2d6..86d4a8c 100644 --- a/docs/09-debugging.md +++ b/docs/09-debugging.md @@ -79,7 +79,53 @@ gdb ./build/hello | `continue` (`c`) | Sigue hasta el siguiente punto de parada | | `quit` (`q`) | Sale | +### Programa que no termina (bucle infinito) + +1. `gdb ./build/hello` y `run`. +2. Cuando lleve un rato colgado, pulsa **Ctrl+C**: gdb detiene el programa + donde esté y te muestra la línea. +3. `bt` para ver en qué función está, y `print` de las variables del bucle + (`print new_cap`). Si al repetir `next` varias veces la variable no cambia, + has encontrado el problema. + +Ejemplo real: un bucle `while (new_cap < min_cap) { new_cap *= 2; }` con +`new_cap` empezando en 0. Cero por dos es cero, así que el bucle no termina +nunca. `print new_cap` da `0` en cada vuelta. + 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. + +## Depurar en VS Code + +La extensión C/C++ (`ms-vscode.cpptools`) lanza **el mismo gdb** y le envía +órdenes por su protocolo de texto (gdb/MI). Cada clic equivale a un comando: + +| VS Code | gdb | +|---|---| +| Clic a la izquierda del número de línea (punto rojo) | `break fichero.c:línea` | +| F5 | `run` / `continue` | +| F10 | `next` | +| F11 | `step` | +| Shift+F11 | `finish` (salir de la función actual) | +| Panel *Variables*, pasar el ratón por encima o panel *Watch* | `print` | +| Panel *Call Stack* | `bt` | +| Botón de pausa | Ctrl+C | + +Configuración del proyecto, en `.vscode/`: + +- `tasks.json`: tareas `make`, `make clean`, `make (clang)` y `valgrind`. + Se lanzan con *Terminal → Run Task*, y Ctrl+Shift+B ejecuta `make`. +- `launch.json`: depura `build/hello` con gdb y ejecuta `make` antes. Tiene ya + los valores para Windows (MSYS2 UCRT64). +- `c_cpp_properties.json`: dice a IntelliSense dónde están las cabeceras + (`include/`) y que el estándar es C17, para que el editor avise de lo mismo + que gcc. +- `settings.json`: salto de línea final, sin espacios al final de línea, y + tabuladores reales en el Makefile. + +Para ver un puntero como array en el panel *Watch*: `*buffer->data@10` (los 10 +primeros bytes de `data`). Es sintaxis de gdb y funciona igual en su consola. + +**En Go**, la extensión de Go hace lo mismo con Delve. diff --git a/docs/README.md b/docs/README.md index c2fe925..14dd11a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,8 @@ Ordenados por tema, en el orden en que se ven en el proyecto. 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 `*`, dónde va el `*`, - nombres de parámetros, paso por valor frente a puntero y el formato de + nombres de parámetros, paso por valor frente a puntero, un puntero no sabe + cuántos elementos hay, `const` en parámetros, arrays 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 @@ -25,14 +26,16 @@ Ordenados por tema, en el orden en que se ven en el proyecto. 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 + estrategia de crecimiento, `memcpy` y desbordamiento al calcular tamaños, fugas, liberar en caminos + de error sin `defer` (`goto cleanup`), 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. + depurar con Valgrind, los comandos mínimos de gdb y la depuración visual en + VS Code. [Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por tarea. diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index cb6d601..0479928 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -30,6 +30,7 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. | `objdump --dwarf=info fichero.o` | Muestra la información de depuración que añade `-g`. | | `xxd fichero.bin` | Volcado hexadecimal (para revisar los bytes ESC/POS). | | `tail -c 5 fichero \| xxd` | Muestra los últimos bytes: sirve para ver si el fichero acaba en `0a` (`\n`). | +| `man 3 memcpy` | Documentación de una función de la librería de C: cabecera, firma y comportamiento. `man 2` para llamadas al sistema. | | `echo $?` | Código de salida del último comando: `0` es éxito. | ## Memoria @@ -54,8 +55,20 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. |---|---| | `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. | +| `Ctrl+C` (dentro de gdb, con el programa en marcha) | Detiene el programa donde esté. Para encontrar bucles infinitos. | | `break f.c:42` / `next` / `step` / `continue` | Dentro de gdb: punto de parada, siguiente línea, entrar en la función, seguir. | +### VS Code + +| Atajo | Qué hace | +|---|---| +| F5 | Compila con `make` y depura `build/hello` | +| F9 o clic junto al número de línea | Punto de parada | +| F10 / F11 / Shift+F11 | Siguiente línea / entrar en la función / salir de la función | +| Ctrl+Shift+B | `make` | +| *Terminal → Run Task* | `make clean`, `make (clang)`, `valgrind` | +| `*buffer->data@10` en *Watch* | Ver los 10 primeros bytes de un puntero | + ## Solo en Windows (MSYS2) | Comando | Qué hace | diff --git a/examples/hello.c b/examples/hello.c index 9a598a8..eed88c0 100644 --- a/examples/hello.c +++ b/examples/hello.c @@ -1,28 +1,46 @@ #include #include "escpos.h" -int main(void) { - +int main(void) +{ + const char *version = escpos_version(); - + printf("%s\n", version); escpos_buffer *buffer = escpos_buffer_new(); - if (buffer == NULL) - { - return 1; - } + if (buffer == NULL) { goto cleanup; } for (int i = 0; i < 100; i++) { int err = escpos_buffer_append_byte(buffer, 'A'); - if (err == -1) - { - return 1; - } + if (err == -1) { goto cleanup; } } - escpos_buffer_free(buffer); - + escpos_buffer *buffer_a = escpos_buffer_new(); + if (buffer_a == NULL) { goto cleanup; } + uint8_t data[] = {0x1B, 0x40}; + int err = 0; + + err = escpos_buffer_append(buffer_a, data); + if (err == -1) { goto cleanup; } + + escpos_buffer *buffer_b = escpos_buffer_new(); + if (buffer_b == NULL) { goto cleanup; } + uint8_t data_b[300]; + for (int i = 0; i < 300; i++) + { + data_b[i] = 'A'; + } + + err = escpos_buffer_append(buffer_b, data_b); + if (err == -1) { goto cleanup; } + return 0; + +cleanup: + escpos_buffer_free(buffer); + escpos_buffer_free(buffer_a); + escpos_buffer_free(buffer_b); + } diff --git a/goescpos/cmd/hello/main.go b/goescpos/cmd/hello/main.go new file mode 100644 index 0000000..767d260 --- /dev/null +++ b/goescpos/cmd/hello/main.go @@ -0,0 +1,21 @@ +// Equivalente a examples/hello.c. +package main + +import ( + "bytes" + "fmt" + + "goescpos/escpos" +) + +func main() { + fmt.Println(escpos.Version) + + // bytes.Buffer sustituye a todo src/buffer.c: crece solo y lo libera el GC. + var buf bytes.Buffer + for range 100 { + buf.WriteByte('A') + } + + fmt.Printf("len=%d cap=%d\n", buf.Len(), buf.Cap()) +} diff --git a/goescpos/escpos/escpos.go b/goescpos/escpos/escpos.go new file mode 100644 index 0000000..c5c651f --- /dev/null +++ b/goescpos/escpos/escpos.go @@ -0,0 +1,5 @@ +// Package escpos es el equivalente en Go de include/escpos.h + src/escpos.c. +package escpos + +// Version equivale a escpos_version(). +const Version = "1.0.0" diff --git a/goescpos/go.mod b/goescpos/go.mod new file mode 100644 index 0000000..ac5c963 --- /dev/null +++ b/goescpos/go.mod @@ -0,0 +1,3 @@ +module goescpos + +go 1.26 diff --git a/goescpos/hello b/goescpos/hello new file mode 100755 index 0000000..fd1f7ac Binary files /dev/null and b/goescpos/hello differ diff --git a/include/escpos.h b/include/escpos.h index 8f04363..07b797b 100644 --- a/include/escpos.h +++ b/include/escpos.h @@ -21,10 +21,17 @@ 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. + * Añade un byte al final del buffer. + * Devuelve 0 si va bien, o -1 si no hay memoria; en ese caso el buffer + * queda intacto. */ int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte); + + + /* + * Añade los bytes al final del buffer + */ + int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[]); #endif diff --git a/src/buffer.c b/src/buffer.c index 25f3172..ff3cc07 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -4,31 +4,67 @@ #include -struct escpos_buffer +struct escpos_buffer { uint8_t *data; size_t len; size_t cap; }; -escpos_buffer *escpos_buffer_new(void) +static int buffer_reserve(escpos_buffer *buffer, size_t min_cap) +{ + if (min_cap <= buffer->cap) + { + return 0; + } + + size_t new_cap; + + if (buffer->cap == 0) + { + new_cap = 64; + } else + { + new_cap = buffer->cap; + } + + + while (new_cap < min_cap) + { + new_cap *= 2; + } + + uint8_t *tmp = realloc(buffer->data, new_cap); + if (tmp == NULL) + { + return -1; + } + + buffer->data = tmp; + buffer->cap = new_cap; + + return 0; +} + + +escpos_buffer *escpos_buffer_new(void) { escpos_buffer *buffer = malloc(sizeof *buffer); - if (buffer == NULL) + if (buffer == NULL) { return NULL; - } - + } + buffer->data = NULL; buffer->len = 0; buffer->cap = 0; - + return buffer; } -void escpos_buffer_free(escpos_buffer *buffer) +void escpos_buffer_free(escpos_buffer *buffer) { - if (buffer == NULL) + if (buffer == NULL) { return; } @@ -39,30 +75,32 @@ void escpos_buffer_free(escpos_buffer *buffer) int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte) { - if (buffer->len == buffer->cap) + + int err = buffer_reserve(buffer, buffer->len + 1); + if (err == -1) { - 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; - + return -1; } - + buffer->data[buffer->len] = byte; buffer->len++; return 0; } + +int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[]) +{ + int err = buffer_reserve(buffer, buffer->len + sizeof *byte); + if (err == -1) + { + return -1; + } + + for (size_t i = 0; i < sizeof *byte; i++) + { + buffer->data[buffer->len] = byte[i]; + buffer->len++; + } + + return 0; +} diff --git a/vgcore.57400 b/vgcore.57400 new file mode 100644 index 0000000..0c2c033 Binary files /dev/null and b/vgcore.57400 differ