Пользовательские функции могут быть написаны на языке C (или на языке, совместимом с C, например, C++). Такие функции компилируются в динамически загружаемые объекты (также называемые разделяемыми библиотеками) и загружаются сервером по запросу. Функциональность динамической загрузки — это то, что отличает «язык C» функции от «внутренние» функций — фактические правила написания кода по существу одинаковы для обоих случаев. (Следовательно, стандартная библиотека внутренних функций является богатым источником примеров кода для пользовательских функций на языке C.)
В настоящее время для функций на языке C используется только одно соглашение о вызовах
(«версия 1»). Поддержка данного соглашения о вызовах обозначается путём написания PG_FUNCTION_INFO_V1() вызов макроса
для функции, как показано ниже.
При первом вызове в рамках сеанса пользовательской функции из конкретного загружаемого объектного файла динамический загрузчик загружает этот объектный файл в память для обеспечения возможности вызова функции. CREATE FUNCTION
Следовательно, для пользовательской функции на языке C необходимо указать два набора данных: имя загружаемого объектного файла и имя на языке C (символ связи) конкретной функции, вызываемой в этом объектном файле. Если имя на языке C не указано явно, оно считается идентичным имени SQL-функции.
Для поиска разделяемого объектного файла по имени, указанному в ключевом слове AS, используется следующий алгоритм: CREATE FUNCTION
команде, используется следующий алгоритм:
Если имя представляет собой абсолютный путь, загружается указанный файл.
Если имя начинается со строки $libdir,
эта часть заменяется на Digital Q.DataBase пакет
каталог библиотек
тип данных name, который определяется во время сборки.
Если тип данных name не содержит указания каталога, поиск файла выполняется по пути, заданному в переменной конфигурации dynamic_library_path.
В противном случае (если файл не найден по указанному пути или содержит относительный путь к каталогу), динамический загрузчик попытается использовать тип данных name в исходном виде, что, скорее всего, приведет к ошибке. (Поскольку полагаться на текущий рабочий каталог ненадежно).
Если эта последовательность действий не приведет к результату, расширение имен файлов разделяемых библиотек, специфичное для данной платформы (часто это .so), добавляется к указанному имени, и попытка повторяется. Если она также окажется неудачной, загрузка завершится ошибкой.
Рекомендуется определять расположение разделяемых библиотек либо относительно
$libdir либо через путь поиска динамических библиотек.
Это упрощает обновление версий, если новая установка расположена в
другом месте. Фактический каталог, которому
$libdir соответствует, можно узнать с помощью команды pg_config --pkglibdir.
Идентификатор пользователя, от имени которого Digital Q.DataBase учетная запись, от имени которой запущен сервер, должна иметь права на чтение всех каталогов в пути к загружаемому файлу. Отсутствие прав на чтение и/или выполнение файла или родительского каталога для postgres является распространенной ошибкой.
В любом случае имя файла, указанное в
CREATE FUNCTION команде, записывается в системные каталоги в исходном виде, поэтому при необходимости повторной загрузки файла выполняется та же процедура.
Digital Q.DataBase не выполняет компиляцию функции на языке C автоматически. Объектный файл должен быть скомпилирован до того, как на него появится ссылка
в CREATE
FUNCTION команды. См. Раздел 5.1.10.5 for additional
information.
Чтобы гарантировать, что динамически загружаемый объектный файл не будет загружен в несовместимый сервер, Digital Q.DataBase проверяет, содержит ли
файл «магический блок» с соответствующим содержимым.
Это позволяет серверу обнаруживать очевидные несовместимости, такие как код,
скомпилированный для другой основной версии
Digital Q.DataBase. Чтобы включить магический блок,
добавьте следующую строку в один (и только в один) из исходных файлов модуля после
включения заголовочного файла fmgr.h:
PG_MODULE_MAGIC;
После первого использования динамически загружаемый объектный файл сохраняется в памяти. Последующие вызовы функций из этого файла в рамках того же сеанса потребуют лишь незначительных затрат ресурсов на поиск в таблице символов. Если необходимо принудительно перезагрузить объектный файл, например, после его перекомпиляции, следует начать новый сеанс.
Динамически загружаемый файл может дополнительно содержать функцию инициализации. Если файл содержит функцию с именем
_PG_init, то эта функция будет вызвана сразу после загрузки файла. Данная функция не принимает параметров и должна возвращать тип данных void. В настоящее время возможность выгрузки динамически загруженного файла отсутствует.
Для написания функций на языке C необходимо понимать, как Digital Q.DataBase реализовано внутреннее представление базовых типов данных и каким образом они передаются в функции и возвращаются из них. На внутреннем уровне Digital Q.DataBase рассматривает базовый тип данных как «область памяти». Пользовательские функции, определенные для конкретного типа данных, в свою очередь определяют механизмы, с помощью которых Digital Q.DataBase может оперировать этим типом. То есть, Digital Q.DataBase будет лишь сохранять данные на диске и извлекать их, используя ваши пользовательские функции для ввода, обработки и вывода этих данных.
Базовые типы данных могут иметь один из трех форматов внутреннего представления:
передача по значению, фиксированная длина
передача по ссылке, фиксированная длина
передача по ссылке, переменная длина
Длина типов данных, передаваемых по значению, может составлять только 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
означает количество XXXX бит. Учитывайте
также, что тип данных C int8 имеет размер 1 байт. Тип данных
SQL int8 называется int64 на языке C. См. также
Таблица 5.1.2.)
С другой стороны, типы данных фиксированной длины любого размера могут передаваться по ссылке. Например, ниже приведена примерная реализация Digital Q.DataBase типа данных:
/* 16-байтовая структура, передаваемая по ссылке */
typedef struct
{
double x, y;
} Point;
При передаче таких типов данных в функции и из них могут использоваться только указатели на эти типы. Digital Q.DataBase функций.
Для возврата значения такого типа данных следует выделить необходимый объем
памяти с помощью функции palloc, заполнить выделенную память
и вернуть указатель на нее. (Кроме того, если требуется вернуть то же
значение, которое было передано в одном из входных аргументов того же типа данных,
можно пропустить дополнительный вызов palloc и просто вернуть указатель на входное значение.)
Наконец, все типы данных переменной длины также должны передаваться по ссылке. Все типы данных переменной длины должны начинаться с непрозрачного поля длины размером ровно 4 байта, значение которого устанавливается SET_VARSIZE; никогда не устанавливайте значение этого поля напрямую! Все данные, сохраняемые в значении такого типа, должны располагаться в памяти непосредственно за полем длины. Поле длины содержит общую длину структуры, то есть включает в себя размер самого поля длины.
Еще одним важным моментом является исключение неинициализированных битов в значениях типов данных; например, необходимо обнулять любые байты выравнивания, которые могут присутствовать в структурах structs. В противном случае логически эквивалентные константы вашего типа данных могут быть восприняты планировщиком как неравные, что приведет к построению неэффективных (хотя и корректных) планов выполнения.
Никогда не изменяйте содержимое входного значения, передаваемого по ссылке. Подобные действия, скорее всего, приведут к повреждению данных на диске, поскольку переданный указатель может ссылаться непосредственно на дисковый буфер. Единственное исключение из этого правила описано в Раздел 5.1.12.
В качестве примера можно определить тип данных text следующим образом:
typedef struct {
int32 length;
char data[FLEXIBLE_ARRAY_MEMBER];
} text;
Обозначение [FLEXIBLE_ARRAY_MEMBER] означает, что фактическая длина части данных не определена в данном объявлении.
При работе с типами данных переменной длины необходимо внимательно следить за выделением соответствующего объема памяти и корректно устанавливать поле длины. Например, если требуется сохранить 40 байт в структуре text
типа данных 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 макроса, а не обычным присваиванием.
Таблица 5.1.2 приведены типы данных языка C,
соответствующие многим встроенным типам данных SQL
в Digital Q.DataBase.
В столбце «Defined In» указан заголовочный файл, который
необходимо подключить для получения определения типа данных. (Фактическое определение может находиться в другом файле, который включен в указанный заголовочный файл. Пользователям рекомендуется придерживаться установленного интерфейса.) Обратите внимание, что всегда следует подключать
postgres.h данный файл должен быть первым в любом исходном файле серверного кода, поскольку он содержит ряд необходимых объявлений, а предварительное включение других заголовочных файлов может привести к проблемам с переносимостью.
Таблица 5.1.2. Соответствие типов данных языка 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 на языке C всегда имеет вид:
Datum funcname(PG_FUNCTION_ARGS)
Кроме того, в исходном коде должен присутствовать вызов макроса:
PG_FUNCTION_INFO_V1(funcname);
в том же самом файле. (По общепринятым правилам он указывается непосредственно перед определением самой функции.) Данный вызов макроса не требуется для внутренние-язычных функций, так как
Digital Q.DataBase предполагает, что все внутренние функции используют соглашение версии 1. Тем не менее, это необходимо для динамически загружаемых функций.
В функции версии 1 каждый фактический аргумент извлекается с помощью
PG_GETARG_
макроса, соответствующего типу данных аргумента. (В нестрогих функциях необходимо предварительно проверять аргумент на значение NULL, используя xxx()PG_ARGISNULL(); см. ниже).
Результат возвращается с помощью
PG_RETURN_
макрос для определения возвращаемого типа данных.
xxx()PG_GETARG_
принимает в качестве аргумента номер извлекаемого аргумента функции, при этом отсчет начинается с 0.
xxx()PG_RETURN_
принимает в качестве аргумента фактическое возвращаемое значение.
xxx()
Ниже приведены примеры использования соглашения о вызовах версии 1 (version-1):
#include "postgres.h" #include#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 представляет собой указатель на область данных новой структуры. Исходные * данные могут быть короткими (short datum), поэтому их следует получать через 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 и скомпилирован в разделяемый объектный файл;
в этом случае определить функции можно Digital Q.DataBase
с помощью следующих команд:
CREATE FUNCTION add_one(integer) RETURNS integer
AS 'DIRECTORY/funcs', 'add_one'
LANGUAGE C STRICT;
-- обратите внимание на перегрузку имени SQL-функции "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
AS 'DIRECTORY/funcs', 'add_one_float8'
LANGUAGE C STRICT;
CREATE FUNCTION makepoint(point, point) RETURNS point
AS 'DIRECTORY/funcs', 'makepoint'
LANGUAGE C STRICT;
CREATE FUNCTION copytext(text) RETURNS text
AS 'DIRECTORY/funcs', 'copytext'
LANGUAGE C STRICT;
CREATE FUNCTION concat_text(text, text) RETURNS text
AS 'DIRECTORY/funcs', 'concat_text'
LANGUAGE C STRICT;
Здесь DIRECTORY означает каталог, в котором расположен файл разделяемой библиотеки (например,
Digital Q.DataBase каталог tutorial, который
содержит исходный код примеров, используемых в данном разделе).
(С точки зрения стиля было бы лучше использовать просто 'funcs' в
ключевое слово AS предложении, после добавления
DIRECTORY в путь поиска. В любом случае, мы можем опустить зависящее от платформы расширение имени файла разделяемой библиотеки, обычно .so.)
Обратите внимание, что мы определили данные функции как «strict»,
что означает,
что система должна автоматически возвращать значение NULL, если любое входное
значение является значением NULL. Таким образом, мы избавляемся от необходимости проверять наличие значений NULL для входных аргументов в программном коде функции. В противном случае нам пришлось бы проверять наличие значений NULL явно, используя PG_ARGISNULL().
Макрос PG_ARGISNULL(
позволяет функции проверить, является ли каждый входной аргумент значением NULL. (Разумеется,
это необходимо только в тех функциях, которые не объявлены как «strict».)
Как и в случае с
n)PG_GETARG_ макросами,
входные аргументы нумеруются начиная с нуля. Обратите внимание, что следует воздерживаться от выполнения
xxx()PG_GETARG_ до тех пор, пока не будет подтверждено, что аргумент не является значением NULL. Для возврата значения NULL выполните xxx()PG_RETURN_NULL();
это применимо как для строгих (strict), так и для нестрогих функций.
На первый взгляд, соглашения о кодировании версии 1 могут показаться излишне сложными по сравнению с использованием обычных C соглашений о вызовах. Однако они позволяют обрабатывать значение NULLаргументы и возвращаемые значения, которые могут принимать значение NULL,
а также «toasted» (сжатые или вынесенные за пределы таблицы) значения.
Другими возможностями интерфейса версии 1 являются два варианта
PG_GETARG_
макросов. Первый из них,
xxx()PG_GETARG_,
гарантирует возврат копии указанного аргумента, которая
доступна для записи. (Обычные макросы иногда возвращают указатель на значение, которое физически хранится в таблице и не подлежит изменению. Использование
xxx_COPY()PG_GETARG_
макросы гарантируют получение результата, доступного для записи.)
Второй вариант состоит из
xxx_COPY()PG_GETARG_
макросы, принимающие три аргумента. Первым параметром является номер аргумента функции (как указано выше). Второй и третий параметры определяют смещение и длину возвращаемого сегмента. Смещение отсчитывается от нуля; отрицательное значение длины означает, что должен быть возвращен остаток значения. Данные макросы обеспечивают более эффективный доступ к частям больших значений в тех случаях, когда для них используется тип хранения
«external». (Тип хранения столбца можно указать с помощью команды
xxx_SLICE()ALTER TABLE . tablename ALTER
COLUMN colname SET STORAGE
storagetypestoragetype может принимать значения
plain, external, extended,
или main.)
Наконец, соглашения о вызове функций версии 1 позволяют возвращать наборы результатов (Раздел 5.1.10.8) и
реализовывать триггерные функции (Глава 5.2) и
обработчики вызовов на процедурных языках (Глава 7.6). Дополнительные сведения
приведены в файле src/backend/utils/fmgr/README в
дистрибутиве исходных кодов.
Прежде чем переходить к более сложным вопросам, необходимо рассмотреть некоторые правила написания кода для Digital Q.DataBase функций на языке C. Хотя загрузка функций, написанных на языках, отличных от C, в Digital Q.DataBase, может быть осуществима, обычно это затруднительно (если вообще возможно), так как другие языки, такие как C++, FORTRAN или Pascal, зачастую не следуют тем же соглашениям о вызовах, что и C. Иными словами, в других языках передача аргументов и возврат значений функций выполняются иначе. По этой причине предполагается, что ваши функции на языке C действительно написаны на языке C.
Основные правила написания и сборки функций на языке C заключаются в следующем:
Используйте pg_config
--includedir-server
чтобы определить, где Digital Q.DataBase заголовочные файлы сервера
установлены в вашей системе (или в системе, в которой
будут работать ваши пользователи).
Компиляция и компоновка кода для обеспечения возможности динамической загрузки в Digital Q.DataBase всегда требуют специальных флагов. См. Раздел 5.1.10.5 для получения подробных разъяснений о том, как это сделать для конкретной операционной системы.
Не забудьте определить «магический блок» для вашей разделяемой библиотеки, как описано в Раздел 5.1.10.1.
При выделении памяти следует использовать
Digital Q.DataBase функции
palloc и pfree
вместо аналогичных функций стандартной библиотеки C
malloc и free.
Память, выделенная функциями palloc будет
автоматически освобождаться в конце каждой транзакции, что предотвращает
утечки памяти.
Всегда обнуляйте байты структур с помощью функции memset
(или изначально выделяйте память под них с помощью функции palloc0 in the first place).
Даже если значения присвоены всем полям структуры, в ней могут присутствовать
заполнители для выравнивания (пустоты в структуре), содержащие
неопределённые значения. Без соблюдения этого условия затруднительно
обеспечить поддержку хеш-индексов или соединений методом хеширования, так как необходимо выбирать только
значимые биты структуры данных для вычисления хеша.
Планировщик также иногда полагается на сравнение констант путём
побитового равенства, поэтому возможны некорректные результаты планирования, если
логически эквивалентные значения не являются побитово равными.
Большинство внутренних Digital Q.DataBase
типов данных объявляются в postgres.h, тогда как
интерфейсы менеджера функций
(PG_FUNCTION_ARGS, и т. д.) находятся в
fmgr.h, поэтому потребуется подключить как
минимум эти два файла. Для обеспечения переносимости кода рекомендуется
включать postgres.h в первую очередь,
перед любыми другими системными или пользовательскими заголовочными файлами. Подключение
postgres.h также повлечет за собой включение
elog.h и palloc.h
для вас.
Имена символов, определенные в объектных файлах, не должны конфликтовать друг с другом или с символами, определенными в Digital Q.DataBase исполняемом файле сервера. Вам потребуется переименовать пользовательские функции или переменные, если вы получите соответствующие сообщения об ошибках.
Прежде чем появится возможность использовать функции расширения Digital Q.DataBase , написанные на языке C, их необходимо скомпилировать и скомпоновать особым образом для получения файла, поддерживающего динамическую загрузку сервером. Если говорить точнее, должна быть разделяемая библиотека создана разделяемая библиотека.
Для получения дополнительных сведений, выходящих за рамки данного раздела, рекомендуется обратиться к документации по операционной системе, в частности к справочным страницам компилятора C,
cc, и редактора компоновки, ld.
Кроме того, в Digital Q.DataBase исходном коде
содержится несколько рабочих примеров в каталоге
contrib directory. Использование данных примеров приведет к возникновению зависимости модулей от наличия Digital Q.DataBase исходного кода.
Создание разделяемых библиотек в целом аналогично компоновке исполняемых файлов: сначала исходные файлы компилируются в объектные файлы, после чего объектные файлы компонуются друг с другом. Объектные файлы необходимо создавать в виде позиционно-независимого кода (PIC), что концептуально означает возможность их размещения по произвольному адресу в памяти при загрузке исполняемым файлом. (Объектные файлы, предназначенные для исполняемых файлов, обычно компилируются иначе.) Команда компоновки разделяемой библиотеки содержит специальные флаги для ее отличия от компоновки исполняемого файла (по крайней мере, теоретически — в некоторых системах на практике этот процесс выглядит гораздо сложнее).
В следующих примерах предполагается, что исходный код содержится в файле foo.c на основе которого будет создана разделяемая библиотека
foo.so. Промежуточный объектный файл будет иметь имя foo.o если не указано иное. Разделяемая библиотека может содержать несколько объектных файлов, однако в данном случае используется только один.
Флаг компилятора для создания PIC —
-fPIC. Для создания разделяемых библиотек применяется флаг компилятора -shared.
cc -fPIC -c foo.c cc -shared -o foo.so foo.o
Данное положение применимо, начиная с версии 13.0
FreeBSD, в более старых версиях использовался
компилятор gcc компилятор.
Флаг компилятора для создания PIC —
-fPIC. Для создания разделяемой библиотеки используется флаг компилятора
-shared. Полный пример выглядит следующим образом:
cc -fPIC -c foo.c cc -shared -o foo.so foo.o
Ниже приведен пример. Предполагается, что инструменты разработчика установлены.
cc -c foo.c cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o
Флаг компилятора для создания PIC —
-fPIC. Для ELF -систем компилятор с флагом -shared используется для компоновки
разделяемых библиотек. В устаревших системах, не поддерживающих формат ELF, ld
-Bshareable используется.
gcc -fPIC -c foo.c gcc -shared -o foo.so foo.o
Флаг компилятора для создания PIC —
-fPIC. ld -Bshareable используется
для компоновки разделяемых библиотек.
gcc -fPIC -c foo.c ld -Bshareable -o foo.so foo.o
Флаг компилятора для создания 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, который скрывает платформенные различия за унифицированным интерфейсом.
Полученный файл разделяемой библиотеки впоследствии может быть загружен в
Digital Q.DataBase. При указании имени файла
в CREATE FUNCTION команде необходимо указывать имя файла разделяемой библиотеки, а не промежуточного объектного файла. Обратите внимание, что стандартное системное расширение разделяемых библиотек (обычно
.so или .sl) в
тексте команды можно опустить CREATE FUNCTION команды, в силу чего для обеспечения максимальной переносимости её обычно следует опускать.
Для получения сведений о том, в каком каталоге сервер ожидает найти файлы разделяемых библиотек, обратитесь к разделу Раздел 5.1.10.1 about where the server expects to find the shared library files.
Составные типы не имеют фиксированной структуры в памяти, в отличие от структур языка C. Экземпляры составного типа могут содержать поля, имеющие значение NULL. Кроме того, составные типы, входящие в иерархию наследования, могут содержать наборы полей, отличные от полей других элементов той же иерархии. Следовательно, Digital Q.DataBase предоставляет программный интерфейс для доступа к полям составных типов из языка C.
Предположим, требуется написать функцию для выполнения следующего запроса:
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
Используя соглашения о вызовах версии 1, можно определить
c_overpaid ключевое слово AS:
#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 — это
Digital Q.DataBase системная функция, возвращающая атрибуты из указанной строки. Она имеет три аргумента: аргумент типа HeapTupleHeader передаваемого в функцию, имени требуемого атрибута и возвращаемого параметра, определяющего, имеет ли данный атрибут значение NULL. GetAttributeByName возвращает Datum
значение, которое можно преобразовать в требуемый тип данных с помощью соответствующей DatumGet
функции. Обратите внимание, что возвращаемое значение не имеет смысла, если установлен флаг значения NULL; всегда проверяйте флаг значения NULL перед тем, как производить какие-либо действия с результатом.
XXX()
Также существует функция GetAttributeByNum, которая выбирает целевой атрибут по номеру столбца, а не через тип данных name.
Следующая команда объявляет функцию
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 может быть указано с помощью ключевого слова AS значение NULL или как адрес локальной переменной для получения значения типа данных Oid, соответствующего типу результата функции. resultTupleDesc должен представлять собой адрес локальной TupleDesc переменной. Убедитесь, что результат имеет значение TYPEFUNC_COMPOSITE; если это так,
resultTupleDesc был заполнен необходимым
TupleDesc. (Если это не так, вы можете вывести сообщение об ошибке вида «функция, возвращающая запись, вызвана в контексте, который не принимает тип данных record».)
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. При написании функции, возвращающей набор данных, результаты выполнения этих функций можно сохранить в
FuncCallContext структуре — используйте
tuple_desc или attinmeta поле
соответственно.
При работе с типом данных Datum используйте:
HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
для формирования тип данных HeapTuple типа данных HeapTuple на основе предоставленных пользовательских данных в формате Datum.
При работе со строками языка C используйте:
тип данных HeapTuple функция BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
для формирования тип данных HeapTuple заданные пользовательские данные в виде строк языка C. values представляет собой массив строк языка C, по одной для каждого атрибута возвращаемой строки. Каждая строка языка C должна иметь формат, ожидаемый функцией ввода соответствующего типа данных атрибута. Для возврата значения NULL для одного из атрибутов
соответствующий указатель в values массив
должен иметь значение значение NULL. Данную функцию потребуется вызывать повторно для каждой возвращаемой строки.
После формирования кортежа для возврата из функции его необходимо преобразовать в Datum. Используйте:
HeapTupleGetDatum(HeapTuple tuple)
для преобразования типа данных тип данных HeapTuple в корректное значение Datum. Это значение
Datum может быть возвращен напрямую, если планируется вернуть только одну строку, либо может использоваться в качестве текущего возвращаемого значения в функции, возвращающей набор данных.
Пример приведен в следующем разделе.
Функции на языке C поддерживают два способа возврата наборов данных (множества строк). Первый метод, называемый режимом ValuePerCall состоит в том, что функция, возвращающая набор данных, вызывается многократно (каждый раз с одними и теми же аргументами) и возвращает по одной новой строке при каждом вызове до тех пор, пока строки не закончатся, о чем она сигнализирует, возвращая значение NULL. Функция, возвращающая набор данных (SRF) должна, следовательно, сохранять достаточное состояние между вызовами, чтобы помнить о выполняемых действиях и возвращать верный следующий элемент при каждом вызове. В другом методе, называемом Materialize в этом режиме функция SRF заполняет и возвращает объект типа tuplestore, содержащий весь результат её работы; в этом случае для получения всего результата выполняется только один вызов, и сохранение состояния между вызовами не требуется.
При использовании режима ValuePerCall важно помнить, что выполнение запроса до завершения не гарантируется; то есть из-за таких
параметров, как LIMIT, исполнитель может прекратить
вызовы функции, возвращающей набор данных, до того как будут выбраны
все строки. Это означает, что выполнять действия по очистке ресурсов при последнем вызове небезопасно, так как он может никогда не произойти. Для функций, которым требуется доступ к внешним ресурсам, таким как дескрипторы файлов, рекомендуется использовать режим Materialize.
В оставшейся части этого раздела описывается набор вспомогательных макросов, которые обычно используются (хотя и не являются обязательными) для функций SRF, работающих в режиме ValuePerCall. Дополнительные сведения о режиме Materialize можно найти в src/backend/utils/fmgr/README. Кроме того, contrib модули, Digital Q.DataBase составе исходного кода содержат множество примеров функций SRF, использующих режимы ValuePerCall и Materialize.
Чтобы использовать описанные здесь вспомогательные макросы для режима ValuePerCall,
подключите файл funcapi.h. Данные макросы работают со структурой FuncCallContext , содержащей состояние, которое должно сохраняться между вызовами. Внутри вызывающей
функции SRF fcinfo->flinfo->fn_extra используется для
хранения указателя на структуру FuncCallContext между
вызовами. Макросы автоматически заполняют это поле при первом использовании и ожидают найти тот же указатель при последующих вызовах.
typedef struct FuncCallContext
{
/*
* Количество предыдущих вызовов
*
* Поле call_cntr инициализируется значением 0 макросом SRF_FIRSTCALL_INIT() и
* инкрементируется при каждом вызове макроса 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() для очистки ресурсов. Это наиболее подходящий контекст памяти MemoryContext
* для любых данных, которые должны повторно использоваться при многократных вызовах
* функции 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.
Контекст памяти MemoryContext, являющийся текущим в момент, SRF когда вызывается функция, представляет собой временный контекст, который очищается между вызовами. Это означает,
что вам не требуется вызывать функцию pfree для всех объектов,
выделенных с помощью palloc; они все равно будут удалены. Однако если требуется выделить какие-либо структуры данных для сохранения между вызовами, их необходимо разместить в другом месте. Контекст памяти MemoryContext, на который ссылается
multi_call_memory_ctx , является подходящим местом для любых данных, которые должны сохраняться до тех пор, пока функция SRF не завершит работу. В большинстве
случаев это означает, что следует переключиться в контекст
multi_call_memory_ctx при выполнении
настройки первого вызова.
Используйте funcctx->user_fctx для хранения указателя на
любые подобные структуры данных, используемые между вызовами.
(Данные, выделяемые
в multi_call_memory_ctx будут удалены автоматически по завершении запроса, поэтому освобождать их вручную также не требуется.)
Хотя фактические аргументы функции остаются неизменными между вызовами, при выполнении распаковки (detoast) значений аргументов (которая обычно выполняется прозрачно с помощью
PG_GETARG_ макрос) в транзитном контексте, то копии, полученные после распаковки (detoasting), будут освобождаться на каждом цикле. Соответственно, если вы сохраняете ссылки на такие значения в вашем xxxuser_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_INT32(0);
/* создание дескриптора кортежа для возвращаемого типа данных */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("функция, возвращающая тип record, вызвана в контексте "
"который не может принимать тип record")));
/*
* создание метаданных атрибутов, необходимых в дальнейшем для формирования кортежей из
* строк языка C
*/
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, которые
* впоследствии будут обработаны входными функциями типов данных.
*/
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 'filename', '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 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
Обратите внимание, что при таком подходе типом выходных данных функции формально является анонимный record типом данных.
Функции на языке C могут быть объявлены для приема и возврата полиморфных типов, описанных в Раздел 5.1.2.5. Когда аргументы или возвращаемый тип функции определены как полиморфные, разработчик функции не может заранее знать, с каким типом данных она будет вызвана или какой тип должна вернуть. В составе предусмотрены две процедуры fmgr.h
позволяющие функции на языке C версии 1 определить фактические типы данных своих аргументов и тип, который она должна вернуть. Данные процедуры
называются get_fn_expr_rettype(FmgrInfo *flinfo) и
get_fn_expr_argtype(FmgrInfo *flinfo, int argnum).
Они возвращают тип данных Oid для результата или аргумента либо значение InvalidOid в случае отсутствия необходимой информации. Доступ к структуре flinfo обычно осуществляется как
fcinfo->flinfo. Параметр argnum
отсчитывается от нуля. get_call_result_type также может быть использовано
в качестве альтернативы get_fn_expr_rettype.
Также существует get_fn_expr_variadic, которая может быть использована для
проверки того, были ли аргументы 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, "не удалось определить тип данных входного значения");
/* получаем переданный элемент, учитывая возможность того, что это значение 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 'DIRECTORY/funcs', 'make_array'
LANGUAGE C IMMUTABLE;
Существует разновидность полиморфизма, доступная только для функций на языке C: они могут быть объявлены с параметрами типа данных
"any". (Обратите внимание, что имя этого типа данных должно быть заключено в двойные кавычки, так как оно также является зарезервированным словом SQL.) Это работает аналогично
тип anyelement за исключением того, что он не требует, чтобы различные
"any" аргументы относились к одному и тому же типу, и не влияет на определение типа результата функции. Функция на языке C также может объявить свой последний параметр как VARIADIC "any". Это обеспечит соответствие одному или нескольким фактическим аргументам любого типа (не обязательно одного и того же). Эти аргументы не будут собраны в массив, как это происходит в обычных функциях с ключевым словом VARIADIC; они будут переданы в функцию по отдельности. 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 в Digital Q.DataBase дереве исходных кодов.
Существует другой, более гибкий способ резервирования разделяемой памяти, который можно использовать после запуска сервера и вне
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
в Digital Q.DataBase дереве исходных кодов.
Дополнения могут резервировать блокировки LWLock при запуске сервера. Как и в случае с разделяемой памятью,
резервируемой при запуске сервера, разделяемая библиотека расширения должна быть предварительно загружена
путем её указания в
shared_preload_libraries,
а сама разделяемая библиотека должна зарегистрировать
shmem_request_hook в своей
_PG_init функции. Данная
shmem_request_hook может зарезервировать блокировки LWLock с помощью вызова:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
Это гарантирует, что массив из num_lwlocks блокировок LWLock будет доступен под именем tranche_name. Указатель на этот массив можно получить, вызвав функцию GetNamedLWLockTranche:
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
Существует другой, более гибкий метод получения блокировок 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 в
Digital Q.DataBase дереве исходных кодов.
Дополнения могут определять пользовательские события ожидания для типа событий ожидания
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 строка)
Точка внедрения с заданным именем 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 представляет собой имя точки внедрения, при достижении которой в процессе выполнения будет вызвана функция функция
загруженная из 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 в команде
конфигурирование или -Dinjection_points=true
с помощью Meson.
Несмотря на то, что Digital Q.DataBase серверная часть написана на языке C, расширения можно писать на языке C++ при соблюдении следующих рекомендаций:
Все функции, к которым обращается серверная часть, должны предоставлять интерфейс на языке C
для серверной части; в этом случае функции на языке C могут вызывать функции на языке C++.
Например, extern "C" спецификатор компоновки (linkage) требуется для
функций, вызываемых серверной частью. Это также необходимо для любых
функций, передаваемых в виде указателей между серверной частью и
кодом на языке C++.
Освобождайте память с помощью подходящего метода деаллокации. Например,
большая часть памяти серверного процесса выделяется с помощью функции palloc(), поэтому для её освобождения следует использовать функцию
pfree() . Применение оператора C++
delete в подобных случаях приведет к сбою.
Не допускайте выхода исключений в код на языке C (используйте универсальный блок
перехвата на верхнем уровне всех extern "C" функций). Это
необходимо даже в том случае, если код на языке C++ явно не генерирует
исключений, поскольку такие события, как нехватка памяти, всё равно могут вызывать
исключения. Все исключения должны быть перехвачены, а соответствующие ошибки —
переданы обратно в интерфейс на языке C. По возможности компилируйте код на языке C++ с параметром
-fno-exceptions полностью исключить возникновение исключений; в таких
случаях необходимо предусматривать обработку ошибок в коде на языке C++, например, проверять
значение NULL, возвращаемое оператором new().
При вызове функций серверной части из кода на языке C++ необходимо убедиться, что
стек вызовов C++ содержит только простые структуры данных
(POD). Это необходимо, поскольку ошибки серверной части
выполняют отдаленный переход longjmp() который не обеспечивает корректное
развертывание стека вызовов C++ при наличии объектов типов, отличных от POD.
В целом, рекомендуется изолировать код на языке C++ слоем
extern "C" функций интерфейса взаимодействия с серверной частью,
не допуская выхода исключений за пределы этого слоя и утечек памяти или стека вызовов.