↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

百科 / 过程语言 / 原生函数语言

c

动态加载的 C 函数

当前查看 PostgreSQL 18.6。

说明

动态加载的 C 函数

种类
原生函数语言
可信
f
校验函数
fmgr_c_validator

手册定义

36.10. C 语言函数

用户定义的函数可以用 C 编写(或者可以与 C 兼容的语言,例如 C++)。这类函数被编译成动态载入对象(也被称为共享库)并且由服务器在需要时载入。动态载入是把“C语言”函数和“内部”函数区分开的特性 — 两者真正的编码习惯实际上是一样的(因此,标准内部函数库为用户定义的 C 函数提供了丰富的编码示例)。

目前 C 函数仅使用一种调用约定(“版本 1”)。通过为函数编写 PG_FUNCTION_INFO_V1() 宏调用来表明支持此调用约定,示例如下。

在一个会话中第一次调用一个特定可载入目标文件中的用户定义函数时,动态载入器会把那个目标文件载入到内存以便该函数被调用。因此用户定义的 C 函数的CREATE FUNCTION必须为该函数指定两块信息:可载入目标文件的名称,以及要在该目标文件中调用的特定函数的 C 名称(链接符号)。如果没有显式指定 C 名称,则它被假定为和 SQL 函数名相同。

下面的算法被用来基于CREATE FUNCTION 命令中给定的名称来定位共享目标文件:

  1. 如果名称是一个绝对路径,则载入给定的文件。

  2. 如果该名称以字符串$libdir开始,那么这一部分会被 PostgreSQL包的库目录名(在编译时确定)替换。

  3. 如果该名称不包含目录部分,会在配置变量dynamic_library_path指定的路径中搜索该文件。

  4. 否则(在该路径中没找到该文件,或者它包含一个非绝对目录),动态载入器将尝试接受给定的名称,这很可能会失败(依赖当前工作目录是不可靠的)。

如果这个序列不起作用,会把平台相关的共享库文件名扩展(通常是 .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 中的习惯是intXX 表示XX 位。注意因此还有尺寸为 1 字节的 C 类型int8。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 Name postgres.h
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* 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()对参数是否为空值提前做检查;见下文。)结果要用对应于返回类型的PG_RETURN_xxx()宏返回。PG_GETARG_xxx()的参数是要取得的函数参数的编号,从零开始计。PG_RETURN_xxx()的参数是实际要返回的值。

要调用另一个版本-1 函数,你可以使用 DirectFunctionCalln(func, arg1, ..., argn)。这在你想调用标准内部库中定义的函数时尤其有用,因为它提供了与其 SQL 签名相似的接口。

这些便利函数以及类似函数都可以在 fmgr.h 中找到。DirectFunctionCalln 系列把 C 函数名作为其第一个参数。另有 OidFunctionCalln 等一些变体,它们接收目标函数的 OID。所有这些函数都要求以 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 教程目录,其中包含本节示例的代码。(更好的写法是先将 DIRECTORY 加入搜索路径,再在 AS 子句中仅写 'funcs'。无论哪种情况,都可以省略共享库的平台特定扩展名,通常为 .so。)

注意我们已经把函数指定为“strict”,这意味着如果有任何输入值为空值,系统应该自动假定结果为空值。通过这种做法,我们避免在函数代码中检查空值输入。如果不这样做,我们必须使用PG_ARGISNULL()明确地检查空值输入。

宏PG_ARGISNULL(n)允许一个函数测试是否每一个输入为空值(当然,只需要在没有声明为“strict”的函数中这样做)。和PG_GETARG_xxx()宏一样,输入参数也是从零开始计数。注意应该在验证了一个参数不是空值之后才执行PG_GETARG_xxx()。要返回空值结果,应执行PG_RETURN_NULL(),它对严格的以及非严格的函数都有用。

乍看之下,与普通的 C 调用约定相比,版本 1 的编码约定似乎只是增加了复杂度。不过,它们确实让我们能够处理可为 NULL 的参数和返回值,以及“经过 TOAST 处理”(压缩或行外)的值。

版本 1 接口还提供了两种 PG_GETARG_xxx() 宏变体。第一种 PG_GETARG_xxx_COPY() 保证返回指定参数的可写副本。普通宏有时会返回指向表中物理存储值的指针,禁止写入;使用 PG_GETARG_xxx_COPY() 则可保证结果可写。第二种是接受三个参数的 PG_GETARG_xxx_SLICE() 宏:第一个参数是函数参数的编号,如上文所述;第二、三个参数分别是要返回片段的偏移量和长度。偏移量从零开始,负长度表示返回该值的剩余部分。对于存储类型为 external 的大值,这些宏可更高效地访问其中一部分。列的存储类型可通过 ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype 指定,其中 storagetype 为 plain、external、extended 或 main 之一。

最后,版本-1 的函数调用约定可以返回集合结果(第 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 来分配它们)。即使你给结构体的每个字段都赋了值,结构体中仍可能存在包含垃圾值的对齐填充字节(也就是结构体中的空洞)。如果不这么做,就很难支持 hash 索引或哈希连接,因为那时你必须只挑出数据结构中真正有意义的位来计算 hash 值。规划器有时也依赖按位相等来比较常量,因此如果逻辑上等价的值在按位上不相等,就可能得到不理想的规划结果。

  • 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。共享库可以包含多个目标文件,但这里我们只使用一个。

FreeBSD

生成 PIC 的编译器选项是 -fPIC。创建共享库时使用的编译器选项是 -shared。

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

这从 FreeBSD 13.0 起适用;更早版本使用的是 gcc 编译器。

Linux

生成 PIC 的编译器选项是 -fPIC。创建共享库的编译器选项是 -shared。完整示例如下:

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

下面是一个示例。它假定开发者工具已经安装。

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

生成 PIC 的编译器选项是 -fPIC。对于 ELF 系统,使用带有 -shared 选项的编译器来链接共享库。对于较早的非 ELF 系统,则使用 ld -Bshareable。

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

生成 PIC 的编译器选项是 -fPIC。使用 ld -Bshareable 来链接共享库。

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

生成 PIC 的编译器选项,对于 Sun 编译器是 -KPIC,而 -fPIC 则用于 GCC。链接共享库时,两种编译器都可以使用 -G,或者也可以改用 -shared,但这只适用于 GCC。

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

或者

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

提示

如果这些内容对你来说过于复杂,可以考虑使用 GNU Libtool,它通过统一接口隐藏了平台差异。

生成的共享库文件随后就可以装载到 PostgreSQL 中。在向 CREATE FUNCTION 命令指定文件名时,必须给出共享库文件名,而不是中间目标文件名。请注意,系统标准的共享库扩展名(通常是 .so 或 .sl)可以在 CREATE FUNCTION 命令中省略,并且通常也应省略,以获得最佳可移植性。

关于服务器期望在何处找到共享库文件,请回头参见第 36.10.1 节。

本节向扩展和其他服务器插件的作者提供有关 PostgreSQL 服务器 API 与 ABI 稳定性的指导。

36.10.6.1. 概述

PostgreSQL 服务器为插件提供了多个边界清晰的 API,例如本章介绍的函数管理器 fmgr、SPI(第 45 章)以及专为扩展设计的各种钩子。这些接口经过谨慎维护,以保持长期稳定性和兼容性。不过,服务器中全部全局函数和变量实际上都构成了可公开使用的 API,其中大部分在设计时并未考虑可扩展性和长期稳定性。

因此,虽然使用这些接口是完全允许的,但越是偏离那些久经实践考验的路径,将来遇到 API 或 ABI 兼容性问题的可能性就越大。鼓励扩展作者反馈自己的需求,以便随着时间推移和新用法的出现,某些接口能够被视为更稳定,或者可以加入新的、设计更完善的接口。

36.10.6.2. API 兼容性

API,即应用程序编程接口,是编译时使用的接口。

36.10.6.2.1. Major Versions

在 PostgreSQL 不同主版本之间,不承诺 API 兼容性。因此,扩展代码可能需要修改源代码才能同时支持多个主版本。这类差异通常可以通过预处理器条件(例如 #if PG_VERSION_NUM >= 160000)来管理。那些使用了边界清晰接口之外能力的复杂扩展,通常每个服务器主版本都需要做少量这类修改。

36.10.6.2.2. Minor Versions

PostgreSQL 会尽力避免在小版本发布中破坏服务器 API。一般来说,能针对某个小版本编译并正常工作的扩展代码,也应当能够针对同一主版本的其他任意小版本(无论过去还是将来)编译并正常工作。

如果确实需要做出变更,也会在考虑扩展需求的前提下谨慎管理。这类变更会在发布说明(附录 E)中进行说明。

36.10.6.3. ABI 兼容性

ABI,即应用程序二进制接口,是运行时使用的接口。

36.10.6.3.1. Major Versions

不同主版本的服务器在 ABI 上是有意不兼容的。因此,使用服务器 API 的扩展必须针对每一个主版本重新编译。包含 PG_MODULE_MAGIC(见第 36.10.1 节)可以确保针对某个主版本编译的代码会被其他主版本拒绝。

36.10.6.3.2. Minor Versions

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 类型参数、所需属性的名称,以及一个用于指示该属性是否为 null 的返回参数。GetAttributeByName 返回一个 Datum 值,你可以用适当的 DatumGetXXX() 函数把它转换为正确的数据类型。注意,如果 null 标志被设置,那么返回值本身没有意义;在尝试对结果做任何处理之前,务必先检查这个 null 标志。

也有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,源文件中必须包含:

#include "funcapi.h"

构造复合数据值(下文简称“元组”)有两种方式:一种是从 Datum 值数组构造,另一种是从 C 字符串数组构造,这些字符串会传给该元组各列数据类型的输入转换函数。无论采用哪种方式,首先都需要获取或构造描述该元组结构的 TupleDesc。处理 Datum 时,需要把 TupleDesc 传给 BlessTupleDesc,然后为每一行调用 heap_form_tuple。处理 C 字符串时,则要把 TupleDesc 传给 TupleDescGetAttInMetadata,然后为每一行调用 BuildTupleFromCStrings。对于返回元组集合的函数,这些准备步骤可以在第一次调用函数时一次性完成。

有一些辅助函数可以用来设置所需的 TupleDesc。在大多数返回复合值的函数中,推荐的做法是调用:

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

传入调用函数本身收到的同一个 fcinfo 结构体(这当然要求使用版本 1 调用约定)。resultTypeId 可以指定为 NULL,也可以指定为一个本地变量的地址,用于接收函数结果类型的 OID。resultTupleDesc 应当是一个本地 TupleDesc 变量的地址。检查返回结果是否为 TYPEFUNC_COMPOSITE;如果是,resultTupleDesc 就会被填入所需的 TupleDesc。(如果不是,可以报告一个类似“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,还有:

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

可以基于一个类型 OID 得到TupleDesc。这可以被用来为一种基本或者复合类型获得TupleDesc。不过,对于返回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 字符串应该是该属性数据类型的输入函数所期望的格式。为了对一个属性返回空值,values数组中对应的指针应该被设置为NULL。对于你返回的每一行都需要再次调用这个函数。

一旦已经构建了一个要从函数中返回的元组,它必须被转换成一个 Datum。使用

HeapTupleGetDatum(HeapTuple tuple)

可把一个HeapTuple转换成合法的 Datum。如果你只想返回一行,那么这个Datum可以被直接返回,在一个集合返回函数中它也可以被当做当前的返回值。

下一节中会有一个示例。

C 语言函数有两种返回集合(多行)的方法。第一种称为 ValuePerCall 模式:反复调用返回集合的函数,每次传入相同参数,每次返回一行新数据,直到没有更多行,并以返回 NULL 表示结束。因此,返回集合的函数(SRF)必须跨调用保存足够的状态,记录执行进度,确保每次调用返回正确的下一项。第二种称为 Materialize 模式:SRF 填充并返回一个包含全部结果的 tuplestore 对象。整个结果只需调用一次,因此无需保存调用之间的状态。

使用 ValuePerCall 模式时,重要的是要记住查询不能保证运行完成;也就是说,由于诸如LIMIT之类的选项,执行器可能会在获取所有行之前停止调用集合返回函数。这意味着在最后一次调用中执行清理活动是不安全的,因为这可能永远不会发生。对于需要访问外部资源(例如文件描述符)的函数,建议使用 Materialize 模式。

本节的其余部分记录了一组使用 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 中分配的数据会在查询结束时自动清理,也无需手动释放。)

警告

虽然函数的实参在多次调用之间保持不变,但如果在瞬时上下文中反 TOAST 了参数(通常由 PG_GETARG_xxx 宏完成),那么被反 TOAST 的拷贝将在每次循环中被释放。相应地,如果你把这些值的引用保存在user_fctx中,你也必须在反 TOAST 之后把它们拷贝到 multi_call_memory_ctx中,或者确保你只在那个上下文中反 TOAST 这些值。

一个完整的伪代码示例:

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 后自行实现它。

36.10.11.1. 启动时请求共享内存

附加模块可以在服务器启动时预留共享内存。为此,必须通过 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)
{
    ... 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。

36.10.11.2. 启动后请求共享内存

还有另一种更灵活的预留共享内存的方法,可以在服务器启动之后,并且无需借助 shmem_request_hook 来完成。为此,每个需要使用该共享内存的后端都应通过调用以下函数来获取一个指针:

void *GetNamedDSMSegment(const char *name, size_t size,
                         void (*init_callback) (void *ptr),
                         bool *found)

如果给定名称的动态共享内存段尚不存在,该函数会分配该段,并使用提供的 init_callback 回调函数对其进行初始化。如果该段已经由另一个后端分配并初始化过,该函数就只会把现有的动态共享内存段附加到当前后端。

与服务器启动时预留的共享内存不同,使用 GetNamedDSMSegment 预留共享内存时,无需获取 AddinShmemInitLock,也无需采取其他措施避免竞态条件。此函数保证仅由一个后端分配并初始化该段,其他所有后端获得的都是指向已完全分配并初始化的段的指针。

GetNamedDSMSegment 的完整使用示例可见于 src/test/modules/test_dsm_registry/test_dsm_registry.c,位于 PostgreSQL 源代码树中。

36.10.12. 轻量级锁(LWLocks)

36.10.12.1. 启动时请求 LWLocks

附加模块可以在服务器启动时预留 LWLocks。与启动时预留共享内存一样,必须通过 shared_preload_libraries 预加载模块的共享库,并在其 _PG_init 函数中注册 shmem_request_hook。此钩子可通过以下调用预留 LWLocks:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

这确保存在一个名为 tranche_name、包含 num_lwlocks 个 LWLocks 的数组。可通过以下调用取得该数组的指针:

LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)

36.10.12.2. 启动后请求 LWLocks

还有另一种更灵活的获取 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)

该等待事件会关联到一个面向用户的自定义字符串。示例可见于 src/test/modules/worker_spi,位于 PostgreSQL 源代码树中。

自定义等待事件可以在 pg_stat_activity 中查看:

=# SELECT wait_event_type, wait_event FROM pg_stat_activity
     WHERE backend_type ~ 'worker_spi';
 wait_event_type |  wait_event
-----------------+---------------
 Extension       | WorkerSpiMain
(1 row)

给定名称 name 的注入点可通过以下宏声明:

INJECTION_POINT(name, 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 加载的函数。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:

extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
                                        const PgStat_KindInfo *kind_info);

在开发新扩展时,应将 PGSTAT_KIND_EXPERIMENTAL 用作 kind。当你准备把扩展发布给用户时,请在 Custom Cumulative Statistics 页面上预留一个 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 代码中(在所有extern C函数的顶层使用一个捕捉全部异常的块)。即使 C++ 代码不会显式地抛出任何异常也需要这样做,因为类似内存不足等事件仍会抛出异常。任何异常都必须被捕捉并且用适当的错误传回给 C 接口。如果可能,用 -fno-exceptions 来编译 C++ 以完全消灭异常。在这种情况下,你必须在 C++ 代码中检查失败,例如检查new() 返回的 NULL。

  • 从 C++ 代码调用后端函数时,必须确保 C++ 调用栈中仅包含普通旧式数据结构(POD)。这是因为后端错误会触发远距离 longjmp(),无法正确展开包含非 POD 对象的 C++ 调用栈。

总之,最好把 C++ 代码放在与后端交互的extern C函数之后,并且避免异常、内存和调用栈泄漏。

文档与源码

来源构建
版本
18.6
构建
https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2
来源指纹
555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f

版本比较

PostgreSQL 17 → 18: 无变化。

比较已记录的接口与属性,排除来源指纹和构建元数据。某个样本中没有记录,不能据此判断实际引入或移除的版本。

相关条目

导出 JSON · 返回过程语言 · 收录范围为 PostgreSQL 10 至 20;最早采样版本不一定是实际引入版本。