用户定义函数可以用 C 语言(或与 C 兼容的语言,例如 C++)编写。此类函数被编译成动态可加载对象(也称为共享库),并由服务器按需加载。动态加载功能是将“C 语言”函数与“内部”函数区分开来的原因 — 实际的编码约定对于两者来说基本上是相同的。(因此,标准内部函数库是用户定义 C 函数的丰富编码示例来源。)
目前,C 函数只使用一种调用约定(“版本 1”)。对该调用约定的支持通过为函数编写 PG_FUNCTION_INFO_V1()
宏调用来指示,如下所示。
会话中首次调用特定可加载对象文件中的用户定义函数时,动态加载器将该对象文件加载到内存中,以便可以调用该函数。因此,用户定义 C 函数的 CREATE FUNCTION
必须为该函数指定两部分信息:可加载对象文件的名称,以及该对象文件中要调用的特定函数的 C 名称(链接符号)。如果未明确指定 C 名称,则假定它与 SQL 函数名称相同。
根据 CREATE FUNCTION
命令中给出的名称来定位共享对象文件使用以下算法
如果名称是绝对路径,则加载给定文件。
如果名称不包含目录部分,则在配置变量 dynamic_library_path 指定的路径中搜索该文件。
否则(在路径中找不到文件,或者它包含非绝对目录部分),动态加载器将尝试按给定名称进行操作,这很可能会失败。(依赖当前工作目录是不可靠的。)
如果此序列不起作用,则会将平台特定的共享库文件扩展名(通常为 .so
)附加到给定名称,并再次尝试此序列。如果也失败,则加载将失败。
建议将共享库相对于 $libdir
或通过动态库路径定位。如果新安装在不同的位置,这会简化版本升级。$libdir
所代表的实际目录可以通过命令 pg_config --pkglibdir
查明。
PostgreSQL 服务器运行的用户 ID 必须能够遍历到您打算加载的文件路径。使文件或更高级别目录对 postgres 用户不可读和/或不可执行是一个常见的错误。
无论如何,CREATE FUNCTION
命令中给出的文件名将原封不动地记录在系统目录中,因此如果需要再次加载文件,将应用相同的过程。
PostgreSQL 不会自动编译 C 函数。对象文件必须在 CREATE FUNCTION
命令中引用之前进行编译。有关更多信息,请参阅 第 36.10.5 节。
为确保动态加载的对象文件不会加载到不兼容的服务器中,PostgreSQL 会检查该文件是否包含具有适当内容的“魔术块”。这允许服务器检测明显的兼容性问题,例如为不同主要版本的 PostgreSQL 编译的代码。要包含魔术块,请在模块的其中一个(且仅一个)源文件中写入以下内容,在包含头文件 fmgr.h
之后
PG_MODULE_MAGIC;
或
PG_MODULE_MAGIC_EXT(parameters
);
PG_MODULE_MAGIC_EXT
变体允许指定有关模块的附加信息;目前,可以添加名称和/或版本字符串。(将来可能会允许更多字段。)编写如下所示
PG_MODULE_MAGIC_EXT( .name = "my_module_name", .version = "1.2.3" );
随后可以通过 pg_get_loaded_modules()
函数检查名称和版本。PostgreSQL 不限制版本字符串的含义,但建议使用语义版本控制规则。
首次使用后,动态加载的对象文件将保留在内存中。在同一会话中对该文件中函数进行后续调用只会产生符号表查找的小开销。如果您需要强制重新加载对象文件,例如在重新编译后,请启动一个新的会话。
或者,动态加载的文件可以包含一个初始化函数。如果文件包含名为 _PG_init
的函数,则该函数将在加载文件后立即调用。该函数不接收任何参数,并且应返回 void。目前无法卸载动态加载的文件。
要了解如何编写 C 语言函数,您需要了解 PostgreSQL 如何在内部表示基本数据类型以及它们如何传递给函数和从函数返回。在内部,PostgreSQL 将基本类型视为“内存块”。您在类型上定义的用户定义函数反过来定义了 PostgreSQL 如何对其进行操作。也就是说,PostgreSQL 只会从磁盘存储和检索数据,并使用您的用户定义函数来输入、处理和输出数据。
基本类型可以有三种内部格式
按值传递,固定长度
按引用传递,固定长度
按引用传递,变长
按值传递的类型长度只能是 1、2 或 4 字节(如果您的机器上 sizeof(Datum)
为 8,也可以是 8 字节)。您应该注意定义您的类型,使其在所有架构上都具有相同的大小(以字节为单位)。例如,long
类型是危险的,因为它在某些机器上是 4 字节,在其他机器上是 8 字节,而 int
类型在大多数 Unix 机器上是 4 字节。在 Unix 机器上 int4
类型的一个合理实现可能是
/* 4-byte integer, passed by value */ typedef int int4;
(实际的 PostgreSQL C 代码将此类型称为 int32
,因为在 C 语言中 int
表示 XX
XX
位。因此还要注意 C 类型 int8
的大小为 1 字节。SQL 类型 int8
在 C 语言中称为 int64
。另请参阅 表 36.2。)
另一方面,任意大小的固定长度类型可以通过引用传递。例如,这是一个 PostgreSQL 类型的一个示例实现
/* 16-byte structure, passed by reference */ typedef struct { double x, y; } Point;
在将此类类型传递进出 PostgreSQL 函数时,只能使用指向此类类型的指针。要返回此类类型的值,请使用 palloc
分配正确的内存量,填充分配的内存,并返回指向它的指针。(此外,如果您只想返回与相同数据类型的输入参数之一相同的值,则可以跳过额外的 palloc
,只返回指向输入值的指针。)
最后,所有变长类型也必须通过引用传递。所有变长类型必须以一个精确为 4 字节的不透明长度字段开头,该字段将由 SET_VARSIZE
设置;切勿直接设置此字段!存储在该类型内的所有数据必须位于该长度字段紧随其后的内存中。长度字段包含结构的总长度,也就是说,它包括长度字段本身的大小。
另一个重要点是避免在数据类型值中留下任何未初始化的位;例如,注意将结构中可能存在的任何对齐填充字节清零。否则,您的数据类型的逻辑等价常量可能会被规划器视为不相等,从而导致低效(但不错误)的计划。
切勿修改传引用输入值的内容。如果这样做,您很可能会损坏磁盘上的数据,因为您获得的指针可能直接指向磁盘缓冲区。此规则的唯一例外在 第 36.12 节 中解释。
例如,我们可以将 text
类型定义如下
typedef struct { int32 length; char data[FLEXIBLE_ARRAY_MEMBER]; } text;
[FLEXIBLE_ARRAY_MEMBER]
符号表示数据部分的实际长度未由此声明指定。
在操作变长类型时,我们必须小心分配正确的内存量并正确设置长度字段。例如,如果我们想在 text
结构中存储 40 字节,我们可能会使用如下代码片段
#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
宏设置,而不是通过简单的赋值。
表 36.2 显示了许多 PostgreSQL 内置 SQL 数据类型对应的 C 类型。“定义于”列给出了需要包含以获取类型定义的头文件。(实际定义可能在由列出的文件包含的其他文件中。建议用户坚持定义的接口。)请注意,在任何服务器代码源文件中,您应该始终首先包含 postgres.h
,因为它声明了您无论如何都会需要的许多东西,并且首先包含其他头文件可能会导致可移植性问题。
表 36.2. 内置 SQL 类型的等效 C 类型
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 |
名称 |
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);
必须出现在同一个源文件中。(按照惯例,它通常写在函数本身之前。)对于 internal
语言函数,不需要此宏调用,因为 PostgreSQL 假定所有内部函数都使用版本 1 约定。但是,对于动态加载的函数,它却是必需的。
在版本 1 函数中,每个实际参数都使用与参数数据类型对应的 PG_GETARG_
宏获取。(在非严格函数中,需要先使用 xxx
()PG_ARGISNULL()
检查参数是否为 NULL;请参见下文。)结果使用返回类型的 PG_RETURN_
宏返回。xxx
()PG_GETARG_
将要获取的函数参数编号作为其参数,计数从 0 开始。xxx
()PG_RETURN_
将要返回的实际值作为其参数。xxx
()
要调用另一个版本 1 函数,可以使用 DirectFunctionCall
。当您想调用标准内部库中定义的函数时,这特别有用,使用与它们的 SQL 签名类似的接口。n
(func, arg1, ..., argn)
这些便利函数和类似函数可以在 fmgr.h
中找到。DirectFunctionCall
系列将 C 函数名作为其第一个参数。还有 n
OidFunctionCall
接受目标函数的 OID,以及其他一些变体。所有这些都期望函数的参数作为 n
Datum
提供,并且它们也返回 Datum
。请注意,使用这些便利函数时,参数和结果都不能为 NULL。
例如,要从 C 调用 starts_with(text, text)
函数,您可以搜索目录并发现其 C 实现是 Datum text_starts_with(PG_FUNCTION_ARGS)
函数。通常,您会使用 DirectFunctionCall2(text_starts_with, ...)
来调用此类函数。但是,starts_with(text, text)
需要排序规则信息,因此如果以这种方式调用,它将失败并出现“无法确定用于字符串比较的排序规则”错误。相反,您必须使用 DirectFunctionCall2Coll(text_starts_with, ...)
并提供所需的排序规则,通常只是从 PG_GET_COLLATION()
传递,如下例所示。
fmgr.h
还提供了方便 C 类型和 Datum
之间转换的宏。例如,要将 Datum
转换为 text*
,可以使用 DatumGetTextPP(X)
。虽然某些类型有像 TypeGetDatum(X)
这样的宏用于反向转换,但 text*
没有;为此,使用通用宏 PointerGetDatum(X)
就足够了。如果您的扩展定义了其他类型,通常也方便为您的类型定义类似的宏。
以下是使用版本 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); } /* A wrapper around starts_with(text, text) */ PG_FUNCTION_INFO_V1(t_starts_with); Datum t_starts_with(PG_FUNCTION_ARGS) { text *t1 = PG_GETARG_TEXT_PP(0); text *t2 = PG_GETARG_TEXT_PP(1); Oid collid = PG_GET_COLLATION(); bool result; result = DatumGetBool(DirectFunctionCall2Coll(text_starts_with, collid, PointerGetDatum(t1), PointerGetDatum(t2))); PG_RETURN_BOOL(result); }
假设以上代码已在文件 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; CREATE FUNCTION t_starts_with(text, text) RETURNS boolean AS 'DIRECTORY
/funcs', 't_starts_with' LANGUAGE C STRICT;
这里,DIRECTORY
代表共享库文件的目录(例如 PostgreSQL 教程目录,其中包含本节中使用的示例代码)。(更好的风格是在 AS
子句中只使用 'funcs'
,在将 DIRECTORY
添加到搜索路径之后。无论如何,我们可以省略共享库的系统特定扩展名,通常是 .so
。)
请注意,我们已将函数指定为“严格”,这意味着如果任何输入值为 null,系统应自动假定结果为 null。通过这样做,我们避免了在函数代码中检查 null 输入。否则,我们必须使用 PG_ARGISNULL()
显式检查 null 值。
宏 PG_ARGISNULL(
允许函数测试每个输入是否为 null。(当然,这样做只在未声明为“严格”的函数中是必要的。)与 n
)PG_GETARG_
宏一样,输入参数从零开始计数。请注意,在验证参数不为 null 之前,应避免执行 xxx
()PG_GETARG_
。要返回 null 结果,请执行 xxx
()PG_RETURN_NULL()
;这在严格和非严格函数中都有效。
乍一看,版本 1 编码约定与使用普通的 C
调用约定相比,可能显得毫无意义的晦涩。然而,它们确实允许我们处理可为空的参数/返回值以及“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 storagetype
storagetype
是 plain
、external
、extended
或 main
之一。)
最后,版本 1 函数调用约定使得返回集合结果(第 36.10.9 节)并实现触发器函数(第 37 章)和过程语言调用处理程序(第 57 章)成为可能。有关更多详细信息,请参阅源代码分发中的 src/backend/utils/fmgr/README
。
在转向更高级的主题之前,我们应该讨论一些 PostgreSQL C 语言函数的编码规则。虽然可能可以将用 C 以外的语言编写的函数加载到 PostgreSQL 中,但这通常很困难(如果可能的话),因为其他语言,例如 C++、FORTRAN 或 Pascal,通常不遵循与 C 相同的调用约定。也就是说,其他语言在函数之间传递参数和返回值的方式不同。因此,我们将假设您的 C 语言函数实际上是用 C 编写的。
编写和构建 C 函数的基本规则如下
使用 pg_config --includedir-server
查找 PostgreSQL 服务器头文件安装在您的系统上(或您的用户将运行的系统上)的位置。
编译和链接您的代码,使其可以动态加载到 PostgreSQL 中,始终需要特殊标志。有关如何在您的特定操作系统上执行此操作的详细说明,请参阅 第 36.10.5 节。
请记住为您的共享库定义一个“魔术块”,如 第 36.10.1 节 所述。
分配内存时,请使用 PostgreSQL 函数 palloc
和 pfree
,而不是相应的 C 库函数 malloc
和 free
。palloc
分配的内存将在每个事务结束时自动释放,从而防止内存泄漏。
始终使用 memset
将您的结构字节清零(或者从一开始就使用 palloc0
分配它们)。即使您为结构的每个字段赋值,也可能存在包含垃圾值的对齐填充(结构中的空洞)。如果没有这一点,很难支持哈希索引或哈希连接,因为您必须只选择数据结构中重要的位来计算哈希。规划器有时也依赖于通过按位相等比较常量,因此如果逻辑等价的值不是按位相等,您可能会得到不理想的规划结果。
大多数内部 PostgreSQL 类型在 postgres.h
中声明,而函数管理器接口(PG_FUNCTION_ARGS
等)在 fmgr.h
中,因此您至少需要包含这两个文件。为了可移植性,最好首先包含 postgres.h
,然后再包含任何其他系统或用户头文件。包含 postgres.h
也会为您包含 elog.h
和 palloc.h
。
对象文件中定义的符号名称不得相互冲突,也不得与 PostgreSQL 服务器可执行文件中定义的符号冲突。如果出现此类错误消息,您将不得不重命名您的函数或变量。
在使用用 C 语言编写的 PostgreSQL 扩展函数之前,必须以特殊方式编译和链接它们,以生成可由服务器动态加载的文件。确切地说,需要创建一个共享库。
除了本节中包含的信息之外,您还应该阅读操作系统的文档,特别是 C 编译器 cc
和链接编辑器 ld
的手册页。此外,PostgreSQL 源代码的 contrib
目录中包含几个工作示例。但是,如果您依赖这些示例,您的模块将依赖于 PostgreSQL 源代码的可用性。
创建共享库通常类似于链接可执行文件:首先将源文件编译为对象文件,然后将对象文件链接在一起。对象文件需要创建为位置无关代码(PIC),这在概念上意味着当它们被可执行文件加载时,它们可以放置在内存中的任意位置。(用于可执行文件的对象文件通常不是这样编译的。)链接共享库的命令包含特殊标志,以将其与链接可执行文件区分开来(至少在理论上是这样——在某些系统上,实际情况要复杂得多)。
在以下示例中,我们假设您的源代码位于文件 foo.c
中,我们将创建一个共享库 foo.so
。除非另有说明,否则中间对象文件将命名为 foo.o
。共享库可以包含多个对象文件,但我们此处只使用一个。
用于创建PIC的编译器标志是 -fPIC
。用于创建共享库的编译器标志是 -shared
。
cc -fPIC -c foo.c cc -shared -o foo.so foo.o
这适用于 FreeBSD 13.0 版及更高版本,旧版本使用 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在 Sun 编译器上使用 -KPIC
,在 GCC 上使用 -fPIC
。要链接共享库,使用任一编译器的 -G
选项,或者在 GCC 上使用 -shared
。
cc -KPIC -c foo.c cc -G -o foo.so foo.o
或
gcc -fPIC -c foo.c gcc -G -o foo.so foo.o
如果这太复杂,您应该考虑使用 GNU Libtool,它将平台差异隐藏在统一的接口后面。
生成的共享库文件随后可以加载到 PostgreSQL 中。在向 CREATE FUNCTION
命令指定文件名时,必须提供共享库文件的名称,而不是中间对象文件的名称。请注意,系统标准共享库扩展名(通常是 .so
或 .sl
)可以从 CREATE FUNCTION
命令中省略,并且通常为了最佳可移植性应省略。
请参阅 第 36.10.1 节,了解服务器期望在哪里找到共享库文件。
本节包含关于 PostgreSQL 服务器中 API 和 ABI 稳定性的扩展和其他服务器插件作者指南。
PostgreSQL 服务器包含几个明确划分的服务器插件 API,例如函数管理器(fmgr,本章描述)、SPI(第 45 章)以及专为扩展设计的各种钩子。这些接口经过精心管理,以实现长期稳定性和兼容性。然而,服务器中的所有全局函数和变量实际上构成了公开可用的 API,其中大部分在设计时并未考虑可扩展性和长期稳定性。
因此,虽然利用这些接口是有效的,但越偏离成熟的路径,就越有可能在某个时候遇到 API 或 ABI 兼容性问题。鼓励扩展作者提供有关其要求的回馈,以便随着时间的推移,随着新使用模式的出现,某些接口可以被认为更稳定,或者可以添加新的、设计更好的接口。
该API,即应用程序编程接口,是在编译时使用的接口。
PostgreSQL 主要版本之间不保证 API 兼容性。因此,扩展代码可能需要修改源代码才能与多个主要版本一起工作。这些通常可以通过预处理器条件(例如 #if PG_VERSION_NUM >= 160000
)进行管理。使用超出明确划分接口的复杂扩展通常需要为每个主要服务器版本进行一些此类更改。
该ABI,或应用程序二进制接口,是运行时使用的接口。
不同主要版本的服务器故意具有不兼容的 ABI。因此,使用服务器 API 的扩展必须针对每个主要版本重新编译。PG_MODULE_MAGIC
的包含(参见 第 36.10.1 节)确保为某个主要版本编译的代码将被其他主要版本拒绝。
PostgreSQL 努力避免在次要版本中出现服务器 ABI 中断。通常,针对任何次要版本编译的扩展应该与同一主要版本的任何其他次要版本(过去或未来)一起工作。
当需要进行更改时,PostgreSQL 将选择侵入性最小的更改,例如将新字段挤入填充空间或将其附加到结构末尾。除非扩展使用非常不寻常的代码模式,否则这些类型的更改不应影响扩展。
然而,在极少数情况下,即使是这种非侵入性更改也可能不切实际或不可能。在这种情况下,将仔细管理更改,同时考虑到扩展的要求。此类更改也将在发行说明中记录(附录 E)。
但请注意,服务器的许多部分并未被设计或维护为可公开使用的 API(在大多数情况下,实际边界也未明确定义)。如果出现紧急需求,对这些部分的更改将自然而然地较少考虑扩展代码,而不是对明确定义和广泛使用的接口的更改。
此外,在没有此类更改的自动化检测的情况下,这不是一个保证,但历史上此类重大更改极为罕见。
复合类型没有像 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
()
还有 GetAttributeByNum
,它通过列号而不是名称选择目标属性。
以下命令在 SQL 中声明函数 c_overpaid
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY
/funcs', 'c_overpaid'
LANGUAGE C STRICT;
请注意,我们使用了 STRICT
,这样就不必检查输入参数是否为 NULL。
要从 C 语言函数返回行或复合类型值,您可以使用一个特殊 API,该 API 提供了宏和函数来隐藏构建复合数据类型的大部分复杂性。要使用此 API,源文件必须包含
#include "funcapi.h"
有两种方法可以构建复合数据值(此后称为“元组”):您可以从 Datum 值数组构建它,或者从 C 字符串数组构建它,这些 C 字符串可以传递给元组列数据类型的输入转换函数。在任何一种情况下,您首先需要获取或构造元组结构的 TupleDesc
描述符。使用 Datums 时,将 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)
基于类型 OID 获取 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)
以 Datum 形式给定用户数据来构建 HeapTuple
。
当使用 C 字符串时,使用
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
根据 C 字符串形式的用户数据构建 HeapTuple
。values
是一个 C 字符串数组,返回行的每个属性一个。每个 C 字符串都应采用属性数据类型的输入函数所期望的格式。为了返回其中一个属性的空值,values
数组中对应的指针应设置为 NULL
。对于您返回的每一行,都需要再次调用此函数。
一旦您构建了一个要从函数返回的元组,它就必须转换为 Datum
。使用
HeapTupleGetDatum(HeapTuple tuple)
将 HeapTuple
转换为有效的 Datum。如果您只打算返回一行,则可以直接返回此 Datum
,或者可以在返回集合的函数中将其用作当前返回值。
下一节将出现一个示例。
C 语言函数有两种返回集合(多行)的选项。在一种称为按调用返回值模式的方法中,集合返回函数被重复调用(每次传递相同的参数),并在每次调用时返回一个新行,直到它没有更多行要返回并通过返回 NULL 来发出信号。因此,集合返回函数(SRF)必须在调用之间保存足够的状态,以记住它正在做什么并在每次调用时返回正确的下一个项目。在另一种称为具体化模式的方法中,SRF 填充并返回一个包含其整个结果的元组存储对象;然后只发生一次对整个结果的调用,并且不需要调用间状态。
在使用按调用返回值模式时,记住不能保证查询会运行完成是很重要的;也就是说,由于 LIMIT
等选项,执行器可能会在所有行都被获取之前停止对集合返回函数的调用。这意味着在最后一次调用中执行清理活动是不安全的,因为这可能永远不会发生。对于需要访问外部资源(例如文件描述符)的函数,建议使用具体化模式。
本节的其余部分文档了一组通常用于(但不是必须使用)使用 ValuePerCall 模式的 SRF 的辅助宏。有关 Materialize 模式的更多详细信息,请参阅 src/backend/utils/fmgr/README
。此外,PostgreSQL 源代码发行版中的 contrib
模块包含许多使用 ValuePerCall 和 Materialize 模式的 SRF 示例。
要使用此处描述的 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时当前的内存上下文是一个瞬态上下文,将在调用之间清除。这意味着您不需要对所有使用 palloc
分配的内容调用 pfree
;它们无论如何都会消失。但是,如果您想分配任何数据结构以在调用之间生存,则需要将它们放在其他地方。由 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;
另一种方法是使用 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 语言函数可以声明为接受和返回 第 36.2.5 节 中描述的多态类型。当函数的参数或返回类型定义为多态类型时,函数作者无法提前知道它将使用什么数据类型调用或需要返回什么数据类型。fmgr.h
中提供了两个例程,允许版本 1 C 函数发现其参数的实际数据类型以及预期返回的类型。这些例程称为 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 "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); }
以下命令在 SQL 中声明函数 make_array
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 中指定共享库来预加载它。共享库还应在其 _PG_init
函数中注册一个 shmem_request_hook
。此 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
为初始化代码提供了一个方便的位置,但并非所有此类代码都必须放置在此钩子中。在 Windows(以及任何定义 EXEC_BACKEND
的地方),每个后端在其附加到共享内存后不久都会执行已注册的 shmem_startup_hook
,因此插件仍应在此钩子内获取 AddinShmemInitLock
,如上例所示。在其他平台上,只有 postmaster 进程执行 shmem_startup_hook
,每个后端都会自动继承指向共享内存的指针。
一个 shmem_request_hook
和 shmem_startup_hook
的示例可以在 PostgreSQL 源代码树的 contrib/pg_stat_statements/pg_stat_statements.c
中找到。
还有另一种更灵活的保留共享内存的方法,可以在服务器启动后和 shmem_request_hook
之外完成。为此,每个将使用共享内存的后端都应通过调用获取指向它的指针
void *GetNamedDSMSegment(const char *name, size_t size, void (*init_callback) (void *ptr), bool *found)
如果具有给定名称的动态共享内存段尚不存在,此函数将分配它并使用提供的 init_callback
回调函数对其进行初始化。如果该段已被另一个后端分配和初始化,此函数只是将现有动态共享内存段附加到当前后端。
与服务器启动时保留的共享内存不同,在使用 GetNamedDSMSegment
保留共享内存时,无需获取 AddinShmemInitLock
或采取其他措施来避免竞态条件。此函数确保只有一个后端分配和初始化该段,并且所有其他后端都接收到指向完全分配和初始化的段的指针。
一个完整的 GetNamedDSMSegment
使用示例可以在 PostgreSQL 源代码树中的 src/test/modules/test_dsm_registry/test_dsm_registry.c
中找到。
插件可以在服务器启动时保留 LWLocks。与服务器启动时保留的共享内存一样,必须通过在 shared_preload_libraries 中指定插件的共享库来预加载它,并且共享库应在其 _PG_init
函数中注册一个 shmem_request_hook
。此 shmem_request_hook
可以通过调用来保留 LWLocks
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这确保在名称 tranche_name
下有一个 num_lwlocks
个 LWLocks 的数组可用。可以通过调用获取指向此数组的指针
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
还有另一种更灵活的获取 LWLocks 的方法,可以在服务器启动后和 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
使用示例可以在 PostgreSQL 源代码树中的 contrib/pg_prewarm/autoprewarm.c
中找到。
插件可以通过调用在等待事件类型 Extension
下定义自定义等待事件
uint32 WaitEventExtensionNew(const char *wait_event_name)
等待事件与面向用户的自定义字符串相关联。一个示例可以在 PostgreSQL 源代码树中的 src/test/modules/worker_spi
中找到。
自定义等待事件可在 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, arg);
服务器代码中已在战略位置声明了一些注入点。在添加新的注入点后,代码需要编译才能使该注入点在二进制文件中可用。C 语言编写的插件可以使用相同的宏在自己的代码中声明注入点。注入点名称应使用小写字符,并用连字符分隔。arg
是在运行时传递给回调的可选参数值。
执行注入点可能需要分配少量内存,这可能会失败。如果您需要在不允许动态分配的关键部分中设置注入点,可以使用以下宏进行两步操作
INJECTION_POINT_LOAD(name); INJECTION_POINT_CACHED(name, arg);
在进入临界区之前,调用 INJECTION_POINT_LOAD
。它会检查共享内存状态,如果回调处于活动状态,则将其加载到后端私有内存中。在临界区内部,使用 INJECTION_POINT_CACHED
执行回调。
插件可以通过调用将回调附加到已声明的注入点
extern void InjectionPointAttach(const char *name, const char *library, const char *function, const void *private_data, int private_data_size);
name
是注入点的名称,当执行到达时,它将执行从 library
加载的 function
。private_data
是一个大小为 private_data_size
的私有数据区域,作为参数传递给执行时的回调。
这是一个 InjectionPointCallback
回调的示例
static void custom_injection_callback(const char *name, const void *private_data, void *arg) { uint32 wait_event_info = WaitEventInjectionPointNew(name); pgstat_report_wait_start(wait_event_info); elog(NOTICE, "%s: executed custom callback", name); pgstat_report_wait_end(); }
此回调将消息以 NOTICE
严重性打印到服务器错误日志中,但回调可以实现更复杂的逻辑。
定义当达到注入点时要执行的操作的另一种方法是将测试代码添加到正常源代码旁边。如果操作例如依赖于加载模块无法访问的局部变量,这会很有用。然后可以使用 IS_INJECTION_POINT_ATTACHED
宏来检查注入点是否已附加,例如
#ifdef USE_INJECTION_POINTS if (IS_INJECTION_POINT_ATTACHED("before-foobar")) { /* change a local variable if injection point is attached */ local_var = 123; /* also execute the callback */ INJECTION_POINT_CACHED("before-foobar", NULL); } #endif
请注意,附加到注入点的回调不会由 IS_INJECTION_POINT_ATTACHED
宏执行。如果要执行回调,您还必须像上面的示例一样调用 INJECTION_POINT_CACHED
。
或者,可以通过调用分离注入点
extern bool InjectionPointDetach(const char *name);
成功时返回 true
,否则返回 false
。
附加到注入点的回调在所有后端都可用,包括在调用 InjectionPointAttach
之后启动的后端。它在服务器运行时或使用 InjectionPointDetach
分离注入点之前一直保持附加状态。
一个示例可以在 PostgreSQL 源代码树中的 src/test/modules/injection_points
中找到。
启用注入点需要使用 configure
时的 --enable-injection-points
或使用 Meson 时的 -Dinjection_points=true
。
用 C 语言编写的插件可以使用在 累积统计系统 中注册的自定义累积统计类型。
首先,定义一个 PgStat_KindInfo
,其中包含与注册的自定义类型相关的所有信息。例如
static const PgStat_KindInfo custom_stats = { .name = "custom_stats", .fixed_amount = false, .shared_size = sizeof(PgStatShared_Custom), .shared_data_off = offsetof(PgStatShared_Custom, stats), .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats), .pending_size = sizeof(PgStat_StatCustomEntry), }
然后,每个需要使用此自定义类型的后端都需要使用 pgstat_register_kind
和一个唯一的 ID 来注册它,该 ID 用于存储与此统计类型相关的条目
extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info);
在开发新扩展时,对 kind
使用 PGSTAT_KIND_EXPERIMENTAL
。当您准备将扩展发布给用户时,请在 自定义累积统计 页面上保留一个类型 ID。
PgStat_KindInfo
的 API 详情可以在 src/include/utils/pgstat_internal.h
中找到。
注册的统计类型与一个名称和在共享内存中共享的唯一 ID 相关联。每个使用自定义统计类型的后端都维护一个本地缓存,用于存储每个自定义 PgStat_KindInfo
的信息。
将实现自定义累积统计类型的扩展模块放置在 shared_preload_libraries 中,以便在 PostgreSQL 启动期间尽早加载它。
一个描述如何注册和使用自定义统计信息的示例可以在 src/test/modules/injection_points
中找到。
尽管 PostgreSQL 后端是用 C 编写的,但如果遵循以下指导方针,则可以用 C++ 编写扩展
所有由后端访问的函数都必须向后端提供 C 接口;这些 C 函数随后可以调用 C++ 函数。例如,后端访问的函数需要 extern C
链接。对于在后端和 C++ 代码之间作为指针传递的任何函数,这也是必需的。
使用适当的解除分配方法释放内存。例如,大多数后端内存使用 palloc()
分配,因此使用 pfree()
释放它。在这种情况下使用 C++ delete
将会失败。
防止异常传播到 C 代码中(在所有 extern C
函数的顶层使用 catch-all 块)。即使 C++ 代码没有显式抛出任何异常,这也是必要的,因为内存不足等事件仍然可以抛出异常。任何异常都必须捕获并将适当的错误传递回 C 接口。如果可能,使用 -fno-exceptions
编译 C++ 以完全消除异常;在这种情况下,您必须检查 C++ 代码中的失败,例如,检查 new()
返回的 NULL。
如果从 C++ 代码调用后端函数,请确保 C++ 调用栈仅包含普通旧数据结构(POD)。这是必要的,因为后端错误会生成一个遥远的 longjmp()
,该 longjmp()
无法正确展开包含非 POD 对象的 C++ 调用栈。
总而言之,最好将 C++ 代码置于 extern C
函数的屏障之后,这些函数与后端进行接口,并避免异常、内存和调用栈泄漏。
如果您在文档中发现任何不正确、与您使用特定功能的经验不符或需要进一步澄清的地方,请使用 此表格 报告文档问题。