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.

Ingeniería de APIs y Gestión de Compatibilidad

Versionado semántico, estabilidad de ABI y performance de interfaces en C

Universidad Nacional de Río Negro

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 to Exercise api-4
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#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
Solution to Exercise api-5
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#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. Preston-Werner, T. (2013). Semantic Versioning 2.0.0. https://semver.org/
  2. Knuth, D. E. (1974). Structured Programming with go to Statements. ACM Computing Surveys, 6(4), 261–301. 10.1145/356635.356640
  3. Mytkowicz, T., Diwan, A., Hauswirth, M., & Sweeney, P. F. (2009). Producing Wrong Data Without Doing Anything Obviously Wrong! Proceedings of the 14th International Conference on Architectural Support for Programming Languages and Operating Systems, 265–276. 10.1145/1508244.1508275
  4. Nethercote, N., & Seward, J. (2007). Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation. ACM SIGPLAN Notices, 42(6), 89–100. 10.1145/1273442.1250746
  5. Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.
  6. Beck, K. (2002). Test Driven Development: By Example. Addison-Wesley.
  7. 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
  8. van Heesch, D. (2023). Doxygen: Generate documentation from source code. https://www.doxygen.nl/
  9. IEEE. (2018). IEEE Std 1003.1-2017 (Revision of IEEE Std 1003.1-2008) - IEEE Standard for Information Technology–Portable Operating System Interface (POSIX(R)) Base Specifications, Issue 7 [Techreport]. IEEE. 10.1109/IEEESTD.2018.8277153
  10. Raymond, E. S. (2003). The Art of Unix Programming. Addison-Wesley.
  11. Hipp, D. R. (2020). SQLite. https://www.sqlite.org/
  12. Chacon, S., & Straub, B. (2014). Pro Git (2nd ed.). Apress.
  13. Stroustrup, B. (2012). The C++ Programming Language (4th ed.). Addison-Wesley.
  14. Kernighan, B. W., & Ritchie, D. M. (1988). The C Programming Language.
  15. van der Linden, P. (1994). Expert C Programming: Deep C Secrets. Prentice Hall.