A C++-ban írt könyvtár egyik legértékesebb tulajdonsága lehet, hogy C
fogyasztók számára is használható marad. Ez a DevOps-gyakorlatban
különösen fontos: a C ABI a legstabilabb közös nevező a nyelvek, a
shared library határok és a különböző build-rendszerek között. A C++
oldalon megmaradhat a típusos, RAII-alapú implementáció, miközben a
publikált felület szándékosan szűk, C-kompatibilis és hosszú távon
verziózható marad. Ez az alfejezet a kétnyelvű header és az
extern "C" határ szerződését tárgyalja; a névtorzítás és a
C++ runtime ABI általános kérdéseit Névtorzítás,
layout és C++ runtime ABI (36.1), a stabil shared library határt és
verziózást Stabil shared library határ és verziózás
(36.3), a Python FFI-kapcsolatot Python és más
nyelvek FFI-kapcsolata (36.4), a laborellenőrzést pedig ABI- és FFI-labor, kompatibilitási teszt (36.5)
tárgyalja.
A C++ standardban a függvények és külső nevek language
linkage-gel rendelkeznek. Az extern "C" nyelvi
kapcsolás azt írja elő, hogy a deklarált függvény a C nyelvi kapcsolás
szabályai szerint legyen elérhető. A gyakorlatban ez legtöbbször a C++
névtorzítás (name mangling) kikapcsolását és a platform C hívási
konvenciójának használatát jelenti. Fontos, hogy az
extern "C" nem változtatja meg a függvény szignatúráját: a
paraméterek és a visszatérési típus ugyanaz marad, csak a külső név és a
hívási szerződés lesz C-kompatibilis. Az osztályok, sablonok, overloadok
és referenciák nem válnak automatikusan C-kompatibilissé attól, hogy egy
extern "C" blokk közelében vannak.
A kétnyelvű header két őrt használ. A header guard
(#ifndef PARSER_H) a többszörös include ellen véd, a
#ifdef __cplusplus pedig azt biztosítja, hogy az
extern "C" blokk csak C++ fordításban jelenjen meg. A C
fordító nem definiálja a __cplusplus makrót, a C++ fordító
viszont igen; C++20-ban az értéke jellemzően 202002L. A
helyes minta:
#ifndef PARSER_H
#define PARSER_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* C-kompatibilis deklarációk */
#ifdef __cplusplus
}
#endif
#endifA headerben csak olyan deklarációk szerepelhetnek, amelyeket a C
fordító is elfogad: typedef, struct tag,
enum, függvénydeklaráció, const char *,
size_t, void. Kerülni kell a
bool-t (C17-ben csak <stdbool.h>-val,
C23-ban kulcsszó), a nullptr-t, az auto-t, a
referenciákat, a default argumenteket és a noexcept-ot;
utóbbit makróval lehet C++ módban noexcept-ká tenni. A C
nem tud C++ osztályt, sablont vagy overloadot fogyasztani, mert a C
szintaxis nem ismeri ezeket, a C++ ABI pedig ezekhez nem ad
C-kompatibilis nevet. Ezért a közös felület csak függvényekből, opaque
pointerekből, C-kompatibilis structokból és enumokból állhat.
Az opaque handle lényege, hogy a C oldal csak egy előre deklarált
struct pointerét látja, a méretet és a layoutot nem. A
typedef struct parser_ctx parser_ctx; deklaráció C és C++
módban is működik; a struct definíciója a C++ implementációban marad. A
C oldal így nem tud sizeof(parser_ctx)-t venni, nem tud
stacken példányt létrehozni, és nem tud a belső tagokhoz hozzáférni. Az
életciklust explicit create/destroy függvények
kezelik; ez az Opaque handle és stabil C könyvtári
interfész (10.4) mintájára épül.
A példa egy egyszerű parser könyvtár. A header a teljes nyilvános
szerződés; a C++ implementáció részletei elrejthetők. A
PARSER_API makró a dinamikus exportálhatóságot jelöli, a
PARSER_NOEXCEPT pedig C++ módban noexcept-ot
ad. A PARSER_BUILD_SHARED makrót a könyvtár fordításakor
definiáljuk; Windowson ez dönti el, hogy dllexport vagy
dllimport legyen.
#ifndef PARSER_H
#define PARSER_H
#include <stddef.h>
#if defined(_WIN32)
# if defined(PARSER_BUILD_SHARED)
# define PARSER_API __declspec(dllexport)
# else
# define PARSER_API __declspec(dllimport)
# endif
#elif defined(__GNUC__) || defined(__clang__)
# define PARSER_API __attribute__((visibility("default")))
#else
# define PARSER_API
#endif
#ifdef __cplusplus
# define PARSER_NOEXCEPT noexcept
extern "C" {
#else
# define PARSER_NOEXCEPT
#endif
typedef struct parser_ctx parser_ctx;
typedef enum parser_status {
PARSER_OK = 0,
PARSER_EINVAL = 1,
PARSER_ENOMEM = 2,
PARSER_ELIMIT = 3,
PARSER_ESYNTAX = 4
} parser_status;
PARSER_API parser_ctx *parser_create(void) PARSER_NOEXCEPT;
PARSER_API void parser_destroy(parser_ctx *ctx) PARSER_NOEXCEPT;
PARSER_API parser_status parser_feed(parser_ctx *ctx,
const char *data,
size_t len) PARSER_NOEXCEPT;
PARSER_API parser_status parser_finish(parser_ctx *ctx) PARSER_NOEXCEPT;
PARSER_API const char *parser_version(void) PARSER_NOEXCEPT;
#ifdef __cplusplus
}
#endif
#endifA PARSER_API és a PARSER_NOEXCEPT makrók a
header guardon belül vannak, így egyszer definiálódnak. A
PARSER_NOEXCEPT a C oldalon üres, a C++ oldalon
noexcept; a C++ hívó így tudja, hogy a függvény nem dob. A
parser_version statikus élettartamú stringre mutat, ezért a
C hívó nem szabadítja fel.
A visibility és a linkage két külön dolog. Az extern "C"
a név és a hívási konvenció szintjén teremt C-kompatibilitást; a
visibility azt szabályozza, hogy a szimbólum bekerül-e a dinamikus
szimbólumtáblába. Ha -fvisibility=hidden mellett fordítunk,
minden szimbólum rejtett lesz, kivéve a
visibility("default")-tal jelölteket. Az
extern "C" önmagában nem tesz egy szimbólumot exportálttá.
A static függvény belső linkage-ű, és nem exportált; ezért
a könyvtár belső segédfüggvényeit static-nak vagy névtelen
névtérbe zártnak kell tenni.
A C++ implementációban a parser_ctx teljes definíciója
szerepel, a C oldal számára láthatatlanul. A std::string és
a bool csak a C++ oldalon jelenik meg. A
parser_create new-val allokál, a
parser_destroy delete-tel szabadít; a C hívó
soha nem hívhat free-t a pointerre. Ez az allocator pairing
alapja: a memóriát az a modul szabadítja fel, amelyik allokálta,
ugyanazzal az allocatorral. Windows DLL-határon ez különösen kritikus,
mert a DLL és az EXE eltérő CRT-heapet használhat.
#include "parser.h"
#include <cstddef>
#include <new>
#include <stdexcept>
#include <string>
struct parser_ctx {
std::string input;
bool finished = false;
};
extern "C" PARSER_API parser_ctx *parser_create(void) PARSER_NOEXCEPT {
try {
return new parser_ctx();
} catch (...) {
return nullptr;
}
}
extern "C" PARSER_API void parser_destroy(parser_ctx *ctx) PARSER_NOEXCEPT {
delete ctx;
}
extern "C" PARSER_API parser_status
parser_feed(parser_ctx *ctx, const char *data, size_t len) PARSER_NOEXCEPT {
if (ctx == nullptr) {
return PARSER_EINVAL;
}
if (data == nullptr) {
return len == 0 ? PARSER_OK : PARSER_EINVAL;
}
try {
ctx->input.append(data, len);
return PARSER_OK;
} catch (const std::bad_alloc &) {
return PARSER_ENOMEM;
} catch (const std::length_error &) {
return PARSER_ELIMIT;
} catch (...) {
return PARSER_ESYNTAX;
}
}
extern "C" PARSER_API parser_status
parser_finish(parser_ctx *ctx) PARSER_NOEXCEPT {
if (ctx == nullptr) {
return PARSER_EINVAL;
}
try {
ctx->finished = true;
return PARSER_OK;
} catch (...) {
return PARSER_ESYNTAX;
}
}
extern "C" PARSER_API const char *parser_version(void) PARSER_NOEXCEPT {
return "1.0";
}A try/catch minden extern "C" függvényben
kötelező elem, ha a C hívó felé nem akarunk C++ kivételt engedni. A C
hívó nem tud try/catch-et használni; a C ABI-n nincs
kivételkezelési protokoll. A catch (...) a legvégső háló; a
konkrétabb catch-ek hibakódot adnak. A noexcept ezt a
szándékot erősíti: ha mégis kijutna egy kivétel, a C++ runtime
std::terminate-et hív. A parser_destroy
destruktora nem dobhat; a std::string destruktora ezt
biztosítja. A noexcept határok és hibapolicy
(25.3) rész a noexcept és a hibapolicy általános
kérdéseit tárgyalja; itt a C határra alkalmazzuk.
A parser_feed a bemenetet másolja, nem tárolja a C hívó
pointerét. Így a C hívó a data puffert a hívás után
felszabadíthatja. A parser_finish a példában nem ad vissza
belső pointert; a 2. feladatban majd igen, és ott a pointer élettartamát
a szerződésben rögzíteni kell. A parser_ctx élettartama
alatt a belső buffer érvényes, de a következő módosító hívás
invalidálhatja. A C hívó nem szabadíthatja fel a belső buffert.
A C fogyasztó a headert C módban includes; a NULL és a
strlen a C könyvtárból jön. A C++ fogyasztó ugyanazt a
headert használja; a extern "C" miatt a hívás C linkage-ű.
A C++ fogyasztó a parser_ctx-et nem tudja
delete-elni, csak a parser_destroy-t
hívhatja.
#include "parser.h"
#include <stdio.h>
#include <string.h>
int main(void) {
parser_ctx *ctx = parser_create();
if (ctx == NULL) {
fputs("parser_create failed\n", stderr);
return 1;
}
const char *chunk = "int x = 42;";
parser_status st = parser_feed(ctx, chunk, strlen(chunk));
if (st != PARSER_OK) {
fprintf(stderr, "feed failed: %d\n", (int)st);
parser_destroy(ctx);
return 2;
}
st = parser_finish(ctx);
parser_destroy(ctx);
return st == PARSER_OK ? 0 : 3;
}#include "parser.h"
#include <cstring>
#include <iostream>
int main() {
parser_ctx *ctx = parser_create();
if (!ctx) {
return 1;
}
const char *chunk = "int x = 42;";
parser_status st = parser_feed(ctx, chunk, std::strlen(chunk));
parser_destroy(ctx);
if (st != PARSER_OK) {
std::cerr << "feed failed: " << static_cast<int>(st) << '\n';
return 2;
}
return 0;
}A könyvtárat C++20-cal, a fogyasztókat C17-tel és C++20-cal
fordítjuk. A -fPIC a shared libraryhoz kell, a
-fvisibility=hidden a rejtett szimbólumokhoz, a
PARSER_BUILD_SHARED a Windows export makróhoz. A C
fogyasztó nem linkel -lstdc++-t; a
libparser.so dinamikus függőségeként a C++ runtime
betöltődik. Statikus linkelésnél ezt külön kell kezelni.
# C++ könyvtár
g++ -std=c++20 -fPIC -fvisibility=hidden -DPARSER_BUILD_SHARED -I. \
-c parser.cpp -o parser.o
g++ -shared -o libparser.so parser.o
# C fogyasztó
gcc -std=c17 -I. -c consumer.c -o consumer.o
gcc consumer.o -L. -Wl,-rpath,'$ORIGIN' -lparser -o consumer
# C++ fogyasztó
g++ -std=c++20 -I. -c consumer.cpp -o consumer_cpp.o
g++ consumer_cpp.o -L. -Wl,-rpath,'$ORIGIN' -lparser -o consumer_cpp
# Ellenőrzés
nm -D --defined-only libparser.so | grep parser_
./consumer
./consumer_cppAz nm -D a dinamikus szimbólumtáblát mutatja. Ha a
parser_create helyett _Z13parser_createv-t
látunk, akkor az extern "C" hiányzik vagy nem érvényesül.
Ha a parser_create egyáltalán nem látszik, akkor a
visibility rejtett, vagy a PARSER_API hiányzik. A
c++filt a mangled nevek visszafejtésére használható. A
readelf -Ws libparser.so | grep parser_ a verziózott
szimbólumokat is megmutatja. A konkrét kimenet target- és
toolchainfüggő; a példa a szerkezetet illusztrálja, nem állítja, hogy a
fenti parancsok eredményét itt ellenőriztük.
| Fogalom | Mit szabályoz | Hol jelenik meg | Tipikus hiba |
|---|---|---|---|
| language linkage | név és hívási konvenció | extern "C" |
hiányzik: mangled név, linkelési hiba |
| visibility | dinamikus szimbólumtábla | PARSER_API, -fvisibility=hidden |
hiányzik: nem exportált szimbólum |
| allocator pairing | ki szabadítja fel a memóriát | create/destroy |
free a new pointerére |
| exception boundary | kivétel átlépése a C határon | try/catch, noexcept |
kivétel a C hívó felé |
| opaque handle | layout elrejtése | előre deklarált struct | a struct definíciója a headerben |
A leggyakoribb hiba, hogy a közös headerbe C++-os elem kerül:
std::string, class, template,
bool, nullptr vagy referencia. A C fordító ezt
nem érti; a header nem lesz kétnyelvű. A javítás: a közös header csak
C-kompatibilis deklarációkat tartalmaz, a C++ részletek a
parser.cpp-be kerülnek.
A második hiba az extern "C" hiánya. A C++ fordító
mangled nevet generál, a C linker nem találja a
parser_create szimbólumot. A javítás: a deklaráció és a
definíció is legyen extern "C" hatókörben; a headerben a
#ifdef __cplusplus blokk, a definíciónál explicit
extern "C".
A harmadik hiba a visibility összekeverése a linkage-pel. Az
extern "C" önmagában nem exportál;
-fvisibility=hidden mellett a PARSER_API kell.
A javítás: a nyilvános függvények deklarációján és definícióján is
legyen PARSER_API, és a build definiálja a
PARSER_BUILD_SHARED makrót ott, ahol kell.
A negyedik hiba a kivétel átengedése a C határon. A C hívó nem tudja
elkapni; a viselkedés nem hordozható. A javítás: minden
extern "C" függvényben try/catch, hibakód
visszaadása, és a noexcept tudatos használata.
Az ötödik hiba az allocator mismatch: a C oldal free-vel
szabadítja a C++ new pointerét, vagy fordítva. A javítás: a
parser_destroy végezze a felszabadítást; a C oldal ne
hívjon free-t a parser_ctx*-re. Windows
DLL-határon a memóriát abban a modulban kell felszabadítani, amelyik
allokálta, vagy közös allocator API-t kell használni.
A hatodik hiba az opaque handle megsértése: a
struct parser_ctx definíciója bekerül a közös headerbe.
Ettől a C oldal látja a layoutot, és a könyvtár ABI-változása esetén a C
fogyasztó újrafordítása nélkül is elromolhat. A javítás: a headerben
csak typedef struct parser_ctx parser_ctx; szerepeljen.
A hetedik hiba a parser_feed(nullptr, 0) hívása. A
std::string::append(nullptr, 0) nem hordozható, mert a
pointerre vonatkozó előfeltétel sérülhet. A javítás: a
nullptr és a nulla hossz külön kezelendő, ahogy a
példában.
Alapfeladat. Add hozzá a
parser_version függvényt a headerhez és az implementációhoz
úgy, hogy C és C++ módban is forduljon, és a visszaadott pointer
statikus élettartamú legyen. Ellenőrizd, hogy a nm -D
mutatja a parser_version szimbólumot, és hogy a C fogyasztó
ki tudja írni az értékét.
Közepes feladat. Módosítsd a
parser_finish függvényt úgy, hogy egy
parser_result structot adjon vissza, amely tartalmaz egy
const char *data pointert, egy size_t len
mezőt és egy parser_status status mezőt. A
data a parser_ctx belső bufferére mutasson, és
a parser_ctx élettartamáig legyen érvényes; a C hívó ne
szabadítsa fel. A parser_ctx továbbra is opaque maradjon.
Írd le a pointer élettartamára vonatkozó szerződést, és készíts C
fogyasztót, amely kiírja a resultot.
Haladó feladat. Készíts Parser nevű
C++ RAII wrappert a C API köré. A wrapper egy
std::unique_ptr-rel tartsa a parser_ctx*-et,
saját deleterrel, amely a parser_destroy-t hívja. A
feed és finish hibakódját alakítsd
std::system_error-ré. A wrapper soha ne engedjen kivételt a
C API felé. Írj rövid C++ tesztet, amely ellenőrzi a sikeres és a hibás
feed viselkedését, valamint azt, hogy a wrapper destruktora
felszabadítja az erőforrást.
A headerben a deklaráció a többi függvény mellett:
PARSER_API const char *parser_version(void) PARSER_NOEXCEPT;Az implementációban:
extern "C" PARSER_API const char *parser_version(void) PARSER_NOEXCEPT {
return "1.0";
}A string literál statikus élettartamú, ezért a pointer a program teljes futása alatt érvényes; a C hívó nem szabadítja fel. A C fogyasztóban:
printf("parser version: %s\n", parser_version());Az ellenőrzés feltétele: a header C17 és C++20 módban is fordul; a
nm -D --defined-only libparser.so | grep parser_version
mutatja a szimbólumot; a C fogyasztó helyesen írja ki az
1.0-t. A PARSER_NOEXCEPT a C oldalon üres, a
C++ oldalon noexcept; a függvény nem dob.
A headerben a parser_finish deklarációja és a result
struct:
typedef struct parser_result {
const char *data;
size_t len;
parser_status status;
} parser_result;
PARSER_API parser_result parser_finish(parser_ctx *ctx) PARSER_NOEXCEPT;Az implementációban:
extern "C" PARSER_API parser_result
parser_finish(parser_ctx *ctx) PARSER_NOEXCEPT {
parser_result r{nullptr, 0, PARSER_EINVAL};
if (ctx == nullptr) {
return r;
}
try {
ctx->finished = true;
r.data = ctx->input.data();
r.len = ctx->input.size();
r.status = PARSER_OK;
return r;
} catch (...) {
r.status = PARSER_ESYNTAX;
return r;
}
}A parser_result C-kompatibilis struct; a C++ oldalon
aggregate inicializálható. A data a
ctx->input belső bufferére mutat, ezért a
parser_ctx élettartamáig érvényes, de a következő
parser_feed vagy parser_finish hívás
invalidálhatja. A C hívó a resultot addig használhatja, amíg a
parser_ctx-et nem módosítja és nem destruálja. A C
fogyasztó:
parser_result r = parser_finish(ctx);
if (r.status == PARSER_OK) {
fwrite(r.data, 1, r.len, stdout);
}
parser_destroy(ctx);Az ellenőrzés feltétele: a parser_result layoutja a C és
C++ oldalon ugyanaz; a parser_finish nem dob; a
data pointer a parser_ctx élettartamához
kötött; a C hívó nem hív free-t a data-ra.
A wrapper a C API fölé épül, a C API-t változatlanul hagyja:
#include "parser.h"
#include <cstddef>
#include <memory>
#include <new>
#include <system_error>
namespace {
struct parser_deleter {
void operator()(parser_ctx *p) const noexcept {
parser_destroy(p);
}
};
} // namespace
class Parser {
public:
Parser() : ctx_(parser_create()) {
if (!ctx_) {
throw std::bad_alloc();
}
}
Parser(const Parser &) = delete;
Parser &operator=(const Parser &) = delete;
Parser(Parser &&) noexcept = default;
Parser &operator=(Parser &&) noexcept = default;
void feed(const char *data, std::size_t len) {
parser_status st = parser_feed(ctx_.get(), data, len);
if (st != PARSER_OK) {
throw std::system_error(static_cast<int>(st),
std::generic_category(),
"parser_feed");
}
}
void finish() {
parser_status st = parser_finish(ctx_.get());
if (st != PARSER_OK) {
throw std::system_error(static_cast<int>(st),
std::generic_category(),
"parser_finish");
}
}
private:
std::unique_ptr<parser_ctx, parser_deleter> ctx_;
};A parser_deleter noexcept, a
parser_destroy is az; a unique_ptr destruktora
így nem dob. A Parser copy tiltott, move engedélyezett. A
feed és finish kivételt dob a C++ hívó felé,
de ez már a C++ oldalon történik; a C API határán a
try/catch és a hibakód véd. A
std::system_error a parser_status int értékét
használja; a std::generic_category nem ad specifikus
üzenetet, de a hibakódot hordozza. A teszt:
#include "parser.h"
#include <cassert>
#include <string>
int main() {
Parser p;
const std::string chunk = "int x = 42;";
p.feed(chunk.data(), chunk.size());
p.finish();
bool threw = false;
try {
p.feed(nullptr, 1);
} catch (const std::system_error &) {
threw = true;
}
assert(threw);
return 0;
}Az ellenőrzés feltétele: a wrapper destruktora a
parser_destroy-t hívja; a sikeres feed nem
dob; a nullptr, 1 bemenet PARSER_EINVAL-t ad,
amelyet a wrapper std::system_error-ré alakít; a C API felé
nem lép ki kivétel. A Parser move műveletei a
unique_ptr miatt működnek; a copy tiltása megakadályozza a
dupla felszabadítást.
A kétnyelvű header lényege, hogy a közös felület C-kompatibilis
maradjon: header guard, #ifdef __cplusplus körüli
extern "C" blokk, opaque handle, C-kompatibilis structok és
enumok. Az extern "C" a nyelvi linkage-et állítja be, a
visibility és az export makrók ettől függetlenül szabályozzák a
dinamikus szimbólumtáblát. A C++ implementáció a
try/catch-csel és hibakódokkal védi a C határt; a
noexcept tudatos használata erősíti a szerződést. Az
allocator pairing szerint a memóriát az allokáló modul szabadítja fel, a
create/destroy páros ezt biztosítja. A C oldal
soha nem lát C++ osztályt, sablont vagy overloadot; a C++ oldal belső
részletei a shared library határ mögött maradnak. A build és az
nm/readelf ellenőrzés a linkage és a
visibility hibáit különíti el; a konkrét ABI- és szabványhivatkozásokat
a laborprofilban kell ellenőrizni.