Documentation PostgreSQL 7.4.29 | ||||
---|---|---|---|---|
Précédent | Arrière rapide | Chapitre 33. Extension de SQL | Avance rapide | Suivant |
Les fonctions définies par l'utilisateur peuvent être écrites en C (ou dans un langage pouvant être rendu compatible avec C, comme C++). Ces fonctions sont compilées en objets dynamiques chargeables (encore appelés bibliothèques partagées) et sont chargées par le serveur à la demande. Cette caractéristique de chargement dynamique est ce qui distingue les fonctions en << langage C >> des fonctions << internes >> --- les véritables conventions de codage sont essentiellement les mêmes pour les deux. (C'est pourquoi la bibliothèque standard de fonctions internes est une source abondante d'exemples de code pour les fonctions C définies par l'utilisateur.)
Deux différentes conventions d'appel sont actuellement en usage pour les fonctions C. La plus récente convention d'appel, << version 1 >>, est indiquée en écrivant une macro d'appel PG_FUNCTION_INFO_V1() comme illustré ci-après. L'absence d'une telle macro indique une fonction écrite selon l'ancien style (<< version 0 >>). Le nom de langage spécifié dans la commande CREATE FUNCTION est C dans les deux cas. Les fonctions suivant l'ancien style sont maintenant déconseillées en raison de problèmes de portabilité et d'un manque de fonctionnalité, mais elles sont encore supportées pour des raisons de compatibilité.
La première fois qu'une fonction définie par l'utilisateur dans un fichier objet particulier chargeable est appelée dans une session, le chargeur dynamique charge ce fichier objet en mémoire de telle sorte que la fonction peut être appelée. La commande CREATE FUNCTION pour une fonction en C définie par l'utilisateur doit par conséquent spécifier deux éléments d'information pour la fonction : le nom du fichier objet chargeable et le nom en C (lien symbolique) de la fonction spécifique à appeler à l'intérieur de ce fichier objet. Si le nom en C n'est pas explicitement spécifié, il est supposé être le même que le nom de la fonction SQL.
L'algorithme suivant, basé sur le nom donné dans la commande CREATE FUNCTION, est utilisé pour localiser le fichier objet partagé :
Si le nom est un chemin absolu, le fichier est chargé.
Si le nom commence par la chaîne $libdir, cette chaîne est remplacée par le nom du répertoire de la bibliothèque du paquetage PostgreSQL, qui est déterminé au moment de la compilation.
Si le nom ne contient pas de partie répertoire, le fichier est recherché par le chemin spécifié dans la variable de configuration dynamic_library_path.
Dans les autres cas, (nom de fichier non trouvé dans le chemin ou ne contenant pas de partie répertoire non absolu), le chargeur dynamique essaiera d'utiliser le nom donné, ce qui échouera très vraisemblablement (dépendre du répertoire de travail en cours n'est pas fiable).
Si cette séquence ne fonctionne pas, l'extension pour les noms de fichier des bibliothèques partagées spécifique à la plateforme (souvent .so) est ajoutée au nom attribué et la séquence est à nouveau tentée. En cas de nouvel échec, le chargement échoue.
L'identifiant utilisateur sous lequel fonctionne le serveur PostgreSQL doit pouvoir suivre le chemin jusqu'au fichier que vous essayez de charger. Une erreur fréquente revient à définir le fichier ou un répertoire supérieur comme non lisible et/ou non exécutable par l'utilisateur postgres.
Dans tous les cas, le nom de fichier donné dans la commande CREATE FUNCTION est enregistré littéralement dans les catalogues systèmes, de sorte que, si le fichier doit être à nouveau chargé, la même procédure sera appliquée.
Note : PostgreSQL ne compilera pas une fonction C automatiquement. Le fichier objet doit être compilé avant d'être référencé dans une commande CREATE FUNCTION. Voir la Section 33.7.6 pour des informations complémentaires.
Après avoir été utilisé pour la première fois, un fichier objet chargé dynamiquement est conservé en mémoire. Les futurs appels de fonction(s) dans ce fichier pendant la même session provoqueront seulement une légère surcharge due à la consultation d'une table de symboles. Si vous devez forcer le chargement d'un fichier objet, par exemple après une recompilation, utilisez la commande LOAD ou commencez une nouvelle session.
Il est recommandé de localiser les bibliothèques dynamiques soit relativement à $libdir soit au moyen du chemin des bibliothèques dynamiques. Ceci simplifie la mise à jour dans le cas d'une nouvelle installation dans un endroit différent. Le répertoire effectif représenté par $libdir peut être retrouvé avec la commande pg_config --pkglibdir.
Avant la version 7.2 de PostgreSQL, seuls les chemins absolus exacts pouvaient être spécifiés dans la commande CREATE FUNCTION. Cette approche est maintenant déconseillée car elle rend la définition de la fonction inutilement non portable. Il vaut mieux seulement spécifier le nom de la bibliothèque partagée sans chemin ni extension, et laisser le mécanisme de recherche générer cette information.
Pour savoir comment écrire des fonctions en langage C, vous devez savoir comment PostgreSQL représente en interne les types de données de base et comment elles peuvent être passés vers et depuis les fonctions. En interne, PostgreSQL considère un type de base comme un << blob de mémoire >>. Les fonctions que vous définissez sur un type définissent à leur tour la façon que PostgreSQL opère sur lui. C'est-à-dire que PostgreSQL ne fera que conserver et retrouver les données sur le disque et utilisera votre fonction pour entrer, traiter et restituer les données.
Les types de base peuvent avoir un des trois formats internes suivants :
passage par valeur, longueur fixe ;
passage par référence, longueur fixe ;
passage par référence, longueur variable.
Les types par valeur peuvent seulement avoir une longueur de 1, 2 ou 4 octets (également 8 octets si sizeof(Datum) est de huit octets sur votre machine). Vous devriez être attentif lors de la définition de vos types de sorte à qu'ils aient la même taille sur toutes les architectures. Par exemple, le type long est dangereux car il a une taille de quatre octets sur certaines machines et huit octets sur d'autres, alors que le type int est de quatre octets sur la plupart des machines Unix. Une implémentation raisonnable du type int4 sur une machine Unix pourrait être
/* entier sur quatre octets, passé par valeur */ typedef int int4;
D'autre part, les types à longueur fixe d'une taille quelconque peuvent être passés par référence. Par exemple, voici l'implémentation d'un type PostgreSQL :
/* structure de 16 octets, passée par référence */ typedef struct { double x, y; } Point;
Seuls des pointeurs vers de tels types peuvent être utilisés en les passant dans et hors des fonctions PostgreSQL. Pour renvoyer une valeur d'un tel type, allouez la quantité appropriée de mémoire avec palloc, remplissez la mémoire allouée et renvoyez un pointeur vers elle. (Vous pouvez aussi renvoyer directement une valeur d'entrée qui a le même type que la valeur de retour en renvoyant le pointeur vers la valeur d'entrée. Cependant, ne jamais modifier le contenu d'une valeur d'entrée passée par référence.)
Enfin, tous les types à longueur variable doivent aussi être passés par référence. Tous les types à longueur variable doivent commencer avec un champ d'une longueur d'exactement quatre octets et toutes les données devant être stockées dans ce type doivent être localisées dans la mémoire à la suite immédiate de ce champ longueur. Le champ longueur contient la longueur totale de la structure, c'est-à-dire incluant la longueur du champ longueur lui-même.
Comme exemple, nous pouvons définir le type text comme ceci :
typedef struct { int4 length; char data[1]; } text;
Il est évident que le champ déclaré ici n'est pas assez long pour contenir toutes les chaînes possibles. Comme il est impossible de déclarer une structure de taille variable en C, nous nous appuyons sur le fait que le compilateur C ne vérifie pas la plage des indices de tableau. Nous allouons juste la quantité d'espace nécessaire et ensuite nous accédons au tableau comme s'il avait été déclaré avec la bonne longueur (c'est une astuce courante que vous pouvez trouver dans beaucoup de manuels de C).
En manipulant les types à longueur variable, nous devons être attentifs à allouer la quantité correcte de mémoire et à fixer correctement le champ longueur. Par exemple, si nous voulons stocker 40 octets dans une structure text, nous devrions utiliser un fragment de code comme celui-ci :
#include "postgres.h" ... char buffer[40]; /* notre donnée source */ ... text *destination = (text *) palloc(VARHDRSZ + 40); destination->length = VARHDRSZ + 40; memcpy(destination->data, buffer, 40); ...
VARHDRSZ est équivalent à sizeof(int4) mais est considéré comme une meilleure tournure de référence à la taille de l'overhead pour un type à longueur variable.
Le Tableau 33-1 spécifie la correspondance entre les types C et les types SQL quand on écrit une fonction en langage C utilisant les types internes de PostgreSQL. La colonne << Défini dans >> donne le fichier d'en-tête devant être inclus pour accéder à la définition du type (la définition effective peut se trouver dans un fichier différent inclus dans le fichier indiqué. Il est recommandé que les utilisateurs s'en tiennent à l'interface définie). Notez que vous devriez toujours inclure postgres.h en premier dans tout fichier source car il déclare un grand nombre d'éléments dont vous aurez besoin de toute façon.
Tableau 33-1. Équivalence des types C et des types SQL intégrés
Type SQL | Type C | Défini dans |
---|---|---|
abstime | AbsoluteTime | utils/nabstime.h |
boolean | bool | postgres.h (intégration au compilateur) |
box | BOX* | utils/geo_decls.h |
bytea | bytea* | postgres.h |
"char" | char | (intégré au compilateur) |
character | BpChar* | postgres.h |
cid | CommandId | postgres.h |
date | DateADT | utils/date.h |
smallint (int2) | int2 or int16 | postgres.h |
int2vector | int2vector* | postgres.h |
integer (int4) | int4 or int32 | postgres.h |
real (float4) | float4* | postgres.h |
double precision (float8) | float8* | postgres.h |
interval | Interval* | utils/timestamp.h |
lseg | LSEG* | utils/geo_decls.h |
name | Name | postgres.h |
oid | Oid | postgres.h |
oidvector | oidvector* | postgres.h |
path | PATH* | utils/geo_decls.h |
point | POINT* | utils/geo_decls.h |
regproc | regproc | postgres.h |
reltime | RelativeTime | utils/nabstime.h |
text | text* | postgres.h |
tid | ItemPointer | storage/itemptr.h |
time | TimeADT | utils/date.h |
time with time zone | TimeTzADT | utils/date.h |
timestamp | Timestamp* | utils/timestamp.h |
tinterval | TimeInterval | utils/nabstime.h |
varchar | VarChar* | postgres.h |
xid | TransactionId | postgres.h |
Maintenant que nous avons passé en revue toutes les structures possibles pour les types de base, nous pouvons donner quelques exemples de vraies fonctions.
Nous présentons l'<< ancien style >> de convention d'appel en premier --- bien que cette approche soit maintenant déconseillée, elle est plus facile à maîtriser au début. Dans la méthode version-0, les arguments et résultats de la fonction C sont simplement déclarés dans le style C normal mais en faisant attention à utiliser la représentation C de chaque type de données SQL comme montré ci-dessus.
Voici quelques exemples :
#include "postgres.h" #include <string.h> /* par valeur */ int add_one(int arg) { return arg + 1; } /* par référence, taille fixe */ float8 * add_one_float8(float8 *arg) { float8 *result = (float8 *) palloc(sizeof(float8)); *result = *arg + 1.0; return result; } Point * makepoint(Point *pointx, Point *pointy) { Point *new_point = (Point *) palloc(sizeof(Point)); new_point->x = pointx->x; new_point->y = pointy->y; return new_point; } /* par référence, taille variable */ text * copytext(text *t) { /* * VARSIZE est la taille totale de la structure en octets. */ text *new_t = (text *) palloc(VARSIZE(t)); VARATT_SIZEP(new_t) = VARSIZE(t); /* * VARDATA est un pointeur sur la région de données de la structure. */ memcpy((void *) VARDATA(new_t), /* destination */ (void *) VARDATA(t), /* source */ VARSIZE(t)-VARHDRSZ); /* nombre d'octets */ return new_t; } text * concat_text(text *arg1, text *arg2) { int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ; text *new_text = (text *) palloc(new_text_size); VARATT_SIZEP(new_text) = new_text_size; memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ); memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ), VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ); return new_text; }
En supposant que le code ci-dessus ait été écrit dans le fichier funcs.c et compilé en objet partagé, nous pourrions définir les fonctions pour PostgreSQL avec des commandes comme ceci :
CREATE FUNCTION add_one(integer) RETURNS integer AS 'DIRECTORY/funcs', 'add_one' LANGUAGE C STRICT; -- notez la surcharge du nom de la fonction SQL "add_one" CREATE FUNCTION add_one(double precision) RETURNS double precision AS 'DIRECTORY/funcs', 'add_one_float8' LANGUAGE C STRICT; CREATE FUNCTION makepoint(point, point) RETURNS point AS 'DIRECTORY/funcs', 'makepoint' LANGUAGE C STRICT; CREATE FUNCTION copytext(text) RETURNS text AS 'DIRECTORY/funcs', 'copytext' LANGUAGE C STRICT; CREATE FUNCTION concat_text(text, text) RETURNS text AS 'DIRECTORY/funcs', 'concat_text', LANGUAGE C STRICT;
Ici, DIRECTORY représente le répertoire contenant le fichier de la bibliothèque partagée (par exemple le répertoire du tutoriel de PostgreSQL, qui contient le code des exemples utilisés dans cette section). (Un meilleur style aurait été d'écrire seulement 'funcs' dans la clause AS, après avoir ajouté DIRECTORY au chemin de recherche. Dans tous les cas, nous pouvons omettre l'extension spécifique au système pour les bibliothèques partagées, communément .so ou .sl.)
Remarquez que nous avons spécifié la fonction comme << STRICT >>, ce qui signifie que le système devra automatiquement supposer un résultat NULL si n'importe quelle valeur d'entrée est NULL. Ainsi, nous évitons d'avoir à vérifier l'existence d'entrées NULL dans le code de la fonction. Sinon, nous aurions dû contrôler explicitement les valeurs NULL en testant un pointeur NULL pour chaque argument passé par référence (pour les arguments passés par valeur, nous n'aurions même aucun moyen de contrôle !).
Bien que cette convention d'appel soit simple à utiliser, elle n'est pas très portable ; sur certaines architectures, il y a des problèmes pour passer de cette manière des types de données plus petits que int. De plus, il n'y a pas de moyen simple de renvoyer un résultat NULL, ni de traiter des arguments NULL autrement qu'en rendant la fonction strict. La convention version-1, présentée ci-après, permet de surmonter ces objections.
La convention d'appel version-1 repose sur des macros pour supprimer la plus grande partie de la complexité du passage d'arguments et de résultats. La déclaration C d'une fonction en version-1 est toujours :
Datum nom_fonction(PG_FUNCTION_ARGS)
De plus, la macro d'appel
PG_FUNCTION_INFO_V1(nom_fonction);
doit apparaître dans le même fichier source (par convention, elle est écrite juste avant la fonction elle-même). Cette macro n'est pas nécessaire pour les fonctions internal puisque PostgreSQL assume que toutes les fonctions internes utilisent la convention version-1. Elle est toutefois requise pour les fonctions chargées dynamiquement.
Dans une fonction version-1, chaque argument existant est traité par une
macro PG_GETARG_xxx()
correspondant au type de donnée de l'argument et le résultat est renvoyé
par une macro
PG_RETURN_xxx()
correspondant au type renvoyé.
PG_GETARG_xxx()
prend comme argument le nombre d'arguments de la fonction à parcourir, le
compteur commençant à 0.
PG_RETURN_xxx()
prend comme
argument la valeur effective à renvoyer.
Voici la même fonction que précédemment, codée en style version-1
#include "postgres.h" #include <string.h> #include "fmgr.h" /* par valeur */ PG_FUNCTION_INFO_V1(add_one); Datum add_one(PG_FUNCTION_ARGS) { int32 arg = PG_GETARG_INT32(0); PG_RETURN_INT32(arg + 1); } /* par référence, longueur fixe */ PG_FUNCTION_INFO_V1(add_one_float8); Datum add_one_float8(PG_FUNCTION_ARGS) { /* La macro pour FLOAT8 cache sa nature de passage par référence. */ float8 arg = PG_GETARG_FLOAT8(0); PG_RETURN_FLOAT8(arg + 1.0); } PG_FUNCTION_INFO_V1(makepoint); Datum makepoint(PG_FUNCTION_ARGS) { /* Ici, la nature de passage par référence de Point n'est pas cachée. */ Point *pointx = PG_GETARG_POINT_P(0); Point *pointy = PG_GETARG_POINT_P(1); Point *new_point = (Point *) palloc(sizeof(Point)); new_point->x = pointx->x; new_point->y = pointy->y; PG_RETURN_POINT_P(new_point); } /* par référence, longueur variable */ PG_FUNCTION_INFO_V1(copytext); Datum copytext(PG_FUNCTION_ARGS) { text *t = PG_GETARG_TEXT_P(0); /* * VARSIZE est la longueur totale de la structure en octets. */ text *new_t = (text *) palloc(VARSIZE(t)); VARATT_SIZEP(new_t) = VARSIZE(t); /* * VARDATA est un pointeur vers la région de données de la structure. */ memcpy((void *) VARDATA(new_t), /* destination */ (void *) VARDATA(t), /* source */ VARSIZE(t)-VARHDRSZ); /* nombre d'octets */ PG_RETURN_TEXT_P(new_t); } PG_FUNCTION_INFO_V1(concat_text); Datum concat_text(PG_FUNCTION_ARGS) { text *arg1 = PG_GETARG_TEXT_P(0); text *arg2 = PG_GETARG_TEXT_P(1); int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ; text *new_text = (text *) palloc(new_text_size); VARATT_SIZEP(new_text) = new_text_size; memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ); memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ), VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ); PG_RETURN_TEXT_P(new_text); }
Les commandes CREATE FUNCTION sont les mêmes que pour leurs équivalents dans la version-0.
Au premier coup d'œil, les conventions de codage de la version-1 peuvent
sembler inutilement obscures. Pourtant, elles offrent nombre
d'améliorations car les macros peuvent cacher les détails superflus. Un
exemple est donné par la fonction add_one_float8
où nous
n'avons plus besoin de prêter attention au fait que le type
float8 est passé par référence. Un autre exemple de
simplification est donné par les macros pour les types à longueur variable
GETARG qui permettent un traitement plus efficace des valeurs
<< toasted >> (compressées ou hors-ligne).
Une des grandes améliorations dans les fonctions version-1 est le meilleur
traitement des entrées et des résultats NULL. La macro
PG_ARGISNULL(n)
permet à une fonction
de tester si chaque entrée est NULL (évidemment, ceci n'est nécessaire que
pour les fonctions déclarées non << STRICT >>). Comme avec les macros
PG_GETARG_xxx()
, les
arguments en entrée sont comptés à partir de zéro. Notez qu'on doit se
garder d'exécuter
PG_GETARG_xxx()
jusqu'à
ce qu'on ait vérifié que l'argument n'est pas NULL. Pour renvoyer un
résultat NULL, exécutez la fonction
PG_RETURN_NULL()
; ceci convient aussi bien dans
les fonctions STRICT que non STRICT.
Les autres options proposées dans l'interface du nouveau style sont deux
variantes des macros
PG_GETARG_xxx()
. La
première d'entre elles,
PG_GETARG_xxx_COPY()
,
garantit le renvoi d'une copie de l'argument spécifié, où nous pouvons
écrire en toute sécurité. (Les macros normales peuvent parfois renvoyer
un pointeur vers une valeur physiquement mise en mémoire dans une table qui
ne doit pas être modifiée. En utilisant les macros
PG_GETARG_xxx_COPY()
, on
garantit l'écriture du résultat.) La seconde variante se compose des macros
PG_GETARG_xxx_SLICE()
qui prennent trois arguments. Le premier est le nombre d'argument de la
fonction (comme ci-dessus). Le second et le troisième sont le décalage et
la longueur du segment qui doit être renvoyé. Les décalages sont comptés à
partir de zéro et une longueur négative demande le renvoi du reste de la
valeur. Ces macros procurent un accès plus efficace à des parties de
valeurs à grande dimension dans le cas où elles ont un type de stockage en
mémoire << external >>. (Le type de stockage d'une colonne peut
être spécifié en utilisant ALTER TABLE
tablename ALTER COLUMN
colname SET STORAGE
typestockage.
typestockage est un type parmi
plain, external, extended ou
main.)
Enfin, les conventions d'appels de la version-1 rendent possible le renvoi de résultats d'ensemble (Section 33.7.9) et implémentent des fonctions trigger (déclencheur) (Chapitre 35) et des opérateurs d'appel de langage procédural (Chapitre 47). Le code version-1 est aussi plus portable que celui de version-0 car il ne contrevient pas aux restrictions du protocole d'appel de fonction en C standard. Pour plus de détails, voyez src/backend/utils/fmgr/README dans les fichiers sources de la distribution.
Avant de nous intéresser à des sujets plus avancés, nous devons discuter de quelques règles de codage des fonctions en langage C de PostgreSQL. Bien qu'il soit possible de charger des fonctions écrites dans des langages autre que le C dans PostgreSQL, c'est habituellement difficile (quand c'est possible) parce que les autres langages comme C++, FORTRAN ou Pascal ne suivent pas fréquemment les mêmes conventions de nommage que le C. C'est-à-dire que les autres langages ne passent pas les arguments et ne renvoient pas les valeurs entre fonctions de la même manière. Pour cette raison, nous supposerons que nos fonctions en langage C sont réellement écrites en C.
Les règles de base pour l'écriture de fonctions C sont les suivantes :
Utilisez pg_config --includedir-server pour découvrir où sont installés les fichiers d'en-tête du serveur PostgreSQL sur votre système (ou sur le système de vos utilisateurs). Cette option est apparue dans PostgreSQL version 7.2. Pour PostgreSQL 7.1, vous devez utiliser l'option --includedir (la commande pg_config terminera avec un statut différent de zéro si elle rencontre une option inconnue). Pour les versions antérieures à la 7.1, vous devez deviner mais, puisque cela remonte avant l'introduction des conventions d'appel actuelles, il y a peu de chance que vous vouliez supporter ces versions.
Quand vous allouez de la mémoire, utilisez les fonctions
PostgreSQL
palloc
et
pfree
au lieu des fonctions correspondantes malloc
et
free
de la bibliothèque C. La mémoire allouée par
palloc
sera libérée automatiquement à la fin de
chaque transaction, empêchant des débordements de mémoire.
Remettez toujours à zéro les octets de vos structures en utilisant
memset
. Sinon, il est difficile de supporter les
index ou les jointures de découpage car vous devez retenir seulement
les bits significatifs de votre structure de donnée pour calculer un
découpage. Même si vous initialisez tous les champs de votre structure,
il peut y avoir des remplissages d'alignement (trous dans la structure)
pouvant contenir des valeurs parasites.
La plupart des types internes PostgreSQL sont déclarés dans postgres.h alors que les interfaces de gestion des fonctions (PG_FUNCTION_ARGS, etc.) sont dans fmgr.h. Du coup, vous aurez besoin d'inclure au moins ces deux fichiers. Pour des raisons de portabilité, il vaut mieux inclure postgres.h en premier avant tout autre fichier d'en-tête système ou utilisateur. En incluant postgres.h, il incluera également elog.h et palloc.h pour vous.
Les noms de symboles définis dans les objets ne doivent pas entrer en conflit entre eux ou avec les symboles définis dans les exécutables du serveur PostgreSQL. Vous aurez à renommer vos fonctions ou variables si vous recevez un message d'erreur à cet effet.
Compiler ou lier votre code pour qu'il soit chargé dynamiquement dans PostgreSQL exige toujours des options spéciales. Voir la Section 33.7.6 pour une explication détaillée sur la meilleure façon de le faire sur votre système particulier.
Avant d'être en mesure d'utiliser des fonctions d'extension écrites en C dans PostgreSQL, elles doivent être compilées et liées d'une manière particulière afin de produire un fichier pouvant être chargé dynamiquement par le serveur. Pour être plus précis, une bibliothèque partagée doit être créée.
Pour obtenir plus d'informations sur ce qui est abordé dans cette section, vous devrez lire la documentation de votre système d'exploitation, en particulier les pages traitant du compilateur C, cc, ainsi que celle sur l'éditeur de lien, ld. Par ailleurs, le code source de PostgreSQL contient plusieurs exemples fonctionnels contenu dans le répertoire contrib. Néanmoins, en vous appuyant sur ces exemples, vous créerez des modules dépendants de la disponibilité du code source de PostgreSQL.
La création de bibliothèques partagées est un processus analogue à celui utilisé pour lier des exécutables : en premier lieu, les sources sont compilées en fichiers objets puis sont liées ensemble. Les fichiers objets doivent être compilés sous la forme de code à position indépendante (PIC, acronyme de position-independent code)</ indexterm>. Conceptuellement, cela correspond au fait qu'ils peuvent être placés à une position arbitraire de la mémoire lorsqu'ils sont chargés par l'exécutable (les fichiers objets destinés aux exécutables ne sont généralement pas compilés de cette manière). La commande permettant de lier des bibliothèques partagées nécessite des options spéciales permettant de la distinguer d'une liaison pour un exécutable. Enfin, ceci est la théorie. La réalité est moins belle sur certains systèmes.
Dans les exemples suivants, nous supposons que le code source est un fichier foo.c et que nous souhaitons créer une bibliothèque partagée foo.so. Le fichier objet intermédiaire sera appelé foo.o sauf si précisé autrement. Une bibliothèque partagée peut contenir plus d'un fichier objet. Ceci dit, nous n'en utiliserons qu'un ici.
L'option du compilateur pour créer des PIC est -fpic. L'option de l'éditeur de liens pour créer des bibliothèques partagées est -shared.
gcc -fpic -c foo.c ld -shared -o foo.so foo.o
Ceci est applicable à partir de la version 4.0 de BSD/OS.
L'option du compilateur pour créer des PIC est -fpic. L'option de l'éditeur de liens pour créer des bibliothèques partagées est -shared.
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
Ceci est applicable à partir de la version 3.0 de FreeBSD.
L'option du compilateur système pour créer des PIC est +z. Lorsque vous utilisez GCC, l'option est -fpic. Le commutateur de l'éditeur de liens pour les bibliothèques partagées est -b. Ainsi
cc +z -c foo.c
ou
gcc -fpic -c foo.c
puis
ld -b -o foo.sl foo.o
HP-UX utilise l'extension .sl pour les bibliothèques partagées, à la différence de la plupart des autres systèmes.
PIC est l'option par défaut. Aucune option de compilation particulière n'est nécessaire. Le commutateur de l'éditeur de liens pour produire des bibliothèques partagées est -shared.
cc -c foo.c ld -shared -o foo.so foo.o
L'option de compilation pour créer des PIC est -fpic. Sur certaines plateformes et dans certaines situations, -fPIC doit être utilisé si -fpic ne fonctionne pas. Reportez-vous au manuel de GCC pour plus d'informations. L'option de compilation pour créer des bibliothèques partagées est -shared. Un exemple complet devrait ressembler à ce qui suit :
cc -fpic -c foo.c cc -shared -o foo.so foo.o
Voici un exemple. Il suppose que les outils de développement sont installés.
cc -c foo.c cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o
L'option de compilation pour créer des PIC est -fpic. Pour les systèmes ELF, l'option de compilation pour lier les bibliothèques partagées est -shared. Sur les systèmes plus anciens n'étant pas ELF, on utilise ld -Bshareable.
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
L'option de compilation pour créer des PIC est -fpic. Les bibliothèques partagées peuvent être créées avec la commande suivante ld -Bshareable.
gcc -fpic -c foo.c ld -Bshareable -o foo.so foo.o
L'option de compilation pour créer des PIC est -KPIC avec le compilateur de Sun et -fpic avec GCC. Pour lier les bibliothèques partagées, l'option de compilation est respectivement -G ou -shared.
cc -KPIC -c foo.c cc -G -o foo.so foo.o
ou
gcc -fpic -c foo.c gcc -G -o foo.so foo.o
Par défaut, on compile des PIC. Ainsi, aucune directive particulière n'est à fournir pour la compilation. Pour lier, des options spécifiques sont à fournir à ld :
cc -c foo.c ld -shared -expect_unresolved '*' -o foo.so foo.o
Une procédure identique doit être employée dans le cas où GCC serait utilisé à la place du compilateur du système ; aucune option particulière n'est nécessaire.
L'option de compilation pour créer des PIC est -KPIC avec le compilateur SCO et -fpic avec GCC. Pour lier des bibliothèques partagées, les options respectives sont -G et -shared.
cc -K PIC -c foo.c cc -G -o foo.so foo.o
ou
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
Astuce : Si cela vous semble compliqué, vous pouvez tenter d'utiliser GNU Libtool. Cet outil permet de s'affranchir des différences entre les nombreux systèmes au travers d'une interface uniformisée.
La bibliothèque partagée résultante peut être chargée dans PostgreSQL. Lorsque l'on spécifie le nom du fichier dans la commande CREATE FUNCTION, on doit lui donner le nom de la bibliothèque partagée et non celui du fichier objet intermédiaire. Notez bien que l'extension standard pour les bibliothèques partagées (en général .so ou .sl) peut être omise dans la commande CREATE FUNCTION et doit l'être pour obtenir une meilleure portabilité.
Référez-vous à la Section 33.7.1 pour savoir à quel endroit le serveur s'attend à trouver les fichiers de bibliothèques partagées.
Les types composites n'ont pas une organisation fixe comme les structures en C. Des instances d'un type composite peuvent contenir des champs NULL. De plus, les types composites faisant partie d'une hiérarchie d'héritage peuvent avoir des champs différents des autres membres de la même hiérarchie. En conséquence, PostgreSQL propose une interface de fonction pour accéder depuis le C aux champs des types composites.
Supposons que nous voulions écrire une fonction pour répondre à la requête
SELECT nom, c_surpaye(emp, 1500) AS surpaye FROM emp WHERE nom = 'Bill' OR nom = 'Sam';
En utilisant les conventions d'appel de la version 0, nous pouvons définir
c_surpaye
comme :
#include "postgres.h" #include "executor/executor.h" /* pour GetAttributeByName() */ bool c_surpaye(TupleTableSlot *t, /* la ligne courante d'emp */ int32 limite) { bool isNULL; int32 salaire; salaire = DatumGetInt32(GetAttributeByName(t, "salaire", &isNULL)); if (isNULL) return false; return salaire > limite; }
Dans le codage version-1, le code ci-dessus devient :
#include "postgres.h" #include "executor/executor.h" /* pour GetAttributeByName() */ PG_FUNCTION_INFO_V1(c_surpaye); Datum c_surpaye(PG_FUNCTION_ARGS) { TupleTableSlot *t = (TupleTableSlot *) PG_GETARG_POINTER(0); int32 limite = PG_GETARG_INT32(1); bool isNULL; int32 salaire; salaire = DatumGetInt32(GetAttributeByName(t, "salaire", &isNULL)); if (isNULL) PG_RETURN_BOOL(false); /* Autrement, nous pourrions préférer de lancer PG_RETURN_NULL() pour un salaire NULL. */ PG_RETURN_BOOL(salaire > limite); }
GetAttributeByName
est la fonction système
PostgreSQL qui renvoie les attributs depuis une
colonne spécifiée. Elle a trois arguments : l'argument de type
TupleTableSlot* passé à la fonction, le nom de l'attribut
recherché et un paramètre renvoyé qui indique si l'attribut est NULL.
GetAttributeByName
renvoie une valeur de type
Datum que vous pouvez convertir dans un type voulu en
utilisant la macro appropriée
DatumGetXXX()
.
La commande suivante déclare la fonction c_surpaye
en SQL :
CREATE FUNCTION c_surpaye(emp, integer) RETURNS boolean AS 'DIRECTORY/funcs', 'c_surpaye' LANGUAGE C;
Pour renvoyer une ligne ou une valeur de type composite à partir d'une fonction en langage C, vous pouvez utiliser une API spéciale qui fournit les macros et les fonctions dissimulant en grande partie la complexité liée à la construction de types de données composites. Pour utiliser cette API, le fichier source doit inclure :
#include "funcapi.h"
L'assistance pour renvoyer des types de donnée composite (ou lignes) commence avec la structure AttInMetadata. Cette structure contient des tableaux d'information sur les attributs individuels, nécessaires pour créer une ligne à partir d'une chaîne C native. L'information contenue dans la structure est dérivée d'une structure TupleDesc mais elle est conservée de façon à éviter des calculs redondants à chaque appel d'une fonction renvoyant un ensemble (voir la section suivante). Dans le cas d'une fonction renvoyant un ensemble, la structure AttInMetadata doit être calculée une fois lors du premier appel et sauvegardée pour une réutilisation dans les appels ultérieurs. AttInMetadata conserve également un pointeur vers la TupleDesc originale.
typedef struct AttInMetadata{ /* TupleDesc complet */ TupleDesc tupdesc; /* tableau de type d'attributs en entrée pour la fonction finfo */ FmgrInfo *attinfuncs; /* tableau de type d'attributs typelem */ Oid *attelems; /* tableau d'attributs typmod */ int32 *atttypmods; } AttInMetadata;
Pour vous aider à remplir cette structure, plusieurs fonctions et une macro sont disponibles. Utilisez
TupleDesc RelationNameGetTupleDesc(const char *relname)
pour obtenir une TupleDesc pour une relation nommée ou
TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)
pour obtenir une TupleDesc basée sur l'OID d'un type. Ceci peut être utilisé pour obtenir un TupleDesc soit pour un type de base, soit pour un type composite. Ensuite
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
renverra un pointer vers une structure AttInMetadata, initialisée suivant la structure TupleDesc donnée. AttInMetadata peut être utilisée en conjonction avec des chaînes C pour produire une valeur de ligne convenablement formée (appelée tuple en interne).
Pour renvoyer un tuple, vous devez créer un emplacement basé sur TupleDesc. Vous pouvez utiliser
TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc)
pour initialiser cet emplacement ou en obtenir un par d'autres moyens (fournis par l'utilisateur). L'emplacement est nécessaire pour créer un Datum devant être renvoyé par la fonction. Le même emplacement peut (et doit) être réutilisé à chaque appel.
Après avoir construit une structure AttInMetadata,
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
peut être utilisé pour construire une structure HeapTuple à partir de données utilisateur sous forme de chaîne C. values est un tableau de chaînes C, une pour chaque attribut de la ligne renvoyée. Chaque chaîne C doit être de la forme attendue par la fonction d'entrée du type de donnée de l'attribut. Afin de renvoyer une valeur NULL pour un des attributs, le pointeur correspondant dans le tableau de valeurs (values) doit être fixé à NULL. Cette fonction demandera à être appelée pour chaque ligne que vous renvoyez.
La construction d'un tuple via TupleDescGetAttInMetadata
et
BuildTupleFromCStrings
est appropriée seulement si votre
fonction calcule naturellement les valeurs à renvoyer comme des chaînes de
texte. Si votre code calcule naturellement les valeurs comme des ensembles
de valeurs Datum, vous devez à la place utiliser la fonction
sous-jacente heap_formtuple
pour convertir directement les
valeurs Datum en tuple. Vous aurez encore besoin de
TupleDesc et d'un TupleTableSlot, mais non
pas de AttInMetadata.
Une fois que vous avez construit un tuple devant être renvoyé par votre fonction, vous devez le convertir en type Datum. Utilisez
TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple)
pour obtenir un type Datum pour un tuple et un emplacement donné. Ce Datum peut être renvoyé directement si vous envisagez de renvoyer juste une simple ligne ou bien il peut être utilisé pour renvoyer la valeur courante dans une fonction renvoyant un ensemble.
Un exemple figure dans la section suivante.
Il existe aussi une API spéciale procurant le moyen de renvoyer des ensembles (lignes multiples) depuis une fonction en langage C. Une fonction renvoyant un ensemble doit suivre les conventions d'appel de la version-1. Aussi, les fichiers source doivent inclure l'en-tête funcapi.h, comme ci-dessus.
Une fonction renvoyant un ensemble (SRF : << set returning function >>) est appelée une fois pour chaque élément qu'elle renvoie. La SRF doit donc sauvegarder suffisamment l'état pour se rappeler ce qu'elle était en train de faire et renvoyer le prochain élément à chaque appel. La structure FuncCallContext est offerte pour assister le contrôle de ce processus. À l'intérieur d'une fonction, fcinfo->flinfo->fn_extra est utilisée pour conserver un pointeur vers FuncCallContext au cours des appels successifs.
typedef struct { /* * Number of times we've been called before * * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and * incremented for you every time SRF_RETURN_NEXT() is called. */ uint32 call_cntr; /* * OPTIONAL maximum number of calls * * max_calls is here for convenience only and setting it is optional. * If not set, you must provide alternative means to know when the * function is done. */ uint32 max_calls; /* * OPTIONAL pointer to result slot * * slot is for use when returning tuples (i.e., composite data types) * and is not needed when returning base data types. */ TupleTableSlot *slot; /* * OPTIONAL pointer to miscellaneous user-provided context information * * user_fctx is for use as a pointer to your own data to retain * arbitrary context information between calls of your function. */ void *user_fctx; /* * OPTIONAL pointer to struct containing attribute type input metadata * * attinmeta is for use when returning tuples (i.e., composite data types) * and is not needed when returning base data types. It * is only needed if you intend to use BuildTupleFromCStrings() to create * the return tuple. */ AttInMetadata *attinmeta; /* * memory context used for structures that must live for multiple calls * * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory * context for any memory that is to be reused across multiple calls * of the SRF. */ MemoryContext multi_call_memory_ctx; } FuncCallContext;
Une SRF utilise plusieurs fonctions et macros qui manipulent automatiquement la structure FuncCallContext (et s'attendent à la trouver via fn_extra). Utilisez
SRF_IS_FIRSTCALL()
pour déterminer si votre fonction est appelée pour la première fois. Au premier appel, utilisez
SRF_FIRSTCALL_INIT()
pour initialiser la structure FuncCallContext. À chaque appel de fonction, y compris le premier, utilisez
SRF_PERCALL_SETUP()
pour une mise à jour correcte en vue de l'utilisation de FuncCallContext et pour nettoyer toutes les données renvoyées précédemment et conservées depuis le dernier passage de la fonction.
Si votre fonction a des données à renvoyer, utilisez
SRF_RETURN_NEXT(funcctx, result)
pour les renvoyer à l'appelant. (result doit être de type Datum, soit une valeur simple, soit un tuple préparé comme décrit ci-dessus.) Enfin, quand votre fonction a fini de renvoyer des données, utilisez
SRF_RETURN_DONE(funcctx)
pour nettoyer et terminer la SRF.
Lors de l'appel de la SRF, le contexte mémoire courant est un
contexte transitoire qui est effacé entre les appels. Cela signifie que
vous n'avez pas besoin d'appeler pfree
sur tout ce que vous
avez alloué en utilisant palloc
; ce sera supprimé de
toute façon. Toutefois, si vous voulez allouer des structures de données
devant persister tout au long des appels, vous avez besoin de les conserver
quelque part. Le contexte mémoire référencé par
multi_call_memory_ctx est un endroit approprié pour toute
donnée devant survivre jusqu'à l'achèvement de la fonction SRF.
Dans la plupart des cas, cela signifie que vous devrez basculer vers
multi_call_memory_ctx au moment de la préparation du
premier appel.
Voici un exemple complet de pseudo-code :
Datum my_set_returning_function(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; Datum result; MemoryContext oldcontext; further declarations as needed if (SRF_IS_FIRSTCALL()) { funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* One-time setup code appears here: */ user code if returning composite build TupleDesc, and perhaps AttInMetadata obtain slot funcctx->slot = slot; endif returning composite user code MemoryContextSwitchTo(oldcontext); } /* Each-time setup code appears here: */ user code funcctx = SRF_PERCALL_SETUP(); user code /* this is just one way we might test whether we are done: */ if (funcctx->call_cntr < funcctx->max_calls) { /* Here we want to return another item: */ user code obtain result Datum SRF_RETURN_NEXT(funcctx, result); } else { /* Here we are done returning items and just need to clean up: */ user code SRF_RETURN_DONE(funcctx); } }
Et voici un exemple complet d'une simple SRF retournant un type composite :
PG_FUNCTION_INFO_V1(testpassbyval); Datum testpassbyval(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; int call_cntr; int max_calls; TupleDesc tupdesc; TupleTableSlot *slot; AttInMetadata *attinmeta; /* stuff done only on the first call of the function */ if (SRF_IS_FIRSTCALL()) { MemoryContext oldcontext; /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); /* switch to memory context appropriate for multiple function calls */ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* total number of tuples to be returned */ funcctx->max_calls = PG_GETARG_UINT32(0); /* Build a tuple description for a __testpassbyval tuple */ tupdesc = RelationNameGetTupleDesc("__testpassbyval"); /* allocate a slot for a tuple with this tupdesc */ slot = TupleDescGetSlot(tupdesc); /* assign slot to function context */ funcctx->slot = slot; /* * generate attribute metadata needed later to produce tuples from raw * C strings */ attinmeta = TupleDescGetAttInMetadata(tupdesc); funcctx->attinmeta = attinmeta; MemoryContextSwitchTo(oldcontext); } /* stuff done on every call of the function */ funcctx = SRF_PERCALL_SETUP(); call_cntr = funcctx->call_cntr; max_calls = funcctx->max_calls; slot = funcctx->slot; attinmeta = funcctx->attinmeta; if (call_cntr < max_calls) /* do when there is more left to send */ { char **values; HeapTuple tuple; Datum result; /* * Prepare a values array for storage in our slot. * This should be an array of C strings which will * be processed later by the type input functions. */ values = (char **) palloc(3 * sizeof(char *)); values[0] = (char *) palloc(16 * sizeof(char)); values[1] = (char *) palloc(16 * sizeof(char)); values[2] = (char *) palloc(16 * sizeof(char)); snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1)); snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1)); snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1)); /* build a tuple */ tuple = BuildTupleFromCStrings(attinmeta, values); /* make the tuple into a datum */ result = TupleGetDatum(slot, tuple); /* clean up (this is not really necessary) */ pfree(values[0]); pfree(values[1]); pfree(values[2]); pfree(values); SRF_RETURN_NEXT(funcctx, result); } else /* do when there is no more left */ { SRF_RETURN_DONE(funcctx); } }
Le code SQL pour déclarer cette fonction est le suivant :
CREATE TYPE __testpassbyval AS (f1 integer, f2 integer, f3 integer); CREATE OR REPLACE FUNCTION testpassbyval(integer, integer) RETURNS SETOF __testpassbyval AS 'filename', 'testpassbyval' LANGUAGE C IMMUTABLE STRICT;
Le répertoire contrib/tablefunc situé dans les fichiers source de la distribution contient d'autres exemples de fonctions renvoyant des ensembles.
Les fonctions en langage C peuvent être déclarées pour accepter et renvoyer les types << polymorphes >> anyelement et anyarray. Voir la Section 33.2.1 pour une explication plus détaillée des fonctions polymorphes. Si les types des arguments ou du renvoi de la fonction sont définis comme polymorphes, l'auteur de la fonction ne peut pas savoir à l'avance quel type de données sera appelé ou bien quel type doit être renvoyé. Il y a deux routines offertes par fmgr.h qui permettent à une fonction en version-1 de découvrir les types de données effectifs de ses arguments et le type qu'elle doit renvoyer. Ces routines s'appellent get_fn_expr_rettype(FmgrInfo *flinfo) et get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Elles renvoient l'OID du type du résultat ou de l'argument ou InvalidOID si l'information n'est pas disponible. L'accès à la structure flinfo se fait normalement avec fcinfo->flinfo. Le paramètre argnum est basé à partir de zéro.
Par exemple, supposons que nous voulions écrire une fonction qui accepte un argument de n'importe quel type et qui renvoie un tableau uni-dimensionnel de ce type :
PG_FUNCTION_INFO_V1(make_array); Datum make_array(PG_FUNCTION_ARGS) { ArrayType *result; Oid element_type = get_fn_expr_argtype(fcinfo->flinfo, 0); Datum element; int16 typlen; bool typbyval; char typalign; int ndims; int dims[MAXDIM]; int lbs[MAXDIM]; if (!OidIsValid(element_type)) elog(ERROR, "could not determine data type of input"); /* get the provided element */ element = PG_GETARG_DATUM(0); /* we have one dimension */ ndims = 1; /* and one element */ dims[0] = 1; /* and lower bound is 1 */ lbs[0] = 1; /* get required info about the element type */ get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); /* now build the array */ result = construct_md_array(&element, ndims, dims, lbs, element_type, typlen, typbyval, typalign); PG_RETURN_ARRAYTYPE_P(result); }
La commande suivante déclare la fonction make_array
en
SQL :
CREATE FUNCTION make_array(anyelement) RETURNS anyarray AS 'DIRECTORY/funcs', 'make_array' LANGUAGE 'C' STRICT;
Notez l'utilisation de STRICT ; ceci est primordial car le code ne se préoccupe pas de tester une entrée NULL.
Les fonctions en langage C peuvent être déclarées pour accepter et recevoir des types polymorphiques anyelement et anyarray. Voir la Section 33.2.6 pour une explication plus détaillée des fonctions polymorphiques. Lorsque les arguments ou les codes de retour de la fonction sont définis comme des types polymorphiques, l'auteur de la fonction ne sait pas à l'avance avec quels types de données celle-ci sera appelée, ou quel type elle aura besoin de renvoyer. Il existe deux routines apportées par fmgr.h permettant à une fonction C en version 1 de découvrir les réels types de données des arguments et celui attendu en sortie. Les routines s'appellent get_fn_expr_rettype(FmgrInfo *flinfo) et get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Elles renvoient le type de l'OID du résultat ou de l'argument. Si l'information n'est pas disponible, elles renvoient InvalidOid. La structure flinfo est normalement accédée comme fcinfo->flinfo. Le paramètre argnum commence à zéro.
Par exemple, supposons que nous voulons écrire une fonction acceptant un seul élément de tout type et renvoyant un tableau à une dimension de ce type :
PG_FUNCTION_INFO_V1(make_array); Datum make_array(PG_FUNCTION_ARGS) { ArrayType *result; Oid element_type = get_fn_expr_argtype(fcinfo->flinfo, 0); Datum element; int16 typlen; bool typbyval; char typalign; int ndims; int dims[MAXDIM]; int lbs[MAXDIM]; if (!OidIsValid(element_type)) elog(ERROR, "could not determine data type of input"); /* get the provided element */ element = PG_GETARG_DATUM(0); /* we have one dimension */ ndims = 1; /* and one element */ dims[0] = 1; /* and lower bound is 1 */ lbs[0] = 1; /* get required info about the element type */ get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); /* now build the array */ result = construct_md_array(&element, ndims, dims, lbs, element_type, typlen, typbyval, typalign); PG_RETURN_ARRAYTYPE_P(result); }
La commande suivante déclare la fonction make_array
en
SQL :
CREATE FUNCTION make_array(anyelement) RETURNS anyarray AS 'DIRECTORY/funcs', 'make_array' LANGUAGE C STRICT;
Notez l'utilisation de STRICT ; c'est essentiel car le code ne s'occupe pas de tester une entrée nulle.
Précédent | Sommaire | Suivant |
Fonctions internes | Niveau supérieur | Surcharge de fonction |