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 soname da 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 MAJOR corresponde ao número de versão da ABI (soname)
  • MINOR e PATCH sã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