用户自定义函数可以用 C 语言(或可以与 C 兼容的语言,如 C++)编写。此类函数被编译为动态可加载对象(亦称共享库),并在需要时由服务器按需动态加载。动态加载特性是区分“C 语言”函数与“内部”函数的关键——二者的实际编码规范在本质上是相同的(因此,标准内部函数库是编写用户自定义 C 函数的丰富范例来源)。
目前 C 函数仅使用一种调用规约(即“version 1”)。对该调用规约的支持通过为该函数编写 PG_FUNCTION_INFO_V1() 宏调用来声明,如下文所示。
在会话中第一次调用特定可加载对象文件中的用户自定义函数时,动态加载器会将该对象文件加载到内存中以便执行该函数。因此,针对用户自定义 C 函数的 CREATE FUNCTION 必须为该函数指定两项信息:可加载对象文件的文件名,以及在该对象文件内部要调用的具体函数的 C 语言符号名(链接符号)。如果未显式指定 C 语言名称,则默认假定其与 SQL 函数名相同。
系统根据 CREATE FUNCTION 命令中给出的名称,采用以下算法定位共享对象文件:
如果名称是绝对路径,则直接加载指定的文件。
如果名称不包含目录路径部分,则在配置变量 dynamic_library_path 指定的搜索路径中查找该文件。
否则(未在路径中找到该文件,或者它包含非绝对目录路径),动态加载器将尝试按原样加载该名称,这通常会导致失败(依赖当前工作目录是不可靠的)。
If this sequence does not work, the platform-specific shared
library file name extension (often .so) is
appended to the given name and this sequence is tried again. If
that fails as well, the load will fail.
建议将共享库定位为相对于 $libdir 的路径或通过动态库路径进行定位。这在将新版本安装到不同目录位置时能简化升级。$libdir 所代表的实际物理目录可以通过命令 pg_config --pkglibdir 查看。
运行 PostgreSQL 服务器的操作系统用户 ID 必须对要加载的文件路径具有遍历权限。将文件或其上层目录设置为对 postgres 用户不可读或不可执行是一个常见错误。
在任何情况下,CREATE FUNCTION 命令中指定的文件名都会原样记录在系统目录中,因此在需要重新加载该文件时,将执行相同的查找流程。
PostgreSQL 不会自动编译 C 语言函数。在 CREATE FUNCTION 命令中引用目标文件之前,必须先将其编译完成。更多信息请参阅 Section 36.10.5。
为了确保动态加载的目标文件不会被误加载到不兼容的服务器中,PostgreSQL 会检查该文件是否包含具备相应内容的“魔数块”(magic block)。这使服务器能够检测出明显的兼容性问题,例如针对不同大版本的 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 对版本字符串的格式没有限制,但推荐采用语义化版本(SemVer)规范。
动态加载的目标文件在首次使用后会驻留在内存中。在同一会话中对该文件中函数的后续调用仅产生微小的符号表查找开销。如果需要强制重新加载目标文件(例如在重新编译后),请开启一个全新的数据库会话。
动态加载的文件可以选择性地包含一个初始化函数。如果该文件导出了名为 _PG_init 的函数,该函数将在文件加载后立即被调用。该函数不接受任何参数且返回类型应为 void。目前尚不支持显式卸载已动态加载的库文件。
要了解如何编写 C 语言函数,需要理解 PostgreSQL 在内部如何表示基本数据类型以及它们如何在函数之间传递。在内核内部,PostgreSQL 将基本类型视为一段“内存二进制块”(blob of memory)。您在类型上定义的用户自定义函数反过来定义了 PostgreSQL 操作该类型的方式。换言之,PostgreSQL 仅负责从磁盘存储和检索数据,并调用您的自定义函数来进行输入解析、数据处理和输出展示。
基本数据类型具有以下三种内部表示格式之一:
按值传递,定长(pass by value, fixed-length)
按引用传递,定长(pass by reference, fixed-length)
按引用传递,变长(pass by reference, variable-length)
按值传递的类型长度只能是 1、2 或 4 字节(如果您的机器上 sizeof(Datum) 为 8,也可以是 8 字节)。在定义类型时,应确保它们在所有硬件体系结构上具有相同的字节大小。例如,使用 long 类型是危险的,因为它在某些机器上是 4 字节,而在另一些机器上是 8 字节;相比之下,int 在绝大多数 Unix 机器上都是 4 字节。在 Unix 机器上对 int4 类型的合理实现可以是:
/* 4 字节整数,按值传递 */ typedef int int4;
(PostgreSQL 实际的 C 源码将此类型称为 int32,因为 C 语言规范中约定 int 代表 XXXX 位。因此还要注意 C 类型 int8 的大小为 1 字节,而 SQL 中的 int8 在 C 语言中称为 int64。另请参阅 Table 36.2)。
另一方面,任意大小的定长类型都可以按引用传递。例如以下是 PostgreSQL 类型的示例实现:
/* 16 字节结构体,按引用传递 */
typedef struct
{
double x, y;
} Point;
在将此类类型传入或传出 PostgreSQL 函数时,只能使用指向该类型的指针。要返回此类类型的值,请使用 palloc 分配恰当大小的内存空间,填充该内存,然后返回指向该内存的指针(另外,如果您只是希望返回与某个输入实参相同的值且数据类型一致,可以省去额外的 palloc,直接返回指向输入值的指针)。
最后,所有变长类型也必须按引用传递。所有变长类型都必须以一个恰好 4 字节的不透明长度头部字段开头,该字段必须通过宏 SET_VARSIZE 设置;切勿直接对该字段赋值!该类型中存储的所有数据必须紧跟在该长度字段之后的内存空间中。长度字段包含整个结构体的总字节长度,也就是说,它包含了长度字段本身的大小。
另一个关键注意事项是避免在数据类型的值中留下任何未初始化的比特位;例如,务必将结构体中可能存在的任何对齐填充字节清零。若未清零,规划器可能会将逻辑上相等的常量视为不相等,从而导致生成低效(尽管计算结果正确)的查询计划。
切勿就地修改按引用传递的输入实参的内容。如果这样做,极有可能破坏磁盘上的持久化数据,因为传入的指针可能直接指向共享磁盘缓冲区。此规则的唯一特例在 Section 36.12 中有说明。
作为示例,我们可以按如下方式定义 text 类型:
typedef struct {
int32 length;
char data[FLEXIBLE_ARRAY_MEMBER];
} text;
其中的 [FLEXIBLE_ARRAY_MEMBER] 语法表示此声明未固定数据部分的实际长度。
在操作变长类型时,必须格外注意分配正确的内存大小并正确设置长度字段。例如,如果我们希望在 text 结构体中存储 40 字节的数据,可以使用如下代码片段:
#include "postgres.h" ... char buffer[40]; /* 我们的源数据 */ ... text *destination = (text *) palloc(VARHDRSZ + 40); SET_VARSIZE(destination, VARHDRSZ + 40); memcpy(destination->data, buffer, 40); ...
VARHDRSZ 与 sizeof(int32) 相同,但使用宏 VARHDRSZ 来指代变长类型的头部开销大小是更规范的编码风格。此外,长度字段必须使用 SET_VARSIZE 宏来设置,切勿直接赋值。
Table 36.2 列出了与 PostgreSQL 诸多内置 SQL 数据类型相对应的 C 语言类型。“定义于”(Defined In)一列给出了获取该类型定义所需包含的头文件(实际定义可能位于被该头文件包含的其他文件中,建议开发者始终遵循规范的公开接口)。请注意,在任何服务端源码文件中,都应始终首先包含 postgres.h,因为它声明了您必然需要的许多底层基础定义,并且先包含其他头文件可能会引发可移植性问题。
Table 36.2. Equivalent C Types for Built-in SQL Types
| SQL Type | C Type | Defined In |
|---|---|---|
boolean | bool | postgres.h(可能是编译器内建) |
box | BOX* | utils/geo_decls.h |
bytea | bytea* | postgres.h |
"char" | char | (compiler built-in) |
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 |
在完整介绍了基本类型的所有可能内部结构之后,我们来看一些实际函数的完整示例。
Version-1 调用规约依靠宏封装隐藏了参数传递与结果返回的大多数复杂细节。Version-1 函数的 C 语言函数声明格式固定为:
Datum funcname(PG_FUNCTION_ARGS)
此外,宏调用:
PG_FUNCTION_INFO_V1(funcname);
必须出现在同一个源文件中(按惯例通常紧邻书写在函数定义之前)。对于 internal 语言的内置函数不需要该宏调用,因为 PostgreSQL 默认假定所有内部函数均采用 version-1 规约。但对于动态加载的函数,该宏声明是强制必须的。
在 version-1 函数内部,每个实际参数都使用对应于该参数数据类型的 PG_GETARG_ 宏来获取(在非 STRICT 严格函数中,需预先使用 xxx()PG_ARGISNULL() 检查参数是否为 NULL;详见下文)。函数的返回值通过对应返回类型的 PG_RETURN_ 宏来返回。xxx()PG_GETARG_ 接受要获取的实参索引作为参数,索引从 0 开始计数;xxx()PG_RETURN_ 则接受要返回的实际数值作为参数。
xxx()
若要在 C 代码中直接调用另一个 version-1 函数,可以使用 DirectFunctionCall 系列函数。这在您希望以类似于其 SQL 签名的方式直接调用标准内部库中定义的函数时尤为便利。
n(func, arg1, ..., argn)
这些便捷函数及相关宏定义位于 fmgr.h 中。DirectFunctionCall 系列函数的第一个参数需要传入 C 函数指针符号名。此外还有接受目标函数 OID 的 nOidFunctionCall 以及其他变体。所有这些函数都要求将实参作为 nDatum 传入,并且同样返回 Datum。请注意,在使用这些便捷函数时,参数和返回值均不允许为 NULL。
例如要在 C 中调用 starts_with(text, text) 函数,可以查询系统目录得知其对应的 C 语言实现是 Datum text_starts_with(PG_FUNCTION_ARGS) 函数。通常可以使用 DirectFunctionCall2(text_starts_with, ...) 调用该函数。然而,由于 starts_with(text, text) 依赖排序规则信息,直接这样调用会报错“could not determine which collation to use for string comparison”。因此必须改用 DirectFunctionCall2Coll(text_starts_with, ...) 并提供目标排序规则,通常可以通过 PG_GET_COLLATION() 透传当前排序规则,如下例所示。
fmgr.h 还提供了在 C 数据类型与 Datum 之间进行类型转换的宏。例如要将 Datum 转换为 text*,可以使用 DatumGetTextPP(X)。对于反向转换,虽然某些类型有命名为 TypeGetDatum(X) 的宏,但 text* 没有;直接使用通用宏 PointerGetDatum(X) 即可。如果您的扩展模块定义了新类型,通常为您的自定义类型也定义类似的转换宏会非常方便。
以下是使用 version-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 教程目录)。(更推荐的规范做法是将 DIRECTORY 添加到搜索路径后,在 AS 子句中仅指定 'funcs'。无论哪种方式,都可以省略特定操作系统平台的共享库后缀名,如通常的 .so)。
请注意,我们将这些函数指定为了“严格模式”(“strict”),这意味着只要任何输入实参为 NULL,系统就会自动判定返回结果为 NULL。通过这样做,我们避免了在函数源码中编写繁琐的判空逻辑。若不指定严格模式,我们必须使用 PG_ARGISNULL() 显式检查输入值是否为 NULL。
宏 PG_ARGISNULL( 允许函数测试第 n 个输入参数是否为 NULL(当然,这仅在未声明为严格模式的函数中才必须)。与 n)PG_GETARG_ 宏类似,输入参数的序号从 0 开始计数。请注意,在确认参数不为 NULL 之前,切勿执行 xxx()PG_GETARG_。要返回 NULL 结果,请执行 xxx()PG_RETURN_NULL();该宏在严格与非严格函数中均可工作。
初看之下,与常规原生的 C 调用规约相比,version-1 编码规范似乎充斥着繁杂的宏包装。然而,正是这些规约使我们能够优雅高效地处理可为 NULL 的参数/返回值,以及经过 TOAST 机制(压缩或线外存储)处理的大对象值。
Version-1 接口提供的其他高级选项是 PG_GETARG_ 宏的两种变体。第一种是 xxx()PG_GETARG_,它保证返回指定参数的一个深拷贝副本,允许安全地向其写入数据(常规宏有时会返回直接指向表中物理存储数据的指针,该指针是绝对只读的,禁止写入。使用 xxx_COPY()PG_GETARG_ 可以确保结果内存安全可写)。第二种变体是接受三个参数的 xxx_COPY()PG_GETARG_ 系列宏。第一个参数是函数实参索引编号(同上);第二和第三个参数分别是要返回数据片段的偏移量和长度。偏移量从 0 开始计数,负数长度表示请求返回剩余的全部数据。当大对象字段具有外部存储策略(“external”)时,这些切片宏提供了对大对象局部切片的高效按需访问(列的存储策略可以通过 xxx_SLICE()ALTER TABLE 进行设置,其中 tablename ALTER COLUMN colname SET STORAGE storagetypestoragetype 为 plain、external、extended 或 main 之一)。
最后,version-1 函数调用规约使得返回集合结果(Section 36.10.9)、实现触发器函数(Chapter 37)以及编写过程语言调用处理器(Chapter 57)成为可能。更多技术细节请参阅源码分发中的 src/backend/utils/fmgr/README。
在深入更高级的主题之前,我们需要讨论编写 PostgreSQL C 语言函数的一些核心编码准则。尽管将其他语言编写的函数加载到 PostgreSQL 中理论上是可行的,但这通常相当困难,因为诸如 C++、FORTRAN 或 Pascal 等其他语言往往不遵循与 C 相同的二进制调用规约(即不同语言在函数之间传递实参和返回值的方式存在差异)。出于这个原因,我们假定您的 C 语言函数均使用纯 C 语言编写。
编写和构建 C 语言函数的基本准则如下:
使用 pg_config --includedir-server 查询 PostgreSQL 服务端头文件在当前系统(或目标运行环境)中的安装路径。
编译并链接动态可加载代码以加载至 PostgreSQL 始终需要特定的编译器与链接器标志。针对特定操作系统的编译指导详见 Section 36.10.5。
务必按 Section 36.10.1 中所述,为您的共享库定义“魔数块”(magic block)。
在进行内存动态分配时,请使用 PostgreSQL 内存管理函数 palloc 和 pfree,而非标准 C 库函数 malloc 和 free。由 palloc 分配的内存会在每个事务或内存上下文结束时自动回收释放,从而有效杜绝内存泄漏隐患。
始终使用 memset 将结构体的内存字节清零(或者直接使用 palloc0 分配)。即便您为结构体的每个字段都赋了值,结构体内部仍可能存在包含随机垃圾数据的字节对齐填充空隙(padding holes)。如果不清零,将很难支持哈希索引或哈希连接(Hash Join),因为计算哈希值时必须排除这些噪声位。此外,优化器有时依赖按位直接比对常量,如果逻辑上等价的值在二进制位上不一致,可能会导致生成次优的执行计划。
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
这适用于 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
使用 Sun 编译器时,创建
PIC 的编译器标志是
-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 命令中指定文件名时,必须给出共享库文件的名称,而非中间目标文件。注意,在
CREATE FUNCTION 命令中可以省略系统的标准共享库扩展名(通常是
.so 或
.sl),并且为了获得最佳可移植性,通常也应当省略。
关于服务器期望在哪里找到共享库文件,请回顾 Section 36.10.1。
本节为扩展程序及其他服务端插件的开发者提供关于 PostgreSQL 服务端 API 与 ABI 稳定性的官方指导。
PostgreSQL 服务端包含若干专为插件设计的、界限明确的标准 API,例如函数管理器(fmgr,如本章所述)、服务端编程接口 SPI(Chapter 45)以及专门面向扩展开发的各类钩子(Hooks)。这些公开接口经过精心维护,保障长期的稳定性和兼容性。然而,服务端中存在的所有全局函数和变量在技术上都可供调用,其中绝大多数在最初设计时并未兼顾外部扩展性和长期稳定性。
因此,尽管使用这些内部接口是允许的,但偏离规范路径越远,越可能在未来的版本升级中遭遇 API 或 ABI 兼容性破坏。我们鼓励扩展作者积极反馈自身的技术诉求,以便随着新使用场景的涌现,将某些关键内部接口进一步标准化,或添加全新设计、面向长期稳定的公开接口。
API(应用程序编程接口)是在编译期使用的代码级接口。
在 PostgreSQL 大版本之间不保证源码级 API 的向后兼容。因此扩展代码若要兼容多个大版本,可能需要微调源码。这通常可以通过预处理器条件编译(例如 #if PG_VERSION_NUM >= 160000)来优雅管理。使用底层非公开接口的高级复杂扩展通常在每个大版本升级时都需要进行少量适配。
PostgreSQL 竭尽全力避免在小版本(minor release)中破坏 API。通常,在某个小版本上能够编译并运行的扩展代码,应同样能够在同一大版本的任何历史或未来小版本中顺利编译和执行。
当不得不作出改动时,官方会审慎评估并充分照顾扩展生态的需求。此类变更将在发行说明中予以明确公告(参见 Appendix E)。
ABI(应用程序二进制接口)是程序在运行时使用的二进制链接与内存布局规范。
不同大版本的服务器在设计上故意保持 ABI 不兼容。因此,使用服务端 API 的扩展必须针对每个大版本重新编译。引入 PG_MODULE_MAGIC(参见 Section 36.10.1)可确保针对某一大版本编译的二进制文件会被其他大版本的服务器安全拒绝加载。
PostgreSQL 尽最大努力避免在小版本中破坏服务端 ABI。通常,针对某小版本编译的扩展二进制文件应直接兼容同一大版本的其他任何小版本。
当确需修改时,PostgreSQL 会选择侵入性最小的方案,例如将新字段挤入结构体填充对齐空间或追加在结构体末尾。此类变更通常不会对扩展造成冲击,除非扩展采用了极为罕见的反常规编码模式。
然而在极少数情况下,即使此类非侵入式调整也无法满足需求。届时变更将审慎处理并在发行说明中详尽记录(参见 Appendix E)。
但请务必注意,服务端的许多内部组件并未设计为面向公众的稳定 API。若遇到紧急缺陷修复等情况,这些内部深层代码的改动将更少顾及外部扩展兼容性。
另外,在缺乏全自动检测工具的情况下,这并非绝对保证,但在历史上小版本中出现此类破坏性变更是极其罕见的。
复合类型不像 C 语言结构体那样具有固定的物理内存布局。复合类型的实例可能包含 NULL 字段。此外,属于继承层次结构的复合类型可能具有与同一层次结构中其他成员不同的字段定义。因此,PostgreSQL 提供了在 C 语言中访问复合类型字段的函数接口。
假设我们希望编写一个函数来响应如下查询:
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
使用 version-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 类型参数、目标属性名称,以及一个指出该属性是否为 NULL 的布尔输出指针。GetAttributeByName 返回一个 Datum 值,您可以使用对应的 DatumGet 函数将其转换为正确的 C 数据类型。请注意,若 null 标志为 true,则返回值毫无意义;在对结果进行任何操作之前,务必先检查 null 标志。
XXX()
此外还有 GetAttributeByNum 函数,它通过列序号(从 1 开始)而非列名来选取目标属性。
以下 SQL 命令在数据库中声明注册 c_overpaid 函数:
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY/funcs', 'c_overpaid'
LANGUAGE C STRICT;
注意我们使用了 STRICT 严格模式,因此在 C 代码中无需检查输入实参是否为 NULL。
要从 C 语言函数返回行或复合类型数据,可以使用专门的内核 API,这些宏和函数封装隐藏了构建复合数据类型的大多数底层细节。要使用此 API,源文件必须包含:
#include "funcapi.h"
构建复合数据值(以下统称为“元组” tuple)有两种途径:从 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 结构体(这自然要求使用 version-1 调用规约)。resultTypeId 可以传入 NULL 或接收函数返回类型 OID 的局部变量地址。resultTupleDesc 应传入局部 TupleDesc 变量的指针。检查返回值是否为 TYPEFUNC_COMPOSITE;如果是,则 resultTupleDesc 已被填充为所需的元组描述符(若不是,可以抛出类似“function returning record called in context that cannot accept type 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 TypeGetTupleDesc(Oid typeoid, List *colaliases)
用于根据类型 OID 获取元组描述符。它可用于获取基本类型或复合类型的元组描述符,但无法用于返回 record 的函数,也无法解析多态类型。
一旦获得 TupleDesc,如果计划使用 Datum 数组,请调用:
TupleDesc BlessTupleDesc(TupleDesc tupdesc)
如果计划使用 C 字符串数组,请调用:
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
如果您正在编写返回集合的函数,可以将这些函数的返回值保存在 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 字符串都应符合该属性数据类型输入函数所期望的格式。为了使某个属性返回 NULL 值,只需将 values 数组中对应的指针设置为 NULL。对于返回的每一行都需要再次调用此函数。
在构建好要从函数返回的元组后,必须将其转换为 Datum。使用:
HeapTupleGetDatum(HeapTuple tuple)
将 HeapTuple 转换为合法的 Datum。若仅返回单行记录,可直接返回该 Datum;若在集合返回函数中,可将其用作当前的单行返回值。
具体的完整示例见下一节。
C 语言函数有两种方式来返回集合(多行数据)。在第一种称为 ValuePerCall(逐次调用)模式下,集合返回函数会被执行器重复调用(每次传入相同的参数),并在每次调用时返回一个新行,直到没有更多行可返回并以返回 NULL 示意结束。因此集合返回函数(SRF)必须在多次调用之间保持足够的状态以记录当前进度并返回正确的下一项。在另一种称为 Materialize(物化)模式下,SRF 填充并返回一个包含完整结果集的 tuplestore 存储对象;整个结果集仅需一次函数调用即可生成,无需跨调用维护状态。
在使用 ValuePerCall 模式时,必须牢记查询并不保证一定会完整执行完毕;也就是说,由于 LIMIT 等子句的存在,执行器可能会在读取完所有行之前终止对集合返回函数的调用。这意味着在最后一次调用中执行资源清理操作是不安全的,因为该最后调用可能根本不会发生。对于需要访问文件描述符等外部资源的函数,强烈推荐使用 Materialize 物化模式。
本节其余部分记录了一组在 ValuePerCall 模式下常用的辅助宏(虽然并非强制使用)。有关 Materialize 模式的更多技术细节可在 src/backend/utils/fmgr/README 中找到。此外,PostgreSQL 源码分发的 contrib 模块中包含大量同时使用这两种模式的丰富示例。
要使用此处描述的 ValuePerCall 支持宏,请包含头文件 funcapi.h。这些宏围绕结构体 FuncCallContext 协同工作,该结构体保存了需要在跨调用之间持久保存的状态。在被调用的 SRF 内部,fcinfo->flinfo->fn_extra 用于跨调用持有指向 FuncCallContext 的指针。相关宏在首次调用时自动填充该字段,并在后续调用中直接复用该指针。
typedef struct FuncCallContext
{
/*
* 此前已被调用的次数
*
* call_cntr 由 SRF_FIRSTCALL_INIT() 自动初始化为 0,
* 并且每次调用 SRF_RETURN_NEXT() 时都会自动自增 1。
*/
uint64 call_cntr;
/*
* 可选的最大调用次数
*
* max_calls 仅为提供便利而设,设置为可选。
* 若未设置,您必须通过其他自定义机制判断函数何时结束返回。
*/
uint64 max_calls;
/*
* 可选的指向用户自定义上下文信息的指针
*
* user_fctx 用作指向您自定义数据的指针,以便在函数的连续调用之间保留任意上下文。
*/
void *user_fctx;
/*
* 可选的指向包含属性类型输入元数据的结构体指针
*
* attinmeta 在返回元组(即复合数据类型)时使用,返回基本类型时不使用。
* 仅当您打算使用 BuildTupleFromCStrings() 构建返回元组时才需要它。
*/
AttInMetadata *attinmeta;
/*
* 供必须跨多次调用存活的结构体使用的内存上下文
*
* multi_call_memory_ctx 由 SRF_FIRSTCALL_INIT() 自动设置,
* 并由 SRF_RETURN_DONE() 用于资源清理。它是用于在 SRF 的多次调用之间
* 复用内存的最适宜内存上下文。
*/
MemoryContext multi_call_memory_ctx;
/*
* 可选的指向包含元组描述符的结构体指针
*
* tuple_desc 用于返回元组(即复合数据类型),并且仅当您打算使用
* heap_form_tuple() 而非 BuildTupleFromCStrings() 构建元组时才需要。
* 请注意,此处存储的 TupleDesc 指针通常应当首先通过 BlessTupleDesc() 处理。
*/
TupleDesc tuple_desc;
} FuncCallContext;
使用该底层设施的 SRF 所需使用的辅助宏包括:
SRF_IS_FIRSTCALL()
用于判断当前函数是首次被调用还是处于后续调用阶段。在首次调用时(仅首次),调用:
SRF_FIRSTCALL_INIT()
以初始化 FuncCallContext。在包括首次在内的每一次函数调用中,均需调用:
SRF_PERCALL_SETUP()
以为使用 FuncCallContext 建立运行环境。
若函数在当前调用中有数据需要返回,请使用:
SRF_RETURN_NEXT(funcctx, result)
向调用方返回结果(result 必须为 Datum 类型,可以是标量值或按前文所述构造的元组)。最后,当函数全部数据返回完毕时,使用:
SRF_RETURN_DONE(funcctx)
以清理上下文并终结该 SRF 的执行。
在调用 SRF 时处于活动状态的当前内存上下文是一个瞬态上下文,会在两次调用之间自动清空重置。这意味着您无需对使用 palloc 分配的所有临时内存显式调用 pfree,它们会在每次调用后自动回收。然而,如果需要分配跨多次调用存活的数据结构,必须将其放置在其他位置。multi_call_memory_ctx 所引用的内存上下文正是存放需要保留至 SRF 执行结束的所有数据的理想场所。在大多数情况下,这意味着您应当在执行首次调用初始化时切换到 multi_call_memory_ctx。使用 funcctx->user_fctx 保存指向此类跨调用数据结构的指针(在 multi_call_memory_ctx 中分配的数据在查询结束时会自动释放,因此同样无需手动清理)。
虽然函数的实际参数在多次调用之间保持不变,但如果您在瞬态内存上下文中对实参进行解 TOAST 展开(通常由 PG_GETARG_ 宏透明完成),则解压缩的副本会在每个循环周期中被释放。因此,如果您在 xxxuser_fctx 中保留对这些解 TOAST 展开值的引用,必须在解压缩后将其复制到 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);
}
}
One way to declare this function in SQL is:
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;
A different way is to use OUT parameters:
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;
Notice that in this method the output type of the function is formally
an anonymous record type.
C 语言函数可以声明为接受并返回 Section 36.2.5 中所述的多态类型。当函数的参数或返回类型定义为多态类型时,函数编写者无法预先知道它将被何种具体数据类型调用或需要返回何种类型。fmgr.h 中提供了两个底层例程,允许 version-1 C 函数动态探查其实参的实际数据类型以及期望返回的类型。这两个函数分别是 get_fn_expr_rettype(FmgrInfo *flinfo) 和 get_fn_expr_argtype(FmgrInfo *flinfo, int argnum)。它们返回结果或实参的类型 OID(若无法获取该信息则返回 InvalidOid)。结构体 flinfo 通常通过 fcinfo->flinfo 访问。argnum 参数从 0 开始计数。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。该钩子函数可以通过调用以下函数来预留共享内存:
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)
{
... 初始化共享内存内容 ...
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)
如果具有给定名称的动态共享内存(DSM)段尚不存在,该函数将分配它并使用提供的 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。该钩子可以通过调用以下函数预留 LWLocks:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这确保了在名称 tranche_name 下有由 num_lwlocks 个 LWLock 组成的数组可用。可以通过调用以下函数获取指向该锁数组的指针:
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
在服务器启动后以及在 shmem_request_hook 外部,还存在另一种更灵活的获取 LWLock 的机制。为此,首先通过调用以下函数分配一个 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):
INJECTION_POINT(name, arg);
PostgreSQL 服务端内核的关键战略位置已经预定义了若干注入点。在源码中新增注入点后需要重新编译,以便该注入点在二进制程序中生效。使用 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"))
{
/* 若注入点已被挂接,则修改局部变量 */
local_var = 123;
/* 同时触发执行回调 */
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:
extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
const PgStat_KindInfo *kind_info);
在开发新扩展期间,kind 请使用 PGSTAT_KIND_EXPERIMENTAL。当准备向用户正式发布扩展时,请在 自定义累积统计 页面预留一个唯一的 kind 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++ 异常传播逃逸到外层 C 代码中(在所有 extern "C" 函数的最外层必须使用全捕获 catch-all 块)。即使 C++ 代码并未显式抛出异常这也是必须的,因为诸如内存不足(out-of-memory)等事件仍可能隐式抛出异常。所有异常都必须在边界处捕获并转换为规范的 C 错误代码返回。若条件允许,建议使用 -fno-exceptions 编译 C++ 以彻底禁用异常机制;此时必须在 C++ 代码中显式检查操作失败(例如检查 new() 返回的 NULL 指针)。
若从 C++ 代码中调用服务端内核函数,必须确保当前 C++ 调用栈中仅包含传统的平凡数据结构(POD)。这是因为内核在遇到错误时会触发远端长跳转 longjmp(),它无法自动为包含非 POD 对象的 C++ 栈帧执行栈展开与析构函数调用。
综上所述,最佳实践是将 C++ 代码安全封装在与服务端交互的 extern "C" 函数接口防火墙之后,彻底杜绝异常、内存以及调用栈破坏。