用户定义的函数可以用 C 语言(或可以与 C 兼容的语言,例如 C++)编写。这些函数被编译成动态可加载的对象(也称为共享库),并在需要时由服务器加载。动态加载功能是区分“C 语言”函数和“内部”函数的关键 —— 两者的实际编码约定基本相同。(因此,标准的内部函数库是用户定义的 C 函数的丰富编码示例来源。)
目前,C 函数只使用一种调用约定(“版本 1”)。通过为函数编写一个 PG_FUNCTION_INFO_V1()
宏调用来表明对该调用约定的支持,如下所示。
当会话中第一次调用特定可加载对象文件中的用户定义函数时,动态加载器会将该对象文件加载到内存中,以便可以调用该函数。因此,用户定义 C 函数的 CREATE FUNCTION
必须为该函数指定两个信息:可加载对象文件的名称,以及要调用该对象文件中的特定函数的 C 名称(链接符号)。如果未显式指定 C 名称,则假定它与 SQL 函数名称相同。
使用以下算法来根据 CREATE FUNCTION
命令中给定的名称查找共享对象文件:
如果名称是绝对路径,则加载给定的文件。
如果名称不包含目录部分,则在配置变量 dynamic_library_path 指定的路径中搜索该文件。
否则(未在路径中找到该文件,或者它包含非绝对目录部分),动态加载器将尝试按给定的名称进行加载,这很可能会失败。(依赖当前工作目录是不可靠的。)
如果此顺序不起作用,则将平台特定的共享库文件名扩展名(通常为 .so
)附加到给定的名称,然后再次尝试此顺序。如果仍然失败,则加载将失败。
建议将共享库定位为相对于 $libdir
或通过动态库路径定位。如果新安装位于不同的位置,这将简化版本升级。可以使用命令 pg_config --pkglibdir
找出 $libdir
所代表的实际目录。
PostgreSQL 服务器运行的用户 ID 必须能够遍历到您要加载的文件的路径。使 postgres 用户无法读取和/或无法执行该文件或更高级别的目录是一个常见错误。
在任何情况下,在 CREATE FUNCTION
命令中给定的文件名都会按字面记录在系统目录中,因此如果需要再次加载该文件,则应用相同的过程。
PostgreSQL 不会自动编译 C 函数。必须在 CREATE FUNCTION
命令中引用它之前编译对象文件。有关其他信息,请参阅 第 36.10.5 节。
为了确保不会将动态加载的对象文件加载到不兼容的服务器中,PostgreSQL 检查该文件是否包含带有适当内容的“魔术块”。这使服务器能够检测到明显的兼容性问题,例如为不同主要版本的 PostgreSQL 编译的代码。要包含魔术块,请在模块源文件之一(且仅一个)中写入此内容,并在包含标头 fmgr.h
之后写入:
PG_MODULE_MAGIC;
首次使用后,动态加载的对象文件将保留在内存中。在同一会话中将来对该文件中的函数进行的调用只会产生符号表查找的小开销。如果需要强制重新加载对象文件,例如在重新编译之后,请开始一个新的会话。
可选地,动态加载的文件可以包含初始化函数。如果该文件包含名为 _PG_init
的函数,则会在加载该文件后立即调用该函数。该函数不接收任何参数,并且应该返回 void。目前没有办法卸载动态加载的文件。
要了解如何编写 C 语言函数,您需要了解 PostgreSQL 在内部如何表示基本数据类型以及如何将它们传递给函数和从函数传递出来。在内部,PostgreSQL 将基本类型视为“内存块”。您在类型上定义的用户定义函数反过来定义 PostgreSQL 可以对其进行操作的方式。也就是说,PostgreSQL 只会从磁盘存储和检索数据,并使用您的用户定义函数来输入、处理和输出数据。
基本类型可以具有以下三种内部格式之一:
按值传递,固定长度
按引用传递,固定长度
按引用传递,可变长度
按值类型只能是 1、2 或 4 个字节长(如果您的机器上的 sizeof(Datum)
是 8 个字节,则也可以是 8 个字节)。您应该小心定义您的类型,使其在所有架构上的大小(以字节为单位)相同。例如,long
类型是危险的,因为它在某些机器上是 4 个字节,而在其他机器上是 8 个字节,而 int
类型在大多数 Unix 机器上是 4 个字节。int4
类型在 Unix 机器上的合理实现可能是:
/* 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 |
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);
必须出现在同一个源文件中。(按照惯例,它写在函数本身之前。)PostgreSQL 假定所有内部函数都使用版本 1 约定,因此 internal
语言函数不需要此宏调用。 但是,对于动态加载的函数,它是必需的。
在版本 1 函数中,每个实际参数都使用与参数数据类型对应的 PG_GETARG_
宏来获取。(在非严格函数中,需要使用 xxx
()PG_ARGISNULL()
先前检查参数是否为空;请参阅下文。)结果使用返回类型的 PG_RETURN_
宏返回。 xxx
()PG_GETARG_
将要获取的函数参数的编号作为其参数,其中计数从 0 开始。xxx
()PG_RETURN_
将要返回的实际值作为其参数。xxx
()
以下是一些使用版本 1 调用约定的示例
#include "postgres.h" #include <string.h> #include "fmgr.h" #include "utils/geo_decls.h" #include "varatt.h" PG_MODULE_MAGIC; /* by value */ PG_FUNCTION_INFO_V1(add_one); Datum add_one(PG_FUNCTION_ARGS) { int32 arg = PG_GETARG_INT32(0); PG_RETURN_INT32(arg + 1); } /* by reference, fixed length */ PG_FUNCTION_INFO_V1(add_one_float8); Datum add_one_float8(PG_FUNCTION_ARGS) { /* The macros for FLOAT8 hide its pass-by-reference nature. */ float8 arg = PG_GETARG_FLOAT8(0); PG_RETURN_FLOAT8(arg + 1.0); } PG_FUNCTION_INFO_V1(makepoint); Datum makepoint(PG_FUNCTION_ARGS) { /* Here, the pass-by-reference nature of Point is not hidden. */ Point *pointx = PG_GETARG_POINT_P(0); Point *pointy = PG_GETARG_POINT_P(1); Point *new_point = (Point *) palloc(sizeof(Point)); new_point->x = pointx->x; new_point->y = pointy->y; PG_RETURN_POINT_P(new_point); } /* by reference, variable length */ PG_FUNCTION_INFO_V1(copytext); Datum copytext(PG_FUNCTION_ARGS) { text *t = PG_GETARG_TEXT_PP(0); /* * VARSIZE_ANY_EXHDR is the size of the struct in bytes, minus the * VARHDRSZ or VARHDRSZ_SHORT of its header. Construct the copy with a * full-length header. */ text *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ); SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ); /* * VARDATA is a pointer to the data region of the new struct. The source * could be a short datum, so retrieve its data through VARDATA_ANY. */ memcpy(VARDATA(new_t), /* destination */ VARDATA_ANY(t), /* source */ VARSIZE_ANY_EXHDR(t)); /* how many bytes */ PG_RETURN_TEXT_P(new_t); } PG_FUNCTION_INFO_V1(concat_text); Datum concat_text(PG_FUNCTION_ARGS) { text *arg1 = PG_GETARG_TEXT_PP(0); text *arg2 = PG_GETARG_TEXT_PP(1); int32 arg1_size = VARSIZE_ANY_EXHDR(arg1); int32 arg2_size = VARSIZE_ANY_EXHDR(arg2); int32 new_text_size = arg1_size + arg2_size + VARHDRSZ; text *new_text = (text *) palloc(new_text_size); SET_VARSIZE(new_text, new_text_size); memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size); memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size); PG_RETURN_TEXT_P(new_text); }
假设上面的代码已在文件 funcs.c
中准备好并编译为共享对象,我们可以使用如下命令将函数定义给 PostgreSQL
CREATE FUNCTION add_one(integer) RETURNS integer AS 'DIRECTORY
/funcs', 'add_one' LANGUAGE C STRICT; -- note overloading of SQL function name "add_one" CREATE FUNCTION add_one(double precision) RETURNS double precision AS 'DIRECTORY
/funcs', 'add_one_float8' LANGUAGE C STRICT; CREATE FUNCTION makepoint(point, point) RETURNS point AS 'DIRECTORY
/funcs', 'makepoint' LANGUAGE C STRICT; CREATE FUNCTION copytext(text) RETURNS text AS 'DIRECTORY
/funcs', 'copytext' LANGUAGE C STRICT; CREATE FUNCTION concat_text(text, text) RETURNS text AS 'DIRECTORY
/funcs', 'concat_text' LANGUAGE C STRICT;
在此,DIRECTORY
代表共享库文件的目录(例如,PostgreSQL 教程目录,其中包含本节中使用的示例代码)。 (更好的风格是在将 DIRECTORY
添加到搜索路径之后,在 AS
子句中只使用 'funcs'
。在任何情况下,我们都可以省略共享库的系统特定扩展名,通常是 .so
。)
请注意,我们将这些函数指定为 “strict”,这意味着如果任何输入值为 null,系统应自动假定为 null 结果。 通过这样做,我们避免了在函数代码中检查 null 输入。 如果没有这一点,我们将必须使用 PG_ARGISNULL()
显式检查 null 值。
宏 PG_ARGISNULL(
允许函数测试每个输入是否为 null。(当然,只有在未声明为 “strict” 的函数中才需要这样做。)与 n
)PG_GETARG_
宏一样,输入参数从零开始计数。 请注意,在验证参数不为 null 之前,不应执行 xxx
()PG_GETARG_
。 要返回 null 结果,请执行 xxx
()PG_RETURN_NULL()
;这在严格和非严格函数中都有效。
乍一看,与使用普通的 C
调用约定相比,版本 1 编码约定似乎只是毫无意义的晦涩难懂。 然而,它们确实允许我们处理可 NULL
的参数/返回值以及 “toasted”(压缩的或行外的)值。
版本 1 接口提供的其他选项是 PG_GETARG_
宏的两个变体。 第一个 xxx
()PG_GETARG_
保证返回指定参数的副本,该副本可以安全写入。(普通宏有时会返回指向物理存储在表中的值的指针,该指针不得写入。使用 xxx
_COPY()PG_GETARG_
宏可确保获得可写的结果。) 第二个变体由带有三个参数的 xxx
_COPY()PG_GETARG_
宏组成。 第一个是函数参数的编号(如上)。 第二个和第三个是要返回的段的偏移量和长度。 偏移量从零开始计数,负长度请求返回值的其余部分。 这些宏在具有 “external” 存储类型的情况下,提供了对大型值的各部分更有效的访问。(可以使用 xxx
_SLICE()ALTER TABLE
指定列的存储类型。 tablename
ALTER COLUMN colname
SET STORAGE storagetype
storagetype
是 plain
、external
、extended
或 main
中的一个。)
最后,版本 1 函数调用约定可以返回集合结果(第 36.10.8 节),并实现触发器函数(第 37 章)和过程语言调用处理程序(第 56 章)。 有关更多详细信息,请参见源发行版中的 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 节,详细了解如何针对您的特定操作系统执行此操作。
请记住为您的共享库定义一个 “magic block”,如 第 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 节。
复合类型没有像 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 字符串数组构建它,这些字符串可以传递给元组的列数据类型的输入转换函数。在任何一种情况下,您首先需要获取或构造元组结构的 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)
如果您计划使用 Datums,或者
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
如果你计划使用 C 字符串,则可以使用 C 字符串。如果你正在编写一个返回集合的函数,你可以将这些函数的结果保存在 FuncCallContext
结构中 — 分别使用 tuple_desc
或 attinmeta
字段。
当使用 Datums 时,请使用
HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
来构建一个 HeapTuple
,给定 Datum 形式的用户数据。
当使用 C 字符串时,请使用
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
来构建一个 HeapTuple
,给定 C 字符串形式的用户数据。values
是一个 C 字符串数组,每个字符串对应返回行的一个属性。每个 C 字符串应该采用属性数据类型输入函数所期望的形式。为了返回一个属性的空值,values
数组中相应的指针应该设置为 NULL
。对于你返回的每一行,都需要再次调用此函数。
一旦你构建了一个要从函数返回的元组,它必须被转换为一个 Datum
。使用
HeapTupleGetDatum(HeapTuple tuple)
将 HeapTuple
转换为有效的 Datum。如果你打算只返回一行,可以直接返回这个 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
中分配的数据将在查询结束时自动消失,因此无需手动释放该数据。)
虽然函数实际参数在调用之间保持不变,但如果在临时上下文中解压缩参数值(这通常由 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
中提供了两个例程,允许 version-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
为初始化代码提供了一个方便的位置,但严格来说,并非所有此类代码都必须放置在此钩子中。每个后端将在其附加到共享内存后不久执行注册的 shmem_startup_hook
。请注意,插件仍然应该在此钩子中获取 AddinShmemInitLock
,如上面的示例所示。
可以在 PostgreSQL 源代码树中的 contrib/pg_stat_statements/pg_stat_statements.c
中找到 shmem_request_hook
和 shmem_startup_hook
的示例。
还有另一种更灵活的保留共享内存的方法,可以在服务器启动后并在 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);
在服务器代码中的战略位置已经声明了一些注入点。在添加新的注入点后,需要编译代码以使该注入点在二进制文件中可用。用 C 语言编写的插件可以使用相同的宏在其自己的代码中声明注入点。
插件可以通过调用以下函数将回调附加到已声明的注入点
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) { uint32 wait_event_info = WaitEventInjectionPointNew(name); pgstat_report_wait_start(wait_event_info); elog(NOTICE, "%s: executed custom callback", name); pgstat_report_wait_end(); }
此回调将消息以 NOTICE
严重性打印到服务器错误日志,但回调可以实现更复杂的逻辑。
可以选择通过调用以下函数来分离注入点
extern bool InjectionPointDetach(const char *name);
成功时,返回 true
,否则返回 false
。
附加到注入点的回调在所有后端(包括在调用 InjectionPointAttach
后启动的后端)中可用。它在服务器运行时或直到使用 InjectionPointDetach
分离注入点之前保持附加状态。
示例可以在 PostgreSQL 源代码树的 src/test/modules/injection_points
中找到。
启用注入点需要在 configure
中使用 --enable-injection-points
或在 Meson 中使用 -Dinjection_points=true
。
尽管 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()
,它不会正确地展开具有非 POD 对象的 C++ 调用堆栈。
总之,最好将 C++ 代码放置在与后端接口的 extern C
函数墙后面,并避免异常、内存和调用堆栈泄漏。
如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用此表单报告文档问题。