memcpy
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user