Функции на языке C
Эта страница переведена при помощи нейросети GigaChat.
Пользовательские функции могут быть написаны на языке C (или на языке, который может быть совместим с C, таком как C++). Такие функции компилируются в динамически загружаемые объекты (также называемые общими библиотеками), которые загружаются сервером по запросу. Функция динамической загрузки отличает функции «на языке Cи» от функций «внутренних». На самом деле фактические соглашения о кодировании практически одинаковы и для тех, и для других (поэтому стандартная внутренняя библиотека функций является богатым источником примеров кода для пользовательских функций Cи).
В настоящее время используется только одна конвенция вызова для функций C («версия 1»). Поддержка этой конвенции вызова указывается путем записи макроса вызова функции, как показано ниже.
Динамическая загрузка
При первом вызове пользовательской функции в определенном файле загружаемого объекта динамический загрузчик загружает этот объектный файл в память, чтобы функция могла быть вызвана. CREATE FUNCTION
для пользовательской функции C, следовательно, должен указывать две части информации о функции: имя файла загружаемого объекта и имя C (символ ссылки) конкретной функции, которую следует вызвать в этом объектном файле. Если имя C явно не указано, предполагается, что оно совпадает с именем SQL-функции.
Используется следующий алгоритм для поиска общего объектного файла на основе имени, указанного в команде CREATE FUNCTION
:
- Если имя является абсолютным путем, загружается указанный файл.
- Если имя начинается со строки
$libdir
, эта часть заменяется именем каталога библиотеки пакета PostgreSQL, которое определяется во время сборки. - Если имя не содержит части каталога, файл ищется в пути, указанном переменной конфигурации dynamic_library_path.
- В противном случае (файл не был найден в пути или он содержит неабсолютную часть каталога) динамический загрузчик попытается взять имя таким, какое оно есть, что, скорее всего, приведет к сбою (ненадежно полагаться на текущий рабочий каталог).
Если эта последовательность не работает, платформа-специфическое расширение имени файла общей библиотеки (часто .so
) добавляется к заданному имени, и эта последовательность пробуется снова. Если это тоже не удается, загрузка завершится неудачно.
Рекомендуется размещать общие библиотеки либо относительно $libdir
, либо через путь к динамической библиотеке. Это упрощает обновление версий, если новая установка находится в другом месте. Фактический каталог, который обозначается $libdir
, можно узнать с помощью команды pg_config --pkglibdir
.
Пользовательский идентификатор сервера PostgreSQL, под которым он работает, должен иметь возможность пройти путь к файлу, который хотите загрузить. Делать файл или каталог более высокого уровня неразрешимым и/или неисполняемым для пользователя postgres
является распространенной ошибкой.
В любом случае имя файла, которое указано в команде CREATE FUNCTION
, буквально записывается в системных каталогах, поэтому если файл нужно будет загружать снова, применяется та же процедура.
PostgreSQL не компилирует функцию C автоматически. Файл объекта должен быть скомпилирован перед тем, как он будет упомянут в команде CREATE FUNCTION
. См. Раздел «Компиляция и связывание динамически загружаемых функции» для получения дополнительной информации.
Чтобы убедиться, что динамически загружаемый объектный файл не загружен в несовместимый сервер, PostgreSQL проверяет, содержит ли файл «магический блок» с соответствующим содержимым. Это позволяет серверу обнаруживать очевидные несовместимости, такие как код, откомпилированный для другой основной версии PostgreSQL. Чтобы включить магический блок, напишите это в одном (и только в одном) из файлов исходного модуля после включения заголовка fmgr.h
.
PG_MODULE_MAGIC;
После первого использования динамически загруженный объектный файл сохраняется в памяти. Будущие вызовы в том же сеансе функции(й) в этом файле будут сопровождаться лишь небольшим накладным расходом поиска таблицы символов. Если нужно принудительно перезагрузить объектный файл, например, после его перекомпиляции, начните новый сеанс.
Необязательно, но динамически загружаемый файл может содержать функцию инициализации. Если файл включает функцию под названием _PG_init
, эта функция будет вызвана сразу после загрузки файла. Функция не принимает параметров и должна возвращать void. В настоящее время нет способа выгрузить динамически загруженный файл.
Базовые типы в функциях языка C
Чтобы узнать, как писать функции на языке C, нужно знать, как PostgreSQL внутренне представляет базовые типы данных и как они могут передаваться в функции и из них. Внутренне, PostgreSQL рассматривает базовый тип как «кусок памяти». Пользовательские функции, которые определяются над типом, в свою очередь определяют способ, которым PostgreSQL может работать с ним. То есть, PostgreSQL будет только хранить и извлекать данные с диска и использовать пользовательские функции для ввода, обработки и вывода данных.
Базовые типы могут иметь один из трех внутренних форматов:
- передача по значению, фиксированная длина;
- передача по ссылке, фиксированная длина;
- передача по ссылке, переменная длина.
Типы значений могут быть только 1, 2 или 4 байта в длину (также 8 байт, если sizeof(Datum)
равно 8 на компьютере пользователя). Нужно быть осторожным, чтобы определить свои типы таким образом, чтобы они были одинакового размера (в байтах) на всех архитектурах. Например, тип long
опасен, потому что он составляет 4 байта на некоторых машинах и 8 байтов на других, тогда как тип int
составляет 4 байта на большинстве машин Unix. Разумная реализация типа int4
на машинах Unix может выглядеть так:
/* 4-byte integer, passed by value */
typedef int int4;
(Фактический код PostgreSQL на языке C называет этот тип int32
, потому что в C принято, что int
XX означает XX биты. Обратите внимание также, что тип C int8
имеет размер 1 байт. Тип SQL int8
называется int64
в C. См. также таблицу «Эквивалентные типы C для встроенных типов SQL».)
С другой стороны, типы фиксированной длины любого размера могут передаваться по ссылке. Пример реализации типа PostgreSQL:
/* 16-byte structure, passed by reference */
typedef struct
{
double x, y;
} Point;
При передаче этих типов в функции и из функций PostgreSQL можно использовать только указатели на такие типы. Чтобы вернуть значение такого типа, выделите нужное количество памяти с помощью palloc
, заполните выделенную память и верните указатель на нее (кроме того, если нужно вернуть то же значение, что и одно из входных аргументов того же типа данных, можно пропустить дополнительное palloc
и просто вернуть указатель на входное значение).
Наконец, все типы переменной длины также должны передаваться по ссылке. Все типы переменной длины должны начинаться с непрозрачного поля длины ровно в 4 байта, которое будет установлено SET_VARSIZE
; никогда не устанавливайте это поле напрямую! Все данные, которые должны храниться в этом типе, должны находиться в памяти сразу после этого поля длины. Поле длины содержит общую длину структуры, т.е. включает в себя размер самого поля длины.
Еще один важный момент заключается в том, чтобы избегать оставления каких-либо неинициализированных битов внутри значений типов данных; например, убедитесь, что все байты выравнивания, которые могут присутствовать в структурах, обнулены. Без этого логически эквивалентные константы типа данных могут быть восприняты планировщиком как неравные, что приведет к неэффективным (хотя и неправильным) планам.
Никогда не изменяйте содержимое входного значения, передаваемого по ссылке. Если сделаете это, то, скорее всего, повредите данные на диске, поскольку указатель, который дан, может указывать непосредственно в буфер диска. Единственное исключение из этого правила объясняется в разделе «Пользовательские агрегатные функции».
Например, можно определить тип text
следующим образом:
typedef struct {
int32 length;
char data[FLEXIBLE_ARRAY_MEMBER];
} text;
Обозначение [FLEXIBLE_ARRAY_MEMBER]
означает, что фактическая длина массива данных не указана этим объявлением.
При манипулировании типами переменной длины необходимо быть осторожными, чтобы выделить правильное количество памяти и правильно установить поле длины. Например, если бы нужно было сохранить 40 байт в структуре text
, то могли бы использовать фрагмент кода, похожий на этот:
#include "postgres.h"
...
char buffer[40]; /* our source data */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...
VARHDRSZ
такой же, как sizeof(int32)
, но считается хорошим стилем использовать макрос VARHDRSZ
для обозначения размера накладных расходов для типа переменной длины. Кроме того, поле длины должно быть установлено с использованием макроса SET_VARSIZE
, а не простым присваиванием.
Таблица ниже показывает типы C, соответствующие многим встроенным типам данных SQL в PostgreSQL. В колонке «Определено в» указан заголовочный файл, который необходимо включить для получения определения типа (фактическое определение может находиться в другом файле, включаемом указанным файлом. Рекомендуется придерживаться определенного интерфейса). Обратите внимание, что всегда необходимо включать postgres.h
сначала в любом исходном файле серверного кода, потому что он объявляет ряд вещей, которые понадобятся в любом случае, и потому что включение других заголовков первым может вызвать проблемы с переносимостью.
Эквивалентные типы C для встроенных типов SQL:
Тип SQL | Тип C | Определено в |
---|---|---|
boolean | bool | postgres.h (может быть встроен в компиляторе) |
box | BOX* | utils/geo_decls.h |
bytea | bytea* | postgres.h |
"char" | char | (встроен в компиляторе) |
character | BpChar* | postgres.h |
cid | CommandId | postgres.h |
date | DateADT | utils/date.h |
float4 (real ) | float4 | postgres.h |
float8 (double precision ) | float8 | postgres.h |
int2 (smallint ) | int16 | postgres.h |
int4 (integer ) | int32 | postgres.h |
int8 (bigint ) | int64 | postgres.h |
interval | Interval* | datatype/timestamp.h |
lseg | LSEG* | utils/geo_decls.h |
name | Name | postgres.h |
numeric | Numeric | utils/numeric.h |
oid | Oid | postgres.h |
oidvector | oidvector* | postgres.h |
path | PATH* | utils/geo_decls.h |
point | POINT* | utils/geo_decls.h |
regproc | RegProcedure | postgres.h |
text | text* | postgres.h |
tid | ItemPointer | storage/itemptr.h |
time | TimeADT | utils/date.h |
time with time zone | TimeTzADT | utils/date.h |
timestamp | Timestamp | datatype/timestamp.h |
timestamp with time zone | TimestampTz | datatype/timestamp.h |
varchar | VarChar* | postgres.h |
xid | TransactionId | postgres.h |
Теперь, когда были рассмотрены все возможные структуры для базовых типов, можно показать несколько примеров реальных функций.
Соглашения о вызовах версии 1
Правила вызова версии 1 полагаются на макросы для подавления большей части сложности передачи аргументов и результатов. Объявление функции версии 1 на языке C всегда выглядит следующим образом:
Datum funcname(PG_FUNCTION_ARGS)
Кроме того, вызов макроса:
PG_FUNCTION_INFO_V1(funcname);
Должен находиться в том же исходном файле (обычно он записывается прямо перед функцией). Этот вызов макроса не требуется для функций языка internal
, поскольку PostgreSQL предполагает, что все внутренние функции используют соглашение версии-1. Однако это необходимо для динамически загружаемых функций.
В функции версии-1 каждый фактический аргумент извлекается с помощью макроса PG_GETARG_
xxx
()
, который соответствует типу данных аргумента. (В нестрогих функциях перед этим должна быть проверка на наличие нулевого аргумента с использованием PG_ARGISNULL()
; см. ниже). Результат возвращается с помощью макроса PG_RETURN_
xxx
()
для типа возвращаемого значения. PG_GETARG_
xxx
()
принимает в качестве своего аргумента номер аргумента функции для выборки, где отсчет начинается с 0. PG_RETURN_
xxx
()
принимает в качестве своего аргумента фактическое значение, которое нужно вернуть.
Вот несколько примеров использования соглашения о вызовах версии 1:
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include "varatt.h"
PG_MODULE_MAGIC;
/* by value */
PG_FUNCTION_INFO_V1(add_one);
Datum
add_one(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}
/* by reference, fixed length */
PG_FUNCTION_INFO_V1(add_one_float8);
Datum
add_one_float8(PG_FUNCTION_ARGS)
{
/* The macros for FLOAT8 hide its pass-by-reference nature. */
float8 arg = PG_GETARG_FLOAT8(0);
PG_RETURN_FLOAT8(arg + 1.0);
}
PG_FUNCTION_INFO_V1(makepoint);
Datum
makepoint(PG_FUNCTION_ARGS)
{
/* Here, the pass-by-reference nature of Point is not hidden. */
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);
}
/* by reference, variable length */
PG_FUNCTION_INFO_V1(copytext);
Datum
copytext(PG_FUNCTION_ARGS)
{
text *t = PG_GETARG_TEXT_PP(0);
/*
* VARSIZE_ANY_EXHDR is the size of the struct in bytes, minus the
* VARHDRSZ or VARHDRSZ_SHORT of its header. Construct the copy with a
* full-length header.
*/
text *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
/*
* VARDATA is a pointer to the data region of the new struct. The source
* could be a short datum, so retrieve its data through VARDATA_ANY.
*/
memcpy(VARDATA(new_t), /* destination */
VARDATA_ANY(t), /* source */
VARSIZE_ANY_EXHDR(t)); /* how many bytes */
PG_RETURN_TEXT_P(new_t);
}
PG_FUNCTION_INFO_V1(concat_text);
Datum
concat_text(PG_FUNCTION_ARGS)
{
text *arg1 = PG_GETARG_TEXT_PP(0);
text *arg2 = PG_GETARG_TEXT_PP(1);
int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
text *new_text = (text *) palloc(new_text_size);
SET_VARSIZE(new_text, new_text_size);
memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
PG_RETURN_TEXT_P(new_text);
}
Предположим, что приведенный выше код был подготовлен в файле funcs.c
и скомпилирован в общий объект, можно было бы определить функции для PostgreSQL с командами вроде этой:
CREATE FUNCTION add_one(integer) RETURNS integer
AS 'DIRECTORY/funcs', 'add_one'
LANGUAGE C STRICT;
-- note overloading of SQL function name "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
AS 'DIRECTORY/funcs', 'add_one_float8'
LANGUAGE C STRICT;
CREATE FUNCTION makepoint(point, point) RETURNS point
AS 'DIRECTORY/funcs', 'makepoint'
LANGUAGE C STRICT;
CREATE FUNCTION copytext(text) RETURNS text
AS 'DIRECTORY/funcs', 'copytext'
LANGUAGE C STRICT;
CREATE FUNCTION concat_text(text, text) RETURNS text
AS 'DIRECTORY/funcs', 'concat_text'
LANGUAGE C STRICT;
Здесь, DIRECTORY
обозначает каталог файла общей библиотеки (например, каталог учебника PostgreSQL, который содержит код примеров, используемых в этом разделе). (Лучше было бы использовать просто 'funcs'
в предложении AS
, после того как добавили DIRECTORY
в путь поиска. В любом случае, можно опустить системно-зависимое расширение для общей библиотеки, обычно .so
.)
Обратите внимание, что функции указаны как «strict» (строгие), что означает, что система должна автоматически предполагать нулевой результат, если любое входное значение равно нулю. Делая это, избегаем необходимости проверять ввод на наличие нулевых значений в коде функции. Без этого пришлось бы явно проверить значения на предмет нулей, используя PG_ARGISNULL()
.
Макрос PG_ARGISNULL
(n) позволяет функции проверять, является ли каждый ввод нулевым. (Конечно, делать это необходимо только в функциях, не объявленных как «strict».) Как и в макросах PG_GETARG_
xxx
()
, аргументы ввода нумеруются начиная с нуля. Обратите внимание, что следует воздержаться от выполнения PG_GETARG_
xxx
()
до тех пор, пока не убедитесь, что аргумент не равен нулю. Чтобы вернуть нулевой результат, выполните PG_RETURN_NULL()
; это работает как в строгих, так и в нестрогих функциях.
На первый взгляд, соглашения о кодировании версии 1 могут показаться просто бессмысленным и трудными для понимания по сравнению с использованием обычных соглашений вызова C
. Однако они позволяют иметь дело с NULL
аргументами и возвращаемыми значениями, а также со значениями в формате TOAST (сжатыми или хранимыми отдельно).
Другие возможности, предоставляемые интерфейсом версии 1, включают две разновидности макросов PG_GETARG_
xxx
()
. Первый из них, PG_GETARG_
xxx
_COPY()
, гарантирует возврат копии указанного аргумента, безопасной для записи. (Обычные макросы иногда возвращают указатель на значение, которое физически хранится в таблице, в которую нельзя записывать. Использование макросов PG_GETARG_
xxx
_COPY()
гарантирует запись результата.) Вторая разновидность состоит из макросов PG_GETARG_
xxx
_SLICE()
, которые принимают три аргумента. Первый - это номер аргумента функции (как указано выше). Второй и третий - смещение и длина сегмента, который должен быть возвращен. Смещения отсчитываются от нуля, а отрицательная длина запрашивает остаток значения, который должен быть возвращен. Эти макросы обеспечивают более эффективный доступ к частям больших значений в случае, если у них есть тип хранения «external». (Тип хранилища столбца можно указать с помощью ALTER TABLE
tablename
ALTER COLUMN
colname
SET STORAGE
storagetype
. storagetype
является одним из plain
, external
, extended
, или main
.)
Наконец, соглашения о вызовах функций версии 1 позволяют возвращать результаты набора (Раздел «Возвращение наборов») и реализовывать триггерные функции (раздел «Триггеры») и обработчики вызова процедурного языка (раздел «Написание обработчика процедурного языка»).
Написание кода
Прежде чем перейти к более сложным темам, следует обсудить некоторые правила кодирования функций PostgreSQL на языке C. Хотя в PostgreSQL можно загружать функции, написанные на языках, отличных от C, это обычно затруднительно (если вообще возможно), поскольку другие языки, такие как C++, FORTRAN или Pascal, часто не следуют тем же соглашениям о вызове, что и C. То есть другие языки не передают аргументы и возвращаемые значения между функциями таким же образом. По этой причине будем считать, что функции на языке C на самом деле написаны на C.
Основные правила написания и построения функций C следующие:
- Используйте
pg_config --includedir-server
, чтобы узнать, где установлены файлы заголовков сервера PostgreSQL в системе (на той системе, которую будут использовать пользователи). - Компиляция и связывание вашего кода таким образом, чтобы он мог быть динамически загружен в PostgreSQL, всегда требует специальных флагов. См. Раздел «Компиляция и компоновка динамически загружаемых функций» для подробного объяснения того, как это сделать для конкретной операционной системы.
- Не забудьте определить «магический блок» для общей библиотеки, как описано в Разделе «Динамическая загрузка».
- При выделении памяти используйте функции PostgreSQL
palloc
иpfree
вместо соответствующих функций библиотеки Cmalloc
иfree
. Память, выделяемая с помощьюpalloc
, будет автоматически освобождена в конце каждой транзакции, предотвращая утечки памяти. - Всегда обнуляйте байты своих структур с использованием
memset
(или выделяйте их с помощьюpalloc0
в первую очередь). Даже если назначаете каждому полю своей структуры, могут быть выравнивающие заполнители (дыры в структуре), которые содержат мусорные значения. Без этого трудно поддерживать хеш-индексы или хеш-соединения, поскольку нужно выбрать только значимые биты структуры данных для вычисления хеша. Планировщик также иногда полагается на сравнение констант через побитовое равенство, поэтому можно получить нежелательные результаты планирования, если логически эквивалентные значения не равны побитно. - Большинство внутренних типов PostgreSQL объявлены в
postgres.h
, а интерфейсы диспетчера функций (PG_FUNCTION_ARGS
и т.д.) находятся вfmgr.h
. Поэтому нужно будет включить хотя бы эти два файла. По причинам переносимости лучше всего включатьpostgres.h
сначала, перед любыми другими системными или пользовательскими заголовочными файлами. Включениеpostgres.h
также включитelog.h
иpalloc.h
. - Имена символов, определенные внутри объектных файлов, не должны конфликтовать друг с другом или с символами, определенными в исполняемом файле сервера PostgreSQL. Придется переименовывать свои функции или переменные, если получите сообщения об ошибках такого рода.
Компиляция и связывание динамически загружаемых функций
Прежде чем будет можно использовать свои функции расширения PostgreSQL, написанные на языке C, они должны быть скомпилированы и связаны особым образом для создания файла, который может быть динамически загружен сервером. Точнее говоря, необходимо создать общую библиотеку.
Для получения информации, выходящей за рамки этого раздела, рекомендуется прочитать документацию используемой операционной системы, особенно страницы руководства для компилятора C, cc
, и редактора связей, ld
. Кроме того, исходный код PostgreSQL содержит несколько рабочих примеров в каталоге contrib
. Если полагаться на эти примеры, модули будут зависеть от наличия исходного кода PostgreSQL.
Создание общих библиотек обычно аналогично связыванию исполняемых файлов: сначала файлы исходного кода компилируются в объектные файлы, а затем объектные файлы связываются вместе. Объектные файлы необходимо создавать как позиционно-независимый код (PIC), что концептуально означает, что они могут быть размещены в произвольном месте памяти при их загрузке исполняемым файлом. (Объектные файлы, предназначенные для исполняемых файлов, обычно не компилируются таким образом.) Команда для связывания общей библиотеки содержит специальные флаги, чтобы отличить ее от связывания исполняемого файла (по крайней мере теоретически - на некоторых системах практика не идеальна).
В следующих примерах предполагаем, что исходный код находится в файле foo.c
, и будет создана общая библиотека foo.so
. Промежуточный объектный файл будет называться foo.o
, если не указано иное. Общая библиотека может содержать более одного объектного файла, но здесь используется только один.
FreeBSD
Флаг компилятора для создания PIC - это -fPIC
. Для создания общих библиотек флаг компилятора - это -shared
.
cc -fPIC -c foo.c
cc -shared -o foo.so foo.o
Это применимо начиная с FreeBSD версии 3.0, в более старых версиях используется компилятор gcc.
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 - это -KPIC
с компилятором Sun и -fPIC
с GCC. Чтобы связать общие библиотеки, опция компилятора - -G
с любым компилятором или альтернативно -shared
с GCC:
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, который скрывает различия платформ за унифицированным интерфейсом.
Полученный файл общей библиотеки затем можно загрузить в PostgreSQL. При указании имени файла команде CREATE FUNCTION
, необходимо указать имя файла общей библиотеки, а не промежуточного объектного файла. Обратите внимание, что стандартное расширение общей библиотеки системы (обычно .so
или .sl
) может быть опущено из команды CREATE FUNCTION
, и обычно должно быть опущено для наилучшей переносимости.
Чтобы уточнить, где сервер будет искать файлы разделяемых библиотек, вернитесь к разделу
Аргументы составного типа
Составные типы не имеют фиксированной компоновки, как структуры C. Экземпляры составного типа могут содержать нулевые поля. Кроме того, в контексте наследования составные типы могут иметь разные поля для разных членов в одной иерархии наследования. Поэтому PostgreSQL предоставляет интерфейс функций для доступа к полям составных типов из C.
Предположим, что пользователь хочет написать функцию для ответа на запрос:
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
Используя соглашения о вызовах версии 1, можно определить c_overpaid
как:
#include "postgres.h"
#include "executor/executor.h" /* for 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);
/* Alternatively, we might prefer to do PG_RETURN_NULL() for null salary. */
PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}
GetAttributeByName
– это функция системы PostgreSQL, которая возвращает атрибуты из указанной строки. У нее есть три аргумента: аргумент типа HeapTupleHeader
, переданный в функцию, имя желаемого атрибута и параметр возврата, который сообщает, является ли атрибут нулевым. GetAttributeByName
возвращает значение Datum
, которое можно преобразовать в соответствующий тип данных с помощью соответствующего DatumGet
XXX
()
. Обратите внимание, что возвращаемое значение бессмысленно, если установлен флаг null; всегда проверяйте флаг null, прежде чем пытаться что-то сделать с результатом.
Существует также GetAttributeByNum
, который выбирает целевой атрибут по номеру столбца вместо имени.
Следующая команда объявляет функцию c_overpaid
в SQL:
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY/funcs', 'c_overpaid'
LANGUAGE C STRICT;
Обратите внимание, что был использован STRICT
, чтобы не пришлось проверять, являются ли входные аргументы NULL.
Возвращение строк (составные типы)
Чтобы вернуть строку или значение составного типа из функции на языке C, можно использовать специальный API, который предоставляет макросы и функции для скрытия большей части сложности создания составных типов данных. Чтобы использовать этот API, файл исходного кода должен включать:
#include "funcapi.h"
Существует два способа создания составного значения данных (в дальнейшем именуемого «кортежем»): можно создать его из массива значений Datum или из массива строк C, которые могут быть переданы функциям преобразования ввода типов данных столбцов кортежа. В любом случае сначала нужно получить или построить TupleDesc
дескриптор для структуры кортежа. При работе с данными Datum передается TupleDesc
в BlessTupleDesc
, а затем вызываете heap_form_tuple
для каждой строки. При работе со строками C передается 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
на основе идентификатора объекта типа. Это можно использовать для получения TupleDesc
для базового или составного типа. Однако это не будет работать для функции, которая возвращает record
, и она не может разрешать полиморфные типы.
Получив TupleDesc
, вызовите:
TupleDesc BlessTupleDesc(TupleDesc tupdesc)
если планируется работать со структурами Datum, или:
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
Если планируется работать со строками C. Если писать функцию, возвращающую набор, можно сохранить результаты этих функций в структуре FuncCallContext
- используйте поле tuple_desc
или attinmeta
соответственно.
При работе со структурами Datum, воспользуйтесь функцией:
HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
Она формирует HeapTuple
из переданных ей данных в форме Datum.
При работе со строками C используйте:
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
Для построения HeapTuple
заданных пользователем в форме строки C. values
является массивом строк C, одной для каждого атрибута возвращаемой строки. Каждая строка C должна быть в формате, ожидаемом функцией ввода типа данных атрибута. Чтобы вернуть нулевое значение для одного из атрибутов, соответствующий указатель в массиве values
должен быть установлен на NULL
. Эту функцию необходимо будет вызывать снова для каждой строки, которую возвращаете.
После того, как построили кортеж для возврата из функции, его необходимо преобразовать в Datum
. Используйте:
HeapTupleGetDatum(HeapTuple tuple)
Для того, чтобы преобразовать HeapTuple
в допустимые данные. Этот Datum
может быть возвращен напрямую, если вернуть всего одну строку, или он может использоваться в качестве текущего возвращаемого значения в функции, возвращающей набор.
Пример приводится в следующем разделе.
Возвращение наборов
Функции на языке C имеют два варианта для возвращения наборов (нескольких строк). В одном методе, называемом ValuePerCall режим, функция, возвращающая набор, вызывается повторно (передавая одни и те же аргументы каждый раз), и она возвращает одну новую строку при каждом вызове до тех пор, пока у нее не останется больше строк для возврата, и сигнализирует об этом, возвращая NULL. Функция, возвращающая набор (SRF), должна поэтому сохранять достаточно состояния между вызовами, чтобы помнить, что она делала, и возвращать правильный следующий элемент при каждом вызове. В другом методе, который называется Materialize режим, SRF заполняет и возвращает объект tuplestore, содержащий его полный результат; затем происходит только один вызов для всего результата, и межвызовное состояние не требуется.
При использовании режима ValuePerCall важно помнить, что запрос не гарантируется выполненным до завершения; то есть из-за таких опций, как LIMIT
, исполнитель может прекратить вызывать функцию, возвращающую набор, прежде чем будут извлечены все строки. Это означает, что небезопасно выполнять операции очистки во время последнего вызова, потому что это может никогда не произойти. Рекомендуется использовать режим Materialise для функций, которым необходим доступ к внешним ресурсам, таким как файловые дескрипторы.
Остальная часть этого раздела документирует набор вспомогательных макросов, которые обычно используются (хотя их использование не является обязательным) для SRF с использованием режима ValuePerCall. Дополнительные сведения о режиме Materialise можно найти в src/backend/utils/fmgr/README
. Кроме того, модули contrib
в дистрибутиве исходного кода PostgreSQL содержат множество примеров SRF, использующих как режим ValuePerCall, так и режим Materialise.
Чтобы использовать макросы поддержки ValuePerCall, описанные здесь, включите funcapi.h
. Эти макросы работают со структурой FuncCallContext
, которая содержит состояние, которое необходимо сохранить между вызовами. В вызывающем SRF используется fcinfo->flinfo->fn_extra
, чтобы удерживать указатель на FuncCallContext
между вызовами. Макросы автоматически заполняют это поле при первом использовании и ожидают найти там же один и тот же указатель при последующих использованиях.
typedef struct FuncCallContext
{
/*
* Number of times we've been called before
*
* call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and
* incremented for you every time SRF_RETURN_NEXT() is called.
*/
uint64 call_cntr;
/*
* OPTIONAL maximum number of calls
*
* max_calls is here for convenience only and setting it is optional.
* If not set, you must provide alternative means to know when the
* function is done.
*/
uint64 max_calls;
/*
* OPTIONAL pointer to miscellaneous user-provided context information
*
* user_fctx is for use as a pointer to your own data to retain
* arbitrary context information between calls of your function.
*/
void *user_fctx;
/*
* OPTIONAL pointer to struct containing attribute type input metadata
*
* attinmeta is for use when returning tuples (i.e., composite data types)
* and is not used when returning base data types. It is only needed
* if you intend to use BuildTupleFromCStrings() to create the return
* tuple.
*/
AttInMetadata *attinmeta;
/*
* memory context used for structures that must live for multiple calls
*
* multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used
* by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory
* context for any memory that is to be reused across multiple calls
* of the SRF.
*/
MemoryContext multi_call_memory_ctx;
/*
* OPTIONAL pointer to struct containing tuple description
*
* tuple_desc is for use when returning tuples (i.e., composite data types)
* and is only needed if you are going to build the tuples with
* heap_form_tuple() rather than with BuildTupleFromCStrings(). Note that
* the TupleDesc pointer stored here should usually have been run through
* BlessTupleDesc() first.
*/
TupleDesc tuple_desc;
} FuncCallContext;
Макросы, которые должны использоваться 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
; он все равно исчезнет. Однако, если нужно выделить какие-либо структуры данных, которые будут существовать между вызовами, то необходимо поместить их куда-то еще. Контекст памяти, на который ссылается multi_call_memory_ctx
, подходит для любых данных, которые должны сохраняться до завершения работы SRF. В большинстве случаев это означает, что во время первой настройки вызова следует переключиться на 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;
further declarations as needed
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* One-time setup code appears here: */
user code
if returning composite
build TupleDesc, and perhaps AttInMetadata
endif returning composite
user code
MemoryContextSwitchTo(oldcontext);
}
/* Each-time setup code appears here: */
user code
funcctx = SRF_PERCALL_SETUP();
user code
/* this is just one way we might test whether we are done: */
if (funcctx->call_cntr < funcctx->max_calls)
{
/* Here we want to return another item: */
user code
obtain result Datum
SRF_RETURN_NEXT(funcctx, result);
}
else
{
/* Here we are done returning items, so just report that fact. */
/* (Resist the temptation to put cleanup code here.) */
SRF_RETURN_DONE(funcctx);
}
}
Полный пример простой функции SRF, возвращающей составной тип:
PG_FUNCTION_INFO_V1(retcomposite);
Datum
retcomposite(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
int call_cntr;
int max_calls;
TupleDesc tupdesc;
AttInMetadata *attinmeta;
/* stuff done only on the first call of the function */
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
/* create a function context for cross-call persistence */
funcctx = SRF_FIRSTCALL_INIT();
/* switch to memory context appropriate for multiple function calls */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* total number of tuples to be returned */
funcctx->max_calls = PG_GETARG_INT32(0);
/* Build a tuple descriptor for our result type */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));
/*
* generate attribute metadata needed later to produce tuples from raw
* C strings
*/
attinmeta = TupleDescGetAttInMetadata(tupdesc);
funcctx->attinmeta = attinmeta;
MemoryContextSwitchTo(oldcontext);
}
/* stuff done on every call of the function */
funcctx = SRF_PERCALL_SETUP();
call_cntr = funcctx->call_cntr;
max_calls = funcctx->max_calls;
attinmeta = funcctx->attinmeta;
if (call_cntr < max_calls) /* do when there is more left to send */
{
char **values;
HeapTuple tuple;
Datum result;
/*
* Prepare a values array for building the returned tuple.
* This should be an array of C strings which will
* be processed later by the type input functions.
*/
values = (char **) palloc(3 * sizeof(char *));
values[0] = (char *) palloc(16 * sizeof(char));
values[1] = (char *) palloc(16 * sizeof(char));
values[2] = (char *) palloc(16 * sizeof(char));
snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1));
snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1));
snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1));
/* build a tuple */
tuple = BuildTupleFromCStrings(attinmeta, values);
/* make the tuple into a datum */
result = HeapTupleGetDatum(tuple);
/* clean up (this is not really necessary) */
pfree(values[0]);
pfree(values[1]);
pfree(values[2]);
pfree(values);
SRF_RETURN_NEXT(funcctx, result);
}
else /* do when there is no more left */
{
SRF_RETURN_DONE(funcctx);
}
}
Один из способов объявления этой функции в SQL:
CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);
CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
RETURNS SETOF __retcomposite
AS 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
Другой способ заключается в использовании выходных параметров:
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;
Обратите внимание, что при таком подходе выходным типом функции формально является анонимный тип record
.
Полиморфные типы аргументов и возвращаемых значений
Функции на языке C могут быть объявлены для приема и возврата полиморфных типов, описанных в разделе «Полиморфные типы». Когда аргументы функции или типы возвращаемых значений определены как полиморфные типы, автор функции не может заранее знать, с каким типом данных она будет вызвана, или какой тип ей нужно вернуть. В fmgr.h
предусмотрены две процедуры, позволяющие версии 1 функции на языке C определить фактические типы данных ее аргументов и тип, который ожидается возвратить. Эти процедуры называются get_fn_expr_rettype(FmgrInfo *flinfo)
и get_fn_expr_argtype(FmgrInfo *flinfo, int argnum)
. Они возвращают идентификатор типа результата или аргумента, или InvalidOid
, если информация недоступна. Структура flinfo
обычно доступна как fcinfo->flinfo
. Параметр argnum
имеет базовый ноль. get_call_result_type
также можно использовать вместо get_fn_expr_rettype
. Существует также 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");
/* get the provided element, being careful in case it's NULL */
isnull = PG_ARGISNULL(0);
if (isnull)
element = (Datum) 0;
else
element = PG_GETARG_DATUM(0);
/* we have one dimension */
ndims = 1;
/* and one element */
dims[0] = 1;
/* and lower bound is 1 */
lbs[0] = 1;
/* get required info about the element type */
get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
/* now build the array */
result = construct_md_array(&element, &isnull, ndims, dims, lbs,
element_type, typlen, typbyval, typalign);
PG_RETURN_ARRAYTYPE_P(result);
}
Следующая команда объявляет функцию make_array
в SQL:
CREATE FUNCTION make_array(anyelement) RETURNS anyarray
AS 'DIRECTORY/funcs', 'make_array'
LANGUAGE C IMMUTABLE;
Существует вариант полиморфизма, который доступен только для функций на языке C: они могут быть объявлены с параметрами типа "any"
. (Обратите внимание, что это имя типа должно быть заключено в кавычки, поскольку оно также является зарезервированным словом SQL.) Это работает так же, как anyelement
, за исключением того, что он не ограничивает различные аргументы "any"
быть одного и того же типа, ни помогают определить тип результата функции. Функция на языке C также может объявить свой последний параметр как VARIADIC "any"
. Это будет соответствовать одному или нескольким фактическим аргументам любого типа (не обязательно одного и того же типа). Эти аргументы будут не собраны в массив, как это происходит с обычными функциями переменного числа аргументов; они просто передаются функции отдельно. Макрос PG_NARGS()
и методы, описанные выше, должны использоваться для определения количества фактических аргументов и их типов при использовании этой функции. Кроме того, пользователи такой функции могут пожелать использовать ключевое слово VARIADIC
в своем вызове функции, ожидая, что функция будет рассматривать элементы массива как отдельные аргументы. Сама функция должна реализовать такое поведение, если оно желательно, после использования get_fn_expr_variadic
для обнаружения того, что фактический аргумент был помечен как VARIADIC
.
Общая память
Запрос общей памяти при запуске сервера
Расширения могут резервировать общую память при запуске сервера. Для этого общая библиотека расширения должна загружаться предварительно путем указания ее в параметре конфигурации shared_preload_libraries. Общие библиотеки также должны зарегистрировать shmem_request_hook
в своей функции _PG_init
. Эта процедура shmem_request_hook
может резервировать общую память, вызывая следующий код:
void RequestAddinShmemSpace(Size size)
Каждый серверный процесс должен получить указатель на зарезервированную область общей памяти, вызвав следующую команду:
void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)
Если эта функция устанавливает значение foundPtr
равным false
, вызывающий объект должен инициализировать содержимое зарезервированной области общей памяти. Если foundPtr
установлено равным true
, это означает, что другая часть уже инициализировала общую память, и вызывающая сторона не обязана выполнять дальнейшую инициализацию.
Чтобы избежать условий гонки, каждый серверный процесс должен использовать блокировку LWLock AddinShmemInitLock
при инициализации своего участка общей памяти, как показано здесь:
static mystruct *ptr = NULL;
bool found;
LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
... initialize contents of shared memory ...
ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);
Процедура shmem_startup_hook
предоставляет удобное место для кода инициализации, но нет строгой необходимости размещать весь подобный код именно в этом хуке. Каждый серверный процесс выполнит зарегистрированный shmem_startup_hook
вскоре после присоединения к общей памяти. Обратите внимание, что расширения по-прежнему должны получать блокировку AddinShmemInitLock
внутри данного хука, как показано в приведенном выше примере.
Пример реализации shmem_request_hook
и shmem_startup_hook
можно найти в файле contrib/pg_stat_statements/pg_stat_statements.c
дерева исходников PostgreSQL.
Запрос общей памяти после запуска сервера
Есть другой, более гибкий способ резервирования общей памяти, который можно выполнить после запуска сервера и вне контекста shmem_request_hook
. Чтобы сделать это, каждый серверный процесс, использующий общую память, должен получить указатель на нее, вызвав следующую команду:
void *GetNamedDSMSegment(const char *name, size_t size,
void (*init_callback) (void *ptr),
bool *found)
Если динамический сегмент общей памяти с указанным именем еще не создан, данная функция создаст его и инициализирует с помощью предоставленной вами функции обратного вызова init_callback
. Если сегмент уже был выделен и инициализирован другим серверным процессом, эта функция просто присоединяет существующий динамический сегмент общей памяти к текущему серверному процессу.
В отличие от общей памяти, зарезервированной при запуске сервера, нет необходимости захватывать блокировку AddinShmemInitLock
или предпринимать другие действия для предотвращения условий гонки при резервировании общей памяти с использованием GetNamedDSMSegment
. Данная функция гарантирует, что только одна часть выделяет и инициализирует сегмент, а все остальные получают указатель на полностью выделенный и инициализированный сегмент.
Полностью рабочий пример использования GetNamedDSMSegment
можно найти в файле src/test/modules/test_dsm_registry/test_dsm_registry.c
дерева исходников PostgreSQL.
Блокировки LWLock
Запрос блокировок LWLock при запуске сервера
Расширения могут резервировать блокировки LWLock при запуске сервера. Как и в случае с общей памятью, зарезервированной при запуске сервера, общие библиотеки расширений должны загружаться предварительно путем указания их в параметре конфигурации shared_preload_libraries, и общие библиотеки должны регистрировать процедуру shmem_request_hook
в своей функции _PG_init
. Процедура shmem_request_hook
может резервировать блокировки LWLock, вызывая следующий код:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
Это обеспечивает наличие под именем tranche_name
массива из num_lwlocks
блокировок LWLock. Указатель на этот массив можно получить, вызвав следующую команду:
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
Запрос блокировок LWLock после запуска сервера
Есть другой, более гибкий способ получения блокировок LWLock, который можно выполнить после запуска сервера и вне контекста shmem_request_hook
. Сначала создайте tranche_id
, вызвав следующую команду:
int LWLockNewTrancheId(void)
Далее инициализируйте каждый LWLock, передавая новый tranche_id
в качестве аргумента:
void LWLockInitialize(LWLock *lock, int tranche_id)
Аналогично разделяемой памяти, каждый бекенд должен обеспечить выделение нового tranche_id
только одним процессом и инициализировать каждый новый LWLock. Один из способов сделать это – вызывать эти функции только в коде инициализации разделяемой памяти при эксклюзивном удержании AddinShmemInitLock
. При использовании GetNamedDSMSegment
достаточно вызвать эти функции в функции обратного вызова init_callback
, чтобы избежать условий гонки.
Наконец, каждый бекенд, использующий tranche_id
, должен связать его с tranche_name
, вызвав:
void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
Полный пример использования LWLockNewTrancheId
, LWLockInitialize
и LWLockRegisterTranche
можно найти в файле contrib/pg_prewarm/autoprewarm.c
в дереве исходных текстов PostgreSQL.
Пользовательские события ожидания
Расширения могут определять пользовательские события ожидания под типом событий ожидания Extension
, вызывая:
uint32 WaitEventExtensionNew(const char *wait_event_name)
Событие ожидания связано со строкой, ориентированной на пользователя. Пример можно найти в файле src/test/modules/worker_spi
в дереве исходных текстов PostgreSQL.
Пользовательские события ожидания можно просмотреть в 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)
Точки инъекции
Точка инъекции с заданным именем name
объявляется с помощью макроса:
INJECTION_POINT(name);
Существует несколько уже объявленных точек инъекций в стратегических точках внутри серверного кода. После добавления новой точки инъекции код необходимо перекомпилировать, чтобы эта точка стала доступной в двоичном файле. Расширения, написанные на языке C, могут объявлять точки инъекций в своем собственном коде, используя тот же макрос. Имена точек инъекций должны использовать строчные буквы, а термины разделять дефисами.
Расширения могут присоединить обратные вызовы к уже объявленной точке инъекции, вызвав:
extern void InjectionPointAttach(const char *name,
const char *library,
const char *function,
const void *private_data,
int private_data_size);
name
--- имя точки инъекции, которая при достижении во время выполнения будет выполнять function
, загруженный из library
. private_data
--- это приватная область данных размером private_data_size
, переданная в обратном вызове в виде аргумента при выполнении.
Вот пример обратного вызова для InjectionPointCallback
:
static void
custom_injection_callback(const char *name, const void *private_data)
{
uint32 wait_event_info = WaitEventInjectionPointNew(name);
pgstat_report_wait_start(wait_event_info);
elog(NOTICE, "%s: executed custom callback", name);
pgstat_report_wait_end();
}
Этот обратный вызов выводит сообщение в журнал ошибок сервера с уровнем серьезности NOTICE
, но обратные вызовы могут реализовывать более сложную логику.
Необязательно, возможно отключить точку инъекции, вызвав:
extern bool InjectionPointDetach(const char *name);
В случае успеха возвращается true
, иначе false
.
Обратный вызов, прикрепленный к точке инъекции, доступен во всех бекендах, включая те, которые были запущены после вызова InjectionPointAttach
. Он остается прикрепленным до тех пор, пока сервер работает или пока точка инъекции не будет отключена с использованием InjectionPointDetach
.
Пример можно найти в файле src/test/modules/injection_points
в дереве исходных текстов PostgreSQL.
Для включения точек инъекций требуется --enable-injection-points
с configure
или -Dinjection_points=true
с помощью Meson.
Использование C++ для расширяемости
Хотя бэкенд PostgreSQL написан на языке C, можно писать расширения на C++, если следовать этим рекомендациям:
- Все функции, к которым обращается бэкенд, должны предоставлять интерфейс C для бэкенда; затем эти функции C могут вызывать функции C++. Например,
extern C
связь требуется для функций, доступных из бэкенда. Это также необходимо для любых функций, которые передаются между бэкендом и кодом C++ в виде указателей. - Освобождайте память с помощью соответствующего метода освобождения. Например, большая часть памяти бэкенда выделяется с использованием
palloc()
, поэтому используйтеpfree()
для ее освобождения. Использование C++delete
в таких случаях приведет к сбою. - Предотвращайте распространение исключений в коде C (используйте блок перехвата всех ошибок на верхнем уровне всех функций
extern C
). Это необходимо даже в том случае, если код C++ явно не генерирует никаких исключений, потому что такие события, как нехватка памяти, все равно могут вызвать исключения. Все исключения должны быть перехвачены и переданы соответствующие ошибки обратно интерфейсу C. Если возможно, компилируйте C++ с-fno-exceptions
для полного устранения исключений; в таких случаях нужно проверять наличие ошибок в своем коде C++, например, проверять возвращаемое значение NULL отnew()
. - Вызывая функции бэкенда из кода C++, убедитесь, что стек вызовов C++ содержит только простые старые структуры данных (POD). Это необходимо, поскольку ошибки бэкенда вызывают удаленное повреждение
longjmp()
, которое неправильно разворачивает стек вызовов C++ с неподовыми объектами.
В заключение, лучше всего разместить код C++ за стеной функций extern C
, которые взаимодействуют с бэкендом, и избегать утечек исключений, памяти и стека вызовов.