Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Diseño de Interfaces y Librerías en C

Principios para crear código modular, reutilizable y robusto.

Universidad Nacional de Rio Negro - Sede Andina

Introducción: El Arte de Diseñar Contratos

Más allá de escribir algoritmos que funcionen, un programador profesional debe saber cómo construir módulos de software que otros puedan utilizar de manera fácil, segura y predecible. En C, la puerta de entrada a un módulo es su interfaz pública o API (Application Programming Interface), definida casi siempre en un archivo de cabecera (.h).

Una API es un contrato formal. Define qué funcionalidades ofrece un módulo, qué datos necesita y qué resultados garantiza. Un buen diseño de interfaz es la diferencia entre una librería que es un placer utilizar y una que es una fuente constante de errores y frustración. Este contrato no es solo a nivel de código fuente (API), sino también a nivel binario (ABI - Application Binary Interface), determinando cómo el código compilado interactúa con otro.

La noción de “contrato” fue formalizada por Bertrand Meyer en su metodología de Diseño por Contrato (Design by Contract) Meyer, 1988, donde las precondiciones, poscondiciones e invariantes definen obligaciones y garantías entre el cliente y el proveedor de un servicio. Aunque Meyer desarrolló esta metodología en el contexto de Eiffel, sus principios son universalmente aplicables y particularmente valiosos en C, donde la falta de mecanismos de seguridad del lenguaje hace que los contratos explícitos sean aún más críticos.

Este apunte establece los lineamientos para diseñar interfaces de alta calidad en C, aplicando los principios de la descomposición funcional y las reglas de estilo para crear código que no solo es correcto, sino también elegante y mantenible.

El Desafío del Diseño en C

C es un lenguaje minimalista que delega gran parte de la responsabilidad de seguridad y corrección al programador. A diferencia de lenguajes modernos con sistemas de tipos más ricos, manejo automático de memoria o espacios de nombres modulares, C ofrece pocas herramientas para encapsulamiento y abstracción. Esta aparente limitación es también su fortaleza: la simplicidad y el control directo que brinda C son la razón por la cual sigue siendo el lenguaje de elección para sistemas operativos, drivers, sistemas embebidos y software de alto rendimiento Kernighan & Ritchie, 1988.

El diseño de APIs en C requiere disciplina y conocimiento profundo de los patrones idiomáticos del lenguaje. Como señalan Spinellis y Gousios Spinellis & Gousios, 2009, el código bien diseñado no es accidental; es el resultado de decisiones conscientes y la aplicación sistemática de principios de ingeniería de software.

Principios Fundamentales del Diseño de Interfaces

Un buen diseño de API se rige por un conjunto de principios que buscan maximizar la claridad, la seguridad y la facilidad de uso.

1. Claridad y Expresividad

Una interfaz debe ser auto-documentada en la medida de lo posible. El código debe comunicar su intención de forma clara y directa. Como observa Martin Martin, 2008, “el código se lee muchas más veces de las que se escribe”, por lo que invertir en claridad es una optimización fundamental.

2. Principio de Mínima Sorpresa

Una función o librería debe comportarse de la manera que un programador esperaría razonablemente. Evitá la “magia” y los comportamientos inesperados que obligan al usuario a leer la implementación para entender qué está pasando.

El Principio de Mínima Sorpresa (también conocido como Principle of Least Astonishment) fue popularizado por la filosofía de diseño Unix Raymond, 2003 y establece que el sistema debe comportarse de la manera que la mayoría de los usuarios esperaría. En el contexto de APIs, esto significa que las funciones deben ser consistentes con convenciones establecidas y con el comportamiento de funciones similares.

Bloch Bloch, 2006 enfatiza que una buena API debe ser “fácil de usar y difícil de usar mal”. Esto se logra cuando el comportamiento predeterminado es el más común y seguro, y cuando las operaciones peligrosas requieren pasos explícitos que alertan al programador sobre lo que está haciendo.

Por ejemplo, si una función modifica sus argumentos, esto debe ser evidente desde su firma y nombre. La biblioteca estándar de C lo hace consistentemente: strcpy copia cadenas y modifica el destino (el primer parámetro es siempre el destino), mientras que strlen solo lee y no modifica nada. Cuando una interfaz viola las expectativas del usuario, la carga cognitiva aumenta, introduciendo errores y frustraciones evitables.

3. Encapsulamiento y Ocultamiento de Información

El usuario de tu librería no necesita (y no debe) conocer los detalles internos de su implementación. La interfaz pública (.h) debe exponer el qué (la capacidad), mientras que la implementación (.c) oculta el cómo (los detalles).

El mecanismo más potente para lograr esto en C es el uso de punteros opacos (opaque pointers), que permiten una verdadera abstracción de datos.

Punteros Opacos: La Clave de la Abstracción en C

Un puntero opaco es un puntero a un tipo de estructura cuya definición está incompleta en el archivo de cabecera. El usuario sabe que existe un tipo mi_tipo_t, pero no conoce sus campos internos, su tamaño, ni su organización en memoria.

Este patrón, también conocido como PIMPL (Pointer to IMPLementation) o Tipo Abstracto de Datos (TAD), es la forma idiomática en C de lograr encapsulamiento real. Aunque el concepto de tipos abstractos de datos fue formalizado por Liskov y Zilles Liskov & Zilles, 1974, su implementación en C mediante punteros opacos se popularizó en los años 80 y es hoy una práctica estándar en todas las librerías C profesionales.

El trabajo seminal de Liskov y Zilles sobre TADs estableció que un tipo de datos debe definirse por sus operaciones y sus propiedades algebraicas, no por su representación interna. En C, los punteros opacos son la herramienta fundamental para lograr esta separación entre interfaz e implementación, permitiendo lo que Parnas Parnas, 1972 llamó information hiding (ocultamiento de información): el principio de que los módulos deben revelar lo mínimo indispensable sobre su funcionamiento interno.

Ventajas Técnicas:

4. Gestión de Recursos y Propiedad (Ownership)

Una de las mayores fuentes de errores en C es la gestión de memoria. Tu API debe ser explícita sobre quién es el responsable (owner) de asignar y liberar cada recurso.

El concepto de ownership (propiedad) es fundamental en la programación de sistemas. Aunque lenguajes modernos como Rust lo formalizan en el sistema de tipos, en C debe ser documentado explícitamente y seguido disciplinadamente. La falta de claridad sobre la propiedad de los recursos es una causa principal de fugas de memoria (memory leaks), dobles liberaciones (double free) y accesos a memoria liberada (use-after-free) Serebryany et al., 2012.

Un estudio de Lu et al. Lu et al., 2008 sobre bugs en sistemas de código abierto encontró que los errores de manejo de memoria y concurrencia representan más del 60% de los bugs críticos que causan crashes y vulnerabilidades de seguridad. El diseño cuidadoso de APIs con semánticas claras de propiedad puede prevenir una gran proporción de estos errores.

5. Manejo de Errores Robusto y Consistente

Una librería no debe terminar el programa abruptamente (ej. con exit()). Debe reportar los errores al llamador para que este decida cómo proceder.

6. Simplicidad y Minimalismo

Una buena interfaz es aquella que es lo más pequeña posible, pero no más. Cada función expuesta públicamente aumenta la “superficie de ataque” (potenciales bugs y vulnerabilidades) y la carga de mantenimiento.

Ejemplos Prácticos de Diseño de APIs

Los principios anteriores cobran vida cuando se aplican a problemas reales. A continuación se presentan ejemplos concretos que ilustran cómo diseñar interfaces robustas y mantenibles en C.

Ejemplo 1: Diseño de una Lista Enlazada

Una lista enlazada es una estructura de datos fundamental que ejemplifica perfectamente los principios de diseño de APIs. El objetivo es ofrecer una interfaz que oculte la complejidad interna de la gestión de nodos y memoria.

Archivo de Cabecera (lista.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#ifndef LISTA_H
#define LISTA_H

#include <stdbool.h>

/**
 * Tipo opaco que representa una lista enlazada.
 * Los detalles de implementación están ocultos al usuario.
 */
typedef struct lista lista_t;

/**
 * Crea una nueva lista vacía.
 *
 * @returns Puntero a la nueva lista, o NULL si falla la asignación.
 * @post El llamador es responsable de liberar la memoria con lista_destruir().
 */
lista_t *lista_crear(void);

/**
 * Destruye una lista y libera toda la memoria asociada.
 *
 * @param lista Puntero a la lista a destruir.
 * @pre lista != NULL
 * @post Todos los nodos internos son liberados. El puntero lista queda inválido.
 */
void lista_destruir(lista_t *lista);

/**
 * Agrega un elemento al final de la lista.
 *
 * @param lista Lista donde se agregará el elemento.
 * @param dato Valor entero a agregar.
 * @returns true si se agregó exitosamente, false si falló la asignación.
 * @pre lista != NULL
 */
bool lista_agregar(lista_t *lista, int dato);

/**
 * Obtiene el número de elementos en la lista.
 *
 * @param lista Lista a consultar.
 * @returns Cantidad de elementos. Si lista es NULL, devuelve 0.
 */
size_t lista_largo(const lista_t *lista);

/**
 * Busca un elemento en la lista.
 *
 * @param lista Lista donde buscar.
 * @param dato Valor a buscar.
 * @returns true si el elemento está en la lista, false en caso contrario.
 * @pre lista != NULL
 */
bool lista_contiene(const lista_t *lista, int dato);

#endif // LISTA_H

Análisis del Diseño:

Ejemplo 2: Módulo de Operaciones Matemáticas Seguras

Un módulo que realiza operaciones matemáticas básicas con manejo de errores robusto demuestra cómo diseñar una API que reporta errores sin terminar el programa.

Archivo de Cabecera (matematica.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#ifndef MATEMATICA_H
#define MATEMATICA_H

#include <stdbool.h>

/**
 * Códigos de error para operaciones matemáticas.
 */
typedef enum {
    MAT_OK = 0,
    MAT_ERROR_DIVISION_CERO = -1,
    MAT_ERROR_RAIZ_NEGATIVA = -2,
    MAT_ERROR_DESBORDAMIENTO = -3
} mat_error_t;

/**
 * Divide dos números enteros de forma segura.
 *
 * @param dividendo Número a dividir.
 * @param divisor Número por el cual dividir.
 * @param resultado Puntero donde se almacenará el resultado.
 * @returns MAT_OK si la operación fue exitosa, MAT_ERROR_DIVISION_CERO en caso contrario.
 * @pre resultado != NULL
 * @post Si retorna MAT_OK, *resultado contiene dividendo/divisor.
 *       Si retorna error, *resultado no es modificado.
 */
mat_error_t mat_dividir(int dividendo, int divisor, int *resultado);

/**
 * Calcula la raíz cuadrada entera de un número.
 *
 * @param n Número del cual calcular la raíz.
 * @param resultado Puntero donde se almacenará el resultado.
 * @returns MAT_OK si n >= 0, MAT_ERROR_RAIZ_NEGATIVA en caso contrario.
 * @pre resultado != NULL
 * @post Si retorna MAT_OK, *resultado contiene la raíz cuadrada entera de n.
 */
mat_error_t mat_raiz_cuadrada(int n, int *resultado);

/**
 * Obtiene una descripción textual del último error.
 *
 * @param error Código de error a describir.
 * @returns Cadena constante con la descripción del error.
 * @post La cadena retornada es propiedad de la librería, no debe ser liberada.
 */
const char *mat_error_str(mat_error_t error);

#endif // MATEMATICA_H

Ejemplo de Implementación (matematica.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "matematica.h"
#include <math.h>

mat_error_t mat_dividir(int dividendo, int divisor, int *resultado)
{
    if (divisor == 0) {
        return MAT_ERROR_DIVISION_CERO;
    }
    *resultado = dividendo / divisor;
    return MAT_OK;
}

mat_error_t mat_raiz_cuadrada(int n, int *resultado)
{
    if (n < 0) {
        return MAT_ERROR_RAIZ_NEGATIVA;
    }
    *resultado = (int)sqrt(n);
    return MAT_OK;
}

const char *mat_error_str(mat_error_t error)
{
    switch (error) {
        case MAT_OK:
            return "Operación exitosa";
        case MAT_ERROR_DIVISION_CERO:
            return "Error: División por cero";
        case MAT_ERROR_RAIZ_NEGATIVA:
            return "Error: Raíz cuadrada de número negativo";
        case MAT_ERROR_DESBORDAMIENTO:
            return "Error: Desbordamiento aritmético";
        default:
            return "Error desconocido";
    }
}

Ejemplo de Uso

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include "matematica.h"

int main(void)
{
    int resultado = 0;
    mat_error_t error = MAT_OK;

    // División segura
    error = mat_dividir(10, 2, &resultado);
    if (error == MAT_OK) {
        printf("10 / 2 = %d\n", resultado);
    } else {
        printf("Error: %s\n", mat_error_str(error));
    }

    // Intento de división por cero
    error = mat_dividir(10, 0, &resultado);
    if (error != MAT_OK) {
        printf("Error detectado: %s\n", mat_error_str(error));
    }

    return 0;
}

Análisis del Diseño:

Ejemplo 3: Lector de Archivos de Configuración

Un módulo que lee archivos de configuración simple (formato clave=valor) ilustra cómo diseñar APIs que gestionan recursos del sistema de forma segura.

Archivo de Cabecera (config.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#ifndef CONFIG_H
#define CONFIG_H

#include <stdbool.h>

#define CONFIG_MAX_CLAVE 64
#define CONFIG_MAX_VALOR 256

/**
 * Tipo opaco que representa una configuración cargada desde un archivo.
 */
typedef struct config config_t;

/**
 * Carga un archivo de configuración.
 *
 * @param ruta_archivo Ruta al archivo a cargar.
 * @returns Puntero a la configuración cargada, o NULL si falla.
 * @pre ruta_archivo != NULL
 * @post El llamador debe liberar la memoria con config_destruir().
 */
config_t *config_cargar(const char *ruta_archivo);

/**
 * Destruye una configuración y libera toda la memoria asociada.
 *
 * @param config Configuración a destruir.
 * @post El puntero config queda inválido después de esta llamada.
 */
void config_destruir(config_t *config);

/**
 * Obtiene un valor de configuración como cadena.
 *
 * @param config Configuración a consultar.
 * @param clave Nombre de la clave a buscar.
 * @param valor_defecto Valor a retornar si la clave no existe.
 * @returns El valor asociado a la clave, o valor_defecto si no se encuentra.
 * @pre config != NULL, clave != NULL
 * @post La cadena retornada es propiedad de la librería y válida hasta
 *       que config_destruir() sea llamado.
 */
const char *config_obtener_cadena(const config_t *config,
                                   const char *clave,
                                   const char *valor_defecto);

/**
 * Obtiene un valor de configuración como entero.
 *
 * @param config Configuración a consultar.
 * @param clave Nombre de la clave a buscar.
 * @param valor_defecto Valor a retornar si la clave no existe o no es un entero válido.
 * @returns El valor entero asociado a la clave, o valor_defecto.
 * @pre config != NULL, clave != NULL
 */
int config_obtener_entero(const config_t *config,
                          const char *clave,
                          int valor_defecto);

/**
 * Verifica si una clave existe en la configuración.
 *
 * @param config Configuración a consultar.
 * @param clave Nombre de la clave a buscar.
 * @returns true si la clave existe, false en caso contrario.
 * @pre config != NULL, clave != NULL
 */
bool config_existe(const config_t *config, const char *clave);

#endif // CONFIG_H

Análisis del Diseño:

Patrones Comunes de Diseño en C

Además de los principios fundamentales, existen patrones de diseño que han demostrado ser efectivos en el desarrollo de APIs en C.

Patrón Constructor/Destructor

Este patrón garantiza que cada recurso tenga un ciclo de vida bien definido. Para cada función X_crear(), debe existir una X_destruir() correspondiente, como exige la regla Regla 0x001Ah: Liberá siempre la memoria dinámica y prevení punteros colgantes.

// Constructor: reserva memoria y la inicializa
recurso_t *recurso_crear(void);

// Destructor: libera memoria y recursos del sistema
void recurso_destruir(recurso_t *recurso);

Ventajas:

Patrón Init/Finalize

Cuando el usuario provee la memoria (por ejemplo, una variable en el stack), se utiliza un par de funciones de inicialización y finalización.

typedef struct buffer {
    char datos[1024];
    size_t usado;
} buffer_t;

// Inicializa un buffer provisto por el usuario
void buffer_init(buffer_t *buffer);

// Limpia los recursos internos, pero no libera buffer
void buffer_finalize(buffer_t *buffer);

Uso:

buffer_t mi_buffer;  // En el stack
buffer_init(&mi_buffer);
// ... usar el buffer ...
buffer_finalize(&mi_buffer);

Este patrón es útil cuando se quiere evitar asignaciones dinámicas o cuando el tamaño del objeto es conocido en tiempo de compilación. También es preferible en sistemas embebidos donde la asignación dinámica puede no estar disponible o es indeseable por razones de determinismo temporal.

Patrón Getter/Setter

Para estructuras opacas, se proveen funciones de acceso que mantienen la encapsulación.

// Getter: obtiene un valor (no modifica la estructura)
int punto_obtener_x(const punto_t *punto);

// Setter: modifica un valor
void punto_establecer_x(punto_t *punto, int nuevo_x);

Ventajas:

Consideraciones de Performance:

El patrón getter/setter introduce una indirección adicional (una llamada a función) comparado con el acceso directo a campos. En código crítico de rendimiento, esto puede ser una preocupación. Sin embargo:

  1. Los compiladores modernos con optimización activada pueden realizar inlining de funciones getter/setter simples, eliminando el overhead.

  2. En la mayoría de los programas, el costo de la abstracción es despreciable comparado con los beneficios de mantenibilidad y evolución del código.

  3. Como enfatiza Knuth Knuth, 1974: “La optimización prematura es la raíz de todos los males”. Optimizá solo después de medir y cuando sea realmente necesario.

Antipatrones: Qué Evitar

Tan importante como saber qué hacer es saber qué NO hacer. Los siguientes son errores comunes en el diseño de APIs en C.

Antipatrón 1: Números Mágicos en la Interfaz

// MALO: ¿Qué significa 0? ¿Qué significa 1?
int archivo_abrir(const char *nombre, int modo);

// Uso poco claro
archivo_abrir("datos.txt", 1);
// BUENO: Usar constantes o enumerados
typedef enum {
    ARCHIVO_LECTURA = 0,
    ARCHIVO_ESCRITURA = 1,
    ARCHIVO_LECTURA_ESCRITURA = 2
} archivo_modo_t;

int archivo_abrir(const char *nombre, archivo_modo_t modo);

// Uso claro
archivo_abrir("datos.txt", ARCHIVO_ESCRITURA);

Este antipatrón viola la regla Regla 0x0012h: Los valores de retorno numéricos deben definirse como constantes de preprocesador, que exige usar constantes simbólicas para valores especiales.

Antipatrón 2: Estado Global Oculto

// MALO: Estado interno global no visible
void motor_inicializar(void);
void motor_procesar(void);  // ¿Sobre qué datos opera?

El uso de estado global hace que la API sea difícil de testear, imposible de usar de forma concurrente (múltiples hilos) y viola el principio de encapsulamiento. El estado global es una de las principales fuentes de acoplamiento en sistemas de software Parnas, 1972, dificultando la comprensión, el testing y la evolución del código.

// BUENO: El estado es explícito
motor_t *motor_crear(void);
void motor_procesar(motor_t *motor);
void motor_destruir(motor_t *motor);

Antipatrón 3: Trampa Booleana (Boolean Trap)

// MALO: ¿Qué significa true? ¿Qué significa false?
void ventana_crear(int ancho, int alto, bool visible, bool modal);

// Uso confuso
ventana_crear(800, 600, true, false);  // ¿Qué hace cada bool?

Este antipatrón, identificado por Reddy Reddy, 2011 como uno de los errores más comunes en diseño de APIs, surge cuando se usan parámetros booleanos cuyo significado no es evidente en el punto de llamada. El problema se agrava cuando hay múltiples parámetros booleanos consecutivos, ya que es fácil confundir su orden.

// BUENO: Usar enums con nombres descriptivos
typedef enum { VENTANA_OCULTA, VENTANA_VISIBLE } ventana_visibilidad_t;
typedef enum { VENTANA_NO_MODAL, VENTANA_MODAL } ventana_modalidad_t;

void ventana_crear(int ancho, int alto, 
                   ventana_visibilidad_t visibilidad,
                   ventana_modalidad_t modalidad);

// Uso claro
ventana_crear(800, 600, VENTANA_VISIBLE, VENTANA_NO_MODAL);

La solución es reemplazar los booleanos por tipos enumerados que hagan explícito el significado de cada valor. Esto mejora dramáticamente la legibilidad y previene errores sutiles causados por invertir accidentalmente el orden de los argumentos.

Antipatrón 4: Abuso de Parámetros de Salida

// MALO: Demasiados parámetros de salida
void parsear_fecha(const char *cadena, int *dia, int *mes, int *anio, bool *valida);

// Uso tedioso y propenso a errores
int d = 0, m = 0, a = 0;
bool ok = false;
parsear_fecha("2024-03-15", &d, &m, &a, &ok);
// BUENO: Retornar una estructura
typedef struct {
    int dia;
    int mes;
    int anio;
} fecha_t;

bool parsear_fecha(const char *cadena, fecha_t *resultado);

// Uso más limpio
fecha_t fecha = {0};
if (parsear_fecha("2024-03-15", &fecha)) {
    // usar fecha.dia, fecha.mes, fecha.anio
}

Versionado y Compatibilidad

Un aspecto crítico del diseño de APIs profesionales es la gestión de versiones y la compatibilidad hacia atrás (backwards compatibility).

Versionado Semántico

Se recomienda seguir el esquema MAJOR.MINOR.PATCH propuesto por Preston-Werner Preston-Werner, 2013:

El versionado semántico comunica explícitamente el impacto de actualizar una dependencia. Un cambio de versión 1.2.3 a 1.2.4 garantiza que es seguro actualizar sin revisar código, mientras que un cambio a 2.0.0 indica que se requiere revisión y posiblemente modificaciones.

Estrategias de Evolución

Cuando es necesario cambiar una función existente:

  1. Deprecación Gradual: Mantener la función antigua, marcarla como obsoleta, y ofrecer una alternativa.

// Función antigua (deprecada)
// DEPRECADO: Usar lista_agregar_v2() en su lugar
bool lista_agregar(lista_t *lista, int dato);

// Nueva función
bool lista_agregar_v2(lista_t *lista, int dato, size_t *indice_out);
  1. Sobrecarga por Nombre: Dado que C no soporta sobrecarga de funciones, se usan nombres distintos.

void dibujar_rectangulo(int x, int y, int ancho, int alto);
void dibujar_rectangulo_ex(int x, int y, int ancho, int alto, color_t color);
  1. Uso de Estructuras de Opciones: Para funciones con muchos parámetros opcionales, se puede usar una estructura de configuración.

typedef struct {
    int ancho;
    int alto;
    color_t color;
    bool borde;
    int grosor_borde;
} rectangulo_config_t;

// Configuración por defecto
rectangulo_config_t rectangulo_config_defecto(void);

// Función que acepta configuración
void dibujar_rectangulo_config(int x, int y, const rectangulo_config_t *config);

Ejercicios sobre Diseño de APIs

Solution to Exercise api-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#ifndef PILA_H
#define PILA_H

#include <stdbool.h>
#include <stddef.h>

/**
 * Tipo opaco que representa una pila de enteros.
 */
typedef struct pila pila_t;

/**
 * Crea una nueva pila vacía.
 *
 * @returns Puntero a la nueva pila, o NULL si falla la asignación de memoria.
 * @post El llamador debe liberar la memoria con pila_destruir().
 */
pila_t *pila_crear(void);

/**
 * Destruye una pila y libera toda la memoria asociada.
 *
 * @param pila Puntero a la pila a destruir.
 * @post Todos los elementos son liberados. El puntero pila queda inválido.
 */
void pila_destruir(pila_t *pila);

/**
 * Apila un elemento en el tope de la pila.
 *
 * @param pila Pila donde apilar el elemento.
 * @param dato Valor entero a apilar.
 * @returns true si se apiló exitosamente, false si falló la asignación.
 * @pre pila != NULL
 */
bool pila_apilar(pila_t *pila, int dato);

/**
 * Desapila y retorna el elemento del tope de la pila.
 *
 * @param pila Pila de donde desapilar.
 * @param dato Puntero donde almacenar el elemento desapilado.
 * @returns true si se desapiló exitosamente, false si la pila estaba vacía.
 * @pre pila != NULL, dato != NULL
 * @post Si retorna true, *dato contiene el valor desapilado.
 */
bool pila_desapilar(pila_t *pila, int *dato);

/**
 * Consulta el elemento en el tope de la pila sin desapilarlo.
 *
 * @param pila Pila a consultar.
 * @param dato Puntero donde almacenar el elemento del tope.
 * @returns true si hay un elemento, false si la pila está vacía.
 * @pre pila != NULL, dato != NULL
 */
bool pila_peek(const pila_t *pila, int *dato);

/**
 * Verifica si la pila está vacía.
 *
 * @param pila Pila a verificar.
 * @returns true si la pila está vacía, false en caso contrario.
 * @pre pila != NULL
 */
bool pila_esta_vacia(const pila_t *pila);

/**
 * Obtiene el número de elementos en la pila.
 *
 * @param pila Pila a consultar.
 * @returns Cantidad de elementos en la pila.
 * @pre pila != NULL
 */
size_t pila_tamano(const pila_t *pila);

#endif // PILA_H
Solution to Exercise api-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Adiciones a matematica.h

#include <limits.h>  // Para INT_MAX, INT_MIN

/**
 * Suma dos enteros con detección de desbordamiento.
 *
 * @param a Primer sumando.
 * @param b Segundo sumando.
 * @param resultado Puntero donde almacenar el resultado.
 * @returns MAT_OK si la suma es válida, MAT_ERROR_DESBORDAMIENTO en caso contrario.
 * @pre resultado != NULL
 * @post Si retorna MAT_OK, *resultado = a + b.
 */
mat_error_t mat_sumar(int a, int b, int *resultado);

/**
 * Multiplica dos enteros con detección de desbordamiento.
 *
 * @param a Primer factor.
 * @param b Segundo factor.
 * @param resultado Puntero donde almacenar el resultado.
 * @returns MAT_OK si la multiplicación es válida, 
 *          MAT_ERROR_DESBORDAMIENTO en caso contrario.
 * @pre resultado != NULL
 */
mat_error_t mat_multiplicar(int a, int b, int *resultado);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Implementación en matematica.c

mat_error_t mat_sumar(int a, int b, int *resultado)
{
    // Detectar desbordamiento positivo
    if (b > 0 && a > INT_MAX - b) {
        return MAT_ERROR_DESBORDAMIENTO;
    }
    // Detectar desbordamiento negativo
    if (b < 0 && a < INT_MIN - b) {
        return MAT_ERROR_DESBORDAMIENTO;
    }
    *resultado = a + b;
    return MAT_OK;
}

mat_error_t mat_multiplicar(int a, int b, int *resultado)
{
    // Casos especiales
    if (a == 0 || b == 0) {
        *resultado = 0;
        return MAT_OK;
    }
    
    // Detectar desbordamiento
    if (a > 0) {
        if (b > 0 && a > INT_MAX / b) {
            return MAT_ERROR_DESBORDAMIENTO;
        }
        if (b < 0 && b < INT_MIN / a) {
            return MAT_ERROR_DESBORDAMIENTO;
        }
    } else { // a < 0
        if (b > 0 && a < INT_MIN / b) {
            return MAT_ERROR_DESBORDAMIENTO;
        }
        if (b < 0 && a < INT_MAX / b) { // a < 0, b < 0
            return MAT_ERROR_DESBORDAMIENTO;
        }
    }
    
    *resultado = a * b;
    return MAT_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Programa de prueba
#include <stdio.h>
#include "matematica.h"
#include <limits.h>

int main(void)
{
    int resultado = 0;
    mat_error_t error = MAT_OK;

    // Prueba de suma normal
    error = mat_sumar(100, 200, &resultado);
    if (error == MAT_OK) {
        printf("100 + 200 = %d\n", resultado);
    }

    // Prueba de desbordamiento en suma
    error = mat_sumar(INT_MAX, 1, &resultado);
    if (error != MAT_OK) {
        printf("Desbordamiento detectado: %s\n", mat_error_str(error));
    }

    // Prueba de multiplicación con desbordamiento
    error = mat_multiplicar(INT_MAX / 2, 3, &resultado);
    if (error != MAT_OK) {
        printf("Desbordamiento en multiplicación: %s\n", mat_error_str(error));
    }

    return 0;
}
Solution to Exercise api-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#ifndef BUFFER_CIRCULAR_H
#define BUFFER_CIRCULAR_H

#include <stddef.h>
#include <stdbool.h>

#define BUFFER_CAPACIDAD_MAXIMA 1024

/**
 * Estructura de buffer circular de tamaño fijo.
 * El usuario puede declararlo en el stack.
 */
typedef struct {
    unsigned char datos[BUFFER_CAPACIDAD_MAXIMA];
    size_t capacidad;
    size_t inicio;    // Índice de lectura
    size_t fin;       // Índice de escritura
    size_t cantidad;  // Número de bytes almacenados
} buffer_circular_t;

/**
 * Inicializa un buffer circular.
 *
 * @param buffer Puntero al buffer a inicializar.
 * @param capacidad Capacidad del buffer (máximo BUFFER_CAPACIDAD_MAXIMA).
 * @returns true si se inicializó correctamente, false si capacidad es inválida.
 * @pre buffer != NULL
 * @post El buffer queda vacío y listo para usar.
 */
bool buffer_init(buffer_circular_t *buffer, size_t capacidad);

/**
 * Limpia un buffer circular.
 *
 * @param buffer Puntero al buffer a limpiar.
 * @pre buffer != NULL
 * @post El buffer queda vacío.
 */
void buffer_finalize(buffer_circular_t *buffer);

/**
 * Escribe datos en el buffer.
 *
 * @param buffer Buffer donde escribir.
 * @param datos Puntero a los datos a escribir.
 * @param longitud Número de bytes a escribir.
 * @returns Número de bytes efectivamente escritos (puede ser menor que longitud
 *          si el buffer está casi lleno).
 * @pre buffer != NULL, datos != NULL
 */
size_t buffer_escribir(buffer_circular_t *buffer, 
                       const unsigned char *datos,
                       size_t longitud);

/**
 * Lee datos del buffer.
 *
 * @param buffer Buffer de donde leer.
 * @param datos Puntero al buffer de destino.
 * @param longitud Número máximo de bytes a leer.
 * @returns Número de bytes efectivamente leídos (puede ser menor que longitud
 *          si hay menos datos disponibles).
 * @pre buffer != NULL, datos != NULL
 */
size_t buffer_leer(buffer_circular_t *buffer,
                   unsigned char *datos,
                   size_t longitud);

/**
 * Consulta cuántos bytes hay disponibles para leer.
 *
 * @param buffer Buffer a consultar.
 * @returns Número de bytes disponibles.
 * @pre buffer != NULL
 */
size_t buffer_disponible(const buffer_circular_t *buffer);

/**
 * Consulta cuánto espacio libre hay para escribir.
 *
 * @param buffer Buffer a consultar.
 * @returns Número de bytes libres.
 * @pre buffer != NULL
 */
size_t buffer_espacio_libre(const buffer_circular_t *buffer);

/**
 * Verifica si el buffer está vacío.
 */
bool buffer_esta_vacio(const buffer_circular_t *buffer);

/**
 * Verifica si el buffer está lleno.
 */
bool buffer_esta_lleno(const buffer_circular_t *buffer);

#endif // BUFFER_CIRCULAR_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Implementación buffer_circular.c
#include "buffer_circular.h"
#include <string.h>

bool buffer_init(buffer_circular_t *buffer, size_t capacidad)
{
    if (capacidad == 0 || capacidad > BUFFER_CAPACIDAD_MAXIMA) {
        return false;
    }
    buffer->capacidad = capacidad;
    buffer->inicio = 0;
    buffer->fin = 0;
    buffer->cantidad = 0;
    return true;
}

void buffer_finalize(buffer_circular_t *buffer)
{
    buffer->inicio = 0;
    buffer->fin = 0;
    buffer->cantidad = 0;
}

size_t buffer_escribir(buffer_circular_t *buffer,
                       const unsigned char *datos,
                       size_t longitud)
{
    size_t espacio = buffer->capacidad - buffer->cantidad;
    size_t a_escribir = (longitud < espacio) ? longitud : espacio;
    
    for (size_t i = 0; i < a_escribir; i++) {
        buffer->datos[buffer->fin] = datos[i];
        buffer->fin = (buffer->fin + 1) % buffer->capacidad;
        buffer->cantidad++;
    }
    
    return a_escribir;
}

size_t buffer_leer(buffer_circular_t *buffer,
                   unsigned char *datos,
                   size_t longitud)
{
    size_t a_leer = (longitud < buffer->cantidad) ? longitud : buffer->cantidad;
    
    for (size_t i = 0; i < a_leer; i++) {
        datos[i] = buffer->datos[buffer->inicio];
        buffer->inicio = (buffer->inicio + 1) % buffer->capacidad;
        buffer->cantidad--;
    }
    
    return a_leer;
}

size_t buffer_disponible(const buffer_circular_t *buffer)
{
    return buffer->cantidad;
}

size_t buffer_espacio_libre(const buffer_circular_t *buffer)
{
    return buffer->capacidad - buffer->cantidad;
}

bool buffer_esta_vacio(const buffer_circular_t *buffer)
{
    return buffer->cantidad == 0;
}

bool buffer_esta_lleno(const buffer_circular_t *buffer)
{
    return buffer->cantidad == buffer->capacidad;
}

````{solution} api_parser_cli
:class: dropdown

```{code-block} c
:linenos:
#ifndef PARSER_CLI_H
#define PARSER_CLI_H

#include <stdbool.h>

/**
 * Tipo opaco que representa un parseador de argumentos de línea de comandos.
 */
typedef struct parser parser_t;

/**
 * Crea un nuevo parseador de argumentos.
 *
 * @param nombre_programa Nombre del programa (para mensajes de ayuda).
 * @param version Versión del programa.
 * @returns Puntero al parseador creado, o NULL si falla.
 * @pre nombre_programa != NULL, version != NULL
 * @post El llamador debe liberar con parser_destruir().
 */
parser_t *parser_crear(const char *nombre_programa, const char *version);

/**
 * Destruye un parseador y libera sus recursos.
 *
 * @param parser Parseador a destruir.
 * @post El puntero parser queda inválido.
 */
void parser_destruir(parser_t *parser);

/**
 * Agrega una opción booleana (flag) al parseador.
 *
 * @param parser Parseador al cual agregar el flag.
 * @param corto Carácter para la forma corta (ej. 'v' para -v).
 * @param largo Cadena para la forma larga (ej. "verbose" para --verbose).
 * @param descripcion Descripción para el mensaje de ayuda.
 * @returns true si se agregó exitosamente, false en caso de error.
 * @pre parser != NULL, largo != NULL
 */
bool parser_agregar_flag(parser_t *parser,
                         char corto,
                         const char *largo,
                         const char *descripcion);

/**
 * Agrega una opción con valor al parseador.
 *
 * @param parser Parseador al cual agregar la opción.
 * @param corto Carácter para la forma corta (ej. 'o' para -o).
 * @param largo Cadena para la forma larga (ej. "output" para --output).
 * @param descripcion Descripción para el mensaje de ayuda.
 * @param valor_defecto Valor por defecto si la opción no es especificada.
 * @returns true si se agregó exitosamente, false en caso de error.
 * @pre parser != NULL, largo != NULL
 */
bool parser_agregar_opcion(parser_t *parser,
                           char corto,
                           const char *largo,
                           const char *descripcion,
                           const char *valor_defecto);

/**
 * Parsea los argumentos de línea de comandos.
 *
 * @param parser Parseador a utilizar.
 * @param argc Número de argumentos (desde main).
 * @param argv Vector de argumentos (desde main).
 * @returns true si el parseo fue exitoso, false si hubo errores.
 * @pre parser != NULL, argv != NULL, argc >= 1
 * @post Si retorna false, usar parser_obtener_error() para obtener detalles.
 */
bool parser_parsear(parser_t *parser, int argc, char *argv[]);

/**
 * Obtiene el valor de un flag booleano.
 *
 * @param parser Parseador a consultar.
 * @param nombre Nombre largo del flag.
 * @returns true si el flag fue especificado, false en caso contrario.
 * @pre parser != NULL, nombre != NULL
 * @pre parser_parsear() debe haber sido llamado previamente.
 */
bool parser_obtener_flag(const parser_t *parser, const char *nombre);

/**
 * Obtiene el valor de una opción.
 *
 * @param parser Parseador a consultar.
 * @param nombre Nombre largo de la opción.
 * @returns Valor de la opción, o el valor por defecto si no fue especificada.
 * @pre parser != NULL, nombre != NULL
 * @pre parser_parsear() debe haber sido llamado previamente.
 * @post La cadena retornada es propiedad del parser y válida hasta
 *       que parser_destruir() sea llamado.
 */
const char *parser_obtener_opcion(const parser_t *parser, const char *nombre);

/**
 * Obtiene los argumentos posicionales (no opciones).
 *
 * @param parser Parseador a consultar.
 * @param cantidad Puntero donde almacenar el número de argumentos posicionales.
 * @returns Vector de cadenas con los argumentos posicionales.
 * @pre parser != NULL, cantidad != NULL
 * @post El vector retornado es propiedad del parser.
 */
const char **parser_obtener_argumentos(const parser_t *parser, int *cantidad);

/**
 * Muestra el mensaje de ayuda en la salida estándar.
 *
 * @param parser Parseador con las opciones definidas.
 * @pre parser != NULL
 */
void parser_mostrar_ayuda(const parser_t *parser);

/**
 * Obtiene el mensaje de error del último parseo fallido.
 *
 * @param parser Parseador a consultar.
 * @returns Cadena con el mensaje de error, o NULL si no hubo error.
 * @pre parser != NULL
 * @post La cadena retornada es propiedad del parser.
 */
const char *parser_obtener_error(const parser_t *parser);

#endif // PARSER_CLI_H

```{exercise}
:label: api_json_simple
:enumerator: api-5

Diseñá una API minimalista para leer archivos JSON simples (solo objetos con pares clave-valor donde los valores son cadenas o números). La API debe permitir cargar un archivo JSON y consultar valores por clave. Aplicá todos los principios de diseño vistos: tipo opaco, manejo de errores consistente, documentación completa, y uso de `const` apropiado.
```

````{solution} api_json_simple
:class: dropdown

```{code-block} c
:linenos:
#ifndef JSON_SIMPLE_H
#define JSON_SIMPLE_H

#include <stdbool.h>

/**
 * Tipo opaco que representa un objeto JSON parseado.
 */
typedef struct json json_t;

/**
 * Tipos de valores JSON soportados.
 */
typedef enum {
    JSON_TIPO_CADENA,
    JSON_TIPO_NUMERO,
    JSON_TIPO_INVALIDO
} json_tipo_t;

/**
 * Carga y parsea un archivo JSON.
 *
 * @param ruta_archivo Ruta al archivo JSON a cargar.
 * @returns Puntero al objeto JSON parseado, o NULL si falla.
 * @pre ruta_archivo != NULL
 * @post El llamador debe liberar con json_destruir().
 *       Si retorna NULL, usar json_obtener_error() para detalles.
 */
json_t *json_cargar(const char *ruta_archivo);

/**
 * Parsea una cadena JSON.
 *
 * @param contenido Cadena con el contenido JSON.
 * @returns Puntero al objeto JSON parseado, o NULL si falla.
 * @pre contenido != NULL
 * @post El llamador debe liberar con json_destruir().
 */
json_t *json_parsear(const char *contenido);

/**
 * Destruye un objeto JSON y libera sus recursos.
 *
 * @param json Objeto JSON a destruir.
 * @post El puntero json queda inválido.
 */
void json_destruir(json_t *json);

/**
 * Verifica si una clave existe en el objeto JSON.
 *
 * @param json Objeto JSON a consultar.
 * @param clave Nombre de la clave a buscar.
 * @returns true si la clave existe, false en caso contrario.
 * @pre json != NULL, clave != NULL
 */
bool json_existe(const json_t *json, const char *clave);

/**
 * Obtiene el tipo de valor asociado a una clave.
 *
 * @param json Objeto JSON a consultar.
 * @param clave Nombre de la clave.
 * @returns Tipo del valor, o JSON_TIPO_INVALIDO si la clave no existe.
 * @pre json != NULL, clave != NULL
 */
json_tipo_t json_obtener_tipo(const json_t *json, const char *clave);

/**
 * Obtiene un valor de cadena del objeto JSON.
 *
 * @param json Objeto JSON a consultar.
 * @param clave Nombre de la clave.
 * @param valor_defecto Valor a retornar si la clave no existe o no es cadena.
 * @returns Valor de la clave como cadena, o valor_defecto.
 * @pre json != NULL, clave != NULL
 * @post La cadena retornada es propiedad del objeto JSON.
 */
const char *json_obtener_cadena(const json_t *json,
                                const char *clave,
                                const char *valor_defecto);

/**
 * Obtiene un valor numérico del objeto JSON.
 *
 * @param json Objeto JSON a consultar.
 * @param clave Nombre de la clave.
 * @param valor_defecto Valor a retornar si la clave no existe o no es número.
 * @returns Valor de la clave como double, o valor_defecto.
 * @pre json != NULL, clave != NULL
 */
double json_obtener_numero(const json_t *json,
                           const char *clave,
                           double valor_defecto);

/**
 * Obtiene el número de pares clave-valor en el objeto JSON.
 *
 * @param json Objeto JSON a consultar.
 * @returns Número de claves en el objeto.
 * @pre json != NULL
 */
size_t json_obtener_num_claves(const json_t *json);

/**
 * Obtiene todas las claves del objeto JSON.
 *
 * @param json Objeto JSON a consultar.
 * @returns Vector de cadenas con las claves, terminado en NULL.
 * @pre json != NULL
 * @post El vector retornado es propiedad del objeto JSON.
 */
const char **json_obtener_claves(const json_t *json);

/**
 * Obtiene el mensaje del último error de parseo.
 *
 * @returns Cadena con el mensaje de error, o NULL si no hubo error.
 * @post La cadena es propiedad de la librería y válida hasta la
 *       próxima operación que pueda generar error.
 */
const char *json_obtener_error(void);

#endif // JSON_SIMPLE_H
```

**Notas sobre el diseño:**

- Se usa `json_obtener_error()` como función global para obtener errores, similar a `errno` en la biblioteca estándar de C.
- Los valores por defecto permiten un uso simple sin necesidad de verificar siempre si una clave existe.
- El uso consistente de `const` indica qué funciones modifican el objeto y qué datos son propiedad de la librería.
- La API es minimalista pero extensible: se podrían agregar funciones para soportar arrays, booleanos, y null en futuras versiones.

Performance y APIs: El Costo de la Abstracción

Una preocupación legítima al diseñar APIs con múltiples capas de abstracción es el impacto en el rendimiento. ¿Cuánto cuesta la llamada a función indirecta? ¿Vale la pena el overhead?

El Mito de la Abstracción Costosa

En sistemas modernos, el costo de una llamada a función bien diseñada es despreciable en la vasta mayoría de los casos. Knuth Knuth, 1974 famosamente advirtió: “La optimización prematura es la raíz de todos los males” (“premature optimization is the root of all evil”). Esta observación, basada en décadas de experiencia, enfatiza que el tiempo de desarrollo debe invertirse en claridad y corrección antes que en optimizaciones especulativas.

El compilador moderno realiza optimizaciones agresivas que eliminan gran parte del overhead de la abstracción, incluyendo:

Un estudio de Mytkowicz et al. Mytkowicz et al., 2009 demostró que diferencias en performance son frecuentemente atribuidas incorrectamente a causas obvias (como llamadas a función), cuando en realidad factores como la alineación de código en memoria, el estado del cache, y efectos del layout de memoria tienen impactos más significativos. Este estudio es una advertencia sobre la importancia de medir, no asumir.

Cuándo Preocuparse por Performance

La performance sí importa en contextos específicos:

  1. Lazos Internos Críticos (Hot Paths): Código que se ejecuta millones o miles de millones de veces por segundo, como kernels de procesamiento de señales, codecs de video/audio, motores de rendering 3D, o algoritmos criptográficos. En estos casos, incluso el overhead de una única instrucción puede acumularse significativamente.

  2. Sistemas de Tiempo Real Duro: Donde límites temporales estrictos (deadlines) son obligatorios y su incumplimiento puede tener consecuencias catastróficas (sistemas de control industrial, aviación, dispositivos médicos). En estos sistemas, no solo importa la performance promedio, sino también la varianza y el peor caso (worst-case execution time, WCET).

  3. Sistemas Embebidos con Recursos Limitados: Microcontroladores con kilobytes de RAM y megahertz de clock, donde cada byte de código y cada ciclo de CPU cuenta. En estos entornos, las asignaciones dinámicas pueden estar completamente prohibidas.

  4. Algoritmos de Complejidad Crítica: Cuando la elección de estructura de datos o algoritmo afecta la complejidad asintótica (por ejemplo, O(n)O(n) vs. O(n2)O(n^2)), el diseño de la API debe facilitar el uso eficiente, no obstaculizarlo.

En estos casos, las APIs pueden exponer versiones “unsafe” optimizadas junto a versiones “safe” con verificaciones completas:

// Versión con verificaciones completas: segura pero más lenta
bool lista_insertar(lista_t *lista, size_t pos, void *elem);

// Versión sin verificaciones para lazos críticos: rápida pero peligrosa
// PRECONDICIÓN: pos < lista->tamanio, lista != NULL, elem != NULL
// El incumplimiento de las precondiciones resulta en comportamiento indefinido
void lista_insertar_unsafe(lista_t *lista, size_t pos, void *elem);

Esta estrategia es común en bibliotecas de sistemas. Por ejemplo, la librería estándar de C ofrece strcpy (rápida pero peligrosa) y strncpy (más segura pero requiere especificar tamaño). Bibliotecas modernas como OpenSSL exponen APIs de alto nivel simples para casos comunes y APIs de bajo nivel complejas para casos que requieren máximo control.

Testing de APIs: Validación del Contrato

El testing de una API no solo verifica que el código funciona, sino que valida que el contrato se cumple. Beck Beck, 2002 popularizó el desarrollo guiado por tests (Test-Driven Development, TDD), donde los tests se escriben antes que el código de producción, sirviendo como especificación ejecutable.

Niveles de testing para APIs:

  1. Tests de Contrato: Verifican que las precondiciones, poscondiciones e invariantes documentados se cumplen. Por ejemplo, si la documentación dice que lista_crear() retorna NULL en caso de fallo, debe haber un test que verifique este comportamiento.

  2. Tests de Casos Límite: Proban comportamiento en fronteras (listas vacías, tamaño máximo, valores nulos, etc.). Muchos bugs se esconden en estos casos extremos.

  3. Tests de Estrés: Crean miles de objetos, realizan millones de operaciones, buscan fugas de memoria con Valgrind Nethercote & Seward, 2007.

  4. Tests de Uso Incorrecto: Verifican que la API se comporta razonablemente (idealmente, falla de forma predecible) cuando se usa incorrectamente. Por ejemplo, pasar NULL donde no está permitido debería causar un assert en modo debug, no un crash silencioso.

// Ejemplo de test de contrato
void test_lista_agregar_retorna_true_en_exito(void) {
    lista_t *lista = lista_crear();
    assert(lista != NULL);
    
    // Postcondición: agregar elemento debe retornar true
    bool resultado = lista_agregar(lista, 42);
    assert(resultado == true);
    
    // Invariante: el tamaño debe incrementarse
    assert(lista_largo(lista) == 1);
    
    lista_destruir(lista);
}

Property-Based Testing:

Una técnica avanzada, popularizada por QuickCheck Claessen & Hughes, 2000, genera automáticamente cientos de casos de test basados en propiedades declaradas. Por ejemplo, para una lista: “agregar N elementos y luego consultar el largo debe retornar N”.

Documentación de APIs: El Contrato Escrito

La documentación no es opcional; es parte integral del contrato entre la API y sus usuarios. Una función sin documentación es una función cuyo comportamiento es indefinido desde la perspectiva del usuario.

Elementos Esenciales de Documentación

Cada función pública debe documentar:

  1. Propósito: ¿Qué hace la función en términos de alto nivel?

  2. Parámetros: Significado, unidades, restricciones de cada parámetro.

  3. Valor de Retorno: Qué representa, qué valores son posibles.

  4. Precondiciones: Qué debe ser verdadero antes de llamar la función.

  5. Poscondiciones: Qué será verdadero después de que la función retorne exitosamente.

  6. Efectos Secundarios: ¿Modifica argumentos? ¿Accede a recursos externos?

  7. Gestión de Memoria: ¿Quién aloja? ¿Quién libera?

  8. Manejo de Errores: ¿Cómo reporta errores? ¿Qué errores son posibles?

  9. Thread-Safety: ¿Es seguro llamar desde múltiples hilos concurrentemente?

  10. Complejidad: Si es relevante, complejidad temporal y espacial (O(n)O(n), etc.).

Formato de Documentación: Doxygen

Doxygen Heesch, 2023 es el estándar de facto para documentación de APIs en C/C++. Usa comentarios especialmente formateados que pueden ser procesados para generar HTML, PDF, y man pages.

/**
 * @brief Busca un elemento en una lista ordenada usando búsqueda binaria.
 *
 * Esta función implementa el algoritmo de búsqueda binaria, que requiere
 * que la lista esté ordenada en orden ascendente.
 *
 * @param lista Lista donde buscar. Debe estar ordenada.
 * @param elemento Elemento a buscar.
 * @param[out] indice_out Si no es NULL y se encuentra el elemento,
 *                        se almacena aquí el índice donde fue encontrado.
 * 
 * @return true si el elemento fue encontrado, false en caso contrario.
 * 
 * @pre lista != NULL
 * @pre La lista debe estar ordenada en orden ascendente.
 * @post Si retorna true, *indice_out contiene el índice del elemento.
 * @post La lista no es modificada.
 * 
 * @note Complejidad: O(log n) donde n es el tamaño de la lista.
 * @note Thread-safe: Sí, siempre que no se modifique la lista concurrentemente.
 * 
 * @see lista_ordenar() para ordenar una lista antes de buscar.
 */
bool lista_buscar_binaria(const lista_t *lista, 
                         int elemento,
                         size_t *indice_out);

Esta documentación es exhaustiva pero necesaria. Comunica el contrato completo y permite al usuario de la API trabajar con confianza.

Estudio de Caso: APIs Exitosas en la Práctica

Analizar APIs exitosas y ampliamente adoptadas revela patrones comunes y lecciones valiosas.

POSIX: El Estándar de Facto

POSIX (Portable Operating System Interface) IEEE, 2018 es quizás el ejemplo más exitoso de diseño de API en C. Define interfaces estándar para interacción con el sistema operativo (archivos, procesos, hilos, señales, etc.) que han sido adoptadas por prácticamente todos los sistemas Unix-like y muchos otros.

Principios de diseño de POSIX:

Lecciones:

La API POSIX IEEE, 2018 define interfaces para sistemas Unix-like y ha sobrevivido décadas. Sus lecciones:

SQLite: La Librería más Deployada del Mundo

SQLite Hipp, 2020 es probablemente la librería C más ampliamente desplegada en el planeta. Se encuentra en miles de millones de dispositivos: smartphones, navegadores web, sistemas operativos, aviones, y prácticamente cualquier sistema que necesite almacenar datos estructurados localmente. Su éxito se debe en gran parte a decisiones de diseño deliberadas:

Principios de diseño de SQLite:

Richard Hipp, creador de SQLite, enfatiza que “SQLite es software embebido, no un producto con clientes”. Esta filosofía de diseño como componente reutilizable, no como servicio independiente, informa cada decisión de API. El objetivo es que SQLite “simplemente funcione” sin que el usuario tenga que pensar en ella.

Git: Porcelain vs Plumbing

Git Chacon & Straub, 2014 es el sistema de control de versiones más utilizado del mundo. Su diseño de API es notable por la separación explícita en dos niveles de abstracción:

Arquitectura de dos niveles:

Lecciones de diseño:

Esta separación es brillante porque permite:

  1. Evolución de UX: Los comandos de alto nivel pueden mejorar (mejor mensajes de error, nuevos flags, comportamiento más intuitivo) sin romper scripts y herramientas que dependen de Git.

  2. Estabilidad para Automatización: Scripts y herramientas de terceros pueden confiar en que los comandos de plumbing mantendrán su comportamiento indefinidamente.

  3. Acceso a Primitivas: Usuarios avanzados y herramientas pueden construir funcionalidad compleja combinando comandos de bajo nivel.

El diseño de Git demuestra que no es necesario elegir entre simplicidad para principiantes y poder para expertos. Una API puede ofrecer ambos mediante niveles de abstracción apropiados, cada uno con su propio contrato de estabilidad.

Conclusión: Diseñar para el Usuario

El diseño de una buena interfaz en C es un ejercicio de empatía y disciplina. Requiere que te pongas en el lugar del programador que utilizará tu código. ¿Es la interfaz clara? ¿Es predecible? ¿Es segura? ¿Oculta la complejidad innecesaria?

Al aplicar estos principios y las reglas de estilo, no solo estarás creando funciones, sino componentes de software robustos, modulares y profesionales. Estarás construyendo “contratos” en los que otros desarrolladores pueden confiar, asegurando la mantenibilidad y longevidad de tu código.

Como observa Stroustrup Stroustrup, 2012, diseñador de C++: “El diseño de bibliotecas es el diseño de lenguajes”. Una buena API extiende el lenguaje con un vocabulario nuevo, expresivo y coherente para resolver problemas de un dominio específico.

Principios Clave a Recordar

  1. Claridad sobre Cleverness: Un código claro y simple es superior a uno “inteligente” pero difícil de entender. Como dice la regla Regla 0x0000h: La claridad y prolijidad son de máxima importancia, la claridad y prolijidad son fundamentales.

  2. Contratos Explícitos: Las precondiciones y poscondiciones no son decoración, son especificaciones formales del comportamiento esperado.

  3. Encapsulamiento Riguroso: La información que no necesita ser pública, no debe serlo. Los tipos opacos son tu herramienta principal para lograr esto.

  4. Errores sin Sorpresas: Los errores deben ser reportados de manera predecible y consistente. El usuario de tu API debe poder manejarlos de forma adecuada a su contexto.

  5. Evolución Controlada: Una API bien diseñada puede evolucionar sin romper código existente, mediante deprecación gradual y versionado semántico.

  6. Testing como Diseño: Los tests no solo verifican corrección; informan y validan el diseño desde la perspectiva del usuario.

  7. Performance Consciente pero no Obsesiva: Optimizá lo que importa, después de medir. La claridad y corrección primero, optimización después.

El dominio de estos principios te diferencia de un programador amateur de uno profesional. Es la diferencia entre escribir código que funciona hoy y escribir código que seguirá siendo valioso dentro de años.

Referencias Adicionales y Lecturas Recomendadas

Para profundizar en los temas tratados, se recomiendan las siguientes lecturas:


References
  1. Meyer, B. (1988). Design by Contract. Advances in Object-Oriented Software Engineering.
  2. Kernighan, B. W., & Ritchie, D. M. (1988). The C Programming Language.
  3. Spinellis, D., & Gousios, G. (2009). Beautiful Architecture: Leading Thinkers Reveal the Hidden Beauty in Software Design.
  4. Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.
  5. Lawrie, D., Morrell, C., Feild, H., & Binkley, D. (2006). What’s in a Name? A Study of Identifiers. 14th IEEE International Conference on Program Comprehension (ICPC’06), 3–12. 10.1109/ICPC.2006.51
  6. Claessen, K., & Hughes, J. (2000). QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs. Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming, 268–279. 10.1145/351240.351266
  7. Raymond, E. S. (2003). The Art of Unix Programming. Addison-Wesley.
  8. Bloch, J. (2006). Effective Java (2nd ed.). Addison-Wesley.
  9. Li, Z., & Zhou, Y. (2005). PR-Miner: Automatically Extracting Implicit Programming Rules and Detecting Violations in Large Software Code. Proceedings of the 10th European Software Engineering Conference, 306–315. 10.1145/1081706.1081755
  10. Hughes, J. (1989). Why Functional Programming Matters. The Computer Journal, 32(2), 98–107. 10.1093/comjnl/32.2.98
  11. Liskov, B. H., & Zilles, S. N. (1974). Programming with Abstract Data Types. SIGPLAN Notices, 9(4), 50–59. 10.1145/942572.807045
  12. Parnas, D. L. (1972). On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12), 1053–1058. 10.1145/361598.361623
  13. Drepper, U. (2011). How To Write Shared Libraries. https://www.akkadia.org/drepper/dsohowto.pdf
  14. Serebryany, K., Bruening, D., Potapenko, A., & Vyukov, D. (2012). AddressSanitizer: A Fast Address Sanity Checker. Proceedings of the 2012 USENIX Annual Technical Conference, 309–318.
  15. Lu, S., Park, S., Seo, E., & Zhou, Y. (2008). Learning from Mistakes: A Comprehensive Study on Real World Concurrency Bug Characteristics. Proceedings of the 13th International Conference on Architectural Support for Programming Languages and Operating Systems, 329–339. 10.1145/1346281.1346323