C/C++でAPIやライブラリを設計する場合、ABI(Application Binary Interface)互換性は常に気を配るべき重要なテーマです。特にライブラリをアップグレードしつつ、既存アプリケーションとの互換性を維持したい場合、ABIを意識しない変更は致命的な問題を引き起こす可能性があります。
1. ABI互換性の問題とは?
例として、ライブラリのバージョン1(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 library:動作する(v1シンボルが残っていれば呼び出し可能)
- new program → old library:動作しない(v2シンボルが存在しない)
つまり、旧アプリと新ライブラリ、新アプリと旧ライブラリの双方で互換性が維持できなくなることがわかります。
2. 構造体でABI互換性を維持する方法
関数だけでなく構造体もABI互換性を壊しやすい領域です。しかし、構造体の先頭部分を**固定されたABI契約(prefix)**として扱うことで、新しいフィールドを追加しても既存プログラムが安全に動作するようにできます。
#include <cstdint>
#include <cstdio>
#pragma pack(push, 1)
// 構造体の先頭は固定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;
};
#pragma pack(pop)
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