Compare commits
10 Commits
3b0f5e365f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4351a40bd2 | |||
| 208a3e9738 | |||
| 03d2ece7d6 | |||
| 6212fc971f | |||
| 8383a725e2 | |||
| b15620838b | |||
| 9d517f3607 | |||
| b8141311aa | |||
| 6e33293ea6 | |||
| 3b458fcd7f |
@@ -0,0 +1,7 @@
|
|||||||
|
# Finales de línea LF en las dos máquinas (Make exige tabuladores y LF)
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Bytes en crudo: git no debe tocarlos nunca
|
||||||
|
*.bin binary
|
||||||
|
*.exe binary
|
||||||
|
*.o binary
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
build/
|
||||||
|
*.o
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Volcados de memoria de valgrind
|
||||||
|
vgcore.*
|
||||||
|
|
||||||
|
# Binarios de Go compilados en Linux
|
||||||
|
goescpos/hello
|
||||||
Vendored
+21
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+37
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"environment": [
|
||||||
|
{ "name": "PATH", "value": "C:/msys64/ucrt64/bin;${env:PATH}" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"files.associations": {
|
||||||
|
"*.h": "c"
|
||||||
|
},
|
||||||
|
"[c]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.formatOnSave": false
|
||||||
|
},
|
||||||
|
"[makefile]": {
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.detectIndentation": false
|
||||||
|
},
|
||||||
|
"terminal.integrated.profiles.windows": {
|
||||||
|
"MSYS2 UCRT64": {
|
||||||
|
"path": "C:/msys64/usr/bin/bash.exe",
|
||||||
|
"args": ["--login", "-i"],
|
||||||
|
"env": {
|
||||||
|
"MSYSTEM": "UCRT64",
|
||||||
|
"CHERE_INVOKING": "1",
|
||||||
|
"MSYS2_PATH_TYPE": "inherit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terminal.integrated.defaultProfile.windows": "MSYS2 UCRT64"
|
||||||
|
}
|
||||||
Vendored
+47
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"windows": {
|
||||||
|
"options": {
|
||||||
|
"shell": {
|
||||||
|
"executable": "C:/msys64/usr/bin/bash.exe",
|
||||||
|
"args": ["--login", "-c"]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"MSYSTEM": "UCRT64",
|
||||||
|
"CHERE_INVOKING": "1",
|
||||||
|
"MSYS2_PATH_TYPE": "inherit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"windows": {
|
||||||
|
"command": "echo 'valgrind no existe en Windows: pruébalo en Fedora'"
|
||||||
|
},
|
||||||
|
"dependsOn": "make",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -21,13 +21,38 @@ pregúntame marca y modelo de cada una.
|
|||||||
|
|
||||||
# Entorno
|
# Entorno
|
||||||
|
|
||||||
- Windows: MSYS2, entorno UCRT64, con GCC, gdb y make
|
Trabajo en dos máquinas y voy cambiando de una a otra. Las sincronizo con git.
|
||||||
- Fedora en WSL: para compilar y probar la parte portable con
|
|
||||||
AddressSanitizer, UBSan y valgrind (en Windows con MinGW no funcionan)
|
- **Sobremesa: Windows** con MSYS2, entorno UCRT64, con GCC, gdb y make.
|
||||||
|
- **Portátil: Fedora** nativo, con gcc, clang, gdb, make y valgrind. Aquí se
|
||||||
|
prueba la parte portable con AddressSanitizer, UBSan y valgrind, que no
|
||||||
|
funcionan en Windows con MinGW.
|
||||||
- Estándar: C17
|
- Estándar: C17
|
||||||
- Flags: -std=c17 -Wall -Wextra -Wpedantic -g
|
- Flags: -std=c17 -Werror -Wall -Wextra -Wpedantic -g
|
||||||
- Makefile único que funcione en Windows (MSYS2) y en Linux
|
- Makefile único que funcione en Windows (MSYS2) y en Linux
|
||||||
|
|
||||||
|
# Adaptar la sesión a la máquina
|
||||||
|
|
||||||
|
Al empezar cada sesión, antes de proponer nada:
|
||||||
|
|
||||||
|
1. Detecta en qué máquina estoy: la plataforma del entorno (`win32` es el
|
||||||
|
sobremesa con Windows y `linux` el portátil con Fedora) o `uname -s`.
|
||||||
|
2. Lee PROGRESO.md, incluida la sección "Pendiente por máquina".
|
||||||
|
3. Dime en una línea en qué máquina estoy y qué toca, sin preguntarme.
|
||||||
|
- Si el paso actual se puede hacer en esta máquina, sigue con él.
|
||||||
|
- Si necesita la otra (por ejemplo, valgrind en Windows o Winsock en
|
||||||
|
Fedora), dímelo, déjalo apuntado en "Pendiente por máquina" y proponme
|
||||||
|
algo que sí se pueda hacer aquí.
|
||||||
|
4. Usa los comandos de esta máquina en enunciados y comprobaciones (por
|
||||||
|
ejemplo, `build/hello.exe` y las rutas de MSYS2 en Windows).
|
||||||
|
|
||||||
|
Qué encaja mejor en cada una:
|
||||||
|
|
||||||
|
- Windows: lo específico de Windows (Makefile con `.exe`, Winsock, spooler,
|
||||||
|
WinUSB) y la parte portable sin comprobación de memoria.
|
||||||
|
- Fedora: validar la parte portable con valgrind y sanitizers (obligatorio
|
||||||
|
antes de cerrar una fase), sockets POSIX y USB en Linux.
|
||||||
|
|
||||||
# Arquitectura a construir
|
# Arquitectura a construir
|
||||||
|
|
||||||
- Núcleo portable: buffer de bytes, constructor de comandos ESC/POS,
|
- Núcleo portable: buffer de bytes, constructor de comandos ESC/POS,
|
||||||
@@ -82,6 +107,14 @@ pregúntame marca y modelo de cada una.
|
|||||||
pase AddressSanitizer y valgrind en Fedora.
|
pase AddressSanitizer y valgrind en Fedora.
|
||||||
- Antes de imprimir en papel, comprobamos los bytes en un .bin con un
|
- Antes de imprimir en papel, comprobamos los bytes en un .bin con un
|
||||||
volcado hexadecimal, para no gastar papel en pruebas fallidas.
|
volcado hexadecimal, para no gastar papel en pruebas fallidas.
|
||||||
|
- Pasos pequeños: me saturo con enunciados largos. Una tarea corta cada vez
|
||||||
|
y teoría larga en `docs/`, no en el chat.
|
||||||
|
- Nada de *vibecoding*. En cada paso de código dame solo el enunciado (qué
|
||||||
|
hace y cómo se comprueba) y pídeme una predicción antes de compilar. Sin
|
||||||
|
pseudocódigo, esqueletos ni líneas exactas. Si me atasco y pido ayuda,
|
||||||
|
sube poco a poco: la pista 1 dice dónde mirar, la 2 es más concreta y la
|
||||||
|
solución solo si la pido. Ante un error, pídeme primero mi diagnóstico.
|
||||||
|
No corrijas mi código salvo que te lo pida.
|
||||||
|
|
||||||
# Seguimiento
|
# Seguimiento
|
||||||
|
|
||||||
@@ -95,6 +128,9 @@ con un índice en `docs/README.md`. Cada vez que explique un concepto nuevo,
|
|||||||
añádelo al fichero del tema que corresponda o crea uno nuevo y enlázalo en el
|
añádelo al fichero del tema que corresponda o crea uno nuevo y enlázalo en el
|
||||||
índice. Incluye siempre la comparación con Go.
|
índice. Incluye siempre la comparación con Go.
|
||||||
|
|
||||||
|
Mantén `docs/cheatsheet.md` con cada comando nuevo que use, en el momento en
|
||||||
|
que aparece, agrupado y con una línea que explique qué hace.
|
||||||
|
|
||||||
# Comunicación
|
# Comunicación
|
||||||
|
|
||||||
- En español
|
- En español
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c17 -Werror -Wall -Wextra -Wpedantic -g
|
||||||
|
CPPFLAGS = -Iinclude
|
||||||
|
|
||||||
|
all: build/hello
|
||||||
|
|
||||||
|
build/%.o: src/%.c include/escpos.h
|
||||||
|
mkdir -p build/
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/hello.o: examples/hello.c include/escpos.h
|
||||||
|
mkdir -p build/
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/hello: build/hello.o build/escpos.o build/buffer.o
|
||||||
|
mkdir -p build/
|
||||||
|
$(CC) $^ -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
valgrind: build/hello
|
||||||
|
valgrind --leak-check=full build/hello
|
||||||
|
|
||||||
|
run: build/hello
|
||||||
|
build/hello
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
# Progreso
|
||||||
|
|
||||||
|
## Estado actual
|
||||||
|
|
||||||
|
- **Fase 2**: buffer dinámico. La Fase 1 está cerrada en Fedora y en Windows
|
||||||
|
(2026-10-05).
|
||||||
|
- 2026-10-06 (Fedora): antes del 2.3b, repaso de punteros. Le costaba qué
|
||||||
|
viaja a una función, `*` frente a `->`, la pila frente al montón y restar en
|
||||||
|
hex. Se creó `docs/02-pointers-visual.html` (interactivo). Pendiente de que
|
||||||
|
conteste: A) por qué `&buffer_a - &buffer` da 4; B) cuántos bytes copia su
|
||||||
|
`append` actual con `data_b`.
|
||||||
|
- Paso actual: 2.3b. `hello.c` ya usa bien `goto cleanup` (punteros a `NULL`
|
||||||
|
uno por línea, `result`, un solo `return`), sin avisos con gcc ni clang, y
|
||||||
|
Valgrind limpio. Falta `escpos_buffer_append`: el parámetro con la cantidad,
|
||||||
|
`const`, reservar `len + cantidad`, `memcpy` y el comentario de la cabecera.
|
||||||
|
Ahora copia 1 byte (`sizeof *byte`).
|
||||||
|
- Máquinas: sobremesa con Windows (MSYS2 UCRT64) y portátil con Fedora 42
|
||||||
|
(gcc 15.2, clang 20, make 4.4, gdb 17, valgrind 3.26). Ver "Pendiente por
|
||||||
|
máquina".
|
||||||
|
|
||||||
|
## Pendiente por máquina
|
||||||
|
|
||||||
|
### Windows (sobremesa)
|
||||||
|
|
||||||
|
- Hay gcc, gdb y make en `C:\msys64`. Faltan clang y `xxd` (opcionales:
|
||||||
|
`pacman -S mingw-w64-ucrt-x86_64-clang vim`; mientras, `hexdump -C`).
|
||||||
|
- Fase 1 cerrada: `make`, `make run` y `Nothing to be done` funcionan sin
|
||||||
|
`EXE` gracias a la emulación de `.exe` de MSYS2 (ver docs/06-make.md). El
|
||||||
|
`ifeq ($(OS),Windows_NT)` se pospone a la Fase 4, cuando haga falta
|
||||||
|
`-lws2_32`.
|
||||||
|
- Probar en VS Code: terminal nueva (debe abrir UCRT64), Ctrl+Shift+B y F5.
|
||||||
|
- El 2.3b se puede avanzar aquí; la comprobación de memoria queda para Fedora.
|
||||||
|
|
||||||
|
### Fedora (portátil)
|
||||||
|
|
||||||
|
- Validar con valgrind el 2.3b cuando esté hecho (y con ASan/UBSan antes de
|
||||||
|
cerrar la Fase 2).
|
||||||
|
- Tras el `git pull` del 2026-10-05, comprobar que `make` y F5 siguen igual
|
||||||
|
(se ha tocado `.vscode/` y se han añadido `.gitattributes` y `.gitignore`).
|
||||||
|
Los binarios de Go ya no se versionan: recompilarlos con `go build` si hacen
|
||||||
|
falta.
|
||||||
|
|
||||||
|
## Hecho
|
||||||
|
|
||||||
|
- `include/escpos.h` + `src/escpos.c` con `escpos_version()`, y `hello.c` que
|
||||||
|
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 *`, `'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.
|
||||||
|
- Liberar en caminos de error sin `defer`: patrón `goto cleanup`.
|
||||||
|
- 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` 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. Tampoco se versionan ejecutables ni volcados (`vgcore.*`).
|
||||||
|
- Dos máquinas transparentes: `.gitattributes` fuerza LF en todo y marca
|
||||||
|
`*.bin` como binario (git no toca los bytes ESC/POS). En Windows, VS Code usa
|
||||||
|
la shell UCRT64 de MSYS2 para la terminal y las tareas, así que `make`,
|
||||||
|
`mkdir -p` y `rm -rf` funcionan igual que en Fedora.
|
||||||
|
|
||||||
|
## Pendiente / puntos débiles
|
||||||
|
|
||||||
|
- Fase 1: probar el Makefile en Windows (MSYS2), con el sufijo `.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.
|
||||||
|
- Declaraciones múltiples: escribió `escpos_buffer *a, *b, *c = NULL;` creyendo
|
||||||
|
que las tres valían `NULL` (solo la última). Lo detectó clang con
|
||||||
|
`-Wsometimes-uninitialized`. Ahora declara una variable por línea.
|
||||||
|
- Variables locales sin inicializar: creía que valían `NULL` como en Go.
|
||||||
|
- `sizeof`: el 2026-10-06 dijo que `sizeof buffer` (un puntero) da 1 byte; da
|
||||||
|
8 en 64 bits. Y lo veía como algo que "va a la casa" mientras se ejecuta,
|
||||||
|
cuando se calcula al compilar a partir del tipo.
|
||||||
|
- Hexadecimal: no sabe restar direcciones (`0xd210 - 0xd1f0`). Se le enseñó
|
||||||
|
a hacerlo con `p` en gdb y con las potencias de 16. Pide ayuda con
|
||||||
|
"ni idea": darle herramientas para medir, no la cuenta hecha.
|
||||||
|
- Se salta la predicción: el 2026-10-06 miró gdb sin predecir tres veces
|
||||||
|
seguidas. Insistir en que la escriba antes de ejecutar.
|
||||||
|
- 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.
|
||||||
@@ -20,6 +20,54 @@
|
|||||||
Sin flags de parada, gcc hace la cadena completa. Si no le das `-o`, el
|
Sin flags de parada, gcc hace la cadena completa. Si no le das `-o`, el
|
||||||
ejecutable se llama `a.exe` en Windows y `a.out` en Linux.
|
ejecutable se llama `a.exe` en Windows y `a.out` en Linux.
|
||||||
|
|
||||||
|
Si juntas dos flags de parada, gana la que para antes: `gcc -E -S` se comporta
|
||||||
|
como `-E`. Además, `-o` no convierte nada: `gcc -E x.c -o x.o` guarda texto C
|
||||||
|
preprocesado en un fichero que se llama `.o`, aunque no sea un fichero objeto.
|
||||||
|
|
||||||
|
### Empezar por cualquier etapa
|
||||||
|
|
||||||
|
gcc decide por la **extensión** del fichero de entrada en qué etapa empieza:
|
||||||
|
|
||||||
|
| Entrada | Empieza en |
|
||||||
|
|---|---|
|
||||||
|
| `.c` | preprocesador |
|
||||||
|
| `.i` | compilador (ya está preprocesado; sobra `-I`) |
|
||||||
|
| `.s` | ensamblador |
|
||||||
|
| `.o` | enlazador |
|
||||||
|
|
||||||
|
Por ejemplo, `gcc -c build/escpos.i -o build/escpos.o` compila un fichero ya
|
||||||
|
preprocesado.
|
||||||
|
|
||||||
|
Lo normal es partir del `.c`: gcc hace la cadena completa y crea el `.i` y el
|
||||||
|
`.s` como ficheros temporales que borra al acabar. Parar en cada etapa solo
|
||||||
|
sirve para aprender o para depurar. `-save-temps` hace la cadena completa y
|
||||||
|
**conserva** los intermedios en la carpeta actual.
|
||||||
|
|
||||||
|
**En Go**: `go build` también genera intermedios, pero en una carpeta temporal
|
||||||
|
que no ves (`go build -work` imprime cuál es y no la borra).
|
||||||
|
|
||||||
|
### Qué hay dentro de un `.i`
|
||||||
|
|
||||||
|
Es el `.c` después del preprocesador: C puro, sin `#include`, `#define` ni
|
||||||
|
`#ifndef`. Es exactamente lo que lee el compilador.
|
||||||
|
|
||||||
|
- Cada `#include` ha desaparecido y en su lugar está **el texto del `.h`
|
||||||
|
pegado**. Esto va en cadena: `stdio.h` incluye otras cabeceras, que a su vez
|
||||||
|
incluyen otras. Un `hello.c` de 10 líneas se convierte en un `.i` de unas 800.
|
||||||
|
- Los *include guards* (`#ifndef`/`#define`/`#endif`) ya se han ejecutado y no
|
||||||
|
aparecen. Solo queda lo que había dentro.
|
||||||
|
- Las líneas como `# 1 "include/escpos.h" 1` no son código. Son **marcas de
|
||||||
|
línea**: le indican al compilador de qué fichero y de qué línea viene cada
|
||||||
|
trozo, para que un error diga `escpos.h:4` en vez de "línea 11 del `.i`".
|
||||||
|
- **En el `.i` no hay código de otros `.c`.** De `escpos.h` solo llega la
|
||||||
|
declaración `const char *escpos_version(void);`, sin cuerpo. Con `printf`
|
||||||
|
pasa igual: el `.i` trae `extern int printf(...);` y su código está en la
|
||||||
|
librería de C del sistema (`libc.so` en Linux), ya compilado.
|
||||||
|
|
||||||
|
**En Go**: no hay preprocesador. `import` no pega texto, y además de darte las
|
||||||
|
firmas mete el código del paquete en el binario. En C son dos cosas separadas:
|
||||||
|
las firmas llegan con `#include` al preprocesar, y el código llega al enlazar.
|
||||||
|
|
||||||
**En Go**: `go build` hace lo mismo por dentro (`go tool compile` genera un
|
**En Go**: `go build` hace lo mismo por dentro (`go tool compile` genera un
|
||||||
`.o` por paquete y `go tool link` los enlaza), pero no lo ves.
|
`.o` por paquete y `go tool link` los enlaza), pero no lo ves.
|
||||||
|
|
||||||
@@ -89,6 +137,15 @@ no sabe nada de `escpos.c`. Cada `.c`, junto con todo lo que incluye, forma una
|
|||||||
función existe y con qué firma. Puede repetirse las veces que quieras.
|
función existe y con qué firma. Puede repetirse las veces que quieras.
|
||||||
- **Definición**: la función con su cuerpo `{ ... }`. Debe aparecer **una sola
|
- **Definición**: la función con su cuerpo `{ ... }`. Debe aparecer **una sola
|
||||||
vez** en todo el programa. Es la *regla de una definición*.
|
vez** en todo el programa. Es la *regla de una definición*.
|
||||||
|
|
||||||
|
**Para compilar una llamada basta con la declaración.** Al compilar
|
||||||
|
`hello.c`, gcc no tiene el código de `escpos_version`, y no le hace falta.
|
||||||
|
Con saber que existe, qué recibe y qué devuelve, comprueba que la llamada es
|
||||||
|
correcta y genera la instrucción de llamada **con la dirección en blanco**.
|
||||||
|
Ese hueco lo rellena después el enlazador. Por eso
|
||||||
|
`gcc -c examples/hello.c` funciona sin `escpos.c`, y `nm build/hello.o`
|
||||||
|
muestra `U escpos_version`. Es como programar en Go contra una interfaz: el
|
||||||
|
compilador comprueba las firmas sin conocer la implementación.
|
||||||
- **`static`** delante de una función la hace privada a su `.c`. Es lo más
|
- **`static`** delante de una función la hace privada a su `.c`. Es lo más
|
||||||
parecido a la minúscula inicial de Go.
|
parecido a la minúscula inicial de Go.
|
||||||
|
|
||||||
@@ -150,6 +207,16 @@ hacen dos programas distintos:
|
|||||||
- En las cabeceras van declaraciones, tipos y macros. **Nunca cuerpos de
|
- En las cabeceras van declaraciones, tipos y macros. **Nunca cuerpos de
|
||||||
funciones**: cada `.c` que incluyera la cabecera tendría su propia definición
|
funciones**: cada `.c` que incluyera la cabecera tendría su propia definición
|
||||||
y el enlazador daría `multiple definition`.
|
y el enlazador daría `multiple definition`.
|
||||||
|
Comprobado: con el cuerpo en `escpos.h`, `nm` muestra `T escpos_version`
|
||||||
|
en `hello.o` **y** en `escpos.o`, y el enlazado falla.
|
||||||
|
- **Si cambias un `.h`, hay que recompilar todos los `.c` que lo incluyen.**
|
||||||
|
Si no lo haces, enlazas `.o` compilados con la versión vieja de la cabecera,
|
||||||
|
y ni el compilador ni el enlazador avisan. Nos pasó: después de mover el
|
||||||
|
cuerpo a `escpos.h` solo se recompiló `escpos.o`. El `hello.o` viejo seguía
|
||||||
|
teniendo `U escpos_version`, así que todo enlazaba y parecía que no pasaba
|
||||||
|
nada. Hay que comparar las fechas (`ls -l`) o, mejor, dejar que un Makefile
|
||||||
|
lleve la cuenta. **En Go** esto no puede pasar: `go build` sabe qué depende
|
||||||
|
de qué y recompila lo necesario.
|
||||||
- Las funciones `static` van en el `.c`. Metidas en un `.h`, cada `.o` recibe
|
- Las funciones `static` van en el `.c`. Metidas en un `.h`, cada `.o` recibe
|
||||||
una copia privada: el código se duplica y aparecen avisos de "no usada".
|
una copia privada: el código se duplica y aparecen avisos de "no usada".
|
||||||
|
|
||||||
@@ -218,6 +285,12 @@ En C17, `int f();` significa "parámetros **sin especificar**", así que
|
|||||||
| `-E` / `-S` / `-c` | control | Paran tras preprocesar, compilar o ensamblar. |
|
| `-E` / `-S` / `-c` | control | Paran tras preprocesar, compilar o ensamblar. |
|
||||||
| `-o nombre` | control | Nombre del fichero de salida. |
|
| `-o nombre` | control | Nombre del fichero de salida. |
|
||||||
|
|
||||||
|
Los flags que llevan un valor admiten el valor pegado o separado: `-Iinclude`
|
||||||
|
y `-I include` son lo mismo, igual que `-obuild/x.o` y `-o build/x.o`. Por
|
||||||
|
costumbre, `-I`, `-L`, `-l` y `-D` se escriben pegados (`-lws2_32`,
|
||||||
|
`-DDEBUG`), y `-o` separado. Los flags largos como `-std=c17` llevan siempre
|
||||||
|
`=`. **En Go** pasa algo parecido con el paquete `flag`: vale `-o x` y `-o=x`.
|
||||||
|
|
||||||
Todo menos `-o` se usa **al compilar**. Al enlazar solo se pasan los `.o`,
|
Todo menos `-o` se usa **al compilar**. Al enlazar solo se pasan los `.o`,
|
||||||
`-o` y, más adelante, las librerías (por ejemplo, `-lws2_32` para Winsock).
|
`-o` y, más adelante, las librerías (por ejemplo, `-lws2_32` para Winsock).
|
||||||
|
|
||||||
@@ -257,12 +330,29 @@ Pueden aparecer símbolos que no escribiste tú. Por ejemplo, `puts`: gcc
|
|||||||
cambia `printf("%s\n", s)` por `puts(s)`. Y `__main`, que es el código de
|
cambia `printf("%s\n", s)` por `puts(s)`. Y `__main`, que es el código de
|
||||||
arranque de MinGW.
|
arranque de MinGW.
|
||||||
|
|
||||||
|
Ese `U puts` no da `undefined reference` aunque no le pases nada al enlazador
|
||||||
|
para resolverlo, porque gcc añade siempre la librería de C (`libc`) al enlazar.
|
||||||
|
Las demás librerías hay que pedirlas explícitamente con `-l`.
|
||||||
|
|
||||||
## Errores: compilador frente a enlazador
|
## Errores: compilador frente a enlazador
|
||||||
|
|
||||||
- **Compilador**: el mensaje empieza por `fichero.c:línea:columna:`, porque
|
- **Compilador**: el mensaje empieza por `fichero.c:línea:columna:`, porque
|
||||||
está leyendo código fuente.
|
está leyendo código fuente.
|
||||||
- **Enlazador**: el mensaje menciona `ld.exe` o `collect2`, además de ficheros
|
- **Enlazador**: el mensaje menciona `ld.exe` (Windows), `/usr/bin/ld` (Linux)
|
||||||
`.o` y nombres de símbolo. A esa etapa ya no llega el código fuente.
|
o `collect2`, además de ficheros `.o` y nombres de símbolo. A esa etapa ya no
|
||||||
|
llega el código fuente. `collect2` es el programa de gcc que lanza `ld`.
|
||||||
|
|
||||||
|
Ejemplo real, enlazando `hello.o` sin `escpos.o`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/bin/ld: build/hello.o: in function `main':
|
||||||
|
.../examples/hello.c:6:(.text+0x9): undefined reference to `escpos_version'
|
||||||
|
collect2: error: ld returned 1 exit status
|
||||||
|
```
|
||||||
|
|
||||||
|
Sale `hello.c:6` aunque el enlazador no lee código fuente. Ese dato viene de la
|
||||||
|
información de depuración que `-g` guardó en el `.o`. Sin `-g` solo verías
|
||||||
|
`(.text+0x9)`, que es la posición dentro del código máquina.
|
||||||
|
|
||||||
## Warning frente a error
|
## Warning frente a error
|
||||||
|
|
||||||
@@ -277,6 +367,25 @@ ejecutando un binario viejo creyendo que es el nuevo. Para evitarlo:
|
|||||||
valor es fallo.
|
valor es fallo.
|
||||||
- `make` se detiene en cuanto un comando falla.
|
- `make` se detiene en cuanto un comando falla.
|
||||||
|
|
||||||
|
## Salto de línea al final del fichero
|
||||||
|
|
||||||
|
C17 exige que todo fichero fuente no vacío termine en `\n`. Si no, el
|
||||||
|
comportamiento es indefinido. El riesgo está en `#include`, que pega el
|
||||||
|
texto: la última línea del `.h` (`#endif`) podría quedar unida a la siguiente
|
||||||
|
línea del fichero que lo incluye. Los preprocesadores actuales lo arreglan por
|
||||||
|
dentro, pero sigue sin ser C correcto.
|
||||||
|
|
||||||
|
- Clang avisa con `-Wpedantic`: `warning: no newline at end of file
|
||||||
|
[-Wnewline-eof]`. gcc ya no avisa.
|
||||||
|
- Para verlo: `tail -c 5 fichero.c | xxd`. El último byte tiene que ser `0a`.
|
||||||
|
- Solución: configurar el editor (`"files.insertFinalNewline": true` en
|
||||||
|
VS Code).
|
||||||
|
|
||||||
|
Compilar con gcc **y** con clang da dos juegos de avisos distintos, sin coste:
|
||||||
|
`make clean && make CC=clang`.
|
||||||
|
|
||||||
|
**En Go**: `gofmt` añade el salto final automáticamente.
|
||||||
|
|
||||||
## Varios
|
## Varios
|
||||||
|
|
||||||
- En bash, un programa de la carpeta actual se ejecuta con `./hello.exe`. Bash
|
- En bash, un programa de la carpeta actual se ejecuta con `./hello.exe`. Bash
|
||||||
|
|||||||
@@ -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,
|
intentas escribir en ellos, el comportamiento es indefinido; en la práctica,
|
||||||
el programa se cierra.
|
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 *`
|
## `const char *`
|
||||||
|
|
||||||
Una función que "devuelve una cadena" en realidad devuelve **la dirección de
|
Una función que "devuelve una cadena" en realidad devuelve **la dirección de
|
||||||
@@ -44,6 +58,208 @@ Si `p` apunta a `"1.0.0"`, `*p` es solo el `'1'`.
|
|||||||
|
|
||||||
**En Go**: es lo mismo que `var p *T` frente a `*p`.
|
**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()`).
|
||||||
|
|
||||||
|
## 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[:]`.
|
||||||
|
|
||||||
|
## Aritmética de punteros: `[]` también lleva un `*` escondido
|
||||||
|
|
||||||
|
Con dibujos interactivos y preguntas: [02-pointers-visual.html](02-pointers-visual.html).
|
||||||
|
|
||||||
|
Sumar un entero a un puntero da **otra dirección**: `p + i` avanza `i`
|
||||||
|
**elementos**, es decir, `i * sizeof *p` bytes. No va a la casa. Solo calcula
|
||||||
|
qué casa es.
|
||||||
|
|
||||||
|
`p[i]` es una abreviatura de `*(p + i)`: calcula la dirección y va a ella.
|
||||||
|
Igual que `->`, los corchetes llevan el `*` dentro.
|
||||||
|
|
||||||
|
La cadena completa con el buffer:
|
||||||
|
|
||||||
|
| Escribes | Equivale a | Tipo | Qué es |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `buffer` | | `escpos_buffer *` | El papel (8 bytes) |
|
||||||
|
| `*buffer` | | `struct escpos_buffer` | La casa (24 bytes) |
|
||||||
|
| `buffer->data` | `(*buffer).data` | `uint8_t *` | Un papel guardado dentro de la casa, con la dirección del bloque de bytes |
|
||||||
|
| `buffer->data + i` | `&buffer->data[i]` | `uint8_t *` | La dirección de la casilla `i` |
|
||||||
|
| `buffer->data[i]` | `*(buffer->data + i)` | `uint8_t` | El byte de la casilla `i` |
|
||||||
|
|
||||||
|
Para saber si una expresión lleva `*`, cuenta: `*`, `->` y `[]` desreferencian
|
||||||
|
una vez cada uno, y `&` quita una desreferencia.
|
||||||
|
|
||||||
|
### Qué viaja cuando llamas a una función
|
||||||
|
|
||||||
|
En C, a una función le llega siempre **una copia** del valor que le pasas.
|
||||||
|
|
||||||
|
- `f(buffer)`: viaja una copia del papel, 8 bytes. La función llega a la misma
|
||||||
|
casa, así que lo que cambie en ella lo ves tú.
|
||||||
|
- `f(*buffer)`: viaja una copia de la casa, 24 bytes. Lo que cambie la función
|
||||||
|
se queda en su copia.
|
||||||
|
- `f(data)`, con `data` un array de 300 bytes: el array decae a la dirección
|
||||||
|
de su primera casilla. Viaja un papel de 8 bytes, no los 300 bytes. Dentro de
|
||||||
|
la función, `sizeof *data` es 1, el tamaño de una casilla.
|
||||||
|
|
||||||
|
### Restar direcciones
|
||||||
|
|
||||||
|
Las direcciones se escriben en hexadecimal, en base 16: cada posición vale 16
|
||||||
|
veces la de su derecha, y `a`–`f` valen 10–15.
|
||||||
|
|
||||||
|
| Hex | Decimal |
|
||||||
|
|---|---|
|
||||||
|
| `0x08` | 8 |
|
||||||
|
| `0x10` | 16 |
|
||||||
|
| `0x18` | 24 |
|
||||||
|
| `0x20` | 32 |
|
||||||
|
| `0x40` | 64 |
|
||||||
|
| `0x80` | 128 |
|
||||||
|
| `0x100` | 256 |
|
||||||
|
|
||||||
|
`0x20` es 2 × 16 + 0 = 32. Para no hacer la cuenta a mano:
|
||||||
|
|
||||||
|
- En gdb: `p 0xd210 - 0xd1f0` da `32`, y `p/x 32` da `0x20`.
|
||||||
|
- En bash: `echo $((0xd210 - 0xd1f0))`.
|
||||||
|
|
||||||
|
Si restas dos **punteros** del mismo tipo (`p &b - &a`), el resultado no son
|
||||||
|
bytes: son **elementos**, igual que en la suma. Dos `escpos_buffer *` separados
|
||||||
|
32 bytes dan `4`, porque cada uno ocupa 8.
|
||||||
|
|
||||||
|
**En Go** pasa lo mismo, todo se copia: un `*Buffer` copia el puntero y un
|
||||||
|
slice copia la cabecera (puntero, len y cap). La diferencia es que en el slice
|
||||||
|
viaja la longitud, y en C tienes que pasarla tú.
|
||||||
|
|
||||||
## `printf` y el formato
|
## `printf` y el formato
|
||||||
|
|
||||||
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
|
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+249
@@ -0,0 +1,249 @@
|
|||||||
|
# Make
|
||||||
|
|
||||||
|
## Qué hacía Go por mí
|
||||||
|
|
||||||
|
`go build` hace tres cosas sin que las pidas:
|
||||||
|
|
||||||
|
1. Lee los `import` y deduce qué paquetes dependen de cuáles.
|
||||||
|
2. Recompila solo lo que cambió, usando la caché de `$GOCACHE`, que va por
|
||||||
|
hash del contenido.
|
||||||
|
3. Llama al compilador y al enlazador con los flags correctos para cada
|
||||||
|
plataforma (`GOOS`/`GOARCH`).
|
||||||
|
|
||||||
|
En C no pasa nada de eso. `gcc` compila lo que le pases y nada más. `make` es
|
||||||
|
la herramienta clásica para escribir a mano ese grafo de dependencias.
|
||||||
|
|
||||||
|
## El modelo: un grafo de ficheros con fechas
|
||||||
|
|
||||||
|
Un Makefile es una lista de **reglas**:
|
||||||
|
|
||||||
|
```make
|
||||||
|
objetivo: prerrequisitos
|
||||||
|
receta # OJO: la línea empieza por TABULADOR, no por espacios
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Objetivo**: el fichero que se quiere producir, por ejemplo `build/escpos.o`.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
**Make no entiende la receta.** Para Make, la receta es texto que pasa a la
|
||||||
|
shell sin leerlo. Que ponga `-Iinclude` no le dice nada: `-I` es un flag de gcc,
|
||||||
|
no de Make. Make solo mira la línea `objetivo: prerrequisitos`. Si un fichero
|
||||||
|
no aparece ahí, sus cambios no provocan ninguna recompilación.
|
||||||
|
|
||||||
|
**El objetivo debe llamarse como el fichero que produce la receta.** Si la
|
||||||
|
regla se llama `link/all` pero genera `build/hello`, Make busca un fichero
|
||||||
|
`link/all`. Como nunca existe, ejecuta la receta siempre.
|
||||||
|
|
||||||
|
**Se declaran dependencias, no pasos.** No hace falta que una receta llame a
|
||||||
|
`make` para construir otras cosas (`make[1]: Entering directory...` indica que
|
||||||
|
se está lanzando otro Make). Basta con escribir de qué depende cada objetivo, y
|
||||||
|
Make deduce el orden:
|
||||||
|
|
||||||
|
```make
|
||||||
|
all: build/hello # sin receta: solo "depende de"
|
||||||
|
build/hello: build/hello.o build/escpos.o # enlazar
|
||||||
|
build/hello.o: examples/hello.c include/escpos.h
|
||||||
|
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 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
|
||||||
|
|
||||||
|
| Pieza | Qué es |
|
||||||
|
|---|---|
|
||||||
|
| `CC = gcc` | Variable. Se usa como `$(CC)`. |
|
||||||
|
| `CFLAGS`, `CPPFLAGS`, `LDFLAGS` | Nombres convencionales: flags de compilación, del preprocesador (`-Iinclude`) y del enlazado. |
|
||||||
|
| `$@` | Nombre del objetivo de la regla. |
|
||||||
|
| `$<` | Primer prerrequisito. |
|
||||||
|
| `$^` | Todos los prerrequisitos. |
|
||||||
|
| `build/%.o: src/%.c` | Regla patrón: sirve para cualquier `.c` → `.o`. |
|
||||||
|
| `.PHONY: all clean` | Objetivos que no son ficheros. Sin esto, si existiera un fichero llamado `clean`, `make clean` no haría nada. |
|
||||||
|
| `$(wildcard src/*.c)` | Lista de ficheros que existen. |
|
||||||
|
| `$(patsubst src/%.c,build/%.o,$(SRCS))` | Transforma una lista de nombres. |
|
||||||
|
|
||||||
|
### `.PHONY`: objetivos que no son ficheros
|
||||||
|
|
||||||
|
`clean` y `all` son nombres de acciones, no ficheros. Pero Make no lo sabe: si
|
||||||
|
alguien crea un fichero llamado `clean`, la regla (sin prerrequisitos) queda
|
||||||
|
"al día" y `make clean` responde `'clean' is up to date` sin borrar nada.
|
||||||
|
Comprobado con `touch clean`.
|
||||||
|
|
||||||
|
```make
|
||||||
|
.PHONY: all clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Con esto Make no busca ningún fichero con esos nombres y ejecuta la receta
|
||||||
|
siempre.
|
||||||
|
|
||||||
|
**En proyectos Go** Make se usa como lanzador de tareas (`migrate`, `test`,
|
||||||
|
`lint`, `run`...). Ninguna produce un fichero, y hasta `build` se delega en
|
||||||
|
`go build`, que tiene su propia caché y conoce las dependencias mejor que
|
||||||
|
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
|
||||||
|
ejecución. Pero **Make no se da cuenta de que han cambiado el compilador o los
|
||||||
|
flags**, porque solo compara fechas de ficheros. Si los `.o` están al día, no
|
||||||
|
hace nada, y si recompila alguno, mezcla `.o` de gcc con `.o` de clang. Cuando
|
||||||
|
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:
|
||||||
|
la regla `build/%.o: src/%.c` no menciona el `.h`. Resultado: un `.o` compilado
|
||||||
|
con la versión vieja de la cabecera. Es un error silencioso que en Go no
|
||||||
|
existe.
|
||||||
|
|
||||||
|
La solución es que gcc genere las dependencias por ti:
|
||||||
|
|
||||||
|
- `-MMD` hace que, al compilar `x.c`, gcc escriba también `x.d`, un fragmento
|
||||||
|
de Makefile con las cabeceras que incluyó.
|
||||||
|
- `-MP` añade reglas vacías para cada cabecera, para que borrar un `.h` no
|
||||||
|
rompa el build.
|
||||||
|
- `-include $(DEPS)` carga esos `.d` en el Makefile. El `-` evita el error si
|
||||||
|
todavía no existen.
|
||||||
|
|
||||||
|
## Windows y Linux en el mismo Makefile
|
||||||
|
|
||||||
|
En MSYS2, la variable de entorno `OS` vale `Windows_NT`. En Linux no está
|
||||||
|
definida.
|
||||||
|
|
||||||
|
```make
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXE = .exe
|
||||||
|
else
|
||||||
|
EXE =
|
||||||
|
endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Más adelante (Fase 4) aquí irán también las librerías que cambian según la
|
||||||
|
plataforma, como `-lws2_32` para Winsock en Windows.
|
||||||
|
|
||||||
|
### La «magia .exe» de MSYS2
|
||||||
|
|
||||||
|
Sin `EXE`, el Makefile con objetivo `build/hello` parece funcionar en Windows:
|
||||||
|
el segundo `make` dice `Nothing to be done`. No es mérito del Makefile:
|
||||||
|
|
||||||
|
- `make`, `ls` y la shell de `/usr/bin` son programas de MSYS2, que emula
|
||||||
|
POSIX encima de Windows.
|
||||||
|
- Si un programa de MSYS2 pregunta por `build/hello` y solo existe
|
||||||
|
`build/hello.exe`, la capa de emulación responde que existe y le da la fecha
|
||||||
|
del `.exe`.
|
||||||
|
|
||||||
|
| Comando | Qué ve |
|
||||||
|
|---|---|
|
||||||
|
| `ls -l build/hello` | Pasa por la emulación: «existe». |
|
||||||
|
| `cmd //c dir build` | Pregunta a Windows: solo hay `hello.exe`. |
|
||||||
|
|
||||||
|
Con un make nativo (`mingw32-make`) esa emulación no existe: no encuentra
|
||||||
|
`build/hello` y vuelve a enlazar cada vez. Por eso se pone el nombre real con
|
||||||
|
`$(EXE)`.
|
||||||
|
|
||||||
|
**Comparación con Go:** `go build` añade `.exe` por sí mismo cuando
|
||||||
|
`GOOS=windows`. En C, el nombre del ejecutable lo decides tú en el Makefile.
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
# 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/<pid>/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]`.
|
||||||
|
|
||||||
|
### Ejemplo: dónde vive cada parte del buffer
|
||||||
|
|
||||||
|
Medido con gdb en `hello` (Fedora, 2026-10-06), en el punto de parada de
|
||||||
|
`main`:
|
||||||
|
|
||||||
|
| Qué | Expresión en gdb | Dirección | Zona |
|
||||||
|
|---|---|---|---|
|
||||||
|
| El papel: la variable local `buffer` de `main` | `p &buffer` | `0x7fffffffd520` | `[stack]` |
|
||||||
|
| La casa: el struct de 24 bytes | `p buffer` | `0x4052b0` | `[heap]` |
|
||||||
|
| Los bytes: el bloque de 128 | `p buffer->data` | `0x4052d0` | `[heap]` |
|
||||||
|
|
||||||
|
- Son **tres sitios**: el puntero está en la pila, y los dos bloques de
|
||||||
|
`malloc`/`realloc` están en el montón, separados entre sí.
|
||||||
|
- Los campos del struct sí van juntos y en orden: `&buffer->len` es
|
||||||
|
`0x4052b8`, 8 bytes después de `data`.
|
||||||
|
- El struct y los datos están a 32 bytes y no a 24: `malloc` redondea el tamaño
|
||||||
|
y guarda delante de cada bloque una cabecera con información para `free`.
|
||||||
|
- `info proc mappings` en gdb (o `/proc/<pid>/maps`) dice a qué zona pertenece
|
||||||
|
cada dirección.
|
||||||
|
|
||||||
|
**En Go** la variable `b := &Buffer{}` también puede vivir en la pila y el
|
||||||
|
struct en el montón, pero lo decide el compilador (*escape analysis*) y no lo
|
||||||
|
ves.
|
||||||
|
|
||||||
|
### Memoria virtual frente a memoria real
|
||||||
|
|
||||||
|
`/proc/<pid>/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 `<stdlib.h>`.
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
**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
|
||||||
|
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×).
|
||||||
|
|
||||||
|
## `memcpy`: copiar bloques de bytes
|
||||||
|
|
||||||
|
`memcpy(dst, src, n)` (en `<string.h>`) 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.
|
||||||
|
|
||||||
|
### `restrict`: la promesa de que no se solapan
|
||||||
|
|
||||||
|
Desde C99 la firma real lleva `restrict`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
|
||||||
|
```
|
||||||
|
|
||||||
|
`restrict` en un puntero es una **promesa del programador al compilador**:
|
||||||
|
mientras dura la función, a la memoria que se toca por ese puntero no se accede
|
||||||
|
por ningún otro. Aquí quiere decir que `dst` y `src` no se solapan.
|
||||||
|
|
||||||
|
- **Para qué sirve:** sabiendo que no se solapan, el compilador puede copiar en
|
||||||
|
bloques de 16 o 32 bytes, reordenar y no volver a leer `src` después de cada
|
||||||
|
escritura en `dst`. Sin la promesa, tendría que suponer que escribir en `dst`
|
||||||
|
puede cambiar `src`.
|
||||||
|
- **Quién lo comprueba:** nadie. Si rompes la promesa (con bloques que se
|
||||||
|
solapan), es comportamiento indefinido. Para bloques solapados está `memmove`,
|
||||||
|
que no lleva `restrict`.
|
||||||
|
- **Para quien llama no cambia nada:** se llama igual que en C89.
|
||||||
|
|
||||||
|
`man 3 memcpy` en Fedora lo escribe como `void dest[restrict .n]`. Es una
|
||||||
|
notación de la página de manual para decir que el bloque mide `n`. No es C
|
||||||
|
válido.
|
||||||
|
|
||||||
|
Versión del estándar: el `Makefile` compila con `-std=c17`, así que
|
||||||
|
`restrict` existe. Para comprobar qué estándar usa el compilador:
|
||||||
|
`echo | gcc -std=c17 -dM -E - | grep __STDC_VERSION__` da `201710L` (C17). Sin
|
||||||
|
`-std`, gcc 15 usa C23 (`202311L`).
|
||||||
|
|
||||||
|
**En Go** no existe. El compilador tiene que suponer lo peor cuando dos slices
|
||||||
|
pueden compartir memoria, y `copy` funciona aunque se solapen, como `memmove`.
|
||||||
|
|
||||||
|
**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
|
||||||
|
`<stdint.h>`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variables locales sin inicializar = basura.** En Go toda variable nace con
|
||||||
|
su valor cero (`nil`, `0`, `""`). En C, una variable local que no inicializas
|
||||||
|
contiene lo que hubiera antes en esa posición de la pila. Si un `goto` salta
|
||||||
|
por encima de la línea `escpos_buffer *b = escpos_buffer_new();`, esa
|
||||||
|
asignación no se ejecuta y `b` vale cualquier cosa. Pasarla a `free` es
|
||||||
|
comportamiento indefinido. (Las variables **globales** y `static` sí empiezan a
|
||||||
|
cero.)
|
||||||
|
|
||||||
|
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
|
||||||
|
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ó |
|
||||||
@@ -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 <tipo existente> <nombre nuevo>;`. 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`... | `<stdint.h>` | `uint8`, `int32`, `uint64` |
|
||||||
|
| `size_t` (tamaños y longitudes) | `<stddef.h>` (también la traen `<stdlib.h>`, `<stdio.h>`, `<string.h>`) | el `int` de `len()`, pero sin signo |
|
||||||
|
| `bool`, `true`, `false` | `<stdbool.h>` | `bool` |
|
||||||
|
| `NULL` | `<stddef.h>` (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` (`<stdbool.h>`) | `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` |
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
|
### 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.
|
||||||
+28
-4
@@ -3,11 +3,17 @@
|
|||||||
Ordenados por tema, en el orden en que se ven en el proyecto.
|
Ordenados por tema, en el orden en que se ven en el proyecto.
|
||||||
|
|
||||||
1. [Compilación y enlazado](01-compilation-and-linking.md): etapas de gcc,
|
1. [Compilación y enlazado](01-compilation-and-linking.md): etapas de gcc,
|
||||||
cómo compila Go, unidades de traducción, cabeceras e `#include`,
|
cómo compila Go, empezar por cualquier etapa (`.i`, `-save-temps`), qué
|
||||||
encapsulación y tipos opacos, *include guards*, flags, qué ve quien recibe
|
hay dentro de un `.i`, unidades de traducción, por qué basta la declaración
|
||||||
los binarios, `nm` y errores del enlazador.
|
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
|
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, aritmética de
|
||||||
|
punteros (`p + i`, `p[i]`) y qué viaja al llamar a una función, un puntero no sabe
|
||||||
|
cuántos elementos hay, `const` en parámetros, arrays y el formato de
|
||||||
|
`printf`. Versión interactiva con dibujos:
|
||||||
|
[02-pointers-visual.html](02-pointers-visual.html) (ábrela en el navegador).
|
||||||
3. [La memoria de un proceso](03-process-memory.md): memoria virtual, fallos
|
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
|
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.
|
memoria de otro proceso, y por qué el peligro real está en la tuya.
|
||||||
@@ -17,3 +23,21 @@ Ordenados por tema, en el orden en que se ven en el proyecto.
|
|||||||
5. [Frameworks y librerías](05-libraries-and-frameworks.md): Qt, por qué en C
|
5. [Frameworks y librerías](05-libraries-and-frameworks.md): Qt, por qué en C
|
||||||
casi no hay frameworks, librerías C relevantes para el proyecto e
|
casi no hay frameworks, librerías C relevantes para el proyecto e
|
||||||
interfaces con punteros a función.
|
interfaces con punteros a función.
|
||||||
|
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, `memcpy` y `restrict`, 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, 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.
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Chuleta de comandos
|
||||||
|
|
||||||
|
Comandos que hemos usado, agrupados por tarea. Los flags del proyecto son
|
||||||
|
`-std=c17 -Wall -Wextra -Wpedantic -g`.
|
||||||
|
|
||||||
|
## Compilar paso a paso
|
||||||
|
|
||||||
|
| Comando | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `gcc -E -Iinclude src/escpos.c -o build/escpos.i` | Solo preprocesa. Genera el `.i`: C con los `.h` pegados. |
|
||||||
|
| `gcc -S ...` | Para después de compilar. Genera el `.s` (ensamblador). |
|
||||||
|
| `gcc <flags> -Iinclude -c src/escpos.c -o build/escpos.o` | Compila hasta el `.o` (fichero objeto), sin enlazar. |
|
||||||
|
| `gcc <flags> -c build/escpos.i -o build/escpos.o` | Compila partiendo de un `.i`. No hace falta `-I`. |
|
||||||
|
| `gcc <flags> -Iinclude -save-temps -c src/escpos.c` | Cadena completa, conservando `.i`, `.s` y `.o`. |
|
||||||
|
| `gcc build/hello.o build/escpos.o -o build/hello` | Enlaza los `.o` en un ejecutable. |
|
||||||
|
| `./build/hello` | Ejecuta un programa de la carpeta actual. |
|
||||||
|
|
||||||
|
Si juntas dos flags de parada (`-E -S`), gana la que para antes.
|
||||||
|
|
||||||
|
## Inspeccionar ficheros
|
||||||
|
|
||||||
|
| Comando | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `nm build/hello.o` | Lista símbolos: `T` definido y visible, `t` definido y `static`, `U` usado pero definido en otro sitio. |
|
||||||
|
| `wc -l fichero` | Cuenta líneas (por ejemplo, para comparar `.c` y `.i`). |
|
||||||
|
| `ls -l build/` | Muestra la fecha de modificación: sirve para ver qué `.o` está obsoleto. |
|
||||||
|
| `file fichero` | Dice qué es: ELF (Linux), COFF o PE (Windows)... |
|
||||||
|
| `strings fichero.o` | Muestra las cadenas legibles que hay dentro de un binario. |
|
||||||
|
| `objdump -d fichero.o` | Desensambla el código máquina. |
|
||||||
|
| `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 \| gcc -std=c17 -dM -E - \| grep __STDC_VERSION__` | Qué versión del estándar de C usa el compilador: `201710L` es C17, `202311L` es C23. |
|
||||||
|
| `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 `<pid>` 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/<pid>/status` | Memoria virtual reservada (`VmSize`) frente a RAM física en uso (`VmRSS`). |
|
||||||
|
| `grep -E 'heap\|stack' /proc/<pid>/maps` | Rangos de direcciones del montón y de la pila de un proceso. |
|
||||||
|
| `pmap -x <pid>` | 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. |
|
||||||
|
| `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. |
|
||||||
|
| `p &x` / `p *p` / `p/x *p@n` | Dentro de gdb: dirección de `x`, lo que hay en la dirección de `p`, y `n` elementos seguidos desde `p` en hexadecimal. |
|
||||||
|
| `info proc mappings` | Dentro de gdb: mapa de memoria del proceso (`[heap]`, `[stack]`, librerías). Dice en qué zona cae una dirección. |
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|---|---|
|
||||||
|
| `objdump -p hello.exe \| grep "DLL Name"` | Lista las DLL de las que depende el ejecutable. |
|
||||||
|
| `strip hello.exe` | Quita símbolos e información de depuración. |
|
||||||
|
| `gcc -static ...` | Enlaza estáticamente todas las librerías posibles. |
|
||||||
|
| `uname -s` | Nombre del sistema: `Linux` en Fedora, `UCRT64_NT-...` en la terminal UCRT64 de Windows. |
|
||||||
|
| `echo $MSYSTEM` | Entorno de MSYS2 activo. Tiene que ser `UCRT64`. |
|
||||||
|
| `pacman -S mingw-w64-ucrt-x86_64-clang` | Instala clang para el entorno UCRT64. |
|
||||||
|
| `pacman -S vim` | Instala `xxd` (viene con vim). Sin él, usa `hexdump -C fichero.bin`. |
|
||||||
|
|
||||||
|
## Make
|
||||||
|
|
||||||
|
| Comando | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `make` | Construye el primer objetivo del `Makefile`. |
|
||||||
|
| `make build/escpos.o` | Construye un objetivo concreto. |
|
||||||
|
| `make CC=clang` | Sobrescribe una variable del Makefile para esta ejecución. No recompila lo que ya está al día. |
|
||||||
|
| `make clean` | Borra lo generado (si el Makefile tiene esa regla y está en `.PHONY`). |
|
||||||
|
| `touch fichero` | Actualiza la fecha del fichero sin cambiar su contenido. Make lo verá como modificado. |
|
||||||
|
| `cat -A Makefile` | Muestra los caracteres invisibles: `^I` es un tabulador y `$` es el fin de línea. |
|
||||||
|
|
||||||
|
## Errores típicos y qué significan
|
||||||
|
|
||||||
|
| Mensaje | Quién lo da | Causa |
|
||||||
|
|---|---|---|
|
||||||
|
| `fichero.c:línea:col: error:` | Compilador | Error en el código fuente. |
|
||||||
|
| `undefined reference to 'x'` | Enlazador (`ld`, `collect2`) | Un `U` sin ninguna `T`: falta pasar un `.o` o una librería. |
|
||||||
|
| `multiple definition of 'x'` | Enlazador | Dos `T` con el mismo nombre. A menudo, un cuerpo de función en un `.h`. |
|
||||||
|
| `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 (`<stdint.h>`, `<stddef.h>`...). |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
| Go | C |
|
||||||
|
|---|---|
|
||||||
|
| `go build -x` | Ver los comandos que se ejecutan por dentro. |
|
||||||
|
| `go build -work` | Conservar los intermedios (como `-save-temps`). |
|
||||||
|
| `go build -ldflags="-s -w"` | Quitar símbolos (como `strip`). |
|
||||||
Binary file not shown.
@@ -0,0 +1,49 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include "escpos.h"
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int result = 1;
|
||||||
|
const char *version = escpos_version();
|
||||||
|
escpos_buffer *buffer = NULL;
|
||||||
|
escpos_buffer *buffer_a = NULL;
|
||||||
|
escpos_buffer *buffer_b = NULL;
|
||||||
|
|
||||||
|
printf("%s\n", version);
|
||||||
|
|
||||||
|
buffer = escpos_buffer_new();
|
||||||
|
if (buffer == NULL) { goto cleanup; }
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
int err = escpos_buffer_append_byte(buffer, 'A');
|
||||||
|
if (err == -1) { goto cleanup; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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, sizeof data);
|
||||||
|
if (err == -1) { goto cleanup; }
|
||||||
|
|
||||||
|
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, sizeof data_b);
|
||||||
|
if (err == -1) { goto cleanup; }
|
||||||
|
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
escpos_buffer_free(buffer);
|
||||||
|
escpos_buffer_free(buffer_a);
|
||||||
|
escpos_buffer_free(buffer_b);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module goescpos
|
||||||
|
|
||||||
|
go 1.26
|
||||||
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include "escpos.h"
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
|
|
||||||
const char *version = escpos_version();
|
|
||||||
|
|
||||||
printf("%s\n", version);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Binary file not shown.
+36
-4
@@ -1,6 +1,38 @@
|
|||||||
#ifndef ESCPOS_H
|
#ifndef ESCPOS_H
|
||||||
#define ESCPOS_H
|
#define ESCPOS_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
const char *escpos_version(void);
|
const char *escpos_version(void);
|
||||||
|
|
||||||
#endif
|
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 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[], size_t size);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
#include "escpos.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
struct escpos_buffer
|
||||||
|
{
|
||||||
|
uint8_t *data;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
|
||||||
|
int err = buffer_reserve(buffer, buffer->len + 1);
|
||||||
|
if (err == -1)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->data[buffer->len] = byte;
|
||||||
|
buffer->len++;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[], size_t size)
|
||||||
|
{
|
||||||
|
int err = buffer_reserve(buffer, buffer->len + size);
|
||||||
|
if (err == -1)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buffer->data, byte, size);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+2
-5
@@ -1,9 +1,6 @@
|
|||||||
#include "escpos.h"
|
#include "escpos.h"
|
||||||
|
|
||||||
const char *escpos_version(void) {
|
const char *escpos_version(void)
|
||||||
|
{
|
||||||
return "1.0.0";
|
return "1.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
static int some_function(void) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user