This commit is contained in:
2026-10-02 21:03:47 +02:00
parent 9d517f3607
commit b15620838b
20 changed files with 579 additions and 60 deletions
+139
View File
@@ -171,6 +171,37 @@ solo entonces asignarlo.
escribir `s = append(s, x)`: si el array subyacente se movió, el slice viejo
apunta al sitio antiguo.
**Colgante no es lo mismo que perdido.** Tras un `realloc` que mueve el bloque,
el puntero viejo no cambia: sigue con la dirección antigua, pero esa memoria ya
está liberada. Es un puntero colgante, y usarlo es comportamiento indefinido.
"Perdido" es lo contrario, una fuga: memoria reservada sin ningún puntero.
| | C (`realloc`) | Go (`append`) |
|---|---|---|
| ¿Copia al bloque nuevo? | Sí | Sí |
| ¿Libera el bloque viejo? | Sí, en el acto | No; el GC lo mantiene vivo mientras algo lo referencie |
| Puntero/slice viejo | Colgante: comportamiento indefinido | Válido pero desconectado: ya no ve los cambios |
| Síntoma del fallo | Cuelgue o corrupción del montón | Datos incorrectos, sin ningún error |
```go
a := make([]byte, 3) // len 3, cap 3: lleno
b := a // b comparte el array con a
a = append(a, 'X') // no cabe: nuevo array
a[0] = 'Z'
fmt.Println(b[0]) // 0: b sigue en el array viejo
```
Si `a` hubiera tenido capacidad libre, `append` no movería nada y `b[0]` valdría
`'Z'`. El resultado depende de `cap`.
**Qué hace `bytes.Buffer` por debajo.** Es el mismo algoritmo que
`escpos_buffer_append_byte`: 64 bytes iniciales (`smallBufferSize`) y duplicar
al llenarse. Diferencias: nunca crece en el sitio (siempre reserva y copia),
pone la memoria a cero (`malloc` no), redondea la capacidad al tamaño de clase
del asignador de Go, comprueba los índices y, si falla, hace `panic` en vez de
devolver un error. El struct `bytes.Buffer` puede vivir en la pila si no
escapa; solo los datos van al montón.
### Estrategia de crecimiento
Si el buffer creciera de byte en byte, cada byte nuevo podría provocar una
@@ -182,6 +213,63 @@ O(1)*).
`append` en Go duplica hasta 256 elementos y a partir de ahí crece más despacio
(unos 1,25×).
## `memcpy`: copiar bloques de bytes
`memcpy(dst, src, n)` (en `<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.
**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
@@ -265,6 +353,57 @@ dice qué hacer con cada una.
El código Go que llame a `escpos_buffer_new` tendrá que hacer
`defer C.escpos_buffer_free(b)`, igual que con un fichero.
## Liberar en los caminos de error: C no tiene `defer`
En Go, `defer b.Close()` se ejecuta salga la función por donde salga. En C no
existe nada así: cada `return` tiene que dejar liberado todo lo que se creó
antes. Con varios recursos, repetir los `free` delante de cada `return` es
largo y es fácil olvidarse de alguno.
El patrón clásico de C es **un único punto de limpieza al final**, al que se
salta con `goto`:
```c
int run(void)
{
int result = 1; /* se supone fallo hasta llegar al final */
thing *a = NULL; /* todo a NULL desde el principio */
thing *b = NULL;
a = thing_new();
if (a == NULL) { goto cleanup; }
b = thing_new();
if (b == NULL) { goto cleanup; }
/* ... trabajo ... */
result = 0; /* todo ha ido bien */
cleanup: /* etiqueta: destino del goto */
thing_free(b); /* seguro aunque sea NULL */
thing_free(a);
return result;
}
```
Por qué funciona:
- Todos los punteros empiezan en `NULL`, y la función de liberar acepta `NULL`
sin hacer nada. Por eso `escpos_buffer_free` se diseñó así. Da igual en qué
punto falle: la limpieza libera lo que exista e ignora lo demás.
- Hay un solo `return`. Al añadir un recurso nuevo, se añade su `free` en un
solo sitio.
- Se libera en orden inverso al de creación, como hace `defer` (LIFO).
`goto` tiene mala fama por el código "espagueti", pero este uso, saltar
**hacia delante** a la limpieza, es idiomático. El núcleo de Linux lo usa por
todas partes.
gcc y clang tienen una extensión, `__attribute__((cleanup(f)))`, que se
parece a `defer`. No es C estándar y no funciona en MSVC, así que en este
proyecto no se usa.
## Valgrind
`valgrind` ejecuta tu programa en una CPU simulada y vigila cada acceso a