36.5 ABI- és FFI-labor, kompatibilitási teszt

A C záróprojekt stream-alapú diagnosztikai szolgáltatása akkor cserélhető biztonságosan C++ implementációra, ha a C fejléc és a C kliens változatlan marad. Ez a labor ezt a cserét teszi ellenőrizhetővé. Nem azt bizonyítja, hogy bármely C++ fordító és bármely standard library között a könyvtár binárisan hordozható, hanem azt, hogy egy rögzített toolchain-profilban a C API szerződése, a szimbólumkészlet, a handle-élettartam és a memóriahatár együtt tesztelhető. A labor eredménye ezért mindig profilhoz kötött: a fordítók, a target ABI, a libc/libstdc++ és a build kapcsolók rögzítése nélkül a „kompatibilis” állítás nem értelmezhető.

36.5.1 A kompatibilitás rétegei és a labor szerződése

Négy fogalmat kell külön tartani. A forráskompatibilitás azt jelenti, hogy a kliens forrása újrafordítható a megváltozott könyvtárral. Az API-kompatibilitás ennél szűkebb: a deklarációk, a paraméterek és a dokumentált szemantika változatlanok. Az ABI-kompatibilitás bináris szintű: a hívási konvenció, a struct-layout, a szimbólumnevek, az allokátor és a kivételkezelés együtt kompatibilis. A bináris kompatibilitás a legszűkebb: a változatlan kliensbináris betölthető és helyesen fut. Ehhez jön a viselkedési kompatibilitás, amely a megfigyelhető eredmények és hibák azonosságát jelenti; ez nem következik automatikusan az ABI-ból.

A labor szerződése a következő. A diagcore.h C fejléc változatlan; a C kliens vagy változatlan bináris, vagy változatlan forrásból újrafordított; a könyvtár SONAME-ja és exportlistája ellenőrzött; a hibaszerződés (errbuf, visszatérési kód, last_error élettartama) dokumentált. A labor nem bizonyítja a cross-compiler stabilitást, és nem teszteli STL-típusok vagy kivételek átadását a határon: ezek a témakör határai, a C API ezeket szándékosan nem használja.

36.5.2 A referencia-implementáció: változatlan C fejléc

A fejléc csak C-típusokat és opaque handle-t tartalmaz. A struct_size és az abi_version a verziózás alapja; a diag_engine definíciója a fejlécben nem látható. A DIAGCORE_NOEXCEPT makró C++-ban noexcept-re, C-ben üresre helyettesítődik: C++17 óta a kivételspecifikáció a függvénytípus része, ezért a deklarációnak és a definíciónak egyeznie kell. Ha a fejléc noexcept nélkül deklarálna, a .cpp pedig noexcept-tel definiálna, a definíció fordítási hiba lenne (declaration ... has a different exception specifier).

/* diagcore.h — stabil C API, C17 és C++20 fordításra */
#ifndef DIAGCORE_H
#define DIAGCORE_H

#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
#  define DIAGCORE_NOEXCEPT noexcept   /* C++-ban a típus része: deklaráción */
extern "C" {                          /* és definíción is azonos legyen      */
#else
#  define DIAGCORE_NOEXCEPT
#endif

#if defined(_WIN32)
#  define DIAGCORE_API __declspec(dllexport)
#elif defined(__GNUC__)
#  define DIAGCORE_API __attribute__((visibility("default")))
#else
#  define DIAGCORE_API
#endif

#define DIAGCORE_ABI_VERSION 1u

typedef struct diag_engine diag_engine; /* opaque handle */

typedef struct diag_config {
    uint32_t struct_size;  /* = sizeof(diag_config) */
    uint32_t abi_version;  /* = DIAGCORE_ABI_VERSION */
    size_t   max_pending;
    uint32_t flags;
} diag_config;

typedef struct diag_stats {
    uint64_t accepted;
    uint64_t rejected;
    uint64_t callbacks;
} diag_stats;

typedef void (*diag_sink)(void *ctx, const char *msg, size_t len);

/* Hibakódok (a Linux errno-értékeivel egyezően). */
#define DIAGCORE_OK      0
#define DIAGCORE_ENOMEM 12
#define DIAGCORE_EBUSY  16
#define DIAGCORE_EINVAL 22

DIAGCORE_API diag_engine *diag_engine_create(const diag_config *cfg,
                                             char *errbuf, size_t errlen) DIAGCORE_NOEXCEPT;
DIAGCORE_API void diag_engine_destroy(diag_engine *e) DIAGCORE_NOEXCEPT;
DIAGCORE_API int  diag_engine_feed(diag_engine *e, const char *data, size_t len) DIAGCORE_NOEXCEPT;
DIAGCORE_API int  diag_engine_set_sink(diag_engine *e, diag_sink sink, void *ctx) DIAGCORE_NOEXCEPT;
DIAGCORE_API int  diag_engine_get_stats(const diag_engine *e, diag_stats *out) DIAGCORE_NOEXCEPT;
DIAGCORE_API const char *diag_engine_last_error(const diag_engine *e) DIAGCORE_NOEXCEPT;

#ifdef __cplusplus
}
#endif
#endif

A C++ implementáció a diag_engine structot a .cpp fájlban definiálja, és minden extern "C" függvényt DIAGCORE_NOEXCEPT-tel lát el. A kivétel nem léphet át a C határon, ezért minden belépési pontnak három hibautat kell lezárnia:

// diagcore.cpp — C++20 implementáció a változatlan C fejléc mögött
#include "diagcore.h"

#include <cstddef>
#include <cstring>
#include <deque>
#include <new>
#include <string>

namespace {

// Hibaszöveg másolása allokáció és kivétel nélkül; mindig lezárja a puffert.
void copy_message(char* dst, std::size_t cap, const char* msg) noexcept {
    if (dst == nullptr || cap == 0) return;
    std::size_t n = std::strlen(msg);
    if (n >= cap) n = cap - 1;
    std::memcpy(dst, msg, n);
    dst[n] = '\0';
}

} // namespace

struct diag_engine {
    explicit diag_engine(const diag_config& c) : cfg(c) {}
    diag_config cfg{};
    std::deque<std::string> pending;   // a libstdc++-ban már a konstruktor allokál
    diag_sink sink = nullptr;
    void* sink_ctx = nullptr;
    diag_stats stats{};
    char last_error[96] = "";          // fix méret: a hibaút nem allokál
    int callback_depth = 0;            // >0: sink fut; reentrancia tiltott

    void set_error(const char* msg) noexcept {
        copy_message(last_error, sizeof last_error, msg);
    }
};

extern "C" DIAGCORE_API diag_engine*
diag_engine_create(const diag_config* cfg, char* errbuf, std::size_t errlen) DIAGCORE_NOEXCEPT {
    auto fail = [&](const char* m) noexcept -> diag_engine* {
        copy_message(errbuf, errlen, m);
        return nullptr;
    };
    if (!cfg) return fail("null config");
    constexpr std::size_t kMin = offsetof(diag_config, flags) + sizeof(uint32_t);
    if (cfg->struct_size < kMin) return fail("config too small");
    if (cfg->abi_version != DIAGCORE_ABI_VERSION) return fail("unsupported abi_version");

    diag_config c{};                   // csak az ismert mezőket másoljuk
    std::memcpy(&c, cfg, sizeof c < cfg->struct_size ? sizeof c : cfg->struct_size);
    c.struct_size = sizeof c;
    if (c.max_pending == 0) c.max_pending = 64;

    try {                               // a TELJES konstrukció a határ mögött
        return new diag_engine(c);
    } catch (const std::bad_alloc&) {
        return fail("allocation failed");
    } catch (...) {
        return fail("internal error");
    }
}

extern "C" DIAGCORE_API void diag_engine_destroy(diag_engine* e) DIAGCORE_NOEXCEPT {
    if (!e) return;
    if (e->callback_depth > 0) {        // a sinkből hívva: tiltott, nincs hatása
        e->set_error("destroy during callback");
        return;
    }
    delete e;
}

extern "C" DIAGCORE_API int
diag_engine_feed(diag_engine* e, const char* data, std::size_t len) DIAGCORE_NOEXCEPT {
    if (!e) return DIAGCORE_EINVAL;
    if (e->callback_depth > 0) {        // reentráns feed a sinkből: tiltott
        e->set_error("feed during callback");
        return DIAGCORE_EBUSY;
    }
    if (!data && len != 0) return DIAGCORE_EINVAL;
    try {
        if (len != 0) e->pending.emplace_back(data, len); // másolat, nem idegen pointer
        while (e->pending.size() > e->cfg.max_pending) e->pending.pop_front();
    } catch (const std::bad_alloc&) {
        e->set_error("allocation failed");
        e->stats.rejected += 1;
        return DIAGCORE_ENOMEM;
    }
    e->stats.accepted += 1;
    if (e->sink) {
        ++e->callback_depth;
        try {                            // egy C++ kliens dobó függvényt is adhat
            e->sink(e->sink_ctx, data, len);
        } catch (...) {
            e->set_error("sink threw");
            e->stats.rejected += 1;
        }
        --e->callback_depth;
        e->stats.callbacks += 1;
    }
    return DIAGCORE_OK;
}

extern "C" DIAGCORE_API int
diag_engine_set_sink(diag_engine* e, diag_sink sink, void* ctx) DIAGCORE_NOEXCEPT {
    if (!e) return DIAGCORE_EINVAL;
    if (e->callback_depth > 0) {
        e->set_error("set_sink during callback");
        return DIAGCORE_EBUSY;
    }
    e->sink = sink;
    e->sink_ctx = ctx;
    return DIAGCORE_OK;
}

extern "C" DIAGCORE_API int
diag_engine_get_stats(const diag_engine* e, diag_stats* out) DIAGCORE_NOEXCEPT {
    if (!e || !out) return DIAGCORE_EINVAL;
    *out = e->stats;
    return DIAGCORE_OK;
}

extern "C" DIAGCORE_API const char*
diag_engine_last_error(const diag_engine* e) DIAGCORE_NOEXCEPT {
    return e ? e->last_error : "null handle";
}

A diag_engine_last_error a handle belső, fix méretű hibapufferére mutat; a pointer a handle élettartamáig érvényes, a tartalma a következő hibáig. A create hibáját nem ez, hanem az errbuf tartalmazza. A feed a bemeneti buffert csak a hívás alatt használja, a pending-be másolat kerül; ez a foreign buffer szerződés lényege.

36.5.3 Build és exportellenőrzés

A könyvtárat -fvisibility=hidden-nal és verziós szkripttel kell építeni, hogy csak a hat C szimbólum exportálódjon, a belső C++ szimbólumok ne.

g++ -std=c++20 -O2 -fPIC -fvisibility=hidden -Wall -Wextra -Wpedantic \
    -Wl,--no-undefined -Wl,-soname,libdiagcore.so.1 \
    -Wl,--version-script=diagcore.map \
    -shared -o libdiagcore.so.1.0.0 diagcore.cpp
ln -sf libdiagcore.so.1.0.0 libdiagcore.so.1
ln -sf libdiagcore.so.1 libdiagcore.so

gcc -std=c17 -O2 -Wall -Wextra -Wpedantic client.c -L. -ldiagcore \
    -Wl,-rpath,'$ORIGIN' -o client
./client

A verziós szkript (diagcore.map):

DIAGCORE_1.0 {
  global:
    diag_engine_create;
    diag_engine_destroy;
    diag_engine_feed;
    diag_engine_set_sink;
    diag_engine_get_stats;
    diag_engine_last_error;
  local: *;
};

A diagcore.map a DIAGCORE_1.0 verzió alatt sorolja fel a hat függvényt, minden mást local: *; rejt. Az exportellenőrzés a dinamikus szimbólumtáblát hasonlítja össze a várt listával (expected-exports.txt, soronként egy függvénynév, rendezve). Az nm a verziózott szimbólumot diag_engine_create@@DIAGCORE_1.0 alakban írja ki, ezért a szkript a @ utáni részt levágja:

#!/usr/bin/env bash
# check-exports.sh — csak a hat C szimbólum exportálódhat
set -euo pipefail
lib=${1:?usage: check-exports.sh libdiagcore.so.1.0.0}
nm -D --defined-only --format=posix "$lib" \
  | awk '$2 ~ /^[Tt]$/ {print $1}' \
  | sed 's/@.*//' \
  | sort -u > actual.txt
diff -u expected-exports.txt actual.txt

A --format=posix kimenetében a típus a második mező; a T/t szűrés kizárja a verziócsomópont szimbólumát (DIAGCORE_1.0, A típus). A nm és a readelf kimenete binutils-verziófüggő, ezért a laborprofilban rögzíteni kell a használt binutils-kiadást.

36.5.4 Handle-élettartam, allokációs hiba és idegen memória

A diag_engine_create sikeres hívása egy tulajdonosi handle-t ad. A diag_engine_destroy felelős a felszabadításért; destroy(NULL) no-op, és a destroy után a handle dangling, ezért a kliensnek NULL-ra kell állítania. A feed(NULL, ...) és a get_stats(NULL, ...) EINVAL-t ad. A create allokációs hibája NULL handle-t és errbuf-ba írt üzenetet eredményez; a feed allokációs hibája ENOMEM és last_error beállítás.

Az allokációs hiba determinisztikus teszteléséhez a tesztprogram saját malloc-ot definiál, amely a glibc __libc_malloc függvényét hívja, és egy számláló elérése után NULL-t ad. Mivel a futtatható állomány szimbóluma a dinamikus linkelésnél elsőbbséget kap, a könyvtár operator new-ja is ezt használja. Ez glibc-specifikus laborsegéd, nem része a könyvtárnak; a dlsym-es LD_PRELOAD megoldással szemben nincs saját, rekurzív allokációja. A teszt sorra minden allokációs pontot meghiúsít, és ellenőrzi, hogy a create NULL-t és allocation failed üzenetet, a feed pedig DIAGCORE_ENOMEM-et és ugyanezt a last_error-t adja.

/* alloc_fail.c — determinisztikus allokációs hiba a diagcore API-n.
   A program saját malloc-ja a glibc __libc_malloc-ját hívja, és a
   fail_after-edik hívástól NULL-t ad; az operator new is ezt használja. */
#include "diagcore.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern void *__libc_malloc(size_t);
static long fail_after = -1;           /* -1: nincs hibainjektálás */

void *malloc(size_t n) {
    if (fail_after == 0) return NULL;
    if (fail_after > 0) --fail_after;
    return __libc_malloc(n);
}

int main(void) {
    diag_config c;
    memset(&c, 0, sizeof c);
    c.struct_size = sizeof c;
    c.abi_version = DIAGCORE_ABI_VERSION;
    char err[64];
    const char msg[] = "a message long enough to need heap storage";
    int create_failures = 0, feed_failures = 0;

    for (long k = 0; k < 16; ++k) {    /* minden allokációs pont sorra */
        err[0] = '\0';
        fail_after = k;
        diag_engine *e = diag_engine_create(&c, err, sizeof err);
        int rc = e ? diag_engine_feed(e, msg, sizeof msg - 1) : -1;
        fail_after = -1;
        if (!e) {
            if (strcmp(err, "allocation failed") != 0) return 1;
            ++create_failures;
            continue;
        }
        if (rc == DIAGCORE_ENOMEM) {
            if (strcmp(diag_engine_last_error(e), "allocation failed") != 0) return 1;
            ++feed_failures;
        } else if (rc != DIAGCORE_OK) {
            return 1;
        }
        diag_engine_destroy(e);
    }
    printf("create_failures=%d feed_failures=%d\n", create_failures, feed_failures);
    return create_failures > 0 && feed_failures > 0 ? 0 : 1;
}

A C kliens a többi szerződést ellenőrzi: NULL handle, hibás és nagyobb struct_size, hibás abi_version, idegen puffer felszabadítása a feed után, valamint a callbackből hívott reentráns feed és destroy:

/* client.c — C17 tesztkliens a diagcore C API-hoz */
#include "diagcore.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CHECK(condition) do {                                      \
    if (!(condition)) {                                            \
        fprintf(stderr, "CHECK failed at line %d\n", __LINE__);    \
        exit(EXIT_FAILURE);                                        \
    }                                                              \
} while (0)

static diag_config make_config(void) {
    diag_config c;
    memset(&c, 0, sizeof c);
    c.struct_size = sizeof c;
    c.abi_version = DIAGCORE_ABI_VERSION;
    c.max_pending = 4;
    return c;
}

struct probe {                 /* a sink kontextusa: a hívó tulajdona */
    diag_engine *e;
    int calls;
    int nested_feed_status;
    int try_destroy;
};

static void sink(void *ctx, const char *msg, size_t len) {
    struct probe *p = ctx;
    (void)msg; (void)len;
    p->calls += 1;
    p->nested_feed_status = diag_engine_feed(p->e, "x", 1);   /* reentráns */
    if (p->try_destroy) diag_engine_destroy(p->e);           /* tiltott */
}

int main(void) {
    char err[64];
    diag_stats st;

    /* NULL és hibás konfiguráció */
    diag_engine_destroy(NULL);
    CHECK(diag_engine_feed(NULL, "a", 1) == DIAGCORE_EINVAL);
    CHECK(diag_engine_get_stats(NULL, &st) == DIAGCORE_EINVAL);
    CHECK(diag_engine_create(NULL, err, sizeof err) == NULL);
    CHECK(strcmp(err, "null config") == 0);
    diag_config bad = make_config();
    bad.struct_size = 4;
    CHECK(diag_engine_create(&bad, err, sizeof err) == NULL);
    CHECK(strcmp(err, "config too small") == 0);
    bad = make_config();
    bad.abi_version = 2;
    CHECK(diag_engine_create(&bad, err, sizeof err) == NULL);
    CHECK(strcmp(err, "unsupported abi_version") == 0);

    /* Nagyobb (újabb) struct: az ismert mezőket olvassa */
    struct { diag_config base; uint64_t future; } bigger;
    memset(&bigger, 0, sizeof bigger);
    bigger.base = make_config();
    bigger.base.struct_size = sizeof bigger;
    diag_engine *e = diag_engine_create(&bigger.base, err, sizeof err);
    CHECK(e != NULL);

    /* Idegen puffer: a feed után a kliens felszabadítja */
    char *buf = malloc(32);
    CHECK(buf != NULL);
    strcpy(buf, "a longer message than sso size");
    CHECK(diag_engine_feed(e, buf, strlen(buf)) == DIAGCORE_OK);
    free(buf);

    /* Reentráns feed és destroy a sinkből */
    struct probe p = { e, 0, -1, 1 };
    CHECK(diag_engine_set_sink(e, sink, &p) == DIAGCORE_OK);
    CHECK(diag_engine_feed(e, "hello", 5) == DIAGCORE_OK);
    CHECK(p.calls == 1);
    CHECK(p.nested_feed_status == DIAGCORE_EBUSY);
    CHECK(strcmp(diag_engine_last_error(e), "destroy during callback") == 0);

    /* A handle a callback után is érvényes */
    CHECK(diag_engine_get_stats(e, &st) == DIAGCORE_OK);
    CHECK(st.accepted == 2 && st.callbacks == 1 && st.rejected == 0);
    CHECK(diag_engine_set_sink(e, NULL, NULL) == DIAGCORE_OK);
    diag_engine_destroy(e);              /* a callback után: tényleges törlés */
    e = NULL;
    puts("client: ok");
    return 0;
}
gcc -std=c17 -O2 -Wall -Wextra alloc_fail.c -L. -ldiagcore \
    -Wl,-rpath,'$ORIGIN' -o alloc_fail && ./alloc_fail
# ASan/UBSan: a könyvtár és a kliens is instrumentált
g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fPIC -shared \
    -o libdiagcore_san.so diagcore.cpp
gcc -std=c17 -O1 -g -fsanitize=address,undefined client.c \
    -L. -ldiagcore_san -Wl,-rpath,'$ORIGIN' -o client_san && ./client_san
bash check-exports.sh libdiagcore.so.1.0.0

Buildjegyzőkönyv (lab-gcc profil). A könyvtár és a kliens figyelmeztetés nélkül fordult; a diagcore.h C17-ben önállóan is lefordul. A client és az ASan/UBSan-os client_san is client: ok kimenettel futott. Az alloc_fail kimenete create_failures=3 feed_failures=1, a kód 0. Az exportellenőrzés pontosan a hat függvényt találta, _Z kezdetű szimbólum nélkül, és a fejlécben nincs std::. Az eredeti minták hibáit is reprodukáltuk: - a noexcept nélküli fejléc a noexcept definícióval has a different exception specifier fordítási hibát adott; - a new (std::nothrow) konstrukció allokációs hibánál terminate called after throwing an instance of 'std::bad_alloc' üzenettel állt le; - a bool-os callback-védelem a „sink → reentráns feed → destroy” sorrendben ASan alatt heap-use-after-free hibát adott.

A foreign buffer tesztje: a C kliens malloc-ol, feed-el, majd a feed visszatérése után free-el. Ha a könyvtár megőrizné a pointert, az AddressSanitizer heap-use-after-free-t jelezne. A callback ctx-e a hívó tulajdona; a könyvtár nem szabadítja fel. A callback alatt hívott destroy a fenti implementációban nem töröl, hanem last_error-t állít és visszatér, így a callback után a handle tovább használható; ez dokumentált, szándékos viselkedés, nem UB. Ugyanígy a callback alatt hívott feed és set_sink DIAGCORE_EBUSY-t ad.

36.5.5 Verziózás és unsupported configuration

A diag_config első két mezője a struct_size és az abi_version. A könyvtár a struct_size-ból tudja, meddig olvashat: a minimum az általa ismert mezők végéig terjed (offsetof(diag_config, flags) + sizeof(uint32_t)). A régi, kisebb structtal fordított kliens addig kompatibilis, amíg eléri ezt a minimumot; a nagyobb struct esetén a könyvtár az ismert mezőket olvassa, az ismeretleneket figyelmen kívül hagyja. Az abi_version eltérése NULL handle-t és errbuf-üzenetet ad. A diag_stats kimeneti struct méretét a kliens adja, ezért azt a könyvtár headerjével kell fordítani; ezt a labor dokumentálja, és külön feladatban lehet struct_size-tal verziózni.

A szimbólumverziózás a DIAGCORE_1.0 csomópontot használja. Egy későbbi, szemantikailag eltérő verzió új csomópontot kaphat (DIAGCORE_1.1), és a régi kliens a dlvsym-mal vagy a linker által a régi verziót kapja. A laborban a verziós szkript és a dlvsym teszt együtt ellenőrzi, hogy a régi kliens a régi szimbólumot látja.

36.5.6 PImpl és C wrapper: döntési táblázat

Megoldás ABI-stabilitás C++ típusok Allokáció Hibakezelés
C API + PImpl erős, ha a szimbólumok és structok verziózottak rejtett egy handle-allokáció hibakód + errbuf
közvetlen C++ API gyenge, toolchainhez kötött teljes a kliens allokátora kivétel
C++ wrapper a C API felett erős, a C API örökli a wrapperben wrapper allokálhat kivétel a wrapperben

A PImpl előnye, hogy a C++ típusok nem szivárognak ki a fejlécbe, és a C API stabil marad; hátránya az extra indirection és a handle-allokáció. A közvetlen C++ API kényelmesebb C++ klienseknek, de a bináris kompatibilitás a fordítóhoz és a standard libraryhez kötött. A C++ wrapper a C API felett kompromisszum: a stabil mag C, a kényelmes felület C++.

36.5.7 A labor tesztmátrixa

Teszt Mit ellenőriz Eszköz Elvárt eredmény
C kliens linkelése a C++ .so kielégíti a C szimbólumokat gcc + ld sikeres link és futás
exportlista csak a hat C szimbólum nm + diff egyezés
handle create/destroy, NULL egységteszt NULL no-op, hibás config NULL
allokációs hiba create NULL + errbuf, feed ENOMEM saját malloc (alloc_fail.c) NULL, „allocation failed”; ENOMEM
buffer lifetime feed után free ASan nincs use-after-free
callback lifetime reentráns feed és destroy a callback alatt client.c + ASan EBUSY, last_error, nincs UAF
verzió kisebb/nagyobb struct_size két kliens megfelelő hiba/elfogadás
PImpl a fejlécben nincs C++ típus grep nincs std:: a headerben

36.5.8 Gyakori hibák és javításuk

A leggyakoribb hiba az extern "C" elhagyása: a C++ névtorzítás miatt a C kliens nem találja a szimbólumot. A második a kivételhatár hiánya: a C++ kivétel átlépne a C határon. A noexcept ezt std::terminate-tel zárja le, de a kivételt magunknak kell elkapnunk és hibakóddá alakítanunk, a konstruktorét és a hibakezelő ágét is. A harmadik, hogy std::string vagy std::vector kerül a fejlécbe: ez nem stabil ABI. A struct_size elhagyása verziózhatatlanná teszi a structot; a callback ctx könyvtárbeli felszabadítása double free-hez vezet. Az nm-teszt akkor ad hamis pozitívot, ha a verziós szimbólumokat is a várt függvények közé sorolja. A -fvisibility=hidden hiánya belső C++ szimbólumokat exportál. Végül a destroy utáni handle-használat dangling pointer; a laborban a destroy után NULL-ra állítás a szabály.

36.5.9 Feladatok

36.5.9.1 Fogalmi különbségtétel

Definiáld az API-, ABI- és viselkedési kompatibilitást, és adj egy példát, amikor a C API változatlan, de a bináris kompatibilitás megsérül.

36.5.9.2 Két implementáció, egy kliens

Készíts egy C és egy C++ implementációt ugyanahhoz a diagcore.h-hoz, és igazold, hogy a C kliens mindkettővel linkel. Mellékeld az exportellenőrzést.

36.5.9.3 Buffer lifetime ASan-nel

Reprodukáld, hogy a könyvtár a feed után megőrzi a data pointert, és a kliens free-je után olvas. Javítsd a szerződést.

36.5.9.4 struct_size verziózás

Implementáld a struct_size ellenőrzést, és teszteld egy régi kisebb és egy új nagyobb diag_config-gal.

36.5.9.5 Callback alatti destroy

Írj tesztet, amelyben a sink callback diag_engine_destroy-t hív. Igazold, hogy nincs use-after-free, és a handle tovább használható.

36.5.10 Megoldások

36.5.10.1 Fogalmi különbségtétel — megoldás

Az API-kompatibilitás a deklarációk és a dokumentált szemantika azonossága; az ABI a bináris hívási konvenció, a layout, a szimbólumnevek és az allokátor kompatibilitása; a viselkedési kompatibilitás a megfigyelhető eredmények azonossága. Példa: a diag_config mezőinek átrendezése vagy egy size_t méretének megváltozása ILP32 és LP64 között API-ban változatlan, de ABI-ban nem. Ellenőrzési feltétel: a válasz nevezze meg a layout, a hívási konvenció és a szimbólumnév szerepét, és ne állítsa, hogy az API-stabilitásból ABI-stabilitás következik.

36.5.10.2 Két implementáció, egy kliens — megoldás

A C implementációban a diag_engine szintén opaque, a .so neve libdiagcore_c.so.1, és ugyanazt a hat szimbólumot exportálja. A kliens -ldiagcore helyett -ldiagcore_c-vel linkel. A két .so exportlistáját a check-exports.sh hasonlítja össze. Ellenőrzési feltétel: a két exportlista egyezik, a kliens mindkét könyvtárral fut, és a nm kimenetében nincs _Z kezdetű szimbólum.

36.5.10.3 Buffer lifetime ASan-nel — megoldás

A hibás változat a feed-ben eltárolja a const char *last = data; pointert, és később a last_error-ben olvassa. A kliens free-je után az ASan heap-use-after-free-t jelez. A javítás: a pending-be std::string másolat kerül, a data-t csak a hívás alatt használjuk, a callback is csak a hívás alatt kapja. Ellenőrzési feltétel: az ASan tiszta, a pending tartalma a másolat, és a feed visszatérése után a kliens szabadon felszabadíthatja a buffert.

36.5.10.4 struct_size verziózás — megoldás

A minimum méret offsetof(diag_config, flags) + sizeof(uint32_t). A régi kliens a régi headerrel fordít, a struct_size a régi méret; ha eléri a minimumot, a könyvtár elfogadja. Az új kliens nagyobb structot ad; a könyvtár az ismert mezőket olvassa, az ismeretleneket nem. A túl kicsi struct NULL handle-t és errbuf-üzenetet ad. Ellenőrzési feltétel: a régi kliens fut, a túl kicsi struct hibát ad, és az abi_version eltérése külön hibaüzenetet kap.

36.5.10.5 Callback alatti destroy — megoldás

A destroy a callback alatt in_callback miatt nem töröl, hanem last_error-t állít és visszatér. A callback után a feed visszatér, a handle érvényes. A teszt a callback alatt destroy-t hív, majd a callback után get_stats-ot kér, és ellenőrzi a last_error tartalmát. Ellenőrzési feltétel: az ASan tiszta, a stats konzisztens, és a handle a callback után explicit destroy-val szabadítható fel.

36.5.11 Összefoglaló

A labor lényege, hogy a C fejléc és a C kliens változatlan maradjon, miközben a mögöttes implementáció C++-ra cserélődik. Ezt nem elég deklarálni: a szimbólumkészletet, a handle-élettartamot, az allokációs hibát, a foreign buffer és a callback élettartamát, valamint a verziózást külön tesztekkel kell ellenőrizni. A struct_size és az abi_version, a noexcept határ, a PImpl és a verziós szkript együtt adják a szerződést. A labor eredménye a rögzített toolchain-profilhoz kötött; a cross-compiler stabilitás és az STL/kivétel átadása a határon nem része a bizonyított állításnak.