36.10. Funciones en lenguaje C #

36.10.1. Carga dinámica
36.10.2. Tipos base en funciones en lenguaje C
36.10.3. Convenciones de llamada de la versión 1
36.10.4. Escritura de código
36.10.5. Compilación y enlace de funciones cargadas dinámicamente
36.10.6. Guía de estabilidad de la API y ABI del servidor
36.10.7. Argumentos de tipo compuesto
36.10.8. Devolución de filas (tipos compuestos)
36.10.9. Devolución de conjuntos
36.10.10. Argumentos polimórficos y tipos de retorno
36.10.11. Memoria compartida
36.10.12. LWLocks
36.10.13. Eventos de espera personalizados
36.10.14. Puntos de inyección (injection points)
36.10.15. Estadísticas acumulativas personalizadas
36.10.16. Uso de C++ para extensibilidad

Puedes escribir funciones definidas por el usuario en C (o en un lenguaje que pueda hacerse compatible con C, como C++). Estas funciones se compilan en objetos cargables dinámicamente (también llamados bibliotecas compartidas) y son cargadas por el servidor a demanda. La funcionalidad de carga dinámica es lo que distingue a las funciones en lenguaje C de las funciones internas; las convenciones de programación reales son esencialmente las mismas para ambas. (Por lo tanto, la biblioteca estándar de funciones internas es una rica fuente de ejemplos de código para funciones C definidas por el usuario).

Actualmente solo se utiliza una convención de llamada para las funciones C (versión 1). El soporte para esa convención de llamada se indica escribiendo una llamada a la macro PG_FUNCTION_INFO_V1() para la función, como se ilustra a continuación.

36.10.1. Carga dinámica #

La primera vez que se llama en una sesión a una función definida por el usuario en un archivo de objeto cargable particular, el cargador dinámico carga ese archivo de objeto en la memoria para que se pueda llamar a la función. Por lo tanto, el comando CREATE FUNCTION para una función C definida por el usuario debe especificar dos elementos de información para la función: el nombre del archivo de objeto cargable y el nombre en C (símbolo de enlace) de la función específica a llamar dentro de ese archivo de objeto. Si no se especifica explícitamente el nombre en C, se asume que es el mismo que el nombre de la función SQL.

Se utiliza el siguiente algoritmo para localizar el archivo de objeto compartido basado en el nombre proporcionado en el comando CREATE FUNCTION:

  1. Si el nombre es una ruta absoluta, se carga el archivo dado.

  2. Si el nombre comienza con la cadena $libdir, esa parte se reemplaza por el nombre del directorio de biblioteca de paquetes de PostgreSQL, el cual se determina en el momento de la compilación.

  3. Si el nombre no contiene una parte de directorio, se busca el archivo en la ruta especificada por la variable de configuración dynamic_library_path.

  4. De lo contrario (el archivo no se encontró en la ruta, o contiene una parte de directorio no absoluta), el cargador dinámico intentará tomar el nombre tal como se proporciona, lo que muy probablemente fallará. (No es fiable depender del directorio de trabajo actual).

Si esta secuencia no funciona, se añade al nombre proporcionado la extensión de nombre de archivo de biblioteca compartida específica de la plataforma (a menudo .so) y se vuelve a intentar esta secuencia. Si eso también falla, la carga fallará.

Se recomienda ubicar las bibliotecas compartidas de forma relativa a $libdir o mediante la ruta de la biblioteca dinámica. Esto simplifica las actualizaciones de versión si la nueva instalación se encuentra en una ubicación diferente. El directorio real al que representa $libdir se puede conocer con el comando pg_config --pkglibdir.

El ID de usuario bajo el cual se ejecuta el servidor de PostgreSQL debe poder recorrer la ruta hacia el archivo que deseas cargar. Hacer que el archivo o un directorio de nivel superior no sea legible y/o no ejecutable por el usuario postgres es un error común.

En cualquier caso, el nombre de archivo que se proporciona en el comando CREATE FUNCTION se registra literalmente en los catálogos del sistema, por lo que si es necesario volver a cargar el archivo se aplica el mismo procedimiento.

Note

PostgreSQL no compilará una función C automáticamente. El archivo de objeto debe compilarse antes de que se haga referencia a él en un comando CREATE FUNCTION. Consulta la Section 36.10.5 para obtener información adicional.

Para garantizar que un archivo de objeto cargado dinámicamente no se cargue en un servidor incompatible, PostgreSQL comprueba que el archivo contenga un bloque mágico con el contenido adecuado. Esto permite al servidor detectar incompatibilidades obvias, como código compilado para una versión principal diferente de PostgreSQL. Para incluir un bloque mágico, escribe esto en uno (y solo uno) de los archivos de origen del módulo, después de haber incluido el encabezado fmgr.h:

PG_MODULE_MAGIC;

o

PG_MODULE_MAGIC_EXT(parameters);

La variante PG_MODULE_MAGIC_EXT permite la especificación de información adicional sobre el módulo; actualmente, se puede añadir un nombre y/o una cadena de versión. (Se podrían permitir más campos en el futuro). Escribe algo como esto:

PG_MODULE_MAGIC_EXT(
    .name = "my_module_name",
    .version = "1.2.3"
);

Posteriormente, el nombre y la versión se pueden examinar mediante la función pg_get_loaded_modules(). El significado de la cadena de versión no está restringido por PostgreSQL, pero se recomienda el uso de las reglas de versionado semántico.

Después de que se utiliza por primera vez, un archivo de objeto cargado dinámicamente se retiene en memoria. Las futuras llamadas en la misma sesión a las funciones en ese archivo solo incurrirán en la pequeña sobrecarga de una búsqueda en la tabla de símbolos. Si necesitas forzar la recarga de un archivo de objeto, por ejemplo después de recompilarlo, inicia una nueva sesión.

Opcionalmente, un archivo cargado dinámicamente puede contener una función de inicialización. Si el archivo incluye una función llamada _PG_init, esa función se llamará inmediatamente después de cargar el archivo. La función no recibe parámetros y debe devolver void. Actualmente no hay forma de descargar un archivo cargado dinámicamente.

36.10.2. Tipos base en funciones en lenguaje C #

Para saber cómo escribir funciones en lenguaje C, necesitas saber cómo representa internamente PostgreSQL los tipos de datos base y cómo se pueden pasar hacia y desde las funciones. Internamente, PostgreSQL considera un tipo base como un bloque de memoria. Las funciones definidas por el usuario que defines sobre un tipo definen a su vez la forma en que PostgreSQL puede operar con él. Es decir, PostgreSQL solo almacenará y recuperará los datos del disco y utilizará tus funciones definidas por el usuario para ingresar, procesar y generar los datos.

Los tipos base pueden tener uno de tres formatos internos:

  • paso por valor, longitud fija

  • paso por referencia, longitud fija

  • paso por referencia, longitud variable

Los tipos por valor solo pueden tener 1, 2 o 4 bytes de longitud (también 8 bytes, si sizeof(Datum) es 8 en tu máquina). Debes tener cuidado al definir tus tipos de manera que tengan el mismo tamaño (in bytes) en todas las arquitecturas. Por ejemplo, el tipo long es peligroso porque tiene 4 bytes en algunas máquinas y 8 bytes en otras, mientras que el tipo int tiene 4 bytes en la mayoría de las máquinas Unix. Una implementación razonable del tipo int4 en máquinas Unix podría ser:

/* entero de 4 bytes, pasado por valor */
typedef int int4;

(El código C real de PostgreSQL llama a este tipo int32, porque es una convención en C que intXX significa XX bits. Ten en cuenta por lo tanto también que el tipo C int8 tiene un tamaño de 1 byte. El tipo SQL int8 se llama int64 en C. Consulta también la Table 36.2).

Por otro lado, los tipos de longitud fija de cualquier tamaño se pueden pasar por referencia. Por ejemplo, aquí tienes una muestra de la implementación de un tipo de PostgreSQL:

/* estructura de 16 bytes, pasada por referencia */
typedef struct
{
    double  x, y;
} Point;

Solo se pueden utilizar punteros a dichos tipos al pasarlos hacia y desde las funciones de PostgreSQL. Para devolver un valor de este tipo, asigna la cantidad correcta de memoria con palloc, llena la memoria asignada, y devuelve un puntero a ella. (Además, si solo quieres devolver el mismo valor que uno de tus argumentos de entrada que es del mismo tipo de datos, puedes omitir el palloc adicional y simplemente devolver el puntero al valor de entrada).

Por último, todos los tipos de longitud variable también deben pasarse por referencia. Todos los tipos de longitud variable deben comenzar con un campo de longitud opaco de exactamente 4 bytes, el cual será establecido por SET_VARSIZE; ¡nunca establezcas este campo directamente! Todos los datos que se almacenen dentro de ese tipo deben ubicarse en la memoria inmediatamente después de ese campo de longitud. El campo de longitud contiene la longitud total de la estructura, es decir, incluye el tamaño del campo de longitud en sí.

Otro punto importante es evitar dejar bits sin inicializar dentro de los valores de los tipos de datos; por ejemplo, ten cuidado de rellenar con ceros cualquier byte de alineación que pueda estar presente en las estructuras. Sin esto, el planificador podría ver constantes lógicamente equivalentes de tu tipo de datos como desiguales, lo que llevaría a planes ineficientes (aunque no incorrectos).

Warning

Nunca modifiques el contenido de un valor de entrada pasado por referencia. Si lo haces, es probable que corrompas los datos en disco, ya que el puntero que se te proporciona podría apuntar directamente a un búfer de disco. La única excepción a esta regla se explica en la Section 36.12.

Como ejemplo, podemos definir el tipo text de la siguiente manera:

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

La notación [FLEXIBLE_ARRAY_MEMBER] significa que la longitud real de la parte de datos no está especificada por esta declaración.

Al manipular tipos de longitud variable, debemos tener cuidado de asignar la cantidad correcta de memoria y establecer el campo de longitud correctamente. Por ejemplo, si quisiéramos almacenar 40 bytes en una estructura text, podríamos utilizar un fragmento de código como este:

#include "postgres.h"
...
char buffer[40]; /* nuestros datos de origen */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ es lo mismo que sizeof(int32), pero se considera un buen estilo utilizar la macro VARHDRSZ para referirse al tamaño del encabezado de un tipo de longitud variable. Además, el campo de longitud debe establecerse utilizando la macro SET_VARSIZE, no mediante una asignación simple.

La Table 36.2 muestra los tipos C correspondientes a muchos de los tipos de datos SQL integrados de PostgreSQL. La columna Definido en indica el archivo de cabecera que debe incluirse para obtener la definición del tipo. (La definición real podría estar en un archivo diferente que sea incluido por el archivo listado. Se recomienda que los usuarios se ciñan a la interfaz definida). Ten en cuenta que siempre debes incluir postgres.h primero en cualquier archivo de origen de código del servidor, porque declara una serie de cosas que necesitarás de todos modos, y porque incluir otros encabezados primero puede causar problemas de portabilidad.

Table 36.2. Tipos C equivalentes para tipos SQL integrados

Tipo SQL Tipo C Definido en
booleanboolpostgres.h (maybe compiler built-in)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(compiler built-in)
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

Ahora que hemos repasado todas las estructuras posibles para los tipos base, podemos mostrar algunos ejemplos de funciones reales.

36.10.3. Convenciones de llamada de la versión 1 #

La convención de llamada de la versión 1 se apoya en macros para suprimir la mayor parte de la complejidad de pasar argumentos y resultados. La declaración en C de una función de la versión 1 siempre es:

Datum funcname(PG_FUNCTION_ARGS)

Además, la llamada a la macro:

PG_FUNCTION_INFO_V1(funcname);

debe aparecer en el mismo archivo de código fuente. (Por convención, se escribe justo antes de la propia función). Esta llamada a la macro no es necesaria para las funciones en lenguaje internal, ya que PostgreSQL asume que todas las funciones internas utilizan la convención de la versión 1. Sin embargo, sí es requerida para funciones cargadas dinámicamente.

En una función de versión 1, cada argumento real se recupera utilizando una macro PG_GETARG_xxx() que corresponde al tipo de datos del argumento. (En funciones no estrictas, es necesario realizar una comprobación previa de la nulidad del argumento utilizando PG_ARGISNULL(); ver más abajo). El resultado se devuelve utilizando una macro PG_RETURN_xxx() para el tipo de retorno. PG_GETARG_xxx() toma como argumento el número del argumento de la función que se desea recuperar, comenzando el conteo en 0. PG_RETURN_xxx() toma como argumento el valor real a devolver.

Para invocar otra función de versión 1, puedes utilizar DirectFunctionCalln(func, arg1, ..., argn). Esto es particularmente útil cuando deseas invocar funciones definidas en la biblioteca interna estándar, utilizando una interfaz similar a su firma SQL.

Estas funciones de conveniencia y otras similares se pueden encontrar en fmgr.h. La familia DirectFunctionCalln espera el nombre de una función C como primer argumento. También existe OidFunctionCalln, que toma el OID de la función destino, y algunas otras variantes. Todas estas esperan que los argumentos de la función se suministren como Datums, e igualmente devuelven Datum. Ten en cuenta que no se permite que los argumentos ni el resultado sean NULL cuando se utilizan estas funciones de conveniencia.

Por ejemplo, para invocar la función starts_with(text, text) desde C, puedes buscar en el catálogo y encontrar que su implementación en C es la función Datum text_starts_with(PG_FUNCTION_ARGS). Normalmente utilizarías DirectFunctionCall2(text_starts_with, ...) para invocar dicha función. Sin embargo, starts_with(text, text) requiere información de ordenamiento (collation), por lo que fallará con could not determine which collation to use for string comparison si se invoca de esa manera. En su lugar, debes utilizar DirectFunctionCall2Coll(text_starts_with, ...) y proporcionar el ordenamiento deseado, el cual típicamente se pasa directamente desde PG_GET_COLLATION(), como se muestra en el ejemplo de abajo.

fmgr.h también suministra macros que facilitan las conversiones entre tipos C y Datum. Por ejemplo, para convertir un Datum en text*, puedes utilizar DatumGetTextPP(X). Mientras que algunos tipos tienen macros llamadas como TypeGetDatum(X) para la conversión inversa, text* no las tiene; basta con utilizar la macro genérica PointerGetDatum(X) para ello. Si tu extensión define tipos adicionales, usualmente también es conveniente definir macros similares para tus tipos.

Aquí tienes algunos ejemplos utilizando la convención de llamada de la versión 1:

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

PG_MODULE_MAGIC;

/* por valor */

PG_FUNCTION_INFO_V1(add_one);

Datum
add_one(PG_FUNCTION_ARGS)
{
    int32   arg = PG_GETARG_INT32(0);

    PG_RETURN_INT32(arg + 1);
}

/* por referencia, longitud fija */

PG_FUNCTION_INFO_V1(add_one_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* Las macros para FLOAT8 ocultan su naturaleza de paso por referencia. */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* Aquí, la naturaleza de paso por referencia de Point no está oculta. */
    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);
}

/* por referencia, longitud variable */

PG_FUNCTION_INFO_V1(copytext);

Datum
copytext(PG_FUNCTION_ARGS)
{
    text     *t = PG_GETARG_TEXT_PP(0);

    /*
     * VARSIZE_ANY_EXHDR es el tamaño de la estructura en bytes, menos el
     * VARHDRSZ o VARHDRSZ_SHORT de su encabezado. Construye la copia con un
     * encabezado de longitud completa.
     */
    text     *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
    SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);

    /*
     * VARDATA es un puntero a la región de datos de la nueva estructura. El origen
     * podría ser un dato corto, por lo que recupera sus datos mediante VARDATA_ANY.
     */
    memcpy(VARDATA(new_t),          /* destino */
           VARDATA_ANY(t),          /* origen */
           VARSIZE_ANY_EXHDR(t));   /* cuántos 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);
}

/* Un wrapper alrededor de starts_with(text, text) */

PG_FUNCTION_INFO_V1(t_starts_with);

Datum
t_starts_with(PG_FUNCTION_ARGS)
{
    text       *t1 = PG_GETARG_TEXT_PP(0);
    text       *t2 = PG_GETARG_TEXT_PP(1);
    Oid         collid = PG_GET_COLLATION();
    bool        result;

    result = DatumGetBool(DirectFunctionCall2Coll(text_starts_with,
                                                  collid,
                                                  PointerGetDatum(t1),
                                                  PointerGetDatum(t2)));
    PG_RETURN_BOOL(result);
}

Suponiendo que el código anterior se ha preparado en el archivo funcs.c y se ha compilado en un objeto compartido, podríamos definir las funciones en PostgreSQL con comandos como este:

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

-- nota la sobrecarga del nombre de función 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;

CREATE FUNCTION t_starts_with(text, text) RETURNS boolean
     AS 'DIRECTORY/funcs', 't_starts_with'
     LANGUAGE C STRICT;

Aquí, DIRECTORY representa el directorio del archivo de biblioteca compartida (por ejemplo, el directorio del tutorial de PostgreSQL, que contiene el código de los ejemplos utilizados en esta sección). (Un estilo mejor sería utilizar simplemente 'funcs' en la cláusula AS, después de haber añadido DIRECTORY a la ruta de búsqueda. En cualquier caso, podemos omitir la extensión específica del sistema para una biblioteca compartida, comúnmente .so).

Observa que hemos especificado las funciones como strict, lo que significa que el sistema debe asumir automáticamente un resultado nulo si cualquier valor de entrada es nulo. Al hacer esto, nos evitamos tener que comprobar entradas nulas en el código de la función. Sin esto, tendríamos que comprobar los valores nulos explícitamente, utilizando PG_ARGISNULL().

La macro PG_ARGISNULL(n) permite a una función comprobar si cada entrada es nula. (Por supuesto, hacer esto solo es necesario en funciones que no se declaran como strict). Al igual que con las macros PG_GETARG_xxx(), los argumentos de entrada se cuentan comenzando desde cero. Ten en cuenta que debes abstenerte de ejecutar PG_GETARG_xxx() hasta haber verificado que el argumento no es nulo. Para devolver un resultado nulo, ejecuta PG_RETURN_NULL(); esto funciona tanto en funciones estrictas como no estrictas.

A primera vista, las convenciones de codificación de la versión 1 podrían parecer simplemente un oscurantismo sin sentido, en comparación con el uso de las convenciones de llamada normales de C. Sin embargo, nos permiten manejar argumentos/valores de retorno que admiten valores NULL, y valores toasted (comprimidos o fuera de línea).

Otras opciones proporcionadas por la interfaz de la versión 1 son dos variantes de las macros PG_GETARG_xxx(). La primera de ellas, PG_GETARG_xxx_COPY(), garantiza devolver una copia del argumento especificado en la que sea seguro escribir. (Las macros normales a veces devolverán un puntero a un valor que está almacenado físicamente en una tabla, en el cual no se debe escribir. El uso de las macros PG_GETARG_xxx_COPY() garantiza un resultado escribible). La segunda variante consiste en las macros PG_GETARG_xxx_SLICE() que toman tres argumentos. El primero es el número del argumento de la función (como se indica arriba). El segundo y el tercero son el desplazamiento y la longitud del segmento a devolver. Los desplazamientos se cuentan desde cero, y una longitud negativa solicita que se devuelva el resto del valor. Estas macros proporcionan un acceso más eficiente a partes de valores grandes en caso de que tengan el tipo de almacenamiento external. (El tipo de almacenamiento de una columna se puede especificar utilizando ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype. storagetype es uno de los siguientes: plain, external, extended o main).

Finalmente, las convenciones de llamada a funciones de la versión 1 hacen posible devolver conjuntos de resultados (Section 36.10.9) e implementar funciones de disparador (Chapter 37) y controladores de llamadas de lenguajes procedimentales (Chapter 57). Para más detalles, consulta src/backend/utils/fmgr/README en la distribución del código fuente.

36.10.4. Escritura de código #

Antes de pasar a temas más avanzados, deberíamos discutir algunas reglas de codificación para las funciones en lenguaje C de PostgreSQL. Si bien podría ser posible cargar funciones escritas en lenguajes distintos de C en PostgreSQL, esto suele ser difícil (cuando es posible en absoluto) porque otros lenguajes, como C++, FORTRAN o Pascal a menudo no siguen la misma convención de llamada que C. Es decir, otros lenguajes no pasan los argumentos y los valores de retorno entre funciones de la misma manera. Por esta razón, asumiremos que tus funciones en lenguaje C están escritas realmente en C.

Las reglas básicas para escribir y compilar funciones en C son las siguientes:

  • Utiliza pg_config --includedir-server para saber dónde están instalados los archivos de cabecera del servidor PostgreSQL en tu sistema (o en el sistema en el que se ejecutarán tus usuarios).

  • Compilar y enlazar tu código para que pueda ser cargado dinámicamente en PostgreSQL siempre requiere flags especiales. Consulta Section 36.10.5 para obtener una explicación detallada de cómo hacerlo para tu sistema operativo en particular.

  • Recuerda definir un magic block (bloque mágico) para tu biblioteca compartida, como se describe en Section 36.10.1.

  • Al asignar memoria, utiliza las funciones de PostgreSQL palloc y pfree en lugar de las funciones correspondientes de la biblioteca estándar de C malloc y free. La memoria asignada por palloc se liberará automáticamente al final de cada transacción, evitando fugas de memoria.

  • Pon siempre a cero los bytes de tus estructuras utilizando memset (o asígnalas con palloc0 en primer lugar). Aunque asignes un valor a cada campo de tu estructura, podría haber un relleno de alineación (huecos en la estructura) que contenga valores basura. Sin esto, es difícil soportar índices hash o uniones hash, ya que debes seleccionar solo los bits significativos de tu estructura de datos para calcular un hash. El planificador también depende a veces de la comparación de constantes mediante igualdad de bits, por lo que puedes obtener resultados de planificación no deseados si los valores lógicamente equivalentes no son idénticos a nivel de bits.

  • La mayoría de los tipos internos de PostgreSQL se declaran en postgres.h, mientras que las interfaces del gestor de funciones (PG_FUNCTION_ARGS, etc.) están en fmgr.h, por lo que necesitarás incluir al menos estos dos archivos. Por razones de portabilidad, es mejor incluir postgres.h primero, antes de cualquier otro archivo de cabecera del sistema o del usuario. Incluir postgres.h también incluirá elog.h y palloc.h por ti.

  • Los nombres de símbolos definidos dentro de los archivos de objeto no deben entrar en conflicto entre sí ni con los símbolos definidos en el ejecutable del servidor PostgreSQL. Tendrás que renombrar tus funciones o variables si recibes mensajes de error al respecto.

36.10.5. Compilación y enlace de funciones cargadas dinámicamente #

Antes de que puedas usar tus funciones de extensión de PostgreSQL escritas en C, deben compilarse y enlazarse de una manera especial para producir un archivo que el servidor pueda cargar dinámicamente. Para ser precisos, se debe crear una biblioteca compartida (shared library).

Para obtener información más allá de lo que se detalla en esta sección, debes leer la documentación de tu sistema operativo, en particular las páginas del manual del compilador de C, cc, y del editor de enlaces, ld. Además, el código fuente de PostgreSQL contiene varios ejemplos prácticos en el directorio contrib. Sin embargo, si te basas en estos ejemplos, harás que tus módulos dependan de la disponibilidad del código fuente de PostgreSQL.

La creación de bibliotecas compartidas es generalmente análoga al enlace de ejecutables: primero los archivos fuente se compilan en archivos objeto, luego los archivos objeto se enlazan entre sí. Los archivos objeto deben crearse como código independiente de la posición (PIC), lo que conceptualmente significa que pueden colocarse en una ubicación arbitraria en la memoria cuando son cargados por el ejecutable. (Los archivos objeto destinados a ejecutables no suelen compilarse de esa manera). El comando para enlazar una biblioteca compartida contiene banderas (flags) especiales para distinguirlo del enlace de un ejecutable (al menos en teoría; en algunos sistemas la práctica es mucho más compleja).

En los siguientes ejemplos asumimos que tu código fuente está en un archivo foo.c y crearemos una biblioteca compartida foo.so. El archivo objeto intermedio se llamará foo.o a menos que se indique lo contrario. Una biblioteca compartida puede contener más de un archivo objeto, pero aquí solo usamos uno.

FreeBSD

La bandera del compilador para crear PIC es -fPIC. Para crear bibliotecas compartidas, la bandera del compilador es -shared.

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

Esto es aplicable a partir de la versión 13.0 de FreeBSD, las versiones anteriores utilizaban el compilador gcc.

Linux

La bandera del compilador para crear PIC es -fPIC. La bandera del compilador para crear una biblioteca compartida es -shared. Un ejemplo completo se ve así:

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

macOS

Aquí tienes un ejemplo. Asume que las herramientas de desarrollo están instaladas.

cc -c foo.c
cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o

NetBSD

La bandera del compilador para crear PIC es -fPIC. Para sistemas ELF, se utiliza el compilador con la bandera -shared para enlazar bibliotecas compartidas. En los sistemas más antiguos que no son ELF, se utiliza ld -Bshareable.

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

OpenBSD

La bandera del compilador para crear PIC es -fPIC. Se utiliza ld -Bshareable para enlazar bibliotecas compartidas.

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

Solaris

La bandera del compilador para crear PIC es -KPIC con el compilador de Sun y -fPIC con GCC. Para enlazar bibliotecas compartidas, la opción del compilador es -G con cualquiera de los compiladores o, alternativamente, -shared con GCC.

cc -KPIC -c foo.c
cc -G -o foo.so foo.o

o

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

Tip

Si esto es demasiado complicado para ti, deberías considerar el uso de GNU Libtool, el cual oculta las diferencias de plataforma detrás de una interfaz uniforme.

El archivo de biblioteca compartida resultante se puede cargar luego en PostgreSQL. Al especificar el nombre del archivo en el comando CREATE FUNCTION, se debe indicar el nombre de la biblioteca compartida, no el del archivo objeto intermedio. Ten en cuenta que la extensión de biblioteca compartida estándar del sistema (generalmente .so o .sl) se puede omitir del comando CREATE FUNCTION, y normalmente debería omitirse para una mejor portabilidad.

Consulta la Section 36.10.1 para saber dónde espera el servidor encontrar los archivos de biblioteca compartida.

36.10.6. Guía de estabilidad de la API y ABI del servidor #

Esta sección contiene orientación para los autores de extensiones y otros plugins del servidor sobre la estabilidad de la API y ABI en el servidor PostgreSQL.

36.10.6.1. General #

El servidor PostgreSQL contiene varias API bien delimitadas para plugins del servidor, como el gestor de funciones (fmgr, descrito en este capítulo), SPI (Chapter 45) y varios hooks diseñados específicamente para extensiones. Estas interfaces se gestionan cuidadosamente para lograr estabilidad y compatibilidad a largo plazo. Sin embargo, todo el conjunto de funciones y variables globales en el servidor constituye efectivamente la API públicamente utilizable, y la mayor parte no fue diseñada con la extensibilidad y la estabilidad a largo plazo en mente.

Por lo tanto, aunque aprovechar estas interfaces es válido, cuanto más te desvíes del camino habitual, más probable será que encuentres problemas de compatibilidad de API o ABI en algún momento. Se anima a los autores de extensiones a proporcionar comentarios sobre sus requisitos, de modo que con el tiempo, a medida que surjan nuevos patrones de uso, ciertas interfaces puedan considerarse más estables o se puedan añadir interfaces nuevas y mejor diseñadas.

36.10.6.2. Compatibilidad de API #

La API, o interfaz de programación de aplicaciones, es la interfaz utilizada en tiempo de compilación.

36.10.6.2.1. Versiones mayores #

No hay ninguna promesa de compatibilidad de API entre versiones mayores de PostgreSQL. Por lo tanto, el código de las extensiones podría requerir cambios en el código fuente para funcionar con múltiples versiones mayores. Normalmente, esto se puede gestionar con condiciones del preprocesador como #if PG_VERSION_NUM >= 160000. Las extensiones sofisticadas que utilizan interfaces más allá de las bien delimitadas suelen requerir algunos de estos cambios para cada versión mayor del servidor.

36.10.6.2.2. Versiones menores #

PostgreSQL hace un esfuerzo para evitar rupturas de la API del servidor en las versiones menores. In general, el código de extensión que se compila y funciona con una versión menor también debería compilarse y funcionar con cualquier otra versión menor de la misma versión mayor, pasada o futura.

Cuando se requiere un cambio, se gestionará cuidadosamente, teniendo en cuenta los requisitos de las extensiones. Dichos cambios se comunicarán en las notas de lanzamiento (Appendix E).

36.10.6.3. Compatibilidad de ABI #

La ABI, o interfaz binaria de aplicación, es la interfaz utilizada en tiempo de ejecución.

36.10.6.3.1. Versiones mayores #

Los servidores de diferentes versiones mayores tienen ABI intencionadamente incompatibles. Por lo tanto, las extensiones que utilizan las API del servidor deben volver a compilarse para cada versión mayor. La inclusión de PG_MODULE_MAGIC (consulta Section 36.10.1) garantiza que el código compilado para una versión mayor sea rechazado por otras versiones mayores.

36.10.6.3.2. Versiones menores #

PostgreSQL hace un esfuerzo para evitar rupturas de la ABI del servidor en las versiones menores. En general, una extensión compilada contra cualquier versión menor debería funcionar con cualquier otra versión menor de la misma versión mayor, pasada o futura.

Cuando se requiere un cambio, PostgreSQL elegirá el cambio menos invasivo posible, por ejemplo, insertando un nuevo campo en el espacio de relleno o añadiéndolo al final de una estructura. Este tipo de cambios no deberían afectar a las extensiones a menos que utilicen patrones de código muy inusuales.

Sin embargo, en casos raros, incluso tales cambios no invasivos pueden ser inviables o imposibles. En tal evento, el cambio se gestionará cuidadosamente, teniendo en cuenta los requisitos de las extensiones. Dichos cambios también se documentarán en las notas de lanzamiento (Appendix E).

Ten en cuenta, sin embargo, que muchas partes del servidor no están diseñadas ni se mantienen como API de consumo público (y que, en la mayoría de los casos, el límite real tampoco está bien definido). Si surgen necesidades urgentes, los cambios en esas partes se realizarán de forma natural con menos consideración por el código de las extensiones que los cambios en interfaces bien definidas y ampliamente utilizadas.

Además, a falta de una detección automatizada de tales cambios, esto no es una garantía, pero históricamente tales cambios que rompen la compatibilidad han sido extremadamente raros.

36.10.7. Argumentos de tipo compuesto #

Los tipos compuestos no tienen una distribución fija como las estructuras de C. Las instancias de un tipo compuesto pueden contener campos nulos. Además, los tipos compuestos que forman parte de una jerarquía de herencia pueden tener campos diferentes a los de otros miembros de la misma jerarquía. Por lo tanto, PostgreSQL proporciona una interfaz de función para acceder a los campos de los tipos compuestos desde C.

Supongamos que queremos escribir una función para responder a la consulta:

SELECT name, c_overpaid(emp, 1500) AS overpaid
    FROM emp
    WHERE name = 'Bill' OR name = 'Sam';

Utilizando las convenciones de llamada de la versión 1, podemos definir c_overpaid como:

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

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    HeapTupleHeader  t = PG_GETARG_HEAPTUPLEHEADER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    Datum salary;

    salary = GetAttributeByName(t, "salary", &isnull);
    if (isnull)
        PG_RETURN_BOOL(false);
    /* Alternativamente, podríamos preferir hacer PG_RETURN_NULL() para salarios nulos. */

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByName es la función del sistema de PostgreSQL que devuelve atributos de la fila especificada. Tiene tres argumentos: el argumento de tipo HeapTupleHeader pasado a la función, el nombre del atributo deseado y un parámetro de retorno que indica si el atributo es nulo. GetAttributeByName devuelve un valor de tipo Datum que puedes convertir al tipo de datos adecuado utilizando la función DatumGetXXX() apropiada. Ten en cuenta que el valor de retorno carece de sentido si la bandera de nulo está activa; comprueba siempre la bandera de nulo antes de intentar hacer cualquier cosa con el resultado.

También existe GetAttributeByNum, que selecciona el atributo objetivo por número de columna en lugar de por nombre.

El siguiente comando declara la función c_overpaid en SQL:

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

Observa que hemos utilizado STRICT para no tener que comprobar si los argumentos de entrada eran NULL.

36.10.8. Devolución de filas (tipos compuestos) #

Para devolver una fila o un valor de tipo compuesto desde una función en el lenguaje C, puedes utilizar una API especial que proporciona macros y funciones para ocultar la mayor parte de la complejidad de la construcción de tipos de datos compuestos. Para utilizar esta API, el archivo fuente debe incluir:

#include "funcapi.h"

Hay dos formas de construir un valor de datos compuesto (en adelante, una tupla): puedes construirlo a partir de un array de valores Datum, o a partir de un array de cadenas de C que se pueden pasar a las funciones de conversión de entrada de los tipos de datos de las columnas de la tupla. En cualquier caso, primero necesitas obtener o construir un descriptor TupleDesc para la estructura de la tupla. Cuando trabajas con Datums, pasas el TupleDesc a BlessTupleDesc, y luego llamas a heap_form_tuple para cada fila. Cuando trabajas con cadenas de C, pasas el TupleDesc a TupleDescGetAttInMetadata, y luego llamas a BuildTupleFromCStrings para cada fila. En el caso de una función que devuelve un conjunto de tuplas, los pasos de configuración se pueden realizar todos una sola vez durante la primera llamada a la función.

Hay varias funciones auxiliares disponibles para configurar el TupleDesc necesario. La forma recomendada de hacer esto en la mayoría de las funciones que devuelven valores compuestos es llamar a:

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

pasando la misma estructura fcinfo que se pasó a la propia función que realiza la llamada. (Esto, por supuesto, requiere que utilices las convenciones de llamada de la versión 1). Se puede especificar resultTypeId como NULL o como la dirección de una variable local para recibir el OID del tipo de resultado de la función. resultTupleDesc debe ser la dirección de una variable local de tipo TupleDesc. Comprueba que el resultado sea TYPEFUNC_COMPOSITE; si es así, resultTupleDesc se habrá rellenado con el TupleDesc necesario. (Si no lo es, puedes informar de un error del estilo de function returning record called in context that cannot accept type record).

Tip

get_call_result_type puede resolver el tipo real del resultado de una función polimórfica; por lo tanto, es útil en funciones que devuelven resultados polimórficos escalares, no solo en funciones que devuelven tipos compuestos. La salida resultTypeId es principalmente útil para funciones que devuelven escalares polimórficos.

Note

get_call_result_type tiene una función hermana, get_expr_result_type, que se puede utilizar para resolver el tipo de salida esperado para una llamada a una función representada por un árbol de expresión. Esto se puede usar cuando se intenta determinar el tipo de resultado desde fuera de la propia función. También existe get_func_result_type, que se puede usar cuando solo está disponible el OID de la función. Sin embargo, estas funciones no son capaces de lidiar con funciones declaradas para devolver record, y get_func_result_type no puede resolver tipos polimórficos, por lo que deberías utilizar preferentemente get_call_result_type.

Las funciones más antiguas, ahora obsoletas, para obtener objetos TupleDesc son:

TupleDesc RelationNameGetTupleDesc(const char *relname)

para obtener un TupleDesc para el tipo de fila de una relación nombrada, y:

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

para obtener un TupleDesc basado en un OID de tipo. Esto se puede usar para obtener un TupleDesc para un tipo base o compuesto. Sin embargo, no funcionará para una función que devuelva record, y no puede resolver tipos polimórficos.

Una vez que tengas un TupleDesc, llama a:

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

si planeas trabajar con Datums, o:

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

si planeas trabajar con cadenas de C. Si estás escribiendo una función que devuelve un conjunto (set), puedes guardar los resultados de estas funciones en la estructura FuncCallContext; utiliza el campo tuple_desc o attinmeta respectivamente.

Cuando trabajes con Datums, utiliza:

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

para construir una HeapTuple a partir de los datos del usuario en forma de Datum.

Cuando trabajes con cadenas de C, utiliza:

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

para construir una HeapTuple a partir de los datos del usuario en forma de cadena de C. values es un array de cadenas de C, una por cada atributo de la fila devuelta. Cada cadena de C debe tener el formato esperado por la función de entrada del tipo de datos del atributo. Para devolver un valor nulo en uno de los atributos, el puntero correspondiente en el array values debe establecerse en NULL. Esta función deberá llamarse de nuevo por cada fila que devuelvas.

Una vez que hayas construido una tupla para devolverla desde tu función, esta debe convertirse en un Datum. Utiliza:

HeapTupleGetDatum(HeapTuple tuple)

para convertir una HeapTuple en un Datum válido. Este Datum se puede devolver directamente si tienes la intención de devolver solo una única fila, o se puede usar como el valor de retorno actual en una función que devuelve un conjunto.

En la siguiente sección se muestra un ejemplo.

36.10.9. Devolución de conjuntos #

Las funciones en lenguaje C tienen dos opciones para devolver conjuntos (múltiples filas). En un método, llamado modo ValuePerCall, una función que devuelve conjuntos es llamada repetidamente (pasando los mismos argumentos cada vez) y devuelve una nueva fila en cada llamada, hasta que no le quedan más filas que devolver y lo indica devolviendo NULL. Por lo tanto, la función que devuelve conjuntos (SRF, por sus siglas en inglés) debe guardar suficiente estado entre llamadas para recordar lo que estaba haciendo y devolver el siguiente elemento correcto en cada llamada. En el otro método, llamado modo Materialize, una SRF llena y devuelve un objeto tuplestore que contiene todo su resultado; en este caso, solo se produce una llamada para todo el resultado y no se necesita un estado entre llamadas.

Al utilizar el modo ValuePerCall, es importante recordar que no se garantiza que la consulta se ejecute hasta el final; es decir, debido a opciones como LIMIT, el ejecutor podría dejar de realizar llamadas a la función que devuelve conjuntos antes de que se hayan obtenido todas las filas. Esto significa que no es seguro realizar actividades de limpieza en la última llamada, porque podría no llegar a producirse nunca. Se recomienda utilizar el modo Materialize para funciones que necesitan acceder a recursos externos, como descriptores de archivos.

El resto de esta sección documenta un conjunto de macros auxiliares que se utilizan comúnmente (aunque no es obligatorio usarlas) para las SRF que utilizan el modo ValuePerCall. Se pueden encontrar detalles adicionales sobre el modo Materialize en src/backend/utils/fmgr/README. Además, los módulos de contrib en la distribución del código fuente de PostgreSQL contienen muchos ejemplos de SRF que utilizan tanto el modo ValuePerCall como el modo Materialize.

Para utilizar las macros de soporte para ValuePerCall descritas aquí, incluye funcapi.h. Estas macros funcionan con una estructura FuncCallContext que contiene el estado que debe guardarse entre llamadas. Dentro de la SRF que realiza la llamada, se utiliza fcinfo->flinfo->fn_extra para mantener un puntero a FuncCallContext entre llamadas. Las macros rellenan automáticamente ese campo en el primer uso, y esperan encontrar el mismo puntero allí en los usos posteriores.

typedef struct FuncCallContext
{
    /*
     * Número de veces que hemos sido llamados anteriormente
     *
     * SRF_FIRSTCALL_INIT() inicializa call_cntr a 0 por ti, y
     * se incrementa automáticamente cada vez que se llama a SRF_RETURN_NEXT().
     */
    uint64 call_cntr;

    /*
     * Número máximo de llamadas OPCIONAL
     *
     * max_calls está aquí solo por conveniencia y su configuración es opcional.
     * Si no se establece, debes proporcionar medios alternativos para saber cuándo
     * ha terminado la función.
     */
    uint64 max_calls;

    /*
     * Puntero OPCIONAL a información de contexto variada proporcionada por el usuario
     *
     * user_fctx sirve para apuntar a tus propios datos para retener información
     * de contexto arbitraria entre las llamadas a tu función.
     */
    void *user_fctx;

    /*
     * Puntero OPCIONAL a la estructura que contiene los metadatos de entrada del tipo de atributo
     *
     * attinmeta se utiliza al devolver tuplas (es decir, tipos de datos compuestos)
     * y no se usa al devolver tipos de datos base. Solo se necesita si tienes la intención
     * de usar BuildTupleFromCStrings() para crear la tupla de retorno.
     */
    AttInMetadata *attinmeta;

    /*
     * contexto de memoria utilizado para estructuras que deben sobrevivir a múltiples llamadas
     *
     * SRF_FIRSTCALL_INIT() establece multi_call_memory_ctx por ti, y SRF_RETURN_DONE()
     * lo utiliza para la limpieza. Es el contexto de memoria más apropiado para cualquier
     * memoria que deba reutilizarse a lo largo de múltiples llamadas de la SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * Puntero OPCIONAL a la estructura que contiene la descripción de la tupla
     *
     * tuple_desc se utiliza al devolver tuplas (es decir, tipos de datos compuestos)
     * y solo se necesita si vas a construir las tuplas con heap_form_tuple() en lugar de
     * con BuildTupleFromCStrings(). Ten en cuenta que el puntero TupleDesc almacenado aquí
     * normalmente debería haber pasado primero por BlessTupleDesc().
     */
    TupleDesc tuple_desc;

} FuncCallContext;

Las macros que debe utilizar una SRF que use esta infraestructura son:

SRF_IS_FIRSTCALL()

Utiliza esto para determinar si tu función está siendo llamada por primera vez o una vez posterior. En la primera llamada (únicamente), llama a:

SRF_FIRSTCALL_INIT()

para inicializar el FuncCallContext. En cada llamada a la función, incluida la primera, llama a:

SRF_PERCALL_SETUP()

para prepararte para utilizar el FuncCallContext.

Si tu función tiene datos que devolver en la llamada actual, utiliza:

SRF_RETURN_NEXT(funcctx, result)

para devolverlos a quien realiza la llamada. (result debe ser de tipo Datum, ya sea un único valor o una tupla preparada como se describió anteriormente). Finalmente, cuando tu función haya terminado de devolver datos, utiliza:

SRF_RETURN_DONE(funcctx)

para limpiar y finalizar la SRF.

El contexto de memoria que está activo cuando se llama a la SRF es un contexto transitorio que se limpiará entre llamadas. Esto significa que no necesitas llamar a pfree para cada cosa que hayas asignado utilizando palloc; desaparecerá de todos modos. Sin embargo, si quieres asignar estructuras de datos que sobrevivan entre llamadas, debes colocarlas en otro lugar. El contexto de memoria referenciado por multi_call_memory_ctx es una ubicación adecuada para cualquier dato que necesite sobrevivir hasta que la SRF termine de ejecutarse. En la mayoría de los casos, esto significa que deberías cambiar a multi_call_memory_ctx mientras realizas la configuración de la primera llamada. Utiliza funcctx->user_fctx para mantener un puntero a cualquier estructura de datos de este tipo que deba persistir entre llamadas. (Los datos que asignes en multi_call_memory_ctx también desaparecerán automáticamente cuando termine la consulta, por lo que tampoco es necesario liberar esos datos manualmente).

Warning

Aunque los argumentos reales de la función no cambian entre llamadas, si realizas el detoast de los valores de los argumentos (lo cual normalmente se hace de forma transparente mediante la macro PG_GETARG_xxx) en el contexto transitorio, las copias resultantes se liberarán en cada ciclo. En consecuencia, si mantienes referencias a dichos valores en tu user_fctx, debes copiarlos a multi_call_memory_ctx después del detoasting, o asegurarte de realizar el detoasting de los valores únicamente en ese contexto.

Un ejemplo completo en pseudocódigo se vería de la siguiente manera:

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);
        /* El código de configuración única va aquí: */
        user code
        if returning composite
            build TupleDesc, and perhaps AttInMetadata
        endif returning composite
        user code
        MemoryContextSwitchTo(oldcontext);
    }

    /* El código de configuración para cada llamada va aquí: */
    user code
    funcctx = SRF_PERCALL_SETUP();
    user code

    /* esta es solo una forma en la que podríamos comprobar si hemos terminado: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Aquí queremos devolver otro elemento: */
        user code
        obtain result Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Aquí hemos terminado de devolver elementos, así que solo informamos de ello. */
        /* (Resiste la tentación de poner código de limpieza aquí). */
        SRF_RETURN_DONE(funcctx);
    }
}

A complete example of a simple SRF returning a composite type looks like:

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    AttInMetadata       *attinmeta;

    /* tareas realizadas solo en la primera llamada de la función */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* crea un contexto de función para persistencia entre llamadas */
        funcctx = SRF_FIRSTCALL_INIT();

        /* cambia al contexto de memoria adecuado para múltiples llamadas de función */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* número total de tuplas a devolver */
        funcctx->max_calls = PG_GETARG_INT32(0);

        /* Construye un descriptor de tupla para nuestro tipo de resultado */
        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")));

        /*
         * genera los metadatos de atributo necesarios más adelante para producir tuplas
         * a partir de cadenas de C sin procesar
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* tareas realizadas en cada llamada de la función */
    funcctx = SRF_PERCALL_SETUP();

    call_cntr = funcctx->call_cntr;
    max_calls = funcctx->max_calls;
    attinmeta = funcctx->attinmeta;

    if (call_cntr < max_calls)    /* hacer cuando queda más por enviar */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Prepara un array de valores para construir la tupla devuelta.
         * Este debe ser un array de cadenas de C que serán
         * procesadas más tarde por las funciones de entrada de tipo.
         */
        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));

        /* construye una tupla */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* convierte la tupla en un datum */
        result = HeapTupleGetDatum(tuple);

        /* limpia (esto no es realmente necesario) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* hacer cuando no quede más */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

Una forma de declarar esta función en SQL es:

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;

Una forma diferente es usar parámetros 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;

Ten en cuenta que en este método el tipo de salida de la función es formalmente un tipo record anónimo.

36.10.10. Argumentos polimórficos y tipos de retorno #

Se puede declarar que las funciones en lenguaje C aceptan y devuelven los tipos polimórficos descritos en Section 36.2.5. Cuando los argumentos o los tipos de retorno de una función se definen como tipos polimórficos, el autor de la función no puede saber de antemano con qué tipo de datos será llamada ni cuál debe devolver. Se proporcionan dos rutinas en fmgr.h para permitir que una función de C de versión 1 descubra los tipos de datos reales de sus argumentos y el tipo que se espera que devuelva. Las rutinas se denominan get_fn_expr_rettype(FmgrInfo *flinfo) y get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Devuelven el OID del tipo de resultado o argumento, o bien InvalidOid si la información no está disponible. Normalmente se accede a la estructura flinfo como fcinfo->flinfo. El parámetro argnum comienza desde cero. get_call_result_type también se puede usar como alternativa a get_fn_expr_rettype. También existe get_fn_expr_variadic, que se puede utilizar para saber si los argumentos variádicos se han fusionado en un array. Esto es útil principalmente para funciones VARIADIC "any", ya que dicha fusión siempre habrá ocurrido para las funciones variádicas que toman tipos de array ordinarios.

Por ejemplo, supongamos que queremos escribir una función que acepte un único elemento de cualquier tipo y devuelva un array unidimensional de ese tipo:

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");

    /* obtiene el elemento proporcionado, teniendo cuidado en caso de que sea NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* tenemos una dimensión */
    ndims = 1;
    /* y un elemento */
    dims[0] = 1;
    /* y el límite inferior es 1 */
    lbs[0] = 1;

    /* obtiene la información necesaria sobre el tipo de elemento */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* ahora construye el array */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

El siguiente comando declara la función make_array en SQL:

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'DIRECTORY/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

Existe una variante de polimorfismo que solo está disponible para las funciones en lenguaje C: se pueden declarar para tomar parámetros de tipo "any". (Ten en cuenta que este nombre de tipo debe estar entre comillas dobles, ya que también es una palabra reservada en SQL). Esto funciona igual que anyelement, excepto que no obliga a que los diferentes argumentos "any" sean del mismo tipo, ni ayudan a determinar el tipo de resultado de la función. Una función en lenguaje C también puede declarar que su último parámetro sea VARIADIC "any". Esto coincidirá con uno o más argumentos reales de cualquier tipo (no necesariamente del mismo tipo). Estos argumentos no se agruparán en un array como ocurre con las funciones variádicas normales; simplemente se pasarán a la función por separado. Se debe utilizar la macro PG_NARGS() y los métodos descritos anteriormente para determinar el número de argumentos reales y sus tipos al utilizar esta característica. Además, los usuarios de dicha función podrían desear utilizar la palabra clave VARIADIC en su llamada a la función, con la expectativa de que la función trate los elementos del array como argumentos separados. La propia función debe implementar ese comportamiento si así se desea, después de usar get_fn_expr_variadic para detectar que el argumento real estaba marcado con VARIADIC.

36.10.11. Memoria compartida #

36.10.11.1. Solicitud de memoria compartida al inicio #

Los complementos (add-ins) pueden reservar memoria compartida al iniciar el servidor. Para hacerlo, la biblioteca compartida del complemento debe cargarse previamente especificándola en shared_preload_libraries. La biblioteca compartida también debe registrar un shmem_request_hook en su función _PG_init. Este shmem_request_hook puede reservar memoria compartida llamando a:

void RequestAddinShmemSpace(Size size)

Cada backend debe obtener un puntero a la memoria compartida reservada llamando a:

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

Si esta función establece foundPtr en false, el invocador debe proceder a inicializar el contenido de la memoria compartida reservada. Si foundPtr se establece en true, la memoria compartida ya fue inicializada por otro backend, y el invocador no necesita inicializarla más.

Para evitar condiciones de carrera, cada backend debe utilizar el LWLock AddinShmemInitLock al inicializar su asignación de memoria compartida, como se muestra aquí:

static mystruct *ptr = NULL;
bool        found;

LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
    ... inicializar el contenido de la memoria compartida ...
    ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);

shmem_startup_hook proporciona un lugar conveniente para el código de inicialización, pero no es estrictamente obligatorio que todo ese código se coloque en este hook. En Windows (y en cualquier otro lugar donde se defina EXEC_BACKEND), cada backend ejecuta el shmem_startup_hook registrado poco después de acoplarse a la memoria compartida, por lo que los complementos aún deben adquirir AddinShmemInitLock dentro de este hook, como se muestra en el ejemplo anterior. En otras plataformas, solo el proceso postmaster ejecuta el shmem_startup_hook, y cada backend hereda automáticamente los punteros a la memoria compartida.

Puedes encontrar un ejemplo de shmem_request_hook y shmem_startup_hook en contrib/pg_stat_statements/pg_stat_statements.c en el árbol de fuentes de PostgreSQL.

36.10.11.2. Solicitud de memoria compartida después del inicio #

Existe otro método más flexible para reservar memoria compartida que se puede realizar después del inicio del servidor y fuera de un shmem_request_hook. Para hacerlo, cada backend que vaya a utilizar la memoria compartida debe obtener un puntero a ella llamando a:

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

Si aún no existe un segmento de memoria compartida dinámica con el nombre indicado, esta función lo asignará y lo inicializará con la función de devolución de llamada (callback) init_callback proporcionada. Si el segmento ya ha sido asignado e inicializado por otro backend, esta función simplemente acopla el segmento de memoria compartida dinámica existente al backend actual.

A diferencia de la memoria compartida reservada al iniciar el servidor, no es necesario adquirir AddinShmemInitLock ni tomar otras medidas para evitar condiciones de carrera al reservar memoria compartida con GetNamedDSMSegment. Esta función garantiza que solo un backend asigne e inicialice el segmento y que todos los demás backends reciban un puntero al segmento completamente asignado e inicializado.

Puedes encontrar un ejemplo de uso completo de GetNamedDSMSegment en src/test/modules/test_dsm_registry/test_dsm_registry.c en el árbol de fuentes de PostgreSQL.

36.10.12. LWLocks #

36.10.12.1. Solicitud de LWLocks al inicio #

Los complementos pueden reservar LWLocks al iniciar el servidor. Al igual que con la memoria compartida reservada al iniciar el servidor, la biblioteca compartida del complemento debe cargarse previamente especificándola en shared_preload_libraries, y la biblioteca compartida debe registrar un shmem_request_hook en su función _PG_init. Este shmem_request_hook puede reservar LWLocks llamando a:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

Esto garantiza que un array de num_lwlocks LWLocks esté disponible bajo el nombre tranche_name. Se puede obtener un puntero a este array llamando a:

LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)

36.10.12.2. Solicitud de LWLocks después del inicio #

Existe otro método más flexible para obtener LWLocks que se puede realizar después del inicio del servidor y fuera de un shmem_request_hook. Para hacerlo, primero asigna un tranche_id llamando a:

int LWLockNewTrancheId(void)

A continuación, inicializa cada LWLock, pasando el nuevo tranche_id como argumento:

void LWLockInitialize(LWLock *lock, int tranche_id)

De manera similar a la memoria compartida, cada backend debe garantizar que solo un proceso asigne un nuevo tranche_id e inicialice cada nuevo LWLock. Una forma de hacerlo es llamar a estas funciones únicamente en tu código de inicialización de memoria compartida con el AddinShmemInitLock adquirido en modo exclusivo. Si utilizas GetNamedDSMSegment, llamar a estas funciones en la función de devolución de llamada init_callback es suficiente para evitar condiciones de carrera.

Finalmente, cada backend que utilice el tranche_id debe asociarlo con un tranche_name llamando a:

void LWLockRegisterTranche(int tranche_id, const char *tranche_name)

Puedes encontrar un ejemplo de uso completo de LWLockNewTrancheId, LWLockInitialize y LWLockRegisterTranche en contrib/pg_prewarm/autoprewarm.c en el árbol de fuentes de PostgreSQL.

36.10.13. Eventos de espera personalizados #

Los complementos pueden definir eventos de espera personalizados bajo el tipo de evento de espera Extension llamando a:

uint32 WaitEventExtensionNew(const char *wait_event_name)

El evento de espera se asocia a una cadena personalizada visible para el usuario. Puedes encontrar un ejemplo en src/test/modules/worker_spi en el árbol de fuentes de PostgreSQL.

Los eventos de espera personalizados se pueden visualizar en 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.14. Puntos de inyección (injection points) #

Un punto de inyección con un name (nombre) dado se declara utilizando la macro:

INJECTION_POINT(name, arg);

Ya existen algunos puntos de inyección declarados en puntos estratégicos dentro del código del servidor. Después de añadir un nuevo punto de inyección, es necesario compilar el código para que ese punto de inyección esté disponible en el binario. Los complementos escritos en lenguaje C pueden declarar puntos de inyección en su propio código utilizando la misma macro. Los nombres de los puntos de inyección deben utilizar caracteres en minúscula, con los términos separados por guiones. arg es un valor de argumento opcional que se proporciona a la devolución de llamada (callback) en tiempo de ejecución.

La ejecución de un punto de inyección puede requerir la asignación de una pequeña cantidad de memoria, lo cual puede fallar. Si necesitas tener un punto de inyección en una sección crítica donde no se permiten asignaciones dinámicas, puedes utilizar un enfoque de dos pasos con las siguientes macros:

INJECTION_POINT_LOAD(name);
INJECTION_POINT_CACHED(name, arg);

Antes de entrar en la sección crítica, llama a INJECTION_POINT_LOAD. Esta macro comprueba el estado de la memoria compartida y carga la devolución de llamada en la memoria privada del backend si está activa. Dentro de la sección crítica, utiliza INJECTION_POINT_CACHED para ejecutar la devolución de llamada.

Los complementos (add-ins) pueden acoplar devoluciones de llamada (callbacks) a un punto de inyección ya declarado llamando a:

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

name es el nombre del punto de inyección, el cual cuando sea alcanzado durante la ejecución ejecutará la function cargada desde library. private_data es un área privada de datos de tamaño private_data_size que se pasa como argumento al callback cuando se ejecuta.

Aquí hay un ejemplo de devolución de llamada para InjectionPointCallback:

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

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

Esta función de devolución de llamada imprime un mensaje en el registro de errores del servidor con severidad NOTICE, pero las devoluciones de llamada pueden implementar una lógica más compleja.

Una forma alternativa de definir la acción a tomar cuando se alcanza un punto de inyección es añadir el código de prueba junto con el código fuente normal. Esto puede ser útil si la acción, por ejemplo, depende de variables locales que no son accesibles para los módulos cargados. La macro IS_INJECTION_POINT_ATTACHED se puede utilizar para comprobar si un punto de inyección está acoplado, por ejemplo:

#ifdef USE_INJECTION_POINTS
if (IS_INJECTION_POINT_ATTACHED("before-foobar"))
{
    /* cambia una variable local si el punto de inyección está acoplado */
    local_var = 123;

    /* ejecuta también la devolución de llamada */
    INJECTION_POINT_CACHED("before-foobar", NULL);
}
#endif

Ten en cuenta que la devolución de llamada acoplada al punto de inyección no será ejecutada por la macro IS_INJECTION_POINT_ATTACHED. Si deseas ejecutar la devolución de llamada, también debes llamar a INJECTION_POINT_CACHED como en el ejemplo anterior.

Opcionalmente, es posible desacoplar un punto de inyección llamando a:

extern bool InjectionPointDetach(const char *name);

En caso de éxito, se devuelve true, de lo contrario se devuelve false.

Una devolución de llamada acoplada a un punto de inyección está disponible en todos los backends, incluidos los backends iniciados después de llamar a InjectionPointAttach. Permanece acoplada mientras el servidor está en ejecución o hasta que el punto de inyección se desacopla utilizando InjectionPointDetach.

Puedes encontrar un ejemplo en src/test/modules/injection_points en el árbol de fuentes de PostgreSQL.

Habilitar los puntos de inyección requiere --enable-injection-points con configure o bien -Dinjection_points=true con Meson.

36.10.15. Estadísticas acumulativas personalizadas #

Es posible que los complementos escritos en lenguaje C utilicen tipos personalizados de estadísticas acumulativas registrados en el sistema de estadísticas acumulativas.

Primero, define una estructura PgStat_KindInfo que incluya toda la información relacionada con el tipo personalizado registrado. Por ejemplo:

static const PgStat_KindInfo custom_stats = {
    .name = "custom_stats",
    .fixed_amount = false,
    .shared_size = sizeof(PgStatShared_Custom),
    .shared_data_off = offsetof(PgStatShared_Custom, stats),
    .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats),
    .pending_size = sizeof(PgStat_StatCustomEntry),
}

Luego, cada backend que necesite utilizar este tipo personalizado debe registrarlo con pgstat_register_kind y un ID único utilizado para almacenar las entradas relacionadas con este tipo de estadísticas:

extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
                                        const PgStat_KindInfo *kind_info);

Mientras desarrollas una nueva extensión, utiliza PGSTAT_KIND_EXPERIMENTAL para el parámetro kind. Cuando estés listo para lanzar la extensión a los usuarios, reserva un ID de tipo en la página de Custom Cumulative Statistics.

Los detalles de la API para PgStat_KindInfo se pueden encontrar en el archivo src/include/utils/pgstat_internal.h.

El tipo de estadísticas registrado se asocia con un nombre y un ID único compartido a través del servidor en la memoria compartida. Cada backend que utiliza un tipo personalizado de estadísticas mantiene una caché local que almacena la información de cada PgStat_KindInfo personalizada.

Coloca el módulo de extensión que implementa el tipo de estadísticas acumulativas personalizadas en shared_preload_libraries para que se cargue al inicio de la ejecución de PostgreSQL.

Puedes encontrar un ejemplo que describe cómo registrar y utilizar estadísticas personalizadas en src/test/modules/injection_points.

36.10.16. Uso de C++ para extensibilidad #

Aunque el motor de PostgreSQL está escrito en C, es posible escribir extensiones en C++ si se siguen estas directrices:

  • Todas las funciones a las que acceda el motor deben presentar una interfaz C al motor; estas funciones C pueden llamar luego a funciones C++. Por ejemplo, se requiere el enlace extern C para las funciones a las que accede el motor. Esto también es necesario para cualquier función que se pase como puntero entre el motor y el código C++.

  • Libera la memoria utilizando el método de liberación adecuado. Por ejemplo, la mayor parte de la memoria del motor se asigna mediante palloc(), por lo que debes utilizar pfree() para liberarla. El uso de delete de C++ en tales casos fallará.

  • Evita que las excepciones se propaguen al código C (utiliza un bloque catch-all en el nivel superior de todas las funciones extern C). Esto es necesario incluso si el código C++ no lanza explícitamente ninguna excepción, porque eventos como quedarse sin memoria aún pueden lanzar excepciones. Cualquier excepción debe ser capturada y los errores adecuados deben devolverse a la interfaz C. Si es posible, compila C++ con -fno-exceptions para eliminar las excepciones por completo; en tales casos, debes comprobar los fallos en tu código C++, por ejemplo, comprobando si new() devuelve NULL.

  • Si llamas a funciones del motor desde código C++, asegúrate de que la pila de llamadas de C++ contenga únicamente estructuras de datos básicas planas (POD, por sus siglas en inglés). Esto es necesario porque los errores del motor generan un longjmp() distante que no desenrolla adecuadamente una pila de llamadas de C++ con objetos que no sean POD.

En resumen, lo mejor es colocar el código C++ detrás de un muro de funciones extern C que sirvan de interfaz con el motor, y evitar fugas de excepciones, memoria y pila de llamadas.