PostgreSQLLa base de données la plus sophistiquée au monde.
Documentation PostgreSQL 17.1 » Programmation serveur » Étendre SQL » Fonctions en langage C

36.10. Fonctions en langage C #

Les fonctions définies par l'utilisateur peuvent être écrites en C (ou dans un langage pouvant être rendu compatible avec C, comme le 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).

Actuellement, seule une convention d'appel est utilisée pour les fonctions C (« version 1 »). Le support pour cette convention d'appel est indiqué en ajoutant un appel à la macro PG_FUNCTION_INFO_V1() pour la fonction, comme illustré ci-dessous.

36.10.1. Chargement dynamique #

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é :

  1. Si le nom est un chemin absolu, le fichier est chargé.

  2. 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.

  3. 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.

  4. 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.

Il est recommandé de localiser les bibliothèques partagées soit relativement à $libdir ou via le chemin dynamique des bibliothèques. Ceci simplifie les mises à jour de versions si la nouvelle installation est à un emplacement différent. Le répertoire actuel représenté par $libdir est trouvable avec la commande pg_config --pkglibdir.

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 36.10.5 pour des informations complémentaires.

Pour s'assurer qu'un fichier objet chargeable dynamiquement n'est pas chargé dans un serveur incompatible, PostgreSQL vérifie que le fichier contient un « bloc magique » avec un contenu approprié. Ceci permet au serveur de détecter les incompatibilités évidentes comme du code compilé pour une version majeure différente de PostgreSQL. Pour inclure un bloc magique, écrivez ceci dans un (et seulement un) des fichiers source du module, après avoir inclus l'en-tête fmgr.h :

PG_MODULE_MAGIC;

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, commencez une nouvelle session.

De façon optionnelle, un fichier chargé dynamiquement peut contenir une fonction d'initialisation. Si le fichier inclut une fonction nommée _PG_init, cette fonction sera appelée immédiatement après le chargement du fichier. La fonction ne reçoit aucun paramètre et doit renvoyer void. Il n'existe actuellement aucun moyen de décharger un fichier chargé dynamiquement.

36.10.2. Types de base dans les fonctions en langage C #

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;
      

(le code C de PostgreSQL appelle ce type int32 car il existe une convention en C disant que intXX signifie XX bits. Il est à noter toutefois que le type C int8 a une taille d'un octet. Le type SQL int8 est appelé int64 en C. Voir aussi Tableau 36.2.)

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 (de plus, si vous souhaitez seulement renvoyer la même valeur qu'un de vos arguments en entrée qui se trouve du même type, vous pouvez passer le palloc supplémentaire et simplement renvoyer le pointeur vers la valeur en entrée).

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, qui sera initialisé à SET_VARSIZE ; ne jamais configurer ce champ directement ! 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.

Un autre point important est d'éviter de laisser des bits non initialisés dans les structures de types de données ;; par exemple, prenez bien soin de remplir avec des zéros tous les octets de remplissage qui sont présents dans les structures de données à des fins d'alignement. A défaut, des constantes logiquement équivalentes de vos types de données pourraient être considérées comme inégales par l'optimiseur, impliquant une planification inefficace (bien que les résultats puissent malgré tout être corrects).

Avertissement

Ne jamais modifier le contenu d'une valeur en entrée passée par référence. Si vous le faites, il y a de forts risques pour que vous réussissiez à corrompre les données sur disque car le pointeur que vous avez reçu pourrait bien pointer directement vers un tampon disque. La seule exception à cette règle est expliquée dans la Section 36.12.

Comme exemple, nous pouvons définir le type text comme ceci :

typedef struct {
    int32 length;
    char data[FLEXIBLE_ARRAY_MEMBER];
} text;
      

La notation [FLEXIBLE_ARRAY_MEMBER] signifie que la longueur actuelle de la donnée n'est pas indiquée par cette déclaration.

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);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ est équivalent à sizeof(int32) mais est considéré comme une meilleure tournure de référence à la taille de l'overhead pour un type à longueur variable. De plus, le champ de longueur doit être configuré en utilisant la macro SET_VARSIZE, pas une simple affectation.

Le Tableau 36.2 spécifie la correspondance entre les types C et certains des types internes SQL 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 du code serveur car il déclare un grand nombre d'éléments dont vous aurez besoin de toute façon et parce qu'inclure d'autres en-têtes en premier pourrait causer des problèmes de portabilité.

Tableau 36.2. Équivalence des types C et des types SQL intégrés

Type SQL Type C Défini dans
booleanboolpostgres.h (peut-être interne compilateur)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(interne compilateur)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
float4 (real)float4postgres.h
float8 (double precision)float8postgres.h
int2 (smallint)int16postgres.h
int4 (integer)int32postgres.h
int8 (bigint)int64postgres.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
numericNumericutils/numeric.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocRegProcedurepostgres.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
timestampTimestampdatatype/timestamp.h
timestamp with time zoneTimestampTzdatatype/timestamp.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.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.

36.10.3. Conventions d'appel de la version 1 #

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. (Dans les fonctions non strictes, il est nécessaire d'avoir une vérification précédente sur la possibilité que l'argument soit NULL en utilisant PG_ARGISNULL() ; voir ci-dessous.) 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 quelques exemples utilisant la convention d'appel version-1 :

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include "varatt.h"

PG_MODULE_MAGIC;

/* 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_PP(0);
    /*
     * VARSIZE_ANY_EXHDR is the size of the struct in bytes, minus the
     * VARHDRSZ or VARHDRSZ_SHORT of its header.  Construct the copy with a
     * full-length header.
     */
    text     *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
    SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);

    /*
     * VARDATA is a pointer to the data region of the new struct.  The source
     * could be a short datum, so retrieve its data through VARDATA_ANY.
     */
    memcpy(VARDATA(new_t),          /* destination */
           VARDATA_ANY(t),          /* source */
           VARSIZE_ANY_EXHDR(t));   /* how many bytes */
    PG_RETURN_TEXT_P(new_t);
}

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_PP(0);
    text  *arg2 = PG_GETARG_TEXT_PP(1);
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    SET_VARSIZE(new_text, new_text_size);
    memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
    memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
    PG_RETURN_TEXT_P(new_text);
}

En supposant que le code ci-dessus a été enregistré dans le fichier funcs.c et compilé en un objet partagé, nous pouvons définir les fonctions dans PostgreSQL avec les commandes suivantes :

CREATE FUNCTION add_one(integer) RETURNS integer
     AS 'DIRECTORY/funcs', 'add_one'
     LANGUAGE C STRICT;

-- note overloading of SQL function name "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 indique le répertoire de la bibliothèque partagée (par exemple le répertoire du tutoriel PostgreSQL qui contient le code des exemples utilisés dans cette section). (Il serait préférable d'utiliser seulement 'funcs' dans la clause AS, après avoir ajouté DIRECTORY au chemin de recherches. Dans tous les cas, nous pouvons omettre l'extension spécifique du système pour une bibliothèque partagée, généralement .so.)

Notez que nous avons spécifié les fonctions comme « strict », ceci signifiant que le système pourrait automatiquement supposer un résultat NULL si une des valeurs en entrée était NULL. En le faisant, nous évitons la vérification des entrées NULL dans le code de la fonction. Sans cela, nous devrions vérifier les valeurs NULL explicitement en utilisant la macro PG_ARGISNULL().

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.

Au premier coup d'œud, les conventions de codage version-1 pourraient ressembler à de l'obscurantisme sans raison. Il pourrait sembler préférable d'utiliser les conventions d'appel C. Néanmoins, elles permettent de gérer des valeurs NULL pour les arguments et la valeur de retour calling, ainsi que des valeurs TOAST (compressées ou hors-ligne).

Les autres options proposées dans l'interface version 1 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'arguments 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 nom_table ALTER COLUMN nom_colonne 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 36.10.8), l'implémentation de fonctions triggers (Chapitre 37) et d'opérateurs d'appel de langage procédural (Chapitre 56). Pour plus de détails, voir src/backend/utils/fmgr/README dans les fichiers sources de la distribution.

36.10.4. Écriture du code #

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).

  • Compilez et liez votre code de façon à ce qu'il soit chargé dynamiquement dans PostgreSQL, ce qui requiert des informations spéciales. Voir Section 36.10.5 pour une explication détaillée sur la façon de le faire pour votre système d'exploitation spécifique.

  • Rappelez-vous de définir un « bloc magique » pour votre bibliothèque partagée, comme décrit dans Section 36.10.1.

  • 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 (ou allouez les avec la fonction palloc0). Même si vous assignez chacun des champs de votre structure, il pourrait rester des espaces de remplissage (trous dans la structure) afin de respecter l'alignement des données qui contiennent des valeurs parasites. Sans cela, il sera difficile de calculer des hachages pour les index ou les jointures, dans la mesure où vous devrez uniquement tenir compte des octets significatifs de vos structures de données pour calculer ces hachages. Le planificateur se base également sur des comparaisons de constantes via des égalités de bits, aussi vous pouvez obtenir des planifications incorrectes si des valeurs logiquement équivalentes ne sont pas identiques bit à bit.

  • 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.

36.10.5. Compiler et lier des fonctions chargées dynamiquement #

Avant de pouvoir être utilisées dans PostgreSQL, les fonctions d'extension écrites en C doivent être compilées et liées d'une certaine façon, ceci afin de produire un fichier dynamiquement chargeable par le serveur. Pour être plus précis, une bibliothèque partagée doit être créée.

Pour obtenir plus d'informations que celles contenues dans cette section, il faut se référer à la documentation du système d'exploitation, en particulier les pages traitant du compilateur C, de cc et de l'éditeur de lien, ld. Par ailleurs, le code source de PostgreSQL contient de nombreux exemples fonctionnels dans le répertoire contrib. Néanmoins, ces exemples entraînent la création de modules qui dépendent 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 : les fichiers sources sont d'abord compilés en fichiers objets puis sont liées ensemble. Les fichiers objets doivent être compilés sous la forme de code indépendant de sa position (PIC, acronyme de position-independent code) . Conceptuellement, cela signifie qu'ils peuvent être placés dans 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 qui permet de lier des bibliothèques partagées nécessite des options spéciales qui la distinguent de celle permettant de lier un exécutable. En théorie, tout du moins. La réalité est, sur certains systèmes, beaucoup plus complexe.

Les exemples suivants considèrent que le code source est un fichier foo.c et qu'une bibliothèque partagée foo.so doit être créée. Sans précision, le fichier objet intermédiaire est appelé foo.o. Une bibliothèque partagée peut contenir plusieurs fichiers objet. Cela dit, un seul est utilisé ici.

FreeBSD

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.

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o
     

Ceci est applicable depuis la version 13.0 de FreeBSD, les versions plus anciennes utilisent le compilateur gcc.

Linux

L'option du compilateur pour créer des PIC est -fPIC. L'option de compilation pour créer des bibliothèques partagées est -shared. Un exemple complet ressemble à :

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o
     

macOS

L'exemple suivant 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
     

NetBSD

L'option du compilateur 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 et non-ELF, on utilise ld -Bshareable.

gcc -fPIC -c foo.c
gcc -shared -o foo.so foo.o
     

OpenBSD

L'option du compilateur pour créer des PIC est -fPIC. Les bibliothèques partagées peuvent être créées avec ld -Bshareable.

gcc -fPIC -c foo.c
ld -Bshareable -o foo.so foo.o
     

Solaris

L'option du compilateur 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
     

Astuce

Si cela s'avère trop compliqué, GNU Libtool peut être utilisé. 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 précise le nom du fichier dans la commande CREATE FUNCTION, il faut indiquer le nom de la bibliothèque partagée et non celui du fichier objet intermédiaire. 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 une meilleure portabilité.

La Section 36.10.1 indique l'endroit où le serveur s'attend à trouver les fichiers de bibliothèques partagées.

36.10.6. Arguments de type composite #

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 1, nous pouvons définir c_surpaye comme :

#include "postgres.h"
#include "executor/executor.h"  /* pour GetAttributeByName() */

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(c_surpaye);

Datum
c_surpaye(PG_FUNCTION_ARGS)
{
    HeapTupleHeader  *t = (HeapTupleHeader *) PG_GETARG_HEAPTUPLEHEADER(0);
    int32            limite = PG_GETARG_INT32(1);
    bool isNULL;
    Datum salaire;

    salaire = 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(DatumGetInt32(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 HeapTupleHeader passé à la fonction, le nom de l'attribut recherché et un paramètre de retour 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 fonction appropriée DatumGetXXX(). Notez que la valeur de retour est insignifiante si le commutateur NULL est positionné ; il faut toujours vérifier le commutateur NULL avant de commencer à faire quelque chose avec le résultat.

Il y a aussi GetAttributeByNum, qui sélectionne l'attribut cible par le numéro de colonne au lieu de son nom.

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 STRICT;
      

Notez que nous avons utilisé STRICT pour que nous n'ayons pas à vérifier si les arguments en entrée sont NULL.

36.10.7. Renvoi de lignes (types composites) #

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"

Il existe deux façons de construire une valeur de données composites (autrement dit un « tuple ») : vous pouvez le construire à partir d'un tableau de valeurs Datum ou à partir d'un tableau de chaînes C qui peuvent passer dans les fonctions de conversion des types de données du tuple. Quelque soit le cas, vous avez d'abord besoin d'obtenir et de construire un descripteur TupleDesc pour la structure du tuple. En travaillant avec des Datums, vous passez le TupleDesc à BlessTupleDesc, puis vous appelez heap_form_tuple pour chaque ligne. En travaillant avec des chaînes C, vous passez TupleDesc à TupleDescGetAttInMetadata, puis vous appelez BuildTupleFromCStrings pour chaque ligne. Dans le cas d'une fonction renvoyant un ensemble de tuple, les étapes de configuration peuvent toutes être entreprises une fois lors du premier appel à la fonction.

Plusieurs fonctions d'aide sont disponibles pour configurer le TupleDesc requis. La façon recommandée de le faire dans la plupart des fonctions renvoyant des valeurs composites est d'appeler :

TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
                                   Oid *resultTypeId,
                                   TupleDesc *resultTupleDesc)
      

en passant la même structure fcinfo que celle passée à la fonction appelante (ceci requiert bien sûr que vous utilisez les conventions d'appel version-1). resultTypeId peut être spécifié comme NULL ou comme l'adresse d'une variable locale pour recevoir l'OID du type de résultat de la fonction. resultTupleDesc devrait être l'adresse d'une variable TupleDesc locale. Vérifiez que le résultat est TYPEFUNC_COMPOSITE ; dans ce cas, resultTupleDesc a été rempli avec le TupleDesc requis (si ce n'est pas le cas, vous pouvez rapporter une erreur pour une « fonction renvoyant un enregistrement appelé dans un contexte qui ne peut pas accepter ce type enregistrement »).

Astuce

get_call_result_type peut résoudre le vrai type du résultat d'une fonction polymorphique ; donc, il est utile pour les fonctions qui renvoient des résultats scalaires polymorphiques, pas seulement les fonctions qui renvoient des types composites. Le résultat resultTypeId est principalement utile pour les fonctions renvoyant des scalaires polymorphiques.

Note

get_call_result_type a une fonction cousine get_expr_result_type, qui peut être utilisée pour résoudre le tupe attendu en sortie en un appel de fonction représenté par un arbre d'expressions. Ceci peut être utilisé pour tenter de déterminer le type de résultat sans entrer dans la fonction elle-même. Il existe aussi get_func_result_type, qui peut seulement être utilisée quand l'OID de la fonction est disponible. Néanmoins, ces fonctions ne sont pas capables de gérer les fonctions déclarées renvoyer des enregistrements (record). get_func_result_type ne peut pas résoudre les types polymorphiques, donc vous devriez utiliser de préférence get_call_result_type.

Les fonctions anciennes, et maintenant obsolètes, qui permettent d'obtenir des TupleDesc sont :

TupleDesc RelationNameGetTupleDesc(const char *relname)
      

pour obtenir un TupleDesc pour le type de ligne d'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. Néanmoins, cela ne fonctionnera pas pour une fonction qui renvoie record et cela ne résoudra pas les types polymorphiques.

Une fois que vous avez un TupleDesc, appelez :

TupleDesc BlessTupleDesc(TupleDesc tupdesc)
      

si vous pensez travailler avec des Datums ou :

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
      

si vous pensez travailler avec des chaînes C. Si vous écrivez une fonction renvoyant un ensemble, vous pouvez sauvegarder les résultats de ces fonctions dans la structure dans le FuncCallContext -- utilisez le champ tuple_desc ou attinmeta respectivement.

Lorsque vous fonctionnez avec des Datums, utilisez :

HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
      

pour construire une donnée utilisateur HeapTuple indiquée dans le format Datum.

Lorsque vous travaillez avec des chaînes C, utilisez :

HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
      

pour construire une donnée utilisateur HeapTuple indiquée dans le format des chaînes C. values est un tableau de chaîne 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.

Une fois que vous avez construit un tuple devant être renvoyé par votre fonction, vous devez le convertir en type Datum. Utilisez :

HeapTupleGetDatum(HeapTuple tuple)
      

pour convertir un type HeapTuple en un Datum valide. 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.

36.10.8. Renvoi d'ensembles #

Les fonctions en langage C ont deux options pour renvoyer des ensembles (plusieurs lignes). Dans la première méthode, appelée mode ValuePerCall, une fonction renvoyant un ensemble de lignes est appelée de façon répétée (en passant les mêmes arguments à chaque fois) et elle renvoit une nouvelle ligne pour chaque appel, jusqu'à ce qu'il n'y ait plus de lignes à renvoyer et qu'elle le signale en renvoyant NULL. La fonction SRF doit de ce fait sauvegarder l'état entre appels pour se rappeler ce qu'elle faisait et renvoyer le bon prochain élément à chaque appel. Dans l'autre méthode, appelée mode Materialize, une SRF remplit et renvoit un objet tuplestore contenant le résultat entier. Un seul appel survient pour le résultat complet, et il n'est pas nécessaire de conserver l'état entre appels.

Lors de l'utilisation du mode ValuePerCall, il est important de se rappeler que la requête n'est pas garantie de se terminer ; c'est-à-dire, avec des options telles que LIMIT, l'exécuteur pourrait stopper les appels à la fonction SRF avant que toutes les lignes ne soient récupérées. Ceci signifie qu'il ne faut pas utiliser le dernier appel pour nettoyer l'activité réalisée, car ce dernier appel pourrait bien ne jamais survenir. Il est recommandé d'utiliser le mode Materialize pour les fonctions ayant besoin d'accéder aux ressources externes, tels que des descripteurs de fichiers.

Le reste de cette section documente un ensemble de macros d'aide qui sont communément utilisées (bien que non requis pour être utilisés) pour les SRF utilisant le mode ValuePerCall. Des détails supplémentaires sur le mode Materialize peuvent être trouvés dans src/backend/utils/fmgr/README. De plus, les modules contrib dans la distribution des sources de the PostgreSQL contiennent de nombreux exemples de SRF utilisant à la fois les modes ValuePerCall et Materialize.

Pour utiliser les macros de support ValuePerCall décrites ici, inclure funcapi.h. Ces macros fonctionnent avec une structure FuncCallContext qui contient l'état devant être sauvegardé au travers des appels. À l'intérieur de la SRF appelante, fcinfo->flinfo->fn_extra est utilisé pour détenir un pointeur vers FuncCallContext entre les appels. Les macros remplissent automatiquement ce champ à la première utilisation et s'attendent à y trouver le même pointeur lors des prochains appels.

typedef struct FuncCallContext
{
    /*
     * 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.
     */
    uint64 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.
     */
    uint64 max_calls;

    /*
     * 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 used 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;

    /*
    * OPTIONAL pointer to struct containing tuple description
    *
    * tuple_desc is for use when returning tuples (i.e. composite data types)
    * and is only needed if you are going to build the tuples with
    * heap_form_tuple() rather than with BuildTupleFromCStrings().  Note that
    * the TupleDesc pointer stored here should usually have been run through
    * BlessTupleDesc() first.
    */
    TupleDesc tuple_desc;

} FuncCallContext;
      

Les macros à utiliser par une SRF utilisant cette infrastructure sont :

SRF_IS_FIRSTCALL()
      

Utilisez ceci pour déterminer si votre fonction est appelée pour la première fois ou la prochaine fois. Au premier appel (seulement), utilisez :

SRF_FIRSTCALL_INIT()
      

pour initialiser la structure FuncCallContext. À chaque appel de fonction, y compris le premier, utilisez :

SRF_PERCALL_SETUP()
      

pour configurer l'utilisation de FuncCallContext.

Si votre fonction a des données à renvoyer dans l'appel courant, 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. Utilisez funcctx->user_fctx pour récupérer un pointeur vers de telles structures de données inter-appels (les données que vous allouez dans multi_call_memory_ctx partiront automatiquement à la fin de la requête, donc il n'est pas nécessaire de libérer cette donnée manuellement).

Avertissement

Quand les arguments réels de la fonction restent inchangés entre les appels, si vous lisez la valeur des arguments (ce qui se fait de façon transparente par la macro PG_GETARG_xxx) dans le contexte, alors les copies seront libérées sur chaque cycle. De la même façon, si vous conservez des références vers de telles valeurs dans votre user_fctx, vous devez soit les copier dans multi_call_memory_ctx, soit vous assurer que vous procédez vous-même au traitement des valeurs dans ce contexte.

Voici un exemple complet de pseudo-code :

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    further declarations as needed

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        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
        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, so just report that fact. */
        /* (Resist the temptation to put cleanup code here.) */
        SRF_RETURN_DONE(funcctx);
    }
}
      

Et voici un exemple complet d'une simple SRF retournant un type composite :

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    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_INT32(0);

        /* Build a tuple descriptor for our result type */
        if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("function returning record called in context "
                            "that cannot accept type record")));

        /*
         * 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;
    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 building the returned tuple.
         * 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 = HeapTupleGetDatum(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);
    }
}

Voici une façon de déclarer cette fonction en SQL :

CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);

CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
    RETURNS SETOF __retcomposite
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;
      

Une façon différente de le faire est d'utiliser des paramètres OUT :

CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
    OUT f1 integer, OUT f2 integer, OUT f3 integer)
    RETURNS SETOF record
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;
      

Notez que dans cette méthode le type en sortie de la fonction est du type record anonyme.

36.10.9. Arguments polymorphes et types renvoyés #

Les fonctions en langage C peuvent être déclarées pour accepter et renvoyer les types « polymorphes » décrits dans Section 36.2.5. 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. get_call_result_type peut aussi être utilisé comme alternative à get_fn_expr_rettype. Il existe aussi get_fn_expr_variadic, qui peut être utilisé pour trouver les arguments variables en nombre qui ont été assemblés en un tableau. C'est principalement utile dans le cadre des fonctions VARIADIC "any" car de tels assemblages surviendront toujours pour les fonctions variadiques prenant des types de tableaux ordinaires.

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;
    bool        isnull;
    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, being careful in case it's NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        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, &isnull, 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' IMMUTABLE;
      

Notez l'utilisation de STRICT ; ceci est primordial car le code ne se préoccupe pas de tester une entrée NULL.

Il existe une variante du polymorphisme qui est seulement disponible pour les fonctions en langage C : elles peuvent être déclarées prendre des paramètres de type "any". (Notez que ce nom de type doit être placé entre des guillemets doubles car il s'agit d'un mot SQL réservé.) Ceci fonctionne comme anyelement sauf qu'il ne contraint pas les différents arguments "any" à être du même type, pas plus qu'ils n'aident à déterminer le type de résultat de la fonction. Une fonction en langage C peut aussi déclarer son paramètre final ainsi : VARIADIC "any". Cela correspondra à un ou plusieurs arguments réels de tout type (pas nécessairement le même type). Ces arguments ne seront pas placés dans un tableau comme c'est le cas pour les fonctions variadic normales ; ils seront passés séparément à la fonction. La macro PG_NARGS() et les méthodes décrites ci-dessus doivent être utilisées pour déterminer le nombre d'arguments réels et leur type lors de l'utilisation de cette fonctionnalité. Ainsi, les utilisateurs d'une telle fonction voudront probablement utilisé le mot-clé VARIADIC dans leur appel de fonction, de manière à ce que la fonction traite les éléments du tableau comme des arguments séparés. La fonction elle-même doit implémenter ce comportement si nécessaire, après avoir utilisé get_fn_expr_variadic pour savoir si les arguments actuels ont été marqués avec VARIADIC.

36.10.10. Mémoire partagée #

36.10.10.1. Réclamer de la mémoire partagée au démarrage #

Les modules peuvent réserver de la mémoire au démarrage du serveur. Pour cela, la bibliothèque partagée du module doit être préchargée en l'ajoutant dans xref linkend="guc-shared-preload-libraries"/>. La bibliothèque partagée doit aussi enregistrer une fonction hook shmem_request_hook dans sa fonction _PG_init. Cette fonction shmem_request_hook peut réserver de la mémoire partagée en appelant :

void RequestAddinShmemSpace(Size size)

Chaque processus doit obtenir un pointeur vers la mémoire partagée réservée en appelant :

void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)

Si cette fonction configure foundPtr à false, l'appelant doit réaliser l'initialisation du contenu de la mémoire partagée réservée. Si foundPtr vaut true, la mémoire partagée était déjà initialisée par un autre processus et l'appelant n'a plus besoin de rien faire concernant l'initialisation.

Pour éviter tout cas problématique, tout processus doit utiliser le verrou LWLock AddinShmemInitLock lors de l'initialisation de l'allocation de mémoire partagée, comme montré ici :

static mystruct *ptr = NULL;
bool        found;

LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
    ... initialize contents of shared memory ...
    ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);

shmem_startup_hook fournit un emplacement correct pour le code d'initialisation mais il n'est pas strictement requis qu'un tel code soit placé dans cette fonction hook. Chaque processus executera le shmem_startup_hook enregistré un peu après s'être attaché à la mémoire partagée. Notez que les modules doivent toujours acquérir AddinShmemInitLock dans ce hook, comme indiqué dans l'exemple ci-dessus.

Un exemple d'un shmem_request_hook et d'un shmem_startup_hook peut se trouver dans contrib/pg_stat_statements/pg_stat_statements.c à partir du répertoire des sources de PostgreSQL.

36.10.10.2. Réclamer de la mémoire partagée après le démarrage #

Il existe une autre méthode, plus flexible, pour réserver de la mémoire partagée. Elle se fait après le démarrage du serveur et en dehors d'un shmem_request_hook. Pour cela, chaque processus qui doit utiliser la mémoire partager doit obtenir un pointeur en appelant :

void *GetNamedDSMSegment(const char *name, size_t size,
                         void (*init_callback) (void *ptr),
                         bool *found)

Si un segment de mémoire partagée dynamique de ce nom n'existe pas encore, cette fonction l'allouera et l'initialisera avec la fonction callback fournie init_callback. Si le segment a été alloué et initialisé par un autre processus, cette fonction attache simplement le segment de mémoire partagée dynamique au processus courant..

Contrairement à la mémoire partagée réservée au démarrage du serveur, il n'est plus nécessaire d'acquérir AddinShmemInitLock ou de réaliser des actions pour éviter les cas spéciaux lors de la réservation de mémoire partagée avec GetNamedDSMSegment. Cette fonction assure que seul un processus alloue et initialise le segment et que tous les autres processus reçoivent un pointeur vers le segment alloué et initialisé.

Une utilisation complète de GetNamedDSMSegment est disponible dans src/test/modules/test_dsm_registry/test_dsm_registry.c à partir du répertoire des sources de PostgreSQL.

36.10.11. LWLocks #

36.10.11.1. Demander des LWLocks au démarrage #

Les modules peuvent réserver des LWLocks au démarrage du serveur. Comme avec la mémoire partagée réservée au démarrage, la bibliothèque partagée du module doit être préchargée en l'ajoutant dans shared_preload_libraries, et la bibliothèque partagée doit enregistrer une fonction shmem_request_hook dans sa fonction _PG_init. Cette fonction shmem_request_hook peut réserver des LWLocks en appelant :

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

Ceci assure qu'un tableau de num_lwlocks LWLocks est disponible sous le nom tranche_name. Un pointeur vers ce tableau peut être obtenu en appelant :

LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)

36.10.11.2. Demander des LWLocks après le démarrage #

Il existe une autre méthode, plus flexible, d'obtenir des LWLocks qui peut se faire après le démarrage du serveur et en dehors d'une fonction shmem_request_hook. Pour cela, allouez tout d'abord un tranche_id en appelant :

int LWLockNewTrancheId(void)

Puis initialisez chaque LWLock, en lui passant le nouveau tranche_id comme argument :

void LWLockInitialize(LWLock *lock, int tranche_id)

De façon similaire à la mémoire partagée, chaque processus doit s'assurer qu'un seul processus alloue un nouveau tranche_id et initialise chaque nouveau LWLock. Une façon de le faire est de seulement appeler ces fonctions dans le code d'initialisation de la mémoire partagée avec AddinShmemInitLock détenu en exclusivité. Si vous utilisez GetNamedDSMSegment, appeler ces fonctions dans la fonction callback init_callback est suffisant pour éviter les cas problématiques rares.

Enfin, chaque processus utilisant tranche_id doit l'associer avec un tranche_name en appelant :

void LWLockRegisterTranche(int tranche_id, const char *tranche_name)

Un exemple complet d'utilisation de LWLockNewTrancheId, LWLockInitialize et LWLockRegisterTranche peut être trouvé dans contrib/pg_prewarm/autoprewarm.c à partir du répertoire des sources de PostgreSQL.

36.10.12. Événements d'attente personnalisés #

Les modules peuvent définir des événements d'attente sous le type d'événement Extension en appelant :

uint32 WaitEventExtensionNew(const char *wait_event_name)

L'événement d'attente est associé à une chaîne personnalisée visible par l'utilisateur. Un exemple est disponible dans src/test/modules/worker_spi à partir du répertoire des sources de PostgreSQL.

Les événements d'attente personnalisés sont visibles dans pg_stat_activity :

=# SELECT wait_event_type, wait_event FROM pg_stat_activity
     WHERE backend_type ~ 'worker_spi';
 wait_event_type |  wait_event
-----------------+---------------
 Extension       | WorkerSpiMain
(1 row)

36.10.13. Points d'injection #

Un point d'injection avec un name donné est déclaré en utilisant la macro :

INJECTION_POINT(name);

Il existe quelques points d'injection déjà déclarés à des endroits stratégiques du code du serveur. Après l'ajout d'un nouveau point d'injection, le code a besoin d'être compilé pour que le point d'injection soit disponible dans le binaire. Les modules écrits en langage C peuvent déclarer des points d'injection dans leur propre code en utilisant la même macro.

Les modules peuvent attacher des callbacks à un point d'injection déjà déclaré en appelant :

extern void InjectionPointAttach(const char *name,
                                 const char *library,
                                 const char *function,
                                 const void *private_data,
                                 int private_data_size);

name est le nom du point d'injection qui, lorsqu'il est atteint à l'exécutionn exécutera la fonction function chargé à partir de la bibliothèque library. private_data est une aire privée de données de taille private_data_size donné en argument à la callback lors de son exécution.

Voici un exemple de fonction callback pour InjectionPointCallback :

static void
custom_injection_callback(const char *name, const void *private_data)
{
     uint32 wait_event_info = WaitEventInjectionPointNew(name);

     pgstat_report_wait_start(wait_event_info);
     elog(NOTICE, "%s: executed custom callback", name);
     pgstat_report_wait_end();
}

Cette fonction callback affiche un message dans les traces du serveur de niveau NOTICE, mais les fonctions callbacks peuvent implémenter une logique bien plus complexe.

En option, il est possible de détacher un point d'injection en appelant :

extern bool InjectionPointDetach(const char *name);

En cas de succès, true est renvoyé, false sinon.

Une fonction callback attachée à un point d'injection est disponible pour tous les processus serveur, y compris ceux démarrés après l'appel à InjectionPointAttach. Elle reste attachée pendant toute l'exécution du serveur, à moins qu'elle ne soit détachée en utilisant InjectionPointDetach.

Un exemple est disponible dans src/test/modules/injection_points du répertoire des sources de PostgreSQL.

Activer les points d'injection requiert l'utilisation de l'option --enable-injection-points avec configure ou -Dinjection_points=true avec Meson.

36.10.14. Coder des extensions en C++ #

Bien que le moteur PostgreSQL soit écrit en C, il est possible de coder des extensions en C++ si les lignes de conduite suivantes sont respectées :

  • Toutes les fonctions accessibles par le serveur doivent présenter une interface en C ; seules ces fonctions C pourront alors appeler du code C++. Ainsi, l'édition de liens extern C est nécessaire pour les fonctions appelées par le serveur. Ceci est également obligatoire pour toutes les fonctions passées comme pointeur entre le serveur et du code C++.

  • Libérez la mémoire en utilisant la méthode de désallocation appropriée. Par exemple, la majeure partie de la mémoire allouée par le serveur l'est par appel de la fonction palloc(), aussi, il convient de libérer ces zones mémoire en utilisant la fonction pfree(). L'utilisation de la fonction C++ delete échouerait pour ces blocs de mémoire.

  • Évitez la propagation d'exceptions dans le code C (utilisez un bloc catch-all au niveau le plus haut de toute fonction extern C. Ceci est nécessaire, même si le code C++ n'émet explicitement aucune exception, dans la mesure où la survenue d'événements tels qu'un manque de mémoire peut toujours lancer une exception. Toutes les exceptions devront être gérées et les erreurs correspondantes transmises via l'interface du code C. Si possible, compilez le code C++ avec l'option -fno-exceptions afin d'éliminer entièrement la venue d'exceptions ; dans ce cas, vous devrez effectuer vous-même les vérifications correspondantes dans votre code C++, par exemple, vérifier les éventuels paramètres NULL retournés par la fonction new().

  • Si vous appelez des fonctions du serveur depuis du code C++, assurez vous que la pile d'appels ne contienne que des structures C (POD). Ceci est nécessaire dans la mesure où les erreurs au niveau du serveur génèrent un saut via l'instruction longjmp() qui ne peut dépiler proprement une pile d'appels C++ comportant des objets non-POD.

Pour résumer, le code C++ doit donc être placé derrière un rempart de fonctions extern C qui fourniront l'interface avec le serveur, et devra éviter toute fuite de mécanismes propres au C++ (exceptions, allocation/libération de mémoire et objets non-POD dans la pile).