用户自定义函数可用 C 编写(或这种语言与 C 兼容,例如 C++)。这种函数被编译成可动态加载的对象(也称为共享库),由服务器根据需要加载。动态加载特性将“C 语言”函数与“内部”函数区分开,实际编码规范这两个函数基本相同。(因此,标准内部函数库是为用户自定义 C 函数提供编码示例的丰富来源。)
目前仅针对 C 函数使用一种调用约定(“版本 1”)。通过为函数编写 PG_FUNCTION_INFO_V1() 宏调用来显示对这种调用约定的支持,如下所示。
会话中第一次调用特定可加载对象文件中的用户自定义函数时,动态加载器将该对象文件加载到内存中,以便可以调用该函数。因此,用户自定义 C 函数的 CREATE FUNCTION 必须针对该函数指定两条信息:可加载对象文件的名称和该对象文件内部调用的特定函数的 C 名称(链接符号)。如果明确指定了 C 名称,则将其视为与 SQL 函数名称相同。
以下算法用于基于 CREATE FUNCTION 命令中指定的名称来查找共享对象文件
如果该名称是绝对路径,将加载指定的文件。
如果该名称以字符串 $libdir 开头,该部分将替换为 PostgreSQL 包库目录名称,在构建时确定该目录名称。此外,请参阅 id-1.8.3.13.5.4.2.2.1.3
如果该名称不包含目录部分,将在配置变量 dynamic_library_path 指定的路径中搜索该文件。此外,请参阅 id-1.8.3.13.5.4.2.3.1.2
否则(文件未在路径中找到,或文件包含非绝对目录部分),动态加载器将尝试按原样获取名称,很有可能失败。(依赖当前工作目录并非可靠。)
如果此序列不起作用,则将特定于平台的共享库文件名扩展名(通常为 .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 个字节。在 Unix 机器上实现 int4
类型的一种合理方法可能是
/* 4-byte integer, passed by value */ typedef int int4;
(实际的 PostgreSQL C 代码将该类型称为 int32
,因为 C 中约定 int
表示 XX
XX
位。因此也请注意,C 类型 int8
的大小为 1 个字节。SQL 类型 int8
在 C 中称为 int64
。另请参见 表 36.2。)
另一方面,任何大小的固定长度类型都可以按引用传递。例如,下面是 PostgreSQL 类型的一个示例实现
/* 16-byte structure, passed by reference */ typedef struct { double x, y; } Point;
在将这些类型传递给 PostgreSQL 函数或从函数中接收时,只能使用指向此类类型的指针。如需返回此类类型的变量,可以使用 palloc
分配适量的内存,将分配的内存填满,然后返回指向它的指针。(此外,如果你只想返回与某个输入参数(类型相同)相同的值,则可以跳过额外的 palloc
,并直接返回指向输入变量的指针。)
最后,所有可变长度类型也必须通过引用传递。所有可变长度类型都必须以一个 4 字节精度的长度字段开头,该字段将由 SET_VARSIZE
设置;切勿直接设置该字段!要存储在此类型中的所有数据都必须位于该长度字段紧后的内存中。长度字段包含结构的总长度,即它包括长度字段本身的大小。
另一个重要要点是避免在数据类型值中保留任何未初始化的位;例如,注意将结构中可能存在的任何对齐填充字节清零。如果没有这样做,数据类型的逻辑等价常量可能会被规划器视为不等于,从而导致低效的(虽然不不正确的)计划。
切勿修改引用传递的输入值的内容。如果您这样做,很有可能损坏磁盘上的数据,因为给您的指针可能直接指向磁盘缓冲区。此规则的唯一例外在 第 36.12 节 中进行了解释。
作为一个示例,我们可以将类型 text
定义如下
typedef struct { int32 length; char data[FLEXIBLE_ARRAY_MEMBER]; } text;
[FLEXIBLE_ARRAY_MEMBER]
表示数据部分的实际长度没有由声明指定。
在处理可变长度类型时,我们必须注意分配正确的内存量并正确设置长度字段。例如,如果我们要将 40 个字节存储在 text
结构中,我们可以使用如下代码段
#include "postgres.h" ... char buffer[40]; /* our source data */ ... text *destination = (text *) palloc(VARHDRSZ + 40); SET_VARSIZE(destination, VARHDRSZ + 40); memcpy(destination->data, buffer, 40); ...
VARHDRSZ
相同于 sizeof(int32)
,但使用宏 VARHDRSZ
来表示可变长度类型的开销大小被认为是一种良好的风格。此外,长度字段必须使用 SET_VARSIZE
宏设置,而不是通过简单赋值设置。
Built-in SQL 类型对应的 C 类型">表 36.2列出了许多 PostgreSQL 内置 SQL 数据类型对应的 C 类型。在 “定义在” 列中给出了获取类型定义所需的标头文件。(实际定义可能在所列文件包含的不同文件中。建议用户坚持使用已定义的界面。)请注意,您应始终在任何服务器代码的源文件中首先包含 postgres.h
,因为它声明了许多您无论如何都会用到的内容,并且首先包含其他标头会导致移植性问题。
表 36.2.Built-in 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);
必须出现在同一个源文件中。(通常,它写在函数本身之前。) 对于 internal
语言函数不需要此宏调用,因为 PostgreSQL 假设所有内部函数都使用 1 版约定。但是,对于动态加载的函数是必需的。
在版本 1 函数中,每个实际参数使用与其数据类型对应的PG_GETARG_xxx
() 代码段获取。(在非严格函数中,需要使用PG_ARGISNULL()先检查参数是否为空;请见下文。)结果使用返回类型的PG_RETURN_xxx
() 代码段返回。PG_GETARG_xxx
() 的参数为要获取的函数参数的编号,编号从 0 开始。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教程目录)。(更好的做法是在AS
条款中仅使用'funcs'
,在将DIRECTORY
添加到搜索路径后。在任何情况下,我们都可以省略共享库的系统特定扩展名,通常为.so
。)
请注意,我们已将函数指定为“strict”,这意味着如果任何输入值为空,系统应自动假定结果为空。通过此操作,我们避免在函数代码中检查空输入。如果不这样做,我们将不得不使用PG_ARGISNULL() 明确检查空值。
代码段PG_ARGISNULL(n
) 允许函数测试每个输入是否为空。(当然,仅在未声明为“strict”的函数中执行此操作才有必要。)与PG_GETARG_xxx
() 代码段一样,输入参数的编号从 0 开始。请注意,应先验证参数不为空,然后才能执行PG_GETARG_xxx
()。若要返回空结果,请执行PG_RETURN_NULL();此操作在严格函数和非严格函数中均有效。
乍一看,与使用纯C
调用约定相比,版本 1 编码约定似乎只是毫无意义的模糊处理。但是,它们确实允许我们处理NULL
able 参数/返回值和““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 节。
记住,如 第 36.10.1 节 中所述,为您的共享库定义一个 “magic block”。
在分配内存时,使用 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
命令指定文件名时,必须向其提供共享库文件的名称,而不是中间对象文件。注意, CREATE FUNCTION
命令可以省略系统的标准共享库扩展名(通常为 .so
或 .sl
),且为了最大程度实现可移植性,通常应将其省略。
请参阅 第 36.10.1 节,了解服务器期望在何处查找共享库文件。
复合类型没有固定的版式,类似于 C 结构。复合类型的实例可能包含 null 字段。此外,构成继承层次结构一部分的复合类型可能有不同于同一层次结构中其他成员的字段。因此,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
值,您可以使用相应的 DatumGet
函数将其转换为适当的数据类型。请注意,如果设置了 null 标志,则返回值没有意义;在尝试对结果执行任何操作之前,始终先检查 null 标志。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,源文件必须包括
#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
填充。(如果不是,则可报告 ““函数返回记录在无法接受记录类型的上下文中调用”” 类的错误。)
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)
如果你计划使用数据,或
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
如果你计划使用 C 字符串。如果你正在编写返回集合的函数,则可将下列函数的结果保存在 FuncCallContext
结构中——分别使用 tuple_desc
或 attinmeta
字段。
在使用数据时,请使用
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 填充并返回一个包含其整个结果的元组存储对象;然后只对整个结果执行一次调用,并且不需要跨调用状态。
在使用 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
中提供了两个例程,它们允许版本 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
。该 shmem_request_hook
可通过调用预留共享内存
void RequestAddinShmemSpace(Size size)
每个后端都应该通过调用获得保留的共享内存的指针
void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)
如果此函数将 foundPtr
设置为 false
,则调用者应继续初始化保留的共享内存的内容。如果将 foundPtr
设置为 true
,则共享内存已被另一个后端初始化,并且调用者无需进一步初始化。
为了避免竞争条件,每个后端都应在初始化其共享内存分配时使用 LWLock AddinShmemInitLock
,如下所示
static mystruct *ptr = NULL; bool found; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); ptr = ShmemInitStruct("my struct name", size, &found); if (!found) { ... initialize contents of shared memory ... ptr->locks = GetNamedLWLockTranche("my tranche name"); } LWLockRelease(AddinShmemInitLock);
shmem_startup_hook
为初始化代码提供了一个便利的位置,但并非严格要求将所有此类代码都放置在此挂钩中。每个后端都将在附加到共享内存后不久执行已注册的 shmem_startup_hook
。请注意,加载项仍应在此挂钩中获取 AddinShmemInitLock
,如上例所示。
shmem_request_hook
和 shmem_startup_hook
实例可以在 PostgreSQL 源代码树的 contrib/pg_stat_statements/pg_stat_statements.c
中找到。
有一种更灵活的方法可以预留共享内存,可以在服务器启动后和一个 shmem_request_hook
之外进行。为此,将要使用共享内存的每个后端都应通过调用获取一个对其指针
void *GetNamedDSMSegment(const char *name, size_t size, void (*init_callback) (void *ptr), bool *found)
如果具有给定名称的动态共享内存段尚不存在,则此函数将分配它并使用提供的 init_callback
回调函数初始化它。如果该段已经被另一个后端分配并初始化,则此函数只是将现有的动态共享内存段附加到当前后端。
与在服务器启动时保留的共享内存不同,在使用 GetNamedDSMSegment
保留共享内存时不需要获取 AddinShmemInitLock
或采取其他操作来避免竞争条件。此函数可确保只有一个后端会分配和初始化该段,并且所有其他后端都将接收到完全分配和初始化的段的指针。
GetNamedDSMSegment
完整用法示例可以在 PostgreSQL 源代码树的 src/test/modules/test_dsm_registry/test_dsm_registry.c
中找到。
外接程序可以在服务器启动时预留 LWLocks。与服务器启动时预留的共享内存一样,外接程序的共享库必须通过在 shared_preload_libraries 中指定它来预加载,并且共享库应该在其 _PG_init
函数中注册一个 shmem_request_hook
。此 shmem_request_hook
可以通过调用来预留 LWLocks
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这可确保有一个 num_lwlocks
个 LWLocks 的数组可以在名字 tranche_name
下得到。可以调用来获取此数组的一个指针
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
另一种更灵活的方法是在服务器启动后和 shmem_request_hook
外部获取 LWLocks。为此,首先通过调用来分配一个 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
函数的顶层使用捕获所有异常的块)。即使 C++ 代码没有明确抛出任何异常,这样做也是必要的,因为像内存不足这样的事件仍然会抛出异常。必须捕获任何异常,并将相应的错误传递回 C 接口。如果可能,请使用 -fno-exceptions
编译 C++ 以完全消除异常;在这种情况下,必须检查 C++ 代码中的故障,例如,检查 new()
返回的 NULL。
如果从 C++ 代码调用后端函数,请确保 C++ 调用堆栈仅包含普通旧数据结构(POD)。这是必要的,因为后端错误会生成一个 distant longjmp()
,它无法正确展开使用非 POD 对象的 C++ 调用堆栈。
综上所述,最好将 C++ 代码置于一个 extern C
函数防火墙之后,该函数与后端接口,并避免异常、内存和调用堆栈泄漏。