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 APIs en C

Universidad Nacional de Río Negro

Para el marco formal de contratos que sustenta el diseño de interfaces, ver 3. Contratos de Estructuras de Datos e Invariantes de Clase. Para el patrón de punteros opacos que implementa el encapsulamiento, ver el capítulo 17_opacos.

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 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar 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 0x2005h: Cada función debe tener una única responsabilidad (Principio de Responsabilidad Única), 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
}

Resumen del Diseño de Interfaces

En este apunte hemos cubierto los fundamentos del diseño de APIs en C:

Para continuar con el versionado, compatibilidad de ABI y optimizaciones avanzadas de APIs, consultá Versionado y Compatibilidad.

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