Egy könyvtári interfész akkor is szerződés, ha a fejlécben mindössze néhány függvénydeklaráció látható. Meg kell mondania, ki birtokolja az állapotot, meddig érvényesek a kölcsönzött pointerek, mi változhat hiba esetén, és milyen párhuzamos használat megengedett. Az opaque handle, vagyis rejtett reprezentációjú objektumra mutató fogantyú az állapot elrendezését elrejti, de ezeket a kötelezettségeket nem helyettesíti.
A paraméterátadásnál látott értékmásolás, a tulajdonlási szerződés és a könyvtári linkelés most egy kis kvótakönyvtárban kapcsolódik össze. A könyvtár egy számlálót kezel felső korláttal, és sikeres módosításkor opcionális callbacket hív.
ISO C17: az alábbi elődeklarálás nem teljes típust (incomplete type) vezet be:
/* C17 deklarációrészlet. */
typedef struct quota quota;A kliens deklarálhat quota * pointert, de nem képezhet
sizeof(quota) kifejezést, nem érheti el a tagokat, és nem
hozhat létre ilyen típusú objektumot érték szerint. A teljes
struktúradefiníció kizárólag az implementációban szerepel.
A pointer másolható, de ettől nem keletkezik új tulajdonos. Ha két pointer ugyanarra az objektumra mutat, az egyik révén végrehajtott megsemmisítés mindkettő hozzáférését érvényteleníti.
Az interfészünk szerződése:
| Elem | Tulajdonos és élettartam | Érvénytelenítés, hibakimenet |
|---|---|---|
quota * objektum |
Sikeres létrehozás után a kliens felel érte | Pontosan egyszeri quota_destroy; minden alias
érvénytelenné válik |
| Belső tároló | A könyvtár foglalja és szabadítja fel | A kliens nem használhat rajta közvetlenül free-t |
| Opcióstruktúra | A hívóé; csak a create idejére kölcsönzött |
A hívás után megszűnhet |
Callback és userdata |
Nem kerül tulajdonba; a kliens biztosítja érvényességüket a megsemmisítésig | A könyvtár nem szabadítja fel a contextet |
| Hibaüzenet | Könyvtári, csak olvasható statikus tároló | Nem felszabadítható; a könyvtár betöltött élettartamáig használható |
A create hibakor null pointert ír az érvényes kimeneti
helyre. Ez a hely nem tartalmazhat elveszíthető, élő tulajdont. A
sikertelen add változatlanul hagyja az állapotot, és nem
hív callbacket.
Szálbiztonsági szerződés: külön handle-ök párhuzamosan használhatók, ha callbackjeik és közös contextadataik is ezt megengedik. Ugyanazon handle minden műveletét, a megsemmisítést is beleértve, a hívó sorosítja. Nincs belső zárolás.
A callback szinkron módon, a hívó szálán fut, már módosított állapot mellett. Nem léphet vissza ugyanazon handle API-jába, nem semmisítheti meg azt, és rendesen vissza kell térnie. C++ callbackből kivétel sem léphet át ezen a határon.
A következő quota.h C17-ből és C++20-ból is használható.
Az állapot layoutja rejtett; az opcióstruktúra layoutja viszont
szándékosan publikus, ezért kompatibilitási kötelezettség.
#ifndef QUOTA_H
#define QUOTA_H
#include <stddef.h>
#ifndef QUOTA_API
#define QUOTA_API
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define QUOTA_API_VERSION 1u
#define QUOTA_OK 0
#define QUOTA_EINVAL 1
#define QUOTA_EVERSION 2
#define QUOTA_ENOMEM 3
#define QUOTA_ELIMIT 4
typedef struct quota quota;
typedef void (*quota_notify)(void *userdata, unsigned long total);
typedef struct quota_options {
size_t struct_size;
unsigned api_version;
unsigned long limit;
quota_notify notify;
void *userdata;
} quota_options;
QUOTA_API int quota_create(const quota_options *options, quota **out);
QUOTA_API int quota_add(quota *q, unsigned long amount);
QUOTA_API void quota_destroy(quota *q);
QUOTA_API const char *quota_status_text(int status);
#ifdef __cplusplus
}
#endif
#endifA fejléc csak a size_t miatt függ a
<stddef.h> headertől. Nem szivárogtat ki allokátort,
zárolási típust vagy Linux-specifikus struktúrát. A prefix csökkenti a
névütközés esélyét.
A QUOTA_API a dinamikus szimbólumláthatóság
konfigurálási pontja. Ez nem azonos a C nyelvi kapcsolással: egy külső
kapcsolású függvényt az ELF-könyvtárból még el lehet rejteni. A
kizárólag implementációs segédfüggvényeket továbbra is
static deklarációval célszerű elkülöníteni.
Az extern "C" őr a későbbi C++ fogyasztás alapja, nem
általános ABI-függetlenségi garancia. Részleteit a két nyelvű headerek tárgyalják.
A teljes quota.c ISO C17-et használ; az exportláthatóság
fordítófüggő beállítása külön, a buildparancsban jelenik meg.
#include "quota.h"
#include <limits.h>
#include <stdlib.h>
struct quota {
unsigned long limit;
unsigned long used;
quota_notify notify;
void *userdata;
};
int quota_create(const quota_options *options, quota **out)
{
if (out == NULL) {
return QUOTA_EINVAL;
}
*out = NULL;
quota_options cfg = {
sizeof(quota_options),
QUOTA_API_VERSION,
ULONG_MAX,
NULL,
NULL
};
if (options != NULL) {
if (options->struct_size < sizeof *options) {
return QUOTA_EINVAL;
}
if (options->api_version != QUOTA_API_VERSION) {
return QUOTA_EVERSION;
}
cfg = *options;
}
quota *q = malloc(sizeof *q);
if (q == NULL) {
return QUOTA_ENOMEM;
}
q->limit = cfg.limit;
q->used = 0;
q->notify = cfg.notify;
q->userdata = cfg.userdata;
*out = q;
return QUOTA_OK;
}
int quota_add(quota *q, unsigned long amount)
{
if (q == NULL) {
return QUOTA_EINVAL;
}
if (amount > q->limit - q->used) {
return QUOTA_ELIMIT;
}
q->used += amount;
if (q->notify != NULL) {
q->notify(q->userdata, q->used);
}
return QUOTA_OK;
}
void quota_destroy(quota *q)
{
free(q);
}
const char *quota_status_text(int status)
{
switch (status) {
case QUOTA_OK: return "success";
case QUOTA_EINVAL: return "invalid argument";
case QUOTA_EVERSION: return "unsupported API version";
case QUOTA_ENOMEM: return "allocation failed";
case QUOTA_ELIMIT: return "quota exceeded";
default: return "unknown status";
}
}A null options alapértelmezett konfigurációt jelent:
ULONG_MAX korlátot és callback nélküli működést. Az
unsigned long itt nyelvi típus, nem hordozhatóan rögzített
szélességű protokollmező. A null handle megsemmisítése megengedett.
A callback minden sikeres add után lefut, nulla
növekménynél is. A könyvtári műveletrész konstans számú műveletet végez,
allokáció nélkül; a callback költségére nincs ilyen vállalás.
Bizonyításvázlat. Legyen az aktuális érték, a korlát, a növekmény. Az invariáns:
Ezért ábrázolható. Ha , a függvény írás nélkül visszatér. Egyébként:
tehát az összeadás sem csordul túl, és az invariáns megmarad. A
közvetlen u + a > L ellenőrzés hibás lenne, mert az
unsigned összeadás már az összehasonlítás előtt körbefordulhatna.
A nullellenőrzés nem pointervalidálás. A nem null out
írható, megfelelő típusú objektumra mutasson, és ne fedje át az
opciókat. A nem null options valódi, olvasható
opcióobjektumot jelöljön. Az élő handle helyett átadott felszabadított
pointer szerződéssértés, nem garantáltan visszaadható hibakód.
A hívó–hívott allokátorhatár (caller/callee
allocator boundary) szabálya egyszerű: aki foglal, az biztosítja a
hozzá tartozó felszabadító műveletet. A kliens ezért akkor sem hívhat
free(q)-t, ha az aktuális implementáció történetesen
malloc-ot használ. Később pool, más runtime vagy kiegészítő
belső erőforrás is megjelenhet.
A hibaüzenet nem handle-ből kölcsönzött adat: a handle megsemmisítése nem érvényteleníti. A szöveg tartalmára nem építünk programlogikát; erre a numerikus státusz szolgál.
A struct_size C bájtban, vagyis a sizeof
egységében értendő. Nem biztonsági ellenőrzés: a könyvtár nem tudja
megállapítani, hogy a hívó igazat állított-e a rendelkezésre álló
objektum méretéről.
A jelenlegi implementáció az első verzió teljes struktúráját
megköveteli, a nagyobb méretet elfogadja, de az ismeretlen folytatást
nem olvassa. Az api_version az inkompatibilis
szerződésváltást jelöli.
Egy későbbi opcionális mező hozzáadásakor három szabály szükséges:
sizeof értéket.Ha egy mező offsetje , mérete , a közölt struktúraméret pedig , akkor az olvashatóság méretfeltétele:
A kivonásos alak elkerüli az ellenőrzésben az túlcsordulását. Az ABI-tervben azt is biztosítani kell, hogy új mező ne kerüljön a régi struktúra záró paddingjébe: különben a régi méret alapján jelen levőnek látszhatna. Ezt minden támogatott target ABI-ra külön kell kezelni.
Az ismeretlen mezők figyelmen kívül hagyása csak opcionális viselkedésnél elfogadható. Egy kötelező biztonsági beállítást nem szabad csendben elhagyni: ahhoz képességlekérdezés, új függvény vagy elutasítható verzióváltás kell.
A forráskompatibilitás újrafordíthatóságot, a bináris kompatibilitás meglévő gépi kód használhatóságát, a viselkedési kompatibilitás pedig a korábbi vállalások megőrzését jelenti. Az opaque handle csak a belső layoutot választja le ezekről; az exportált függvények és a publikus opcióstruktúra továbbra is ABI-felület.
A következő teljes client.cpp C++20-as program. A
probe objektum tulajdonosa a main; a könyvtár
csak a címét őrzi, és a handle megszűnése előtt minden callback
befejeződik.
#include "quota.h"
#include <cstdio>
struct probe {
unsigned count;
unsigned long last;
};
extern "C" void record_total(void *userdata, unsigned long total)
{
auto *p = static_cast<probe *>(userdata);
++p->count;
p->last = total;
}
int main()
{
probe events{};
quota_options options{};
options.struct_size = sizeof options;
options.api_version = QUOTA_API_VERSION;
options.limit = 10;
options.notify = record_total;
options.userdata = &events;
quota *q = nullptr;
int rc = quota_create(&options, &q);
if (rc != QUOTA_OK) {
std::puts(quota_status_text(rc));
return 1;
}
int first = quota_add(q, 9);
int second = quota_add(q, 2);
quota_destroy(q);
q = nullptr;
std::puts(quota_status_text(second));
return first == QUOTA_OK && second == QUOTA_ELIMIT
&& events.count == 1 && events.last == 9 ? 0 : 1;
}Kijelölt, itt nem ellenőrzött laborprofil: Linux x86-64 System V, LP64; GCC/G++ 14.2.0, glibc 2.40, libstdc++ 14.2.0, GNU binutils 2.43. A parancsok ezekre beállított eszközneveket feltételeznek. A példát itt nem fordítottuk és nem futtattuk.
GCC-kiterjesztés, Linux/ELF: a rejtett
alapláthatóság mellett a QUOTA_API exportálja a publikus
függvényeket.
gcc -std=c17 -Wall -Wextra -Wpedantic -fPIC -fvisibility=hidden \
'-DQUOTA_API=__attribute__((visibility("default")))' \
-c quota.c -o quota.o
gcc -shared quota.o -o libquota.so
g++ -std=c++20 -Wall -Wextra -Wpedantic \
client.cpp -L. -lquota -Wl,-rpath,'$ORIGIN' -o quota-clientA C implementációt C fordítóval készítjük el; a C++ fordító csak a klienst dolgozza fel. A várt működés egy sikeres növelés, egy állapotváltozás nélküli elutasítás és egyetlen callback. Ez elvárt eredmény, nem mérési jegyzőkönyv.
A quota_destroy(q) nem nullázza a hívó változóját: a
pointerparaméter is érték szerint adódik át. A későbbi
q = NULL vagy q = nullptr hasznos helyi
védelem, de más aliasokat nem javít meg.
A userdata megőrzése nem élettartam-hosszabbítás. Ha a
kliens egy már visszatért függvény lokális objektumának címét adta meg,
a későbbi callback érvénytelen hozzáférést végezhet.
Ugyancsak hibás az „opaque, tehát szálbiztos” következtetés. A reprezentáció elrejtése hozzáférési korlát, nem szinkronizáció.
Magyarázó: miért szűnhet meg az opcióstruktúra
közvetlenül a sikeres létrehozás után, miközben a userdata
által jelölt objektum nem? Mi történik a handle másolatával
megsemmisítéskor?
Kódolási: egészítsd ki a C++ klienst úgy, hogy az elutasított növelés után még egy egységet hozzáadjon. Ellenőrizd, hogy a végső callbackérték tíz, a hívásszám kettő.
Diagnosztikai: egy új könyvtármező hozzáadása
után a fejlesztő változatlanul hagyja a
struct_size < sizeof *options ellenőrzést. Miért hibás
ez régi kliensekkel? Miért nem elegendő pusztán eltávolítani az
ellenőrzést?
A create lemásolja az opciók értékeit, ezért az
opcióobjektumot később nem olvassa. A userdata pointerének
másolása viszont nem másolja le a mögöttes objektumot. Annak a
callbackek teljes ideje alatt élnie kell.
Megsemmisítéskor minden handle-alias érvénytelenné válik. Az ellenőrzési feltétel: ne maradjon később használható hozzáférési út a megszűnt objektumhoz.
A kliensben az első két add után, a megsemmisítés és az
eredeti visszatérés helyére kerüljön az alábbi
C++20-részlet:
int third = quota_add(q, 1);
bool ok = first == QUOTA_OK && second == QUOTA_ELIMIT
&& third == QUOTA_OK
&& events.count == 2 && events.last == 10;
quota_destroy(q);
q = nullptr;
return ok ? 0 : 1;A korábbi buildparancs használható. A vizsgált feltétel nem pusztán a hibakód: az elutasított művelet nem fogyaszthat kvótát és nem generálhat callbacket.
Az új sizeof nagyobb lehet a régi kliens által átadott
méretnél, így az ellenőrzés kompatibilis hívást utasítana el.
Eltávolítása viszont a hiányzó mező olvasásához vezethet.
A javítás a régi minimális méret megőrzése, mezőnkénti méretellenőrzés és alapértékadás. Ellenőrzési feltétel, hogy az új könyvtár régi klienssel is működjön, és semmilyen hiányzó mezőt ne olvasson.
A stabil C interfész rejtett állapotból és látható szerződésből áll.
A create/destroy pár rögzíti az ownership- és
allokátorhatárt; a callback, a context és a hibaadat külön
élettartam-szabályt kap. A méretmező támogatja a bővítést, de csak
megőrzött ABI-prefixszel, alapértékekkel és tudatos verziópolitikával.
Az extern "C" előkészíti a C++ fogyasztást; a helyességet
továbbra is az explicit szerződés biztosítja.