3 min read

C/C++에서 ABI 호환성을 지키며 API 설계하기

C/C++,ABI,호환성,API,설계

C/C++로 API를 설계할 때, ABI(Application Binary Interface) 호환성 문제는 항상 고민해야 하는 중요한 주제입니다.

특히 라이브러리를 업그레이드하면서 기존 프로그램과 호환성을 유지하고 싶다면, ABI를 고려하지 않은 변경은 치명적일 수 있습니다.

1. ABI 호환성 문제란?

예를 들어, 라이브러리 v1에서는 다음과 같이 심볼을 정의했다고 가정합시다.

int32_t my_rad_symbol(int32_t x) {
    return x * 2;
}

이후 라이브러리 v2로 업그레이드하면서, 함수 원형을 바꿨습니다.

int64_t my_rad_symbol(int64_t x) {
    return x * 3;
}
  • 문제점:
    • old program → new lib: 동작 ✅ (v1 심볼이 존재하면 호출 가능)
    • new program → old lib: 동작 ❌ (v2 심볼 없음)

즉, 기존 프로그램과 새 라이브러리가 서로 호환되지 않음을 알 수 있습니다.


2. 구조체에서 ABI 호환성을 유지하는 방법

함수뿐 아니라 구조체도 ABI 호환성을 깨뜨리기 쉽습니다. 하지만 구조체의 앞부분을 **고정된 ABI 계약(prefix)**로 정의하면, 새 필드를 추가해도 기존 프로그램이 안전하게 동작합니다.

#include <cstdint>
#include <cstdio>

// 구조체 앞부분은 고정된 ABI 계약
struct MyRadTypeV1 {
    uint32_t size;       // 구조체 전체 크기
    uint32_t version;    // 버전 정보
    uint64_t handle_id;  // 기존 필드
};

// V2에서는 새로운 필드를 추가
struct MyRadTypeV2 {
    uint32_t size;
    uint32_t version;
    uint64_t handle_id;

    // 새 필드
    uint64_t creation_time;
    uint32_t access_rights;
};

3. API 설계 시 포인터와 버전 체크 활용

라이브러리 함수는 항상 포인터 기반으로 처리하고, 버전을 체크하여 새 필드를 안전하게 처리할 수 있습니다.

void use_val(const void* p) {
    auto base = static_cast<const MyRadTypeV1*>(p);
    printf("[use_val] size=%u version=%u handle_id=%llu\n",
           base->size, base->version,
           static_cast<unsigned long long>(base->handle_id));

    // 버전 체크 후 추가 필드 처리
    if (base->version >= 2 && base->size >= sizeof(MyRadTypeV2)) {
        auto v2 = static_cast<const MyRadTypeV2*>(p);
        printf("[use_val] creation_time=%llu access_rights=%u\n",
               static_cast<unsigned long long>(v2->creation_time),
               v2->access_rights);
    }
}

이 방식의 장점:

  • 기존 프로그램은 V1 구조체만 읽고 동작 가능
  • 새 프로그램은 V2 구조체의 새 필드도 활용 가능
  • ABI 깨짐 없이 하위호환성 유지

4. 객체 생성 함수

버전에 맞는 구조체를 생성하는 함수도 제공합니다.

MyRadTypeV1* make_val_v1() {
    auto* val = new MyRadTypeV1{};
    val->size = sizeof(MyRadTypeV1);
    val->version = 1;
    val->handle_id = 12345;
    return val;
}

MyRadTypeV2* make_val_v2() {
    auto* val = new MyRadTypeV2{};
    val->size = sizeof(MyRadTypeV2);
    val->version = 2;
    val->handle_id = 67890;
    val->creation_time = 1729412345678;
    val->access_rights = 0x3;
    return val;
}

5. 테스트

int main() {
    auto* v1 = make_val_v1();
    auto* v2 = make_val_v2();

    use_val(v1); // V1 객체 사용
    use_val(v2); // V2 객체 사용

    delete v1;
    delete v2;
    return 0;
}

출력 예시:

[use_val] size=16 version=1 handle_id=12345
[use_val] size=32 version=2 handle_id=67890
[use_val] creation_time=1729412345678 access_rights=3