Функции на нативном языке

Пользовательские нативные функции могут быть написаны на C, RUST, С++ или других компилируемых с С языках. Такие функции компилируются в динамически загружаемые объекты (также называемые разделяемыми библиотеками) и загружаются сервером по требованию. Именно возможность динамической загрузки отличает «нативные» функции от «внутренних» — фактические стандарты оформления кода у них по сути одинаковы. (Соответственно, стандартная библиотека внутренних функций является обширным источником примеров кода для пользовательских нативных функций.)

В настоящее время для нативных функций применяется только одно соглашение о вызовах, называемое «версия 1» («extern C», все означенные языки имеют возможность следовать такому соглашению). Поддержка этого соглашения (для языка С) обозначается вызовом функции с макросом PG_FUNCTION_INFO_V1(), как показано ниже.



Динамическая загрузка

При первом вызове в сеансе пользовательской функции в определенном загружаемом объектном файле динамический загрузчик загружает этот объектный файл в память, чтобы можно было вызвать эту функцию. Поэтому в команде CREATE FUNCTION для пользовательской функции на C/RUST нужно указывать две детали: имя загружаемого объектного файла и имя уровня С (символ ссылки) конкретной функции, вызываемой в этом объектном файле. Если имя уровня С не указано явно, предполагается, что оно совпадает с именем функции в SQL.

Для поиска разделяемого объектного файла по имени, указанному в команде CREATE FUNCTION, используется следующий алгоритм:

  1. Если имя является абсолютным путем, данный файл загружается.

  2. Если имя начинается со строки $libdir, эта часть заменяется именем каталога библиотек пакетов QHB, которое определяется во время сборки.

  3. Если имя не содержит часть с каталогом, поиск файла производится по пути, указанному в переменной конфигурации dynamic_library_path.

  4. В противном случае (файл не найден в пути или содержит не абсолютный путь к каталогу), динамический загрузчик попытается принять имя как есть, что, скорее всего, не удастся. (Ненадежно полагаться на текущий рабочий каталог).

Если эта последовательность не работает, к указанному имени добавляется расширение имени файла разделяемой библиотеки, принятое на данной платформе (зачастую .so), и эта последовательность повторяется. Если это также не удается, происходит сбой загрузки.

Рекомендуется размещать разделяемые библиотеки относительно $libdir или по пути динамической библиотеки. Это упрощает обновление версий, если новая установка находится в другом месте. Какой именно каталог обозначается как $libdir, можно узнать с помощью команды pg_config --pkglibdir.

Идентификатор пользователя, от имени которого работает сервер QHB, должен иметь возможность пройти путь к файлу, который вы собираетесь загрузить. Создание файла или каталога более высокого уровня, который недоступен для чтения или исполнения пользователем qhb, — распространенная ошибка.

В любом случае имя файла, указанное в команде CREATE FUNCTION, записывается в системные каталоги буквально, поэтому если файл необходимо загрузить снова, применяется та же процедура.

Примечание
QHB не будет компилировать функцию на C/RUST автоматически. Прежде чем ссылаться на объектный файл в команде CREATE FUNCTION, его необходимо скомпилировать. Дополнительную информацию см в подразделе Компиляция и связывание динамически загружаемых функций.

Чтобы убедиться, что динамически загружаемый объектный файл не загружается на несовместимый сервер, QHB проверяет, содержит ли этот файл «магический блок» с надлежащим содержимым. Это позволяет серверу обнаруживать очевидные несовместимости, такие как код, скомпилированный для другой основной версии QHB. Чтобы включить в модуль магический блок, запишите это в один (и только один) из исходных файлов модуля после включения заголовочного файла fmgr.h:

PG_MODULE_MAGIC;

После первого использования динамически загруженный объектный файл сохраняется в памяти. Последующие вызовы в том же сеансе функции (или функций) в этом файле приведут только к небольшим затратам, связанным с поиском в таблице символов. Если вам необходимо принудительно перезагрузить объектный файл, например, после его перекомпиляции, начните новый сеанс.

Динамически загружаемый файл может дополнительно содержать функции инициализации и завершения. Если файл содержит функцию с именем _PG_init, эта функция будет вызвана сразу после загрузки файла. Эта функция не принимает параметры и не должна ничего возвращать. В настоящее время выгрузить динамически загруженный файл невозможно.



Базовые типы в функциях на нативном языке

Чтобы знать, как писать функции на нативном языке, нужно знать, как QHB внутренне представляет базовые типы данных и как их можно передавать в функции и из функций. Внутри QHB рассматривает базовый тип как «блок памяти». Пользовательские функции, которые вы определяете для типа, в свою очередь, определяют, каким образом QHB может с ним работать. То есть QHB будет только сохранять и извлекать данные с диска, а ваши пользовательские функции использовать для ввода, обработки и вывода данных.

Базовые типы могут иметь один из трех внутренних форматов:

  • передается по значению, фиксированной длины

  • передается по ссылке, фиксированной длины

  • передается по ссылке, переменной длины

Типы, передаваемые по значению, могут иметь длину только 1, 2 или 4 байта (а также 8 байт, если sizeof(Datum) равен 8 на вашей машине). Собственные типы следует определять таким образом, чтобы они были одинакового размера (в байтах) на всех архитектурах. Например, тип long опасен, потому что на одних машинах его размер составляет 4 байта, а на других — 8 байт, тогда как размер типа int на большинстве машин Unix составляет 4 байта. Разумной реализацией типа int4 на машинах Unix может быть:

/* 4-байтовое целое, передаваемое по значению */
typedef int int4;

(В настоящем коде С/RUST QHB этот тип называется int32, потому что в C существует соглашение, согласно которому int XX означает XX бит. Поэтому обратите внимание также на то, что тип int8 в С имеет размер 1 байт. Тип int8, принятый в SQL, в С называется int64. См. также Таблицу 2.)

С другой стороны, типы фиксированной длины любого размера могут передаваться по ссылке. Например, вот пример реализации типа QHB:

/* 16-байтовая структура, передаваемая по ссылке */
typedef struct
{
    double  x, y;
} Point;

В функции QHB и из них могут передаваться только указатели на такие типы. Чтобы вернуть значение такого типа, выделите требуемый объем памяти с помощью palloc, заполните выделенную память и верните указатель на нее. (При этом, если вы просто хотите вернуть то же значение, что было получено в одном из ваших входных аргументов того же типа данных, вы можете пропустить лишнюю palloc и просто вернуть указатель на входное значение.)

Наконец, все типы переменной длины также должны передаваться по ссылке. Все типы переменной длины должны начинаться с непрозрачного поля длины размером ровно 4 байта, которое будет установлено с помощью SET_VARSIZE; никогда не устанавливайте это поле напрямую! Все данные, которые будут храниться в этом типе, должны быть расположены в памяти сразу после этого поля длины. Поле длины содержит общую длину структуры, то есть включает в себя размер самого поля длины.

Еще один важный момент — старайтесь не оставлять неинициализированных фрагментов в значениях типа данных; например, обязательно обнуляйте любые байты заполнителя выравнивания, которые могут присутствовать в структурах. Без этого планировщик может рассматривать логически равнозначные константы вашего типа данных как неравные, что приведет к построению неэффективных (хотя и корректных) планов.

ПРЕДУПРЕЖДЕНИЕ
Никогда не изменяйте содержимое входного значения, передаваемого по ссылке. Если вы это сделаете, то, скорее всего, повредите данные на диске, поскольку полученный вами указатель может указывать напрямую на буфер диска. Единственное исключение из этого правила описано в разделе Пользовательские агрегаты.

В качестве примера мы можем определить тип text следующим образом:

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

Запись [FLEXIBLE_ARRAY_MEMBER] означает, что фактическая длина части данных в этом объявлении не указана.

При манипулировании типами переменной длины следует быть внимательными, чтобы правильно распределить объем памяти и правильно задать поле длины. Например, если мы хотим сохранить 40 байт в структуре типа text, можно использовать такой фрагмент кода:

#include "qhb.h"
...
char buffer[40]; /* наши исходные данные */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ — это то же самое, что и sizeof(int32), но для обозначения размера заголовка типа переменной длины хорошим стилем считается использование макроса VARHDRSZ. Кроме того, поле длины должно устанавливаться макросом SET_VARSIZE, а не путем простого присваивания.

В Таблице 2 представлены типы языка C/RUST, соответствующие многим встроенным типам данных SQL в QHB. В столбце «Определен в» указан заголовочный файл, который необходимо включить, чтобы получить определение типа. (Фактическое определение может быть в другом файле, который включается из указанного. Рекомендуется, чтобы пользователи придерживались определенного интерфейса.) Обратите внимание, что в любой исходный файл всегда следует сначала включать qhb.h, поскольку он объявляет ряд вещей, которые вам в любом случае понадобятся, и потому что включение первыми других заголовков может вызвать проблемы с переносимостью.

Таблица 2. Эквивалентные типы C для встроенных типов SQL

Тип SQLТип CОпределен в
booleanboolqhb.h (возможно, встроенный компилятор)
boxBOX*utils/geo_decls.h
byteabytea*qhb.h
"char"char(встроенный компилятор)
characterBpChar*qhb.h
cidCommandIdqhb.h
dateDateADTutils/date.h
float4 (real)float4*qhb.h
float8 (double precision)float8*qhb.h
int2 (smallint)int16qhb.h
int4 (integer)int32qhb.h
int8 (bigint)int64qhb.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNameqhb.h
numericNumericutils/numeric.h
oidOidqhb.h
oidvectoroidvector*qhb.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocregprocqhb.h
texttext*qhb.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADT***utils/date.h ***
timestampTimestamp*datatype/timestamp.h
timestamp with time zoneTimestampTzdatatype/timestamp.h
varcharVarChar*qhb.h
xidTransactionIdqhb.h

Теперь, когда мы рассмотрели все возможные структуры для базовых типов, мы можем показать некоторые примеры реальных функций.



Соглашение о вызовах версии 1

Соглашение о вызовах версии 1 опирается на макросы, которые устраняют большую часть сложности передачи аргументов и результатов. Функция на C/RUST версии 1 всегда объявляется так:

Datum funcname(PG_FUNCTION_ARGS)

И дополнительно в том же исходном файле должен присутствовать вызов макроса:

PG_FUNCTION_INFO_V1(funcname);

(Обычно его записывают непосредственно перед самой функцией.) Этот вызов макроса не требуется для функций internal (внутренних), поскольку QHB предполагает, что все внутренние функции используют соглашение версии 1. Однако для динамически загружаемых функций этот макрос необходим.

В функции версии 1 каждый фактический аргумент выбирается с помощью макроса PG_GETARG_xxx(), который соответствует типу данных аргумента. (В нестрогих функциях необходимо предварительно проверить аргумент на значения NULL, используя PG_ARGISNULL(); см. ниже.) Результат возвращается макросом PG_RETURN_xxx() для возвращаемого типа. PG_GETARG_xxx() принимает в качестве аргумента номер выбираемого аргумента функции, причем нумерация начинается с 0. PG_RETURN_xxx() принимает в качестве аргумента фактическое значение, которое нужно вернуть.

Вот несколько примеров использования соглашения о вызовах версии 1:

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

PG_MODULE_MAGIC;

/* по значению */

PG_FUNCTION_INFO_V1(add_one);

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

    PG_RETURN_INT32(arg + 1);
}

/* по ссылке, фиксированной длины */

PG_FUNCTION_INFO_V1(add_one_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* Макрос для FLOAT8 скрывает свою способность передачи по ссылке. */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* Здесь способность Point к передаче по ссылке не скрыта. */
    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);
}

/* по ссылке, переменной длины */

PG_FUNCTION_INFO_V1(copytext);

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

    /*
     * VARSIZE_ANY_EXHDR — это размер структуры в байтах минус VARHDRSZ или VARHDRSZ_SHORT
     * ее заголовочного файла. Постройте копию с заголовочным файлом полной длины.
     */
    text     *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
    SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);

    /*
     * VARDATA — это указатель на область данных новой структуры. Источником может быть
     * короткий элемент данных, поэтому извлекайте его данные посредством VARDATA_ANY.
     */
    memcpy(VARDATA(new_t),          /* цель */
           VARDATA_ANY(t),          /* источник */
           VARSIZE_ANY_EXHDR(t));   /* сколько байтов */
    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);
}

Предполагая, что приведенный выше код был подготовлен в файле funcs.c и скомпилирован в общий объект, мы могли бы определить функции для QHB с помощью таких команд:

CREATE FUNCTION add_one(integer) RETURNS integer
     AS 'КАТАЛОГ/funcs', 'add_one'
     LANGUAGE C STRICT;

-- обратите внимание — это перегрузка имени функции SQL "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
     AS 'КАТАЛОГ/funcs', 'add_one_float8'
     LANGUAGE C STRICT;

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'КАТАЛОГ/funcs', 'makepoint'
     LANGUAGE C STRICT;

CREATE FUNCTION copytext(text) RETURNS text
     AS 'КАТАЛОГ/funcs', 'copytext'
     LANGUAGE C STRICT;

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'КАТАЛОГ/funcs', 'concat_text'
     LANGUAGE C STRICT;

Здесь КАТАЛОГ обозначает каталог файла разделяемой библиотеки (например, каталог учебного руководства по QHB, который содержит код для примеров, используемых в этом разделе). (Лучше было бы просто написать 'funcs' в предложении AS после добавления КАТАЛОГА в путь поиска. В любом случае мы можем опустить принятое в системе расширение для разделяемых библиотек, обычно .so.)

Обратите внимание, что мы указали функции как «строгие» (strict), имея в виду, что система должна автоматически подразумевать результат NULL, если в каком-то из входных значений передается NULL. Благодаря этому, мы избегаем необходимости проверять входные значения на NULL в коде функции. Без этого нам пришлось бы явно проверять значения на NULL, используя PG_ARGISNULL().

Макрос PG_ARGISNULL(n) позволяет функции проверять каждое входное значение на NULL. (Конечно, это необходимо делать только в функциях, не объявленных как «строгие»). Как и в случае с макросом PG_GETARG_xxx(), нумерация входных аргументов начинается с нуля. Обратите внимание, что не следует выполнять PG_GETARG_xxx(), не убедившись, что соответствующий аргумент не равен NULL. Чтобы вернуть результат NULL, выполните макрос PG_RETURN_NULL(); это работает как в строгих, так и в нестрогих функциях.

На первый взгляд соглашения о кодировании в версии 1 могут показаться просто бессмысленным мракобесием по сравнению с использованием простых соглашений о вызовах языка C/RUST. Тем не менее они позволяют работать с аргументами и возвращаемыми значениями, в которых может передаваться NULL, а также со значениями в формате TOAST (сжатыми или находящимися вне основной программы).

Другие возможности, предоставляемые интерфейсом версии 1, — это два варианта макроса PG_GETARG_xxx(). Первый из них, PG_GETARG_xxx_COPY(), гарантирует возврат копии указанного аргумента, который безопасен для внесения записей. (Обычные макросы иногда будут возвращать указатель на значение, физически хранящееся в таблице, в которую нельзя записывать. Использование макроса PG_GETARG_xxx_COPY() гарантирует доступный для записи результат). Второй вариант состоит из макроса PG_GETARG_xxx_SLICE(), который принимает три аргумента. Первый — это номер аргумента функции (как указано выше). Второй и третий — это смещение и длина возвращаемого сегмента. Смещения отсчитываются от нуля, а отрицательная длина требует возврата оставшейся части значения. Эти макросы обеспечивают более эффективный доступ к частям больших значений в том случае, если они имеют «внешний» (external) тип хранения. (Тип хранения столбца можно указать командой ALTER TABLE имя_таблицы ALTER COLUMN имя_столбца SET STORAGE тип_хранения. тип_хранения может быть plain, external, extended или main.)

Наконец, соглашения о вызовах функций версии 1 позволяют возвращать множества результатов (подраздел Возврат множеств) и реализовывать триггерные функции (глава Триггеры) и обработчики вызовов процедурного языка (глава Написание обработчика процедурного языка).



Написание кода

Прежде чем перейти к более сложным темам, мы должны рассмотреть некоторые правила кодирования для функций на нативном языке QHB. Хотя и возможно загрузить в QHB функции, написанные на других языках помимо C/RUST, обычно это сложно (если вообще возможно), потому что другие языки, такие как C++, FORTRAN или Pascal, зачастую не следуют соглашению о вызовах, принятому в C. То есть другие языки передают аргументы и возвращают значения между функциями разными способами. По этой причине мы будем предполагать, что ваши функции на нативном языке на самом деле написаны на C/RUST.

Основные правила написания и построения функций C/RUST следующие:

  • Чтобы узнать, где в вашей системе (или в системе, на которой будут работать ваши пользователи) установлены заголовочные файлы сервера QHB, используйте pg_config --includedir-server.

  • Для компиляции и компоновки вашего кода, который можно динамически загрузить в QHB, всегда требуются специальные флаги. Подробное объяснение того, как это сделать для вашей конкретной операционной системы, см. в подразделе Компиляция и связывание динамически загружаемых функций.

  • Не забудьте определить «магический блок» для вашей разделяемой библиотеки, как описано в подразделе Динамическая загрузка.

  • При выделении памяти используйте функции QHB palloc и pfree вместо соответствующих функций библиотеки C malloc и free. Память, выделенная palloc, будет автоматически освобождаться в конце каждой транзакции, предотвращая утечки памяти.

  • Всегда обнуляйте байты ваших структур, используя функцию memset (или сразу выделяйте их функцией palloc0). Даже если вы присвоите значение каждому полю вашей структуры, в ней могут остаться заполняющие байты выравнивания («прорехи» в структуре), содержащие «мусорные »значения. Без этого будет трудно поддерживать индексы или соединения по хешу, так как для вычисления хеша придется выбирать из вашей структуры данных только значимые биты. Планировщик тоже иногда полагается на сравнение констант с помощью побитового равенства, так что если логически равные значения окажутся неравными побитово, можно получить нежелательные результаты планирования.

  • Большинство внутренних типов QHB объявлены в qhb.h, в то время как интерфейсы менеджера функций ( PG_FUNCTION_ARGS и т. д.) находятся в fmgr.h, поэтому нужно будет включить как минимум эти два файла. По причинам переносимости лучше сначала включить qhb.h, а не заголовочные файлы других систем или пользователей. При включении qhb.h также автоматически будут включены elog.h и palloc.h.

  • Имена символов, определенные в объектных файлах, не должны конфликтовать друг с другом или с символами, определенными в исполняемых файлах сервера QHB. При получении сообщения об ошибках в связи с подобными конфликтами вам придется переименовать ваши функции или переменные.



Компиляция и связывание динамически загружаемых функций

Прежде чем вы сможете использовать свои функции C/RUST, расширяющие возможности QHB, их нужно скомпилировать и скомпоновать особым образом для создания файла, который сервер сможет динамически загрузить. Точнее говоря, необходимо создать разделяемую библиотеку.

Для получения информации, выходящей за рамки этого раздела, вам следует прочитать документацию по вашей операционной системе, в частности, страницы руководства по компилятору C, cc, и компоновщику, ld. Кроме того, исходный код QHB содержит несколько рабочих примеров в каталоге share/extension. Однако, полагаясь на эти примеры, вы сделаете свои модули зависимыми от доступности исходного кода QHB.

Создание разделяемых библиотек обычно аналогично сборке исполняемых файлов: сначала исходные файлы компилируются в объектные файлы, затем объектные файлы связываются вместе. Объектные файлы должны быть созданы как позиционно-независимый код (position-independent code, PIC), что концептуально означает, что при загрузке исполняемым файлом они могут быть размещены в произвольном месте в памяти и загружаются исполняемым файлом. (Объектные файлы, предназначенные для компоновки самих исполняемых файлов, обычно компилируются по-другому). Команда для сборки разделяемой библиотеки содержит специальные флаги, чтобы отличать ее от сборки исполняемого файла (по крайней мере, теоретически — в некоторых системах эта практика более неприглядна).

В следующих примерах предполагается, что ваш исходный код находится в файле foo.c, и мы создадим разделяемую библиотеку foo.so Промежуточный объектный файл будет называться foo.o, если не указано иное. Разделяемая библиотека может содержать более одного объектного файла, но здесь мы используем только один.

FreeBSD

Флаг компилятора для создания PIC — -fPIC. Флаг компилятора для создания разделяемой библиотеки — -shared.

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

Это применимо, начиная с FreeBSD версии 3.0.

Linux

Флаг компилятора для создания PIC — -fPIC. Флаг компилятора для создания разделяемой библиотеки — -shared. Полный пример выглядит так:

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

macOS

Рассмотрим следующий пример с необходимыми командами. Здесь предполагается, что установлены инструменты разработчика.

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

NetBSD

Флаг компилятора для создания PIC — -fPIC. В системах ELF для компоновки разделяемых библиотек используется компилятор с флагом -shared. В более старых системах без ELF применяется команда ld -Bshareable.

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

OpenBSD

Флаг компилятора для создания PIC — -fPIC. Для компоновки разделяемых библиотек используется команда ld -Bshareable.

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

Solaris

Флаг для создания PIC с компилятором Sun — -KPIC, с компилятором GCC — -fPIC. Для компоновки разделяемых библиотек с обоими компиляторами можно использовать флаг -G, а с GCC — флаг -shared.

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

или:

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

Совет
Если для вас это слишком сложно, следует рассмотреть возможность использования средства GNU Libtool, которое скрывает различия платформ за единым интерфейсом.

Полученный файл разделяемой библиотеки затем можно загрузить в QHB. Задавая имя файла команде CREATE FUNCTION, необходимо указать имя файла разделяемой библиотеки, а не промежуточного объектного файла. Обратите внимание, что стандартное расширение разделяемой библиотеки, принятое в системе (обычно .so или .sl ), в команде CREATE FUNCTION можно опустить, и обычно так и следует делать для лучшей переносимости.

Где именно сервер ожидает найти файлы разделяемой библиотеки, см. в подразделе Динамическая загрузка.



Аргументы составного типа

У составных типов нет фиксированного макета, как у структур C/RUST. Вхождения составного типа могут содержать поля NULL. Кроме того, составные типы, являющиеся частью иерархии наследования, могут иметь иные поля, нежели остальные члены той же иерархии. Поэтому QHB предоставляет функциям интерфейс для доступа к полям составных типов из C/RUST.

Предположим, мы хотим написать функцию для ответа на запрос:

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

Используя соглашения о вызовах версии 1, мы можем определить c_overpaid как:

#include "qhb.h"
#include "executor/executor.h"  /* для 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);
    /* Как вариант, можно записать PG_RETURN_NULL() для значения поля «зарплата», равного NULL. */

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByName — системная функция QHB, которая возвращает атрибуты из указанной строки. Она принимает три аргумента: аргумент типа HeapTupleHeader, передаваемый в функцию, имя требуемого атрибута и возвращаемый параметр, который сигнализирует о том, что атрибут имеет значение NULL. GetAttributeByName возвращает значение Datum, которое можно преобразовать в правильный тип данных с помощью соответствующей функции DatumGetXXX(). Обратите внимание, что возвращаемое значение не имеет смысла, если установлен флаг NULL; всегда проверяйте этот флаг, прежде чем пытаться что-либо сделать с результатом.

Существует также функция GetAttributeByNum, которая выбирает целевой атрибут по номеру столбца, а не по имени.

Следующая команда объявляет функцию c_overpaid в SQL:

CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
    AS 'КАТАЛОГ/funcs', 'c_overpaid'
    LANGUAGE C STRICT;

Обратите внимание, что мы использовали STRICT, чтобы не пришлось проверять, имеют ли входные аргументы значение NULL.



Возврат строк (составных типов)

Чтобы вернуть значение строки или составного типа из функции на нативном языке, можно использовать специальный API, который предоставляет макросы и функции, скрывающие большую часть сложности построения составных типов данных. Чтобы использовать этот API, исходный файл должен включать:

#include "funcapi.h"

Существует два способа создания составного значения данных (далее «кортеж»): его можно построить из массива значений Datum или из массива строк C/RUST, которые можно передать во функции преобразования ввода для типов данных столбца кортежа. В любом случае сначала необходимо получить или создать дескриптор TupleDesc для структуры кортежа. При работе со значениями Datum вы передаете TupleDesc функции BlessTupleDesc, а затем вызываете для каждой строки heap_form_tuple. При работе со строками C/RUST вы передаете TupleDesc функции TupleDescGetAttInMetadata, а затем вызываете для каждой строки BuildTupleFromCStrings. В случае функции, возвращающей множество кортежей, все этапы настройки можно выполнить один раз при первом вызове функции.

Для настройки необходимого TupleDesc имеется несколько вспомогательных функций. Рекомендуемый способ сделать это в большинстве функций, возвращающих составные значения, это вызвать:

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

передавая ту же самую структуру fcinfo, которая была передана самой вызывающей функции. (Для этого, конечно, нужно использовать соглашения о вызовах версии 1). В resultTypeId можно передать NULL или адрес локальной переменной для получения OID типа результата функции. resultTupleDesc должен быть адресом локальной переменной TupleDesc. Убедитесь, что результатом является TYPEFUNC_COMPOSITE; если это так, значит, resultTupleDesc был заполнен требуемой структурой TupleDesc. (Если это не так, вы можете сообщить об ошибке по образцу: «функция, возвращающая запись, вызвана в контексте, не допускающем запись типа»).

Совет
Из функции get_call_result_type можно получить фактический тип результата полиморфной функции, так что она полезна тех в функциях, которые возвращают скалярные полиморфные результаты, а не только в тех, которые возвращают составные типы. Выходной параметр resultTypeId полезен в первую очередь для функций, возвращающих полиморфные скаляры.

Примечание
У get_call_result_type есть родственная функция get_expr_result_type, которую можно использовать для получения ожидаемого типа результата для вызова функции, представленного деревом выражения. Ее можно использовать, если требуется определить тип результата вне самой функции. Существует также функция get_func_result_type, которую можно использовать, когда известен только OID функции. Однако обе эти функции не могут работать с функциями, объявленными как возвращающие record, а функция get_func_result_type не может привести к полиморфным типам, поэтому вместо них рекомендуется все-таки пользоваться функцией get_call_result_type.

Ранее для получения TupleDesc использовались также следующие, сейчас признанные устаревшими функции:

TupleDesc RelationNameGetTupleDesc(const char *relname)

(возвращает TupleDesc** для типа строки указанного отношения) и:

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

(возвращает TupleDesc на основе OID типа). Ее можно использовать для получения TupleDesc для базового или составного типа. Однако она не подходит для функции, которая возвращает record, и не может привести к полиморфным типам.

После получения TupleDesc вызовите:

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

если планируете работать со значениями Datum, или:

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

если планируете работать со строками C/RUST. Если вы пишете функцию, возвращающую множество данных, можно сохранить результаты этих функций в структуре FuncCallContext — используйте поле tuple_desc или attinmeta соответственно.

При работе с Datum используйте функцию:

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

чтобы скомпоновать HeapTuple из переданных ей пользовательских данных в форме Datum.

При работе со строками C/RUST используйте функцию:

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

чтобы скомпоновать HeapTuple из переданных ей пользовательских данных в форме строки C/RUST. *values — это массив строк C/RUST, по одной на каждый атрибут возвращаемой строки. Каждая строка C/RUST должна иметь формат, ожидаемый функцией ввода типа данных атрибута. Чтобы вернуть значение NULL для одного из этих атрибутов, нужно задать NULL в соответствующем указателе в массиве values. Эту функцию нужно будет вызывать снова для каждой возвращаемой строки.

Как только вы соберете кортеж для возврата из своей функции, его следует преобразовать в тип Datum. Воспользуйтесь функцией:

HeapTupleGetDatum(HeapTuple tuple)

чтобы преобразовать HeapTuple в надлежащий Datum. Этот тип Datum можно вернуть напрямую, если вы намереваетесь вернуть только одну строку, или использовать как текущее возвращаемое значение в функции, возвращающей множество.

Пример приведен в следующем разделе.



Возврат множеств

Для функций на нативном языке существует две возможности возврата множеств (нескольких строк). Первый метод, называемый режимом ValuePerCall (значение за вызов), состоит в многократном вызове функции, возвращающей множество (при этом ей каждый раз передаются одни и те же аргументы), и при каждом вызове она возвращает по одной новой строке, пока те не закончатся, о чем функция просигнализирует, возвращая NULL. Таким образом, функция, возвращающая множество (Set-Returning Function, SRF), должна от вызова к вызову сохранять свое состояние в достаточной степени, чтобы помнить, что она делает, и при каждом вызове возвращать надлежащие данные. Другой метод, называемый режимом Materialize (материализация), состоит в том, что SRF заполняет и возвращает объект tuplestore, содержащий сразу все результирующее множество; таким образом, для получения всего результата производится всего один вызов, и никакое состояние между вызовами сохранять не нужно.

Пользуясь режимом ValuePerCall, важно помнить, что выполнение запроса до завершения не гарантировано; иными словами, при указании такого параметра, как LIMIT, исполнитель запроса может перестать вызывать функцию, возвращающую множества, раньше, чем будут выбраны все строки. Это означает, что выполнять в последнем вызове действия, связанные с очисткой, небезопасно, так как он может вообще не произойти. Для функций, которым необходим доступ к внешним ресурсам, например файловым дескрипторам, рекомендуется использовать режим Materialize.

В оставшейся части данного раздела описывается ряд наиболее часто применяющихся (хотя и необязательных) вспомогательных макросов для SRF, реализующих режим ValuePerCall. Кроме того, в модулях share/extension установки QHB содержится много примеров SRF, реализующих как режим ValuePerCall, так и режим Materialize.

Для использования описанных здесь макросов поддержки ValuePerCall включите funcapi.h. Эти макросы работают со структурой FuncCallContext, содержащей состояние, которое нужно сохранять между вызовами. Внутри вызываемой SRF указатель на FuncCallContext удерживается между вызовами в поле fcinfo->flinfo->fn_extra. Макросы автоматически заполняют данное поле при первом использовании и в при последующих вызовах ожидают обнаружить в нем этот же указатель.

typedef struct FuncCallContext
{
    /*
     * Счетчик числа произведенных ранее вызовов
     *
     * посредством макроса SRF_FIRSTCALL_INIT() call_cntr присваивается начальное
     * значение 0 , которое увеличивается на 1 при каждом вызове SRF_RETURN_NEXT().
     */
    uint64 call_cntr;

    /*
     * (НЕОБЯЗАТЕЛЬНО) максимальное число вызовов
     *
     * max_calls не является обязательным и присутствует здесь только для удобства.
     * Если это значение не задано, вы должны предоставить альтернативный способ
     * определить, когда функция закончила работу.
     */
    uint64 max_calls;

    /*
     * (НЕОБЯЗАТЕЛЬНО) указатель на разнообразную дополнительную информацию,
     * предоставленную пользователем
     *
     * user_fctx используется как указатель на ваши собственные данные, позволяя
     * сохранять произвольную контекстную информацию между вызовами вашей функции.
     */
    void *user_fctx;

    /*
     * (НЕОБЯЗАТЕЛЬНО) указатель на структуру, содержащую метаданные ввода типа
     * атрибута
     *
     * attinmeta применяется при возврате кортежей (т. е. составных типов данных)
     * и не применяется при возврате базовых типов данных. Он нужен, только
     * если вы намерены использовать BuildTupleFromCStrings() для создания
     * возвращаемого кортежа.
     */
    AttInMetadata *attinmeta;

    /*
     * Контекст памяти, используемый для структур, которые должны пережить
     * несколько вызовов
     *
     * multi_call_memory_ctx настраивается при помощи SRF_FIRSTCALL_INIT() и
     * используется SRF_RETURN_DONE() для очистки. Это наиболее подходящий
     * контекст для любых блоков памяти, которые предназначены для многократного
     * использования при повторных вызовах SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * (НЕОБЯЗАТЕЛЬНО) указатель на структуру, содержащую описание кортежа
     *
     * tuple_desc применяется при возврате кортежей (т. е. составных типов данных)
     * и требуется, только если вы собираетесь формировать кортежи с помощью функции
     * heap_form_tuple(), а не BuildTupleFromCStrings().  Обратите внимание, что
     * сохраняемый здесь указатель TupleDesc обычно должен сначала пройти через
     * вызов BlessTupleDesc().
     */
    TupleDesc tuple_desc;

} FuncCallContext;

Для SRF предоставляется несколько макросов, использующих эту инфраструктуру:

SRF_IS_FIRSTCALL()

Используйте этот макрос, чтобы определить, вызывается ли ваша функция в первый раз или нет. При первом вызове (но не при последующих) используйте:

SRF_FIRSTCALL_INIT()

чтобы инициализировать FuncCallContext. При каждом вызове функции, включая первый, используйте:

SRF_PERCALL_SETUP()

чтобы подготовиться к применению FuncCallContext.

Если у вашей функции есть данные для возврата, используйте:

SRF_RETURN_NEXT(funcctx, result)

чтобы передать их вызывающей функции. (Переменная result должна иметь тип Datum и содержать либо одно значение, либо кортеж, подготовленный, как описано выше). Наконец, когда ваша функция закончит возвращать данные, используйте:

SRF_RETURN_DONE(funcctx)

чтобы провести очистку и завершить работу SRF.

Текущий контекст памяти, в котором вызывается SRF, является временным и будет очищаться между вызовами. Это означает, что вам не нужно вызывать pfree для всех блоков памяти, которые вы выделили с помощью palloc; они все равно будут освобождены. Однако если вы хотите распределить какие-либо структуры данных между вызовами, вам следует поместить их в другое место. Подходящим местом для любых данных, которые должны сохраняться до завершения работы SRF, является контекст памяти, на который ссылается multi_call_memory_ctx. В большинстве случаев это означает, что вы должны переключиться на multi_call_memory_ctx при выполнении настройки первого вызова. Для сохранения указателя на такие долгоживущие структуры данных используйте поле funcctx->user_fctx. (Данные, выделенные в контексте multi_call_memory_ctx, автоматически освободятся в конце запроса, поэтому их также нет необходимости освобождать вручную.)

ПРЕДУПРЕЖДЕНИЕ
В то время как фактические аргументы функции остаются неизменными между вызовами, если вы распаковываете значения аргументов (что обычно делается прозрачно с помощью макроса PG_GETARG_xxx) во временном контексте, то распакованные копии будут освобождаться в каждом цикле. Соответственно, если вы сохраняете ссылки на такие значения в вашем user_fctx, следует либо скопировать их после распаковки в multi_call_memory_ctx, либо обязательно распаковывать значения только в этом контексте.

Полный пример псевдокода выглядит следующим образом:

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    другие необходимые объявления

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* Здесь содержится код подготовки при первом вызове: */
        пользовательский код
        если возвращается составной тип
            сформировать TupleDesc и, возможно, AttInMetadata
        конец условия возвращаемого составного типа
        пользовательский код
        MemoryContextSwitchTo(oldcontext);
    }

    /* Здесь содержится код подготовки при каждом вызове: */
    пользовательский код
    funcctx = SRF_PERCALL_SETUP();
    пользовательский код

    /* это единственный способ, которым мы можем проверить, последний ли это вызов: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Здесь мы возвращаем еще один результат: */
        пользовательский код
        получение результирующих значений Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Здесь мы прекращаем возвращать результаты, так что просто отражаем этот факт. */
        /* (Воздержаться от искушения вставить сюда код очистки.) */
        SRF_RETURN_DONE(funcctx);
    }
}

Полный пример простой SRF, возвращающей составной тип, выглядит следующим образом:

PG_FUNCTION_INFO_V1(retcomposite);

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

    /* действия, производимые только при первом вызове функции */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* создать контекст функции для сохранения данных между вызовами */
        funcctx = SRF_FIRSTCALL_INIT();

        /* переключить на контекст памяти, подходящий для нескольких вызовов функции */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* общее число кортежей, подлежащих возврату */
        funcctx->max_calls = PG_GETARG_UINT32(0);

        /* Сформировать дескриптор кортежей для нашего результирующего типа */
        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")));
                            /* "функция, возвращающая запись, вызвана в контексте, который не может принять запись типа" /*

        /*
         * создать метаданные атрибута, которые позже понадобятся для формирования
         * кортежей из исходных строк C/RUST
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* действия, производимые при каждом вызове функции */
    funcctx = SRF_PERCALL_SETUP();

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

    if (call_cntr < max_calls)    /* выполняется, когда предстоит еще несколько вызовов */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * подготовить массив значений для формирования возвращаемого кортежа.
         * Это должен быть массив строк C/RUST, который
         * позже будет обработан функциями ввода для типов данных.
         */
        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));

        /* сформировать кортеж */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* преобразовать кортеж в datum */
        result = HeapTupleGetDatum(tuple);

        /* провести очистку (на самом деле в ней нет необходимости) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* выполняется, когда вызовов больше не осталось */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

Один из способов объявить эту функцию в SQL:

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

CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
    RETURNS SETOF __retcomposite
    AS 'имя_файла', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

Другой способ — использовать параметры OUT:

CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
    OUT f1 integer, OUT f2 integer, OUT f3 integer)
    RETURNS SETOF record
    AS 'имя_файла', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

Обратите внимание, что в этом методе выходной тип функции формально является анонимным типом record.



Полиморфные аргументы и возвращаемые типы

Функции на нативном языке можно объявить как принимающие и возвращающие полиморфные типы, описанные в подразделе Полиморфные типы. Когда аргументы или возвращаемые типы функции определены как полиморфные типы, автор функции не может заранее знать, с каким типом данных она будет вызываться или какой должна возвращать. В fmgr.h предусмотрены две подпрограммы, позволяющие функции C/RUST версии 1 определить фактические типы данных своих аргументов и тип, который ей нужно вернуть. Эти подпрограммы называются get_fn_expr_rettype(FmgrInfo *flinfo) и get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Они возвращают OID типа результата или аргумента, либо InvalidOid, если информация недоступна. Структура flinfo обычно доступна по ссылке fcinfo->flinfo. Параметр argnum (номер аргумента) задается, начиная с нуля. В качестве альтернативы get_fn_expr_rettype также можно использовать get_call_result_type. Помимо этого, существует функция get_fn_expr_variadic, которая позволяет определить, были ли переменные аргументы объединены в массив. Это полезно прежде всего для функций с VARIADIC "any", поскольку такое объединение всегда будет происходить для функций с переменными аргументами, принимающих обычные типы массивов.

Например, предположим, что мы хотим написать функцию, которая принимает один элемент любого типа и возвращает одномерный массив этого типа:

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

    /* получить предоставляемый элемент; будьте осторожны, если это NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* мы имеем дело с одной размерностью */
    ndims = 1;
    /* и одним элементом */
    dims[0] = 1;
    /* и нижняя граница равна 1 */
    lbs[0] = 1;

    /* получить требуемую информацию о типе элемента */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* теперь сформировать массив */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

Следующая команда объявляет функцию make_array в SQL:

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'КАТАЛОГ/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

Существует вариант полиморфизма, который доступен только для функций на нативном языке: они могут быть объявлены как принимающие параметры типа "any". (Обратите внимание, что это имя типа должно быть заключено в кавычки, поскольку оно также является зарезервированным словом SQL). Он работает так же, как anyelement, за исключением того, что он не требует, чтобы различные аргументы "any" имели одинаковый тип, и не помогает определить тип результата функции. Функцию на нативном языке также можно объявить с последним параметром VARIADIC "any". Он будет соответствовать одному или нескольким фактическим аргументам любого типа (необязательно одинакового). Эти аргументы не будут собраны в массив, как это происходит с обычными функциями с переменными аргументами; они просто будут переданы функции по отдельности. Если применяется это свойство, то определять количество фактических аргументов и их типов нужно с помощью макроса PG_NARGS() и методов, описанных выше. Кроме того, пользователи такой функции могут захотеть использовать ключевое слово VARIADIC в вызове своей функции, ожидая, что та будет рассматривать элементы массива как отдельные аргументы. При необходимости это поведение должна реализовать сама функция, предварительно определив с помощью get_fn_expr_variadic, был ли фактический аргумент помечен как VARIADIC.



Разделяемая память и легкие блокировки

Встраиваемые расширения могут резервировать легкие блокировки (LWLock) и область в разделяемой памяти при запуске сервера. Разделяемую библиотеку расширения нужно загрузить заранее, указав ее в параметре shared_preload_libraries. Разделяемая библиотека должна зарегистрировать обработчик shmem_request_hook в своей функции _PG_init. Этот shmem_request_hook может резервировать легкие блокировки или разделяемую память. Разделяемая память резервируется путем вызова:

void RequestAddinShmemSpace(int size)

из вашего shmem_request_hook.

Легкие блокировки резервируются путем вызова:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

из shmem_request_hook. Это обеспечит доступность массива легких блокировок num_lwlocks под именем tranche_name. Для получения указателя на этот массив воспользуйтесь функцией GetNamedLWLockTranche.

Во избежание возможных условий гонки каждый сервер должен использовать легкую блокировку AddinShmemInitLock при подключении и инициализации распределения разделяемой памяти, как показано здесь:

static mystruct *ptr = NULL;

if (!ptr)
{
        bool    found;

        LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
        ptr = ShmemInitStruct("my struct name", size, &found);
        if (!found)
        {
             /* инициализировать содержимое области shmem (разделяемой памяти); */
             /* получить все требуемые легкие блокировки с помощью: */
                ptr->locks = GetNamedLWLockTranche("my tranche name");
        }
        LWLockRelease(AddinShmemInitLock);
}


Использование C++ для расширяемости

Хотя серверная часть QHB написана на C/RUST, расширения для него можно написать на C++, если следовать этим рекомендациям:

  • Все функции, к которым обращается сервер, должны представлять для него интерфейс C/RUST; затем эти функции C/RUST могут вызывать функции C++. Например, для функций, к которым обращается сервер, нужно указать extern C. Это также необходимо для любых функций, которые передаются в виде указателей между серверной частью и кодом C++.

  • Освободите память, используя подходящий метод освобождения. Например, большая часть памяти сервера выделяется с помощью palloc(), поэтому для ее освобождения воспользуйтесь pfree(). В таких случаях использование принятой в C++ операции delete приведет к ошибке.

  • Не допускайте распространения исключений в коде C/RUST (используйте универсальный блок на верхнем уровне всех функций extern C). Это необходимо, даже если код C++ явно не генерирует какие-либо исключения, потому исключения все равно могут генерироваться такими событиями, как нехватка памяти. Любые исключения должны быть перехвачены, и соответствующие ошибки переданы обратно в интерфейс C/RUST. Если возможно, скомпилируйте код C++ с указанием -fno-exception, чтобы полностью убрать исключения; в таких случаях вы должны выявлять ошибки в вашем коде C++, например, проверять на NULL адрес, возвращенный new().

  • При вызове серверных функций из кода C++ убедитесь, что стек вызовов C++ содержит только простые старые структуры данных (plain old data, POD). Это необходимо, потому что ошибки сервера генерируют удаленную функцию longjmp (), которая не разворачивает стек вызовов C++ надлежащим образом для объектов, отличных от POD.

Резюмируя вышесказанное, лучше всего поместить код C++ за стену из функций extern C, которые взаимодействуют с сервером и предотвращают возникновение исключений, утечки памяти и потери стека вызовов.