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ő.
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.
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
#endifA 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:
new (std::nothrow) csak
a tároló foglalásának hibáját alakítja nullptr-ré; a
konstruktor kivételét nem fogja el. A std::deque
alapértelmezett konstruktora a libstdc++-ban már allokál, így ez a
kivétel a noexcept függvényből kiszökve
std::terminate-et okozna. Ezért a teljes konstrukció egy
try/catch határ mögött fut, külön ágon kezelve
a std::bad_alloc-ot és minden más kivételt.std::string értékadása a catch ágban újabb
std::bad_alloc-ot dobhatna, kezeletlenül. A hibaszöveg
ezért statikus literál, és fix méretű pufferbe kerül
memcpy-vel; a create hibája a hívó
errbuf-jába, a többi hiba a handle last_error
tömbjébe.feed, a set_sink és a destroy
tiltott. Az első kettő DIAGCORE_EBUSY-t ad, a
destroy pedig hatástalan, és last_error-t
állít. A védelem mélységszámláló, amelyet minden érintett belépési pont
ellenőriz. Egyetlen bool nem elég: ha a reentráns feed
futni engedné a callbacket, a belső hívás végén a flag hamisra állna, és
a külső callbackből hívott destroy a még futó külső
feed alól törölné az objektumot.// 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.
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
./clientA 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.txtA --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.
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.0Buildjegyző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.
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.
| 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++.
| 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 |
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.
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.
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.
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.
struct_size
verziózásImplementáld a struct_size ellenőrzést, és teszteld egy
régi kisebb és egy új nagyobb diag_config-gal.
Í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ó.
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.
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.
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.
struct_size
verziózás — megoldásA 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.
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.
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.