본문으로 건너뛰기
Abseil Code Review · 5/79

Abseil Versioning과 ABI 호환성 정책

· Hawk · 5분 읽기

한 줄 요약: Abseil의 ABI 정책은 “같은 빌드 단위 안에서만 호환을 보장”한다. 이 제약은 inline namespace로 강제되고, 다른 옵션으로 빌드된 두 Abseil 코드를 같은 실행 파일에 링크하면 컴파일 또는 링크 단계에서 실패하도록 설계되어 있다.

#어떤 문제를 푸는가

C++ 라이브러리의 ABI는 미묘하다. 같은 헤더, 같은 함수 시그니처라도 컴파일 옵션이 다르면 메모리 레이아웃이나 calling convention이 달라질 수 있다.

// 시나리오: 라이브러리 A는 std=c++17로 빌드, 라이브러리 B는 c++20로 빌드.
// 둘 다 absl::Time을 인자로 받는 함수를 export.
// 같은 binary에 링크하면? — undefined behavior.

C++ 표준 라이브러리는 이런 충돌을 막기 위해 inline namespace를 쓴다. libstdc++는 __cxx11, libc++는 __1 같은 이름이 그 예다. Abseil도 같은 기법을 더 적극적으로 활용한다.

#정책: 단일 빌드 단위 내 ABI 호환

Abseil의 공식 약속은 한 줄이다.

같은 컴파일 옵션으로 같은 시점에 빌드된 코드끼리만 ABI 호환을 보장한다.

이 정책은 두 가지 자유를 가져온다.

  1. 내부 구현을 자유롭게 바꿀 수 있다flat_hash_map의 probing 방식이 LTS 사이에 바뀐 적이 있다.
  2. 사용자가 잘못된 mix를 하면 빌드 시점에 잡힌다 — runtime crash가 아니라 link error.

대신 사용자는 다음 두 가지 시나리오를 피해야 한다.

  • 시나리오 A — 사전 빌드된 absl.so에 사용자가 빌드한 Abseil 사용 코드를 링크. 두 쪽의 컴파일 옵션이 다를 수 있어 위험하다.
  • 시나리오 B — 두 third-party가 각자 prebuild Abseil을 들고 온다. 같은 심볼이 두 번 정의되어 ODR violation이 날 수 있다.

#inline namespace로 강제

Abseil은 ABSL_NAMESPACE_BEGIN / ABSL_NAMESPACE_END라는 매크로를 모든 헤더 안에 둔다.

// absl/base/config.h에서 발췌
#define ABSL_NAMESPACE_BEGIN \
inline namespace lts_20240722 {
#define ABSL_NAMESPACE_END }

namespace 이름은 LTS 날짜 또는 HEAD commit hash에서 유래한다. 다른 버전으로 빌드된 Abseil의 심볼이 같은 binary 안에 들어오면 namespace 이름이 달라 link 단계에서 분리된다. 같은 심볼이 두 정의를 갖지 않게 된다.

// 사용자 입장에서는 inline namespace이므로 보이지 않음
absl::Status s = absl::OkStatus();
// 실제 mangled name은 absl::lts_20240722::Status
// 두 버전이 섞이면 두 개의 다른 type으로 인식됨
// 컴파일 또는 link 단계에서 에러 발생

inline namespace의 핵심 속성은 다음과 같다.

  • 사용자 코드는 absl::Status로 그대로 쓴다 (inline의 의미).
  • 컴파일러가 보는 실제 namespace는 버전이 인코딩된 이름.
  • ADL, template argument deduction도 정상 작동.

#ABI에 영향을 주는 옵션

같은 Abseil 소스 코드라도 다음 옵션이 다르면 ABI가 달라질 수 있다.

옵션ABI 영향
C++ 표준 (c++17 vs c++20)sizeof, layout 변경 가능
_GLIBCXX_USE_CXX11_ABIlibstdc++의 string/list 표현
-fno-exceptionsexception 비활성화 코드 경로
-fno-rttiRTTI 없는 환경
NDEBUGassert 코드 포함 여부
_LIBCPP_HARDENING_MODElibc++ 안전성 모드

Abseil은 이 중 일부를 inline namespace 이름에 인코딩한다. 모든 옵션을 잡지는 않지만, 가장 흔한 mismatch는 잡는다.

#ABSL_OPTION_USE_STD_*

Abseil은 polyfill 타입에 대해 컴파일 시점에 std 버전을 쓸지 absl 버전을 쓸지 선택하는 옵션을 제공한다.

absl/base/options.h
#define ABSL_OPTION_USE_STD_OPTIONAL 2
// 0 = absl::optional 자체 구현
// 1 = std::optional 사용 (C++17 이상에서)
// 2 = 자동 감지
#define ABSL_OPTION_USE_STD_STRING_VIEW 2
#define ABSL_OPTION_USE_STD_VARIANT 2
#define ABSL_OPTION_USE_STD_ANY 2

옵션 2가 default다. C++ 표준 버전을 감지해서 자동으로 선택한다.

ABI 관점에서 중요한 것은 다음이다.

// 시나리오: A는 ABSL_OPTION_USE_STD_OPTIONAL=1로 빌드 (std::optional alias)
// 시나리오: B는 ABSL_OPTION_USE_STD_OPTIONAL=0로 빌드 (absl 자체 구현)
// A의 absl::optional<int>와 B의 absl::optional<int>는 다른 type
// → link 단계에서 잡혀야 한다

옵션이 다르면 inline namespace 이름이 달라지도록 빌드 시스템이 처리한다.

#prebuilt 사용 시 함정

ABI 정책을 가장 자주 어기는 시나리오는 시스템 패키지 사용이다.

Terminal window
# Ubuntu apt
sudo apt install libabsl-dev
# /usr/lib/x86_64-linux-gnu/libabsl_*.so 설치
# 사용자 코드에서
g++ -std=c++17 main.cc -labsl_strings -labsl_status

이때 /usr/lib의 libabsl_*.so가 어떤 옵션으로 빌드되었는지 사용자는 모른다. 사용자의 -std=c++17와 시스템 라이브러리의 빌드 옵션이 일치하지 않으면 미묘한 ABI 불일치가 발생한다. inline namespace로 잡히면 link error로 끝나지만, 어떤 경우는 runtime에 문제가 드러난다.

권장하는 빌드 모델은 다음과 같다.

  1. 사용자 프로젝트가 Abseil 소스를 가져와 자체 빌드한다.
  2. Bazel, CMake FetchContent, vcpkg manifest mode 모두 무방하다.
  3. apt/yum/brew의 시스템 패키지는 비권장이다. 다만 toy나 learning 용도에는 무방하다.

#두 라이브러리가 Abseil을 각자 갖고 오는 경우

my_app
├── lib_x.so → Abseil 20240116.0 prebuilt 포함
├── lib_y.so → Abseil 20240722.0 prebuilt 포함
└── 사용자 코드 → Abseil 20240722.0 source build

이 시나리오에서 lib_x와 lib_y의 Abseil 심볼은 inline namespace 이름이 다르다. dynamic linker가 같은 심볼을 두 번 보지 않는다. 표면상은 동작한다.

문제는 인터페이스에 Abseil type이 노출된 경우다.

lib_x.h
#include "absl/status/status.h"
absl::Status DoSomething(); // 20240116 namespace
// lib_y.h
#include "absl/status/status.h"
absl::Status DoOther(); // 20240722 namespace
// 사용자 코드 — Status를 두 라이브러리 사이에 주고받으려 함
auto s = DoSomething();
return DoOther(); // 다른 type, 컴파일 에러

해법은 인터페이스에 Abseil type을 노출하지 않는 것이다. 라이브러리는 자기 내부에서만 Abseil을 쓰고, 인터페이스는 std 또는 자체 type으로.

#ABSL_LTS_RELEASE_VERSION

Abseil은 LTS 버전을 컴파일러 매크로로 노출한다.

#include "absl/base/config.h"
#ifdef ABSL_LTS_RELEASE_VERSION
// LTS 빌드. 값은 예: 20240722
static_assert(ABSL_LTS_RELEASE_VERSION >= 20240116,
"Need at least 20240116 LTS");
#else
// HEAD 빌드
#endif

이 매크로로 LTS 간 차이를 #if로 분기하는 것은 가능하지만 권장하지 않는다. 한 LTS 안에서 일관된 코드를 쓰는 게 낫다.

#std로의 마이그레이션 부담

ABI 정책 때문에 마이그레이션도 신중해야 한다.

// 코드베이스 일부가 absl::optional, 나머지가 std::optional
void Process(absl::optional<int>);
void Process(std::optional<int>);
// 다른 type. 같은 함수 두 번 정의해야 하나? — 마이그레이션이 한 번에 끝나야 한다는 신호
// ABSL_OPTION_USE_STD_OPTIONAL=1로 두면 둘이 같은 type이 됨
// 하지만 이 옵션은 전체 코드베이스에서 통일되어야 함

마이그레이션은 코드베이스 전체를 한 단위로 본다. 점진적 변환은 어렵고, 한 commit으로 끊는 것이 깔끔하다.

#코드 리뷰 포인트

// 리뷰 질문 1: 라이브러리 인터페이스에 absl type이 들어가 있는가?
// my_lib.h (public header)
#include "absl/status/status.h"
absl::Status MyAPI(); // 사용자가 같은 LTS의 Abseil을 써야만 한다는 제약
// Good — 가능하면 std 또는 자체 type
std::error_code MyAPI();
// 또는 라이브러리 사용자에게 명확히 "Abseil 20240722.0 이상 필요" 문서화
# 리뷰 질문 2: 시스템 Abseil + bundled Abseil이 mix되는가?
find_package(absl REQUIRED) # 시스템
add_subdirectory(third_party/absl) # bundled
# 컴파일 에러는 안 나지만 link 시점에 충돌 가능
// 리뷰 질문 3: ABSL_OPTION_USE_STD_*를 임의로 변경하는가?
// 새 옵션을 코드베이스 일부에만 적용하면 type mismatch
#define ABSL_OPTION_USE_STD_OPTIONAL 1 // 절대 부분 변경 금지

#자주 보는 안티패턴

// 회피 — Abseil 내부 namespace를 직접 참조
absl::lts_20240722::Status s = ...;
// inline namespace는 추상화. 직접 참조하면 LTS 업데이트 시 깨짐.
// Good
absl::Status s = ...;
// 회피 — 두 ABI 버전을 같은 binary에 mix
// 위에서 다룬 시나리오 — link error로 잡힐 수도 있고 안 잡힐 수도 있다.

#정리

  • Abseil의 ABI 호환은 “같은 빌드 단위 내”에서만.
  • inline namespace에 LTS 버전과 옵션을 인코딩해서 mismatch를 link 단계에서 잡으려 한다.
  • 시스템 패키지(apt 등)는 권장하지 않는다. source build가 안전.
  • 라이브러리 인터페이스에 Abseil type을 노출하면 사용자에게 같은 Abseil 버전을 강제하게 됨.
  • ABSL_OPTION_USE_STD_*는 코드베이스 전체에서 통일되어야 한다.

#다음 편

Part 1을 마치고 Part 2로 넘어간다. Part 2-01에서 ABSL_HAVE_*, ABSL_ATTRIBUTE_* 매크로 군을 본다. 컴파일러와 플랫폼 차이를 흡수하는 첫 번째 도구.

#관련 항목

Abseil Code Review · 6 of 79

  1. 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
  2. 2 Abseil 개요 — Google이 std를 보완한 이유
  3. 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
  4. 4 Abseil 빌드와 의존성 — Bazel vs CMake
  5. 5 Abseil LTS vs HEAD 릴리스 모델 분석
  6. 6 Abseil Versioning과 ABI 호환성 정책
  7. 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
  8. 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
  9. 9 absl::LogSeverity — 로그 레벨 타입
  10. 10 Abseil type_traits — negation·conjunction·void_t
  11. 11 Abseil Conformance·Policy 분석
  12. 12 Abseil Memory utilities 분석
  13. 13 Abseil raw_logging — heap-free 로깅
  14. 14 Abseil thread_annotations — clang TSA 통합
  15. 15 absl::Status — exception-free error handling
  16. 16 absl::StatusOr<T> — 값 또는 에러
  17. 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
  18. 18 absl::Status payload — 구조화된 에러 컨텍스트
  19. 19 absl::Status ↔ exception 변환 패턴
  20. 20 absl::string_view — non-owning 문자열 참조
  21. 21 absl::string_view 함정 — dangling·c_str·임시 객체
  22. 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
  23. 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
  24. 24 absl::StrJoin — 컨테이너 결합과 Formatter
  25. 25 absl::StrFormat — type-safe printf·FormatSpec
  26. 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
  27. 27 Abseil Escape — CEscape·HexEscape·Base64
  28. 28 absl::flat_hash_map — Swiss Table 기반 hash map
  29. 29 absl::flat_hash_set — set 버전 Swiss Table
  30. 30 absl::node_hash_map — stable pointer가 필요할 때
  31. 31 absl::btree_map — sorted·cache-friendly B-tree
  32. 32 absl::FixedArray — 런타임 크기 stack 배열
  33. 33 absl::InlinedVector — small buffer optimization
  34. 34 Abseil Swiss Table internals — control byte·SIMD probing
  35. 35 absl::Mutex — reader-writer·fairness·deadlock 검출
  36. 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
  37. 37 absl::Notification — once-only signal
  38. 38 absl::BlockingCounter·Barrier — 다중 thread 조율
  39. 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
  40. 40 absl::Time·Duration 분석 — 단단한 type
  41. 41 absl::Time Format·Parse
  42. 42 absl::CivilTime 분석
  43. 43 absl::time_zone 분석
  44. 44 absl::Time mocking — 테스트 친화 시간
  45. 45 absl::BitGen — 모던 난수 생성기
  46. 46 Abseil Random Distributions — Uniform·Exponential
  47. 47 Abseil Mocking Random — 테스트 결정성
  48. 48 Abseil Random Seeding·Entropy
  49. 49 absl::int128·uint128 분석
  50. 50 absl::bits — popcount·countl_zero
  51. 51 absl::optional vs std::optional
  52. 52 absl::variant 분석
  53. 53 absl::span 분석
  54. 54 absl::any 분석
  55. 55 absl::compare — three-way 비교
  56. 56 Abseil utility — apply·in_place
  57. 57 Abseil AbslHashValue 분석
  58. 58 Abseil HashState chaining
  59. 59 Abseil Custom hashable 구현
  60. 60 Abseil LOG·VLOG·CHECK 분석
  61. 61 Abseil LogSink 분석
  62. 62 Abseil LogEntry·structured logging
  63. 63 Abseil Stack trace·failure_signal_handler
  64. 64 ABSL_FLAG 정의 분석
  65. 65 Abseil ParseCommandLine 동작
  66. 66 Abseil Flag introspection·validation
  67. 67 Google 스타일의 Abseil 사용 패턴
  68. 68 Abseil 자주 보는 anti-pattern
  69. 69 std → absl 마이그레이션 전략
  70. 70 absl::Cleanup — 함수 종료 시 실행 보장
  71. 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
  72. 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
  73. 73 absl::bind_front와 Overload — 함수 객체 보조 도구
  74. 74 absl::Cord — 분산 시스템용 대용량 문자열
  75. 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
  76. 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
  77. 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
  78. 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
  79. 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피