Exemples d'implémentation¶
Cette section présente deux projets complets : une API REST CRUD en C++ avec cpp-httplib, SQLite3 et nlohmann/json, puis une bibliotheque de calcul en C pur avec des bindings Python (ctypes) et Rust (FFI). Les deux projets sont fonctionnels et suivent les conventions de production.
Projet 1 — API REST CRUD avec cpp-httplib¶
Structure du projet¶
api/
├── CMakeLists.txt
├── src/
│ ├── main.cpp # Point d'entree, demarrage du serveur
│ ├── database.hpp # Gestion SQLite3 (C API via RAII)
│ ├── database.cpp
│ ├── handlers.hpp # Declarations des handlers HTTP
│ └── handlers.cpp # Implementation GET/POST/PUT/DELETE
└── third_party/
├── httplib.h # cpp-httplib (header-only)
└── json.hpp # nlohmann/json (header-only)
database.hpp — Wrapper RAII autour de SQLite3¶
// database.hpp
// Wrapper C++ RAII autour de l'API C de SQLite3
#pragma once
#include <sqlite3.h>
#include <string>
#include <vector>
#include <optional>
#include <stdexcept>
// Representation d'un item en memoire
struct Item {
int id = 0;
std::string nom;
double prix = 0.0;
int stock = 0;
};
// Exception levee en cas d'erreur SQLite
class DbException : public std::runtime_error {
public:
explicit DbException(const std::string& msg) : std::runtime_error(msg) {}
};
class Database {
public:
// Constructeur : ouvre ou cree le fichier SQLite et cree la table si necessaire
explicit Database(const std::string& chemin);
// Destructeur : ferme la connexion (RAII)
~Database();
// Pas de copie — une seule connexion par instance
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
std::vector<Item> listerItems() const;
std::optional<Item> trouverItem(int id) const;
Item creerItem(const std::string& nom, double prix, int stock);
bool mettreAJourItem(const Item& item);
bool supprimerItem(int id);
private:
sqlite3* db_ = nullptr;
// Execution d'une requete sans resultat (INSERT, UPDATE, DELETE, CREATE)
void executer(const std::string& sql);
};
database.cpp — Implémentation¶
// database.cpp
#include "database.hpp"
#include <stdexcept>
Database::Database(const std::string& chemin) {
int rc = sqlite3_open(chemin.c_str(), &db_);
if (rc != SQLITE_OK) {
throw DbException(std::string("Impossible d'ouvrir la DB : ") + sqlite3_errmsg(db_));
}
executer(R"(
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
prix REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0
)
)");
}
Database::~Database() {
if (db_) sqlite3_close(db_);
}
void Database::executer(const std::string& sql) {
char* msg = nullptr;
int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &msg);
if (rc != SQLITE_OK) {
std::string err(msg ? msg : "Erreur inconnue");
sqlite3_free(msg);
throw DbException(err);
}
}
std::vector<Item> Database::listerItems() const {
std::vector<Item> items;
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db_, "SELECT id, nom, prix, stock FROM items", -1, &stmt, nullptr);
while (sqlite3_step(stmt) == SQLITE_ROW) {
Item item;
item.id = sqlite3_column_int(stmt, 0);
item.nom = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
item.prix = sqlite3_column_double(stmt, 2);
item.stock = sqlite3_column_int(stmt, 3);
items.push_back(std::move(item));
}
sqlite3_finalize(stmt);
return items;
}
std::optional<Item> Database::trouverItem(int id) const {
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db_, "SELECT id, nom, prix, stock FROM items WHERE id = ?",
-1, &stmt, nullptr);
sqlite3_bind_int(stmt, 1, id);
std::optional<Item> resultat;
if (sqlite3_step(stmt) == SQLITE_ROW) {
Item item;
item.id = sqlite3_column_int(stmt, 0);
item.nom = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
item.prix = sqlite3_column_double(stmt, 2);
item.stock = sqlite3_column_int(stmt, 3);
resultat = std::move(item);
}
sqlite3_finalize(stmt);
return resultat;
}
Item Database::creerItem(const std::string& nom, double prix, int stock) {
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db_,
"INSERT INTO items (nom, prix, stock) VALUES (?, ?, ?)", -1, &stmt, nullptr);
sqlite3_bind_text(stmt, 1, nom.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_double(stmt, 2, prix);
sqlite3_bind_int(stmt, 3, stock);
if (sqlite3_step(stmt) != SQLITE_DONE)
throw DbException("Echec insertion");
sqlite3_finalize(stmt);
Item item;
item.id = static_cast<int>(sqlite3_last_insert_rowid(db_));
item.nom = nom;
item.prix = prix;
item.stock = stock;
return item;
}
bool Database::mettreAJourItem(const Item& item) {
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db_,
"UPDATE items SET nom=?, prix=?, stock=? WHERE id=?", -1, &stmt, nullptr);
sqlite3_bind_text(stmt, 1, item.nom.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_double(stmt, 2, item.prix);
sqlite3_bind_int(stmt, 3, item.stock);
sqlite3_bind_int(stmt, 4, item.id);
sqlite3_step(stmt);
int rows = sqlite3_changes(db_);
sqlite3_finalize(stmt);
return rows > 0;
}
bool Database::supprimerItem(int id) {
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db_, "DELETE FROM items WHERE id = ?", -1, &stmt, nullptr);
sqlite3_bind_int(stmt, 1, id);
sqlite3_step(stmt);
int rows = sqlite3_changes(db_);
sqlite3_finalize(stmt);
return rows > 0;
}
handlers.cpp — Routes HTTP CRUD¶
// handlers.cpp
// Compilation : g++ -std=c++17 -Wall -Ithird_party src/*.cpp -lsqlite3 -lpthread
#include "handlers.hpp"
#include "database.hpp"
#include <nlohmann/json.hpp>
#include <httplib.h>
#include <string>
using json = nlohmann::json;
// Serialisation Item -> JSON
static json itemVersJson(const Item& item) {
return {{"id", item.id}, {"nom", item.nom},
{"prix", item.prix}, {"stock", item.stock}};
}
void enregistrerHandlers(httplib::Server& srv, Database& db) {
// GET /items — liste tous les items
srv.Get("/items", [&](const httplib::Request&, httplib::Response& res) {
json tableau = json::array();
for (const auto& item : db.listerItems())
tableau.push_back(itemVersJson(item));
res.set_content(tableau.dump(), "application/json");
});
// GET /items/:id — recupere un item par ID
srv.Get(R"(/items/(\d+))", [&](const httplib::Request& req, httplib::Response& res) {
int id = std::stoi(req.matches[1]);
auto item = db.trouverItem(id);
if (!item) {
res.status = 404;
res.set_content(R"({"error":"Item non trouve"})", "application/json");
return;
}
res.set_content(itemVersJson(*item).dump(), "application/json");
});
// POST /items — cree un nouvel item
srv.Post("/items", [&](const httplib::Request& req, httplib::Response& res) {
try {
auto body = json::parse(req.body);
// Validation des champs obligatoires
if (!body.contains("nom") || !body.contains("prix")) {
res.status = 400;
res.set_content(R"({"error":"Champs 'nom' et 'prix' requis"})",
"application/json");
return;
}
auto item = db.creerItem(
body["nom"].get<std::string>(),
body["prix"].get<double>(),
body.value("stock", 0)
);
res.status = 201;
res.set_content(itemVersJson(item).dump(), "application/json");
} catch (const json::exception& e) {
res.status = 400;
res.set_content(R"({"error":"JSON invalide"})", "application/json");
}
});
// PUT /items/:id — mise a jour complete
srv.Put(R"(/items/(\d+))", [&](const httplib::Request& req, httplib::Response& res) {
int id = std::stoi(req.matches[1]);
try {
auto body = json::parse(req.body);
Item item;
item.id = id;
item.nom = body["nom"].get<std::string>();
item.prix = body["prix"].get<double>();
item.stock = body.value("stock", 0);
if (!db.mettreAJourItem(item)) {
res.status = 404;
res.set_content(R"({"error":"Item non trouve"})", "application/json");
return;
}
res.set_content(itemVersJson(item).dump(), "application/json");
} catch (const json::exception&) {
res.status = 400;
res.set_content(R"({"error":"JSON invalide"})", "application/json");
}
});
// DELETE /items/:id — supprime un item
srv.Delete(R"(/items/(\d+))", [&](const httplib::Request& req, httplib::Response& res) {
int id = std::stoi(req.matches[1]);
if (!db.supprimerItem(id)) {
res.status = 404;
res.set_content(R"({"error":"Item non trouve"})", "application/json");
return;
}
res.status = 204;
});
}
main.cpp et test¶
// main.cpp
#include "handlers.hpp"
#include "database.hpp"
#include <httplib.h>
#include <iostream>
int main() {
Database db("items.db");
httplib::Server srv;
enregistrerHandlers(srv, db);
std::cout << "API demarree sur http://localhost:8080\n";
srv.listen("0.0.0.0", 8080);
return 0;
}
# Test avec curl
curl -X POST http://localhost:8080/items \
-H "Content-Type: application/json" \
-d '{"nom":"Widget","prix":9.99,"stock":100}'
# => {"id":1,"nom":"Widget","prix":9.99,"stock":100}
curl http://localhost:8080/items
# => [{"id":1,"nom":"Widget","prix":9.99,"stock":100}]
curl -X PUT http://localhost:8080/items/1 \
-H "Content-Type: application/json" \
-d '{"nom":"Widget Pro","prix":14.99,"stock":50}'
curl -X DELETE http://localhost:8080/items/1
Projet 2 — Bibliotheque C avec bindings Python et Rust¶
Ce projet illustre le cas d'usage classique : écrire une bibliotheque de calcul performante en C pur et l'exposer a d'autres langages via une ABI stable.
calcul.h — Header public de la bibliotheque¶
/* calcul.h — API publique de la bibliotheque de calcul */
/* La macro CALCUL_API gere l'export/import selon la plateforme */
#pragma once
#include <stddef.h>
#ifdef _WIN32
#ifdef CALCUL_BUILD
#define CALCUL_API __declspec(dllexport)
#else
#define CALCUL_API __declspec(dllimport)
#endif
#else
#define CALCUL_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* Produit scalaire de deux vecteurs de longueur n */
CALCUL_API double calcul_produit_scalaire(const double* a,
const double* b,
size_t n);
/* Norme euclidienne d'un vecteur */
CALCUL_API double calcul_norme(const double* v, size_t n);
/* Multiplication matricielle : C = A * B
A est m x k, B est k x n, C est m x n
C doit etre alloue par l'appelant (m * n doubles) */
CALCUL_API void calcul_matmul(const double* A, const double* B, double* C,
size_t m, size_t k, size_t n);
#ifdef __cplusplus
}
#endif
calcul.c — Implémentation¶
/* calcul.c — Implementation de la bibliotheque de calcul */
/* Compilation : gcc -O2 -shared -fPIC -DCALCUL_BUILD calcul.c -o libcalcul.so */
#define CALCUL_BUILD
#include "calcul.h"
#include <math.h> /* sqrt() */
double calcul_produit_scalaire(const double* a, const double* b, size_t n) {
double resultat = 0.0;
for (size_t i = 0; i < n; ++i)
resultat += a[i] * b[i];
return resultat;
}
double calcul_norme(const double* v, size_t n) {
return sqrt(calcul_produit_scalaire(v, v, n));
}
void calcul_matmul(const double* A, const double* B, double* C,
size_t m, size_t k, size_t n) {
/* Initialisation de C a zero */
for (size_t i = 0; i < m * n; ++i) C[i] = 0.0;
/* Multiplication naive O(m*k*n) */
for (size_t i = 0; i < m; ++i) {
for (size_t p = 0; p < k; ++p) {
double a_ip = A[i * k + p];
for (size_t j = 0; j < n; ++j)
C[i * n + j] += a_ip * B[p * n + j];
}
}
}
Bindings Python (ctypes)¶
# bindings_python.py
# Utilisation de ctypes pour appeler la bibliotheque C depuis Python
# Prerequis : libcalcul.so dans le meme repertoire ou dans LD_LIBRARY_PATH
import ctypes
import os
from typing import List
# Chargement de la bibliotheque partagee
_lib_chemin = os.path.join(os.path.dirname(__file__), "libcalcul.so")
_lib = ctypes.CDLL(_lib_chemin)
# Declaration des signatures de fonctions
_lib.calcul_produit_scalaire.argtypes = [
ctypes.POINTER(ctypes.c_double), # const double* a
ctypes.POINTER(ctypes.c_double), # const double* b
ctypes.c_size_t, # size_t n
]
_lib.calcul_produit_scalaire.restype = ctypes.c_double
_lib.calcul_norme.argtypes = [
ctypes.POINTER(ctypes.c_double),
ctypes.c_size_t,
]
_lib.calcul_norme.restype = ctypes.c_double
def _vers_tableau_c(liste: List[float]) -> ctypes.Array:
"""Convertit une liste Python en tableau C de doubles."""
return (ctypes.c_double * len(liste))(*liste)
def produit_scalaire(a: List[float], b: List[float]) -> float:
"""Produit scalaire de deux vecteurs Python."""
if len(a) != len(b):
raise ValueError("Les vecteurs doivent avoir la meme longueur")
arr_a = _vers_tableau_c(a)
arr_b = _vers_tableau_c(b)
return _lib.calcul_produit_scalaire(arr_a, arr_b, len(a))
def norme(v: List[float]) -> float:
"""Norme euclidienne d'un vecteur Python."""
arr = _vers_tableau_c(v)
return _lib.calcul_norme(arr, len(v))
# --- Test ---
if __name__ == "__main__":
a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]
print(f"Produit scalaire : {produit_scalaire(a, b)}") # 32.0
print(f"Norme de a : {norme(a):.6f}") # 3.741657
Bindings Rust (FFI)¶
// bindings_rust/src/main.rs
// Liaison FFI avec la bibliotheque C libcalcul.so
// Cargo.toml : [dependencies] (rien) + [build-dependencies] cc = "1"
// build.rs : println!("cargo:rustc-link-lib=calcul");
// Declaration de l'interface C (extern "C" desactive le name mangling)
extern "C" {
fn calcul_produit_scalaire(a: *const f64, b: *const f64, n: usize) -> f64;
fn calcul_norme(v: *const f64, n: usize) -> f64;
fn calcul_matmul(a: *const f64, b: *const f64, c: *mut f64,
m: usize, k: usize, n: usize);
}
// Wrappers Rust safe — encapsulent le code unsafe
fn produit_scalaire(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "Vecteurs de longueur differente");
// unsafe : appel C, les pointeurs sont valides car issus de slices Rust
unsafe { calcul_produit_scalaire(a.as_ptr(), b.as_ptr(), a.len()) }
}
fn norme(v: &[f64]) -> f64 {
unsafe { calcul_norme(v.as_ptr(), v.len()) }
}
fn matmul(a: &[f64], b: &[f64], m: usize, k: usize, n: usize) -> Vec<f64> {
let mut c = vec![0.0f64; m * n];
unsafe { calcul_matmul(a.as_ptr(), b.as_ptr(), c.as_mut_ptr(), m, k, n) }
c
}
fn main() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
println!("Produit scalaire : {}", produit_scalaire(&a, &b)); // 32
println!("Norme de a : {:.6}", norme(&a)); // 3.741657
// Multiplication 2x2 * 2x2
let mat_a = vec![1.0, 2.0, 3.0, 4.0];
let mat_b = vec![5.0, 6.0, 7.0, 8.0];
let mat_c = matmul(&mat_a, &mat_b, 2, 2, 2);
println!("Matmul : {:?}", mat_c); // [19, 22, 43, 50]
}
ABI stable en C
L'ABI C (extern "C") est le standard d'interopérabilité entre langages. Rust, Python, Julia, Go et pratiquement tous les langages modernes peuvent appeler une bibliotheque partagee avec une API C. C++ seul ne convient pas : son name mangling dépend du compilateur et de sa version.