Versionamento semântico e ABI compatibility
1. Introdução ao Versionamento Semântico (SemVer) em Bibliotecas C
1.1. O padrão MAJOR.MINOR.PATCH e seu significado para C
O versionamento semântico (SemVer) utiliza o formato MAJOR.MINOR.PATCH para comunicar o impacto de mudanças em uma biblioteca. Em C, esses números têm implicações diretas na compatibilidade binária:
- MAJOR: Incrementado quando há mudanças que quebram a ABI (Application Binary Interface). Exige alteração no
sonameda biblioteca compartilhada. - MINOR: Incrementado quando novas funcionalidades são adicionadas de forma compatível com a ABI existente.
- PATCH: Incrementado para correções de bugs que não alteram API nem ABI.
1.2. Diferenças entre versionamento de aplicações e de bibliotecas compartilhadas
Aplicações geralmente usam SemVer para comunicar mudanças de funcionalidade ao usuário final. Bibliotecas C, por outro lado, precisam considerar a compatibilidade binária com programas já compilados. Uma mudança que não quebra a API pode quebrar a ABI, tornando inviável a simples substituição do arquivo .so.
1.3. A relação entre versão semântica e número de versão do SO (.so.X.Y.Z)
O sistema de numeração de bibliotecas compartilhadas no Linux segue o padrão libfoo.so.MAJOR.MINOR.PATCH, onde:
- O
MAJORcorresponde ao número de versão da ABI (soname) MINORePATCHsão versões internas para referência
Exemplo:
libfoo.so.1.0.0 → soname = libfoo.so.1
libfoo.so.2.0.0 → soname = libfoo.so.2 (ABI quebrou)
2. ABI (Application Binary Interface) vs API
2.1. Definição de ABI: layout de structs, calling conventions, alinhamento
A ABI define como os componentes binários interagem em nível de máquina. Inclui:
- Layout e alinhamento de structs na memória
- Convenções de chamada de funções (passagem de argumentos em registradores/pilha)
- Tamanho e representação de tipos primitivos
- Mecanismo de tratamento de exceções (quando aplicável)
2.2. Diferenças críticas: mudanças na API que não quebram ABI vs mudanças que quebram
Uma mudança na API nem sempre quebra a ABI. Por exemplo, adicionar uma nova função não quebra a ABI, pois programas antigos simplesmente não a chamam. No entanto, modificar a assinatura de uma função existente quebra a ABI.
2.3. Exemplos concretos em C: adicionar campo no meio de uma struct vs no final
Exemplo 1: Adicionar campo no meio de uma struct (quebra ABI)
// Versão 1.0.0
struct Point {
int x;
int y;
};
// Versão 2.0.0 (quebra ABI!)
struct Point {
int x;
int z; // Novo campo no meio
int y;
};
Programas compilados contra a versão 1.0.0 esperam que y esteja no offset 4, mas na versão 2.0.0 ele está no offset 8.
Exemplo 2: Adicionar campo no final da struct (pode ser compatível)
// Versão 1.0.0
struct Point {
int x;
int y;
};
// Versão 1.1.0 (compatível se clientes não alocarem diretamente)
struct Point {
int x;
int y;
int z; // Novo campo no final
};
Isso é compatível apenas se os clientes nunca alocarem struct Point diretamente, mas sim usarem funções alocadoras fornecidas pela biblioteca.
3. Regras Práticas para Manter ABI Compatibility
3.1. O que NÃO pode mudar
- Tamanho de structs expostas publicamente (se alocadas pelo cliente)
- Ordem dos campos em structs públicas
- Assinaturas de funções públicas
- Significado de valores de retorno ou parâmetros
- Layout de uniões expostas
3.2. O que é seguro adicionar
- Novas funções (não conflitantes com símbolos existentes)
- Novos campos no final de structs opacas (não alocadas pelo cliente)
- Novos enumeradores (se o tamanho do enum não mudar)
3.3. Uso de opacidade (opaque types) e handles para isolar ABI interna
// mylib.h (público)
typedef struct mylib_handle* mylib_t;
mylib_t mylib_create(void);
void mylib_destroy(mylib_t handle);
void mylib_set_value(mylib_t handle, int value);
int mylib_get_value(const mylib_t handle);
// mylib.c (privado)
struct mylib_handle {
int version;
int value;
// campos internos podem mudar livremente
};
4. Versionamento de Símbolos com __attribute__((version)) e .symver
4.1. Controle de versão de símbolos no linker GNU (symver)
O GNU linker permite expor múltiplas versões de um mesmo símbolo usando o recurso de versionamento de símbolos.
4.2. Exemplo prático: expor foo_v1 e foo_v2 com compatibilidade retroativa
// mylib.c
#include <stdio.h>
int foo_v1(int x) {
return x + 1;
}
int foo_v2(int x) {
return x + 2;
}
__asm__(".symver foo_v1, foo@MYLIB_1.0");
__asm__(".symver foo_v2, foo@@MYLIB_2.0");
4.3. Como versionar funções e variáveis globais sem quebrar clientes existentes
// mylib.map (arquivo de versão do linker)
MYLIB_1.0 {
global:
foo;
local:
*;
};
MYLIB_2.0 {
global:
foo;
} MYLIB_1.0;
5. Ferramentas para Verificação de ABI
5.1. abidiff e abidw da libabigail
# Gerar representação XML da ABI
abidw libfoo.so.1.0.0 > libfoo-1.0.0.abi
# Comparar duas versões
abidiff libfoo-1.0.0.abi libfoo-2.0.0.abi
5.2. pahole (dwarves) para inspecionar layout de structs
pahole libfoo.so.1.0.0 > struct-layout.txt
# Mostra offsets, tamanhos e alinhamentos de cada struct
5.3. Integração em CI
# .gitlab-ci.yml
abi-check:
script:
- abidiff --suppressions abi-suppressions.txt \
libfoo.so.$OLD_VERSION libfoo.so.$NEW_VERSION
only:
- merge_requests
6. Boas Práticas de Projeto para Evolução de Bibliotecas C
6.1. Uso de structs versionadas com campo size ou version no início
typedef struct {
size_t size; // Tamanho real da struct, preenchido pelo cliente
int x;
int y;
// Futuros campos adicionados aqui
} mylib_config_t;
void mylib_init(mylib_config_t* config) {
if (config->size >= sizeof(mylib_config_t)) {
// Inicializar todos os campos conhecidos
}
}
6.2. Padrão de "callback" com contextos opacos
typedef struct mylib_callback_ctx* mylib_callback_ctx_t;
typedef void (*mylib_callback_t)(mylib_callback_ctx_t ctx, int event);
void mylib_register_callback(mylib_callback_t cb, mylib_callback_ctx_t ctx);
6.3. Documentação de ABI contracts no cabeçalho
/**
* @brief Configura o modo de operação
* @param mode Novo modo (desde v1.0.0)
* @return 0 em sucesso, -1 em erro
* @note ABI estável desde v1.0.0
* @deprecated Use mylib_set_mode_v2() desde v2.0.0
*/
int mylib_set_mode(int mode);
7. Estratégias de Transição e Deprecação
7.1. Ciclo de vida de uma função
// v1.0.0: Introduzida como experimental
// v2.0.0: Promovida a estável
// v3.0.0: Marcada como deprecated
// v4.0.0: Removida
7.2. Sinalização de deprecação
__attribute__((deprecated("Use mylib_new_function() instead")))
void mylib_old_function(void);
7.3. Exemplo de remoção gradual
// libfoo.c - mantém ambas as versões no mesmo .so
int mylib_old_function(void) {
fprintf(stderr, "Warning: mylib_old_function is deprecated\n");
return mylib_new_function();
}
8. Integração com Packaging e Deployment
8.1. Nomenclatura de pacotes
libfoo2_2.0.0-1_amd64.deb # soname = libfoo.so.2
libfoo3_3.0.0-1_amd64.deb # soname = libfoo.so.3
8.2. Geração de dependências
# debian/control
Package: libfoo2
Provides: libfoo
Conflicts: libfoo (<< 2.0.0)
# Geração automática de shlibdeps
dh_shlibdeps
8.3. Gerenciamento de múltiplas versões
# ldconfig gerencia múltiplos sonames
ls -la /usr/lib/libfoo.so*
libfoo.so -> libfoo.so.2.0.0
libfoo.so.2 -> libfoo.so.2.0.0
libfoo.so.2.0.0
libfoo.so.3 -> libfoo.so.3.0.0
libfoo.so.3.0.0
Referências
- GNU C Library: Symbol Versioning — Documentação oficial do glibc sobre versionamento de símbolos e compatibilidade ABI
- Semantic Versioning Specification — Especificação oficial do versionamento semântico com diretrizes gerais aplicáveis a bibliotecas C
- libabigail Documentation — Documentação da ferramenta abidiff e abidw para análise de compatibilidade ABI
- Linux Foundation: Library Interface Versioning — Especificação LSB sobre versionamento de bibliotecas compartilhadas no Linux
- Ulrich Drepper: How to Write Shared Libraries — Guia clássico sobre criação e manutenção de bibliotecas compartilhadas em C, incluindo ABI compatibility
- pahole Manual (dwarves) — Manual da ferramenta pahole para inspeção de layout de structs em binários compilados