Créer votre librairie
Introduction
Si vous avez des fonctions souvent utilisées dans vos programmes ou si vous souhaitez simplement réutiliser vos fonctions dans plusieurs programmes vous avez certainement intérêt à créer votre propre librairie. Cet article explique comment créer une librairie C et comment la compiler.
Avant de vous plonger dans l'exemple posez vous quelques questions :
1) Quels sont les objectifs de votre librairie ?
- Quelle est la principale raison pour laquelle vous souhaitez créer cette librairie ?
- Quelles fonctionnalités spécifiques souhaitez-vous inclure ?
2) Quels sont les besoins de réutilisabilité ?
- À quelle fréquence allez-vous utiliser ces fonctions dans vos différents projets ?
- Y a-t-il des fonctionnalités communes à plusieurs projets qui pourraient être centralisées dans une librairie ?
3) Quelle est la portée de la librairie ?
- La librairie sera-t-elle utilisée uniquement en interne ou sera-t-elle partagée avec d'autres développeurs ?
- Quelle documentation et quels exemples seront nécessaires pour faciliter l'utilisation de la librairie ?
4) Quels types de fonctions ou de modules allez-vous inclure ?
- Quelles sont les fonctions de base que vous envisagez d'inclure dans la librairie ?
- Comment allez-vous organiser ces fonctions en modules ou en catégories ?
5) Quelle est l'architecture cible ?
- La librairie doit-elle être optimisée pour une architecture spécifique (par exemple,
x86_64) ? - Y a-t-il des considérations de portabilité à prendre en compte pour d'autres architectures ?
6) Quels mécanismes de vérification et de gestion des erreurs allez-vous implémenter ?
- Comment allez-vous gérer les erreurs et les exceptions dans vos fonctions ?
- Avez-vous besoin de mécanismes spécifiques pour vérifier les dépassements ou autres conditions critiques ?
7) Comment allez-vous assurer la sécurité du code ?
- Y a-t-il des fonctions internes qui doivent être déclarées
staticpour limiter leur portée ? - Quels sont les risques potentiels de sécurité et comment allez-vous les atténuer ?
8) Quels outils et méthodes de compilation allez-vous utiliser ?
- Quels sont les outils de compilation et les options que vous allez utiliser pour compiler la librairie ?
- Avez-vous besoin de créer des scripts de build ou des fichiers Makefile pour automatiser le processus de compilation ?
9) Comment allez-vous tester et valider votre librairie ?
- Quels types de tests allez-vous mettre en place pour valider les fonctionnalités de la librairie ?
- Avez-vous des critères de performance à respecter et comment allez-vous les mesurer ?
10) Comment allez-vous documenter et maintenir la librairie ?
- Avez-vous un plan pour documenter chaque fonction et module de la librairie ?
- Comment allez-vous gérer les mises à jour et les améliorations futures de la librairie ?
Et cette liste n'est pas exhaustive. Croyez-moi, plus vous prendrez de recul avant de vous lancer dans le développement de votre librairie, moins vous rencontrerez de difficutés à la réaliser et plus elle sera pertinente.
Exemple
Pour créer un exemple qui change un peu, nous allons créer une bibliothèque avec une fonction permettant de faire
la somme entre deux entiers int. Mais, à la différence des autres tutoriels que vous trouverez sur la toile,
le code que je vous fournis ici permet de s'assurer qu'il n'y a pas de dépassement des limites lorsque la
somme est effectuée.
Le code devient ainsi plus robuste et en contrepartie ces vérifications le rende plus complexe ce qui justifie encore plus la nécessité de le mettre dans une librairie.
Enfin, la librairie est d'abord proposée dans une version qui propose deux fonctions permettant d'effectuer la somme de deux entiers de manière sécurisée.
L'une int safe_sum_int(int a, int b) est écrite en pure language C et la seconde
int safe_sum_int_asm(int a, int b) tente de tirer partie de la plateforme x86_64 en
implémentant du code assembleur embarqué.
Cette première version est spécifiquement faite pour effectuer un benchmark qui permettra de déterminer quelle technique est la plus rapide.
Le code source du programme de benshmark se trouve à la suite du code de la librairie avec les directives nécessaires permettant de le compiler.
Code sources
libsum.h:
#ifndef LIBSUM_H
#define LIBSUM_H
#include <stdbool.h>
bool is_sum_int_overflow(int a, int b);
int safe_sum_int(int a, int b);
int safe_sum_int_asm(int a, int b);
# endif // LIBSUM_H
libsum.c:
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
bool is_sum_int_overflow(int a, int b)
{
if ((a > 0 && b > 0 && a > INT_MAX - b) ||
(a < 0 && b < 0 && a < INT_MIN - b)) {
return false;
}
return true;
}
int safe_sum_int(int a, int b)
{
if( is_sum_int_overflow(a, b) )
{
return a + b;
} else {
fprintf(stderr, "Overflow or underflow detected!\n");
exit(EXIT_FAILURE);
}
}
int safe_sum_int_asm(int a, int b) {
int result;
int overflow_flag;
__asm__ (
"movl %2, %%eax\n\t" // Mettre a dans eax
"addl %3, %%eax\n\t" // Ajouter b à eax
"jno no_overflow\n\t" // S'il n'y a pas de dépassement, sauter à no_overflow
"movl $1, %1\n\t" // Mettre 1 dans overflow_flag (dépassement)
"jmp end\n\t" // Sauter à la fin
"no_overflow:\n\t"
"movl %%eax, %0\n\t" // Mettre le résultat dans result
"movl $0, %1\n\t" // Mettre 0 dans overflow_flag (pas de dépassement)
"end:"
: "=r" (result), "=r" (overflow_flag)
: "r" (a), "r" (b)
: "%eax"
);
if (overflow_flag) {
fprintf(stderr, "Overflow detected!\n");
exit(EXIT_FAILURE);
}
return result;
}
main.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "libsum.h"
#define NUM_ITERATIONS 1000000
void benchmark_sum_functions() {
int a, b;
clock_t start, end;
double cpu_time_used;
srand(time(NULL));
start = clock();
for (int i = 0; i < NUM_ITERATIONS; i++) {
a = rand() % 1000;
b = rand() % 1000;
safe_sum_int(a, b);
}
end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("Time taken by safe_sum_int: %f seconds\n", cpu_time_used);
start = clock();
for (int i = 0; i < NUM_ITERATIONS; i++) {
a = rand() % 1000;
b = rand() % 1000;
safe_sum_int_asm(a, b);
}
end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("Time taken by safe_sum_int_asm: %f seconds\n", cpu_time_used);
}
int main() {
benchmark_sum_functions();
return 0;
}
Compilation
Ci-dessous, les indicommandes pour compiler le code:
gcc -c libsum.c
gcc -c main.c
gcc -o benchlibsum main.o libsum.o
Pour le moment les commandes de compilations restent simples. Gardez en tête que des options supplémentaires seraient utilisées pour produire un exécutable pour un environnement de production avec des exigences critiques.
Résultats
Voici un exemple de sortie de programme:
Time taken by safe_sum_int: 0.033516 seconds
Time taken by safe_sum_int_asm: 0.027813 seconds
Ici, on perçoit un léger avantage pour le code avec de l'assembleur embarqué.
Remarque
Le code ci-dessus est destiné à réaliser un benchmark. Par contre il faut rester conscient que l'usage de code assembleur peut rendre le code moins portable. Mais, avec un peu d'imagination et surtout avec les macros du pré-processeur il est possible de trouver un compromis entre portabilité et performance.
Portabilité du code
Pour rendre le code portable tout en tirant parti des optimisations spécifiques à l'architecture x86_64 lorsque c'est possible, nous pouvons utiliser des macros conditionnelles.
Version finale
Voici une nouvelle version du code de notre petite librairie:
libsum.h
#ifndef LIBSUM_H
#define LIBSUM_H
#include <stdbool.h>
int safe_sum_int(int a, int b);
# endif // LIBSUM_H
La nouvelle interface de la librairie ne contient plus que la fonction permettant de faire une somme de deux int de
manière robuste en s'assurant qu'il n'y a pas de dépassement.
libsum.c:
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __x86_64__
#define IS_X86_64 1
#else
#define IS_X86_64 0
#endif
#if IS_X86_64
int safe_sum_int(int a, int b) {
int result;
int overflow_flag;
__asm__ (
"movl %2, %%eax\n\t" // Mettre a dans eax
"addl %3, %%eax\n\t" // Ajouter b à eax
"jno no_overflow\n\t" // S'il n'y a pas de dépassement, sauter à no_overflow
"movl $1, %1\n\t" // Mettre 1 dans overflow_flag (dépassement)
"jmp end\n\t" // Sauter à la fin
"no_overflow:\n\t"
"movl %%eax, %0\n\t" // Mettre le résultat dans result
"movl $0, %1\n\t" // Mettre 0 dans overflow_flag (pas de dépassement)
"end:"
: "=r" (result), "=r" (overflow_flag)
: "r" (a), "r" (b)
: "%eax"
);
if (overflow_flag) {
fprintf(stderr, "Overflow detected!\n");
exit(EXIT_FAILURE);
}
return result;
}
#else
static bool is_sum_int_overflow(int a, int b)
{
if ((a > 0 && b > 0 && a > INT_MAX - b) ||
(a < 0 && b < 0 && a < INT_MIN - b)) {
return false;
}
return true;
}
int safe_sum_int(int a, int b)
{
if( is_sum_int_overflow(a, b) )
{
return a + b;
} else {
fprintf(stderr, "Overflow or underflow detected!\n");
exit(EXIT_FAILURE);
}
}
#endif
Dans cette nouvelle implémentation, les macros permettent de détecter l'architecture et de compiler la version la plus appropriée.
Au passage, la fonction is_sum_int_overflow() est déclarée static. Ce mot permet de faire en sorte qu'une fonction ne soit visible que depuis le code présent dans le même fichier source. Cela évite toute collision
de noms de fonctions avec d'autres librairies ou le programme principal lui-même.
Cette modification permet de maintenir la portabilité d'un code robuste tout en utilisant les optimisations
spécifiques à x86_64 lorsqu'elles sont disponibles.