用户定义函数可以使用(或语言与 C 兼容的,例如 C++)的方式编写。此类函数被编译到可动态加载的对象中(也称为共享库)中,并且由服务器按需加载。动态加载特性是将“C 语言”函数从“内部”函数中区分出来的内容 — 实际编码规范对于两者基本相同。(因此,标准内部函数库是为用户定义的 C 函数提供编码示例的丰富资源。)
目前仅使用一种调用约定用于 C 函数(“版本 1”)。对于该调用约定的支持通过编写PG_FUNCTION_INFO_V1()
宏调用为函数指示,如下所示。
在一个会话中首次调用特定可加载对象文件中的用户定义函数时,动态加载器将该对象文件加载到内存中,以便可以调用该函数。CREATE FUNCTION
对于用户定义的 C 函数因此必须为函数指定两部分信息:可加载对象文件的名称,以及在该对象文件中调用的特定函数的 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 个字节。在 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 内置 SQL 类型与 C 类型对照表。)
另一方面,固定长度且长度任意类型的对象可以按引用传递。例如,以下是 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* |
datatype/timestamp.h |
lseg |
LSEG* |
utils/geo_decls.h |
名称 |
Name |
postgres.h |
数值 |
Numeric |
utils/numeric.h |
oid |
Oid |
postgres.h |
oidvector |
oidvector* |
postgres.h |
路径 |
PATH* |
utils/geo_decls.h |
点 |
POINT* |
utils/geo_decls.h |
regproc |
RegProcedure |
postgres.h |
文本 |
text* |
postgres.h |
tid |
ItemPointer |
storage/itemptr.h |
时间 |
TimeADT |
utils/date.h |
带时区的time |
TimeTzADT |
utils/date.h |
时间戳 |
Timestamp |
datatype/timestamp.h |
带时区的timestamp |
TimestampTz |
datatype/timestamp.h |
varchar |
VarChar* |
postgres.h |
xid |
TransactionId |
postgres.h |
在研究完所有基本类型可能的结构后,我们可以展示一些真实函数的示例。
版本 1 调用约定依赖宏来消除传递参数和结果的大部分复杂性。版本 1 函数的 C 声明始终是
Datum funcname(PG_FUNCTION_ARGS)
此外,宏调用
PG_FUNCTION_INFO_V1(funcname);
必须出现在同一个源文件中。(通例,它写在函数本身之前。)对于 internal
语言函数不需要该宏调用,因为 PostgreSQL 假设所有内部函数都使用版本 1 约定。但是,动态加载函数需要它。
在版本 1 函数中,可以使用与参数数据类型相对应的 PG_GETARG_
宏来获取每个实际参数。(在非严格函数中,需要事前使用 xxx
()PG_ARGISNULL()
检查参数是否为 NULL;见下文。)使用 PG_RETURN_
宏为返回类型返回结果。 xxx
()PG_GETARG_
以要获取的函数参数编号为其参数,计数从 0 开始。 xxx
()PG_RETURN_
以要返回的实际值为其参数。xxx
()
以下是使用版本 1 调用约定的部分示例
#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(
允许函数测试每个输入是否为空。(当然,只有在未声明为 “strict” 的函数中才必须执行此操作。)与 n
)PG_GETARG_
宏一样,输入参数从零开始计数。请注意,在确认参数不为 null 之前,应避免执行 xxx
()PG_GETARG_
。要返回 null 结果,请执行 xxx
()PG_RETURN_NULL()
; 它适用于 strict 和 nonstrict 函数。
乍一看,与使用一般的 C
调用约定的版本 1 编码约定似乎只是无意义的晦涩处理。但它确实允许我们处理 NULL
able 参数/返回值以及 “烤制””(压缩或不在线)的值。
版本 1 接口提供的其他选项是 PG_GETARG_
宏的两个变量。其中第一个是 xxx
()PG_GETARG_
,它确保返回指定参数的可安全写入的副本。(普通宏有时会返回物理存储在表中的值的指针,该值不得写入。使用 xxx
_COPY()PG_GETARG_
宏确保可写结果。)第二个变量包括 xxx
_COPY()PG_GETARG_
,它接受三个参数。第一个是函数参数的编号(如上)。第二个和第三个是返回段的偏移量和长度。偏移量从 0 开始计数,而负长度请求返回余留值。在存储类型为 “external” 的情况下,这些宏提供对大值部分更高效的访问。(可以使用 xxx
_SLICE()ALTER TABLE
来指定列的存储类型。tablename
ALTER COLUMN colname
SET STORAGE 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
(对于两个编译器)或 -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
命令指定文件名时,必须为其提供共享库文件名,而不是中间对象文件。请注意,可以从 CREATE FUNCTION
命令中省略系统的标准共享库扩展名(通常为 .so
或 .sl
),并且通常应省略以获得最佳可移植性。
请参阅 第 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
函数将其转换为适当的数据类型。请注意,如果 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结构相同的struct。 (这当然要求你使用版本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,调用get_call_result_type
TupleDesc BlessTupleDesc(TupleDesc tupdesc)
如果你打算使用Datum,或者
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
如果你打算使用C字符串。如果你正在编写一个返回集合的函数,你可以将这些函数的结果保存在FuncCallContext结构中——分别使用tuple_desc或attinmeta字段。
在使用Datum时,使用heap_form_tuple
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 来发出信号。因此,返回集合函数 (SF) 必须在所有调用期间保存足够的状态,以记住自己的操作,并每次调用返回正确的下一个项目。在另一种称为 Materialize 模式的方法中,SRF 填充并返回包含其整个结果的元组存储对象;然后只会对整个结果进行一次调用,不需要调用间状态。
在使用 ValuePerCall 模式时,务必记住,不能保证查询运行到完成;也就是说,由于 LIMIT
等选项,执行器可能会在获取所有行之前停止调用返回集合函数。这意味着,在最后一次调用中执行清理活动是不安全的,因为这可能永远不会发生。建议对需要访问外部资源的函数(如文件描述符)使用 Materialize 模式。
本部分的其余内容记录了一组通常用于(虽然不是必须用于)使用 ValuePerCall 模式的 SRF 的帮助器宏。可以在 src/backend/utils/fmgr/README
中找到有关 Materialize 模式的其他详细信息。另外,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;
将使用该SF基础架构的宏是
SRF_IS_FIRSTCALL()
使用此选项可确定您的函数是首次或后续调用。在第一次(仅第一次)调用中,调用
SRF_FIRSTCALL_INIT()
以初始化 FuncCallContext
。在每次函数调用中(包括第一次),调用
SRF_PERCALL_SETUP()
以设置使用 FuncCallContext
。
如果您的函数具有的数据在当前调用中返回,请使用
SRF_RETURN_NEXT(funcctx, result)
将其返回给调用方。(result
必须为 Datum
类型,可以是单个值或一个准备好的元组,如上所述。)最后,当您的函数的数据返回完毕后,请使用
SRF_RETURN_DONE(funcctx)
清除并结束SF.
当前内存上下文SF被调用时是一个瞬态上下文,将在调用之间清除。也就是说,您不必对使用 palloc
分配的一切调用 pfree
;它无论如何都会消失。但是,如果您希望分配任何数据结构以跨调用,则需要将它们放在其他位置。由 multi_call_memory_ctx
引用的内存上下文是要生存到SF运行完毕之前的所有数据的合适位置。在大多数情况下,这意味着您应该在执行第一次调用设置时切换到 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); } }
一个完整的简单SF返回复合类型的示例如下
PG_FUNCTION_INFO_V1(retcomposite); Datum retcomposite(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; int call_cntr; int max_calls; TupleDesc tupdesc; AttInMetadata *attinmeta; /* stuff done only on the first call of the function */ if (SRF_IS_FIRSTCALL()) { MemoryContext oldcontext; /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); /* switch to memory context appropriate for multiple function calls */ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* total number of tuples to be returned */ funcctx->max_calls = PG_GETARG_INT32(0); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("function returning record called in context " "that cannot accept type record"))); /* * generate attribute metadata needed later to produce tuples from raw * C strings */ attinmeta = TupleDescGetAttInMetadata(tupdesc); funcctx->attinmeta = attinmeta; MemoryContextSwitchTo(oldcontext); } /* stuff done on every call of the function */ funcctx = SRF_PERCALL_SETUP(); call_cntr = funcctx->call_cntr; max_calls = funcctx->max_calls; attinmeta = funcctx->attinmeta; if (call_cntr < max_calls) /* do when there is more left to send */ { char **values; HeapTuple tuple; Datum result; /* * Prepare a values array for building the returned tuple. * This should be an array of C strings which will * be processed later by the type input functions. */ values = (char **) palloc(3 * sizeof(char *)); values[0] = (char *) palloc(16 * sizeof(char)); values[1] = (char *) palloc(16 * sizeof(char)); values[2] = (char *) palloc(16 * sizeof(char)); snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1)); snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1)); snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1)); /* build a tuple */ tuple = BuildTupleFromCStrings(attinmeta, values); /* make the tuple into a datum */ result = HeapTupleGetDatum(tuple); /* clean up (this is not really necessary) */ pfree(values[0]); pfree(values[1]); pfree(values[2]); pfree(values); SRF_RETURN_NEXT(funcctx, result); } else /* do when there is no more left */ { SRF_RETURN_DONE(funcctx); } }
在 SQL 中声明此函数的一种方法是
CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);
CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
RETURNS SETOF __retcomposite
AS 'filename
', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
另一种方法是使用 OUT 参数
CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
OUT f1 integer, OUT f2 integer, OUT f3 integer)
RETURNS SETOF record
AS 'filename
', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
请注意,在这种方法中,函数的输出类型正式是一个匿名 record
类型。
C 语言函数可以声明为接受和返回 第 36.2.5 节 中介绍的多态类型。当函数的参数或返回类型被定义为多态类型时,函数作者无法预先知道它将使用什么数据类型被调用,或需要返回什么数据类型。fmgr.h
中提供了两个例程,以允许版本 1 的 C 函数识别其参数的实际数据类型以及期望它返回的数据类型。这些例程称为 get_fn_expr_rettype(FmgrInfo *flinfo)
和 get_fn_expr_argtype(FmgrInfo *flinfo, int argnum)
。如果无法获取信息,则它们会返回结果或参数类型的 OID,或者 InvalidOid
。结构 flinfo
通常访问为 fcinfo->flinfo
。参数 argnum
为零基。get_call_result_type
也可以用作 get_fn_expr_rettype
的替代。还有 get_fn_expr_variadic
,该函数可以用来找出可变参数是否已被合并成一个数组。这对 VARIADIC "any"
函数很有用,因为对于采用普通数组类型的可变函数,总是会发生这种合并。
例如,假设我们要编写一个函数来接受任何类型的单个元素,并返回该类型的一维数组
PG_FUNCTION_INFO_V1(make_array); Datum make_array(PG_FUNCTION_ARGS) { ArrayType *result; Oid element_type = get_fn_expr_argtype(fcinfo->flinfo, 0); Datum element; bool isnull; int16 typlen; bool typbyval; char typalign; int ndims; int dims[MAXDIM]; int lbs[MAXDIM]; if (!OidIsValid(element_type)) elog(ERROR, "could not determine data type of input"); /* get the provided element, being careful in case it's NULL */ isnull = PG_ARGISNULL(0); if (isnull) element = (Datum) 0; else element = PG_GETARG_DATUM(0); /* we have one dimension */ ndims = 1; /* and one element */ dims[0] = 1; /* and lower bound is 1 */ lbs[0] = 1; /* get required info about the element type */ get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); /* now build the array */ result = construct_md_array(&element, &isnull, ndims, dims, lbs, element_type, typlen, typbyval, typalign); PG_RETURN_ARRAYTYPE_P(result); }
以下命令可在 SQL 中声明函数 make_array
CREATE FUNCTION make_array(anyelement) RETURNS anyarray
AS 'DIRECTORY
/funcs', 'make_array'
LANGUAGE C IMMUTABLE;
多态有一个变体只能用于 C 语言函数:它们可声明为采用类型为 "any"
的参数。(注意,此类型名称必须用双引号引用,因为它也是一个 SQL 保留字。)它的工作原理类似于 anyelement
,不同之处在于它不会将不同的 "any"
参数限制为相同类型,它们也不会帮助确定函数的结果类型。C 语言函数还可以将其最终参数声明为 VARIADIC "any"
。这将匹配一个或多个任意类型(不一定相同类型)的实际参数。这些参数将不会像普通可变参数函数那样被收集到一个数组中;它们会单独传递给函数。使用此功能时,必须使用 PG_NARGS()
宏和上面描述的方法来确定实际参数的数量及其类型。此外,此类函数的用户可能希望在其函数调用中使用 VARIADIC
关键字,并期望该函数将数组元素视为单独的参数。如果需要,函数本身必须在使用 get_fn_expr_variadic
检测实际参数已用 VARIADIC
标记后,才能实现该行为。
加载项可在服务器启动时预留共享内存。为此,加载项的共享库必须在 shared_preload_libraries 中通过指定其来预加载。共享库还应在其 _PG_init
函数中注册一个 shmem_request_hook
。此 shmem_request_hook
可通过调用预留共享内存
void RequestAddinShmemSpace(Size size)
每个后端都应通过调用来获取对保留的共享内存的指针
void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)
如果此函数将 foundPtr
设置为 false
,调用者应继续初始化保留的共享内存的内容。如果 foundPtr
设置为 true
,则共享内存已由另一个后端初始化,调用者无需进一步初始化。
为避免竞争条件,每个后端在初始化其共享内存分配时都应使用 LWLock AddinShmemInitLock
,如下所示
static mystruct *ptr = NULL; bool found; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); ptr = ShmemInitStruct("my struct name", size, &found); if (!found) { ... initialize contents of shared memory ... ptr->locks = GetNamedLWLockTranche("my tranche name"); } LWLockRelease(AddinShmemInitLock);
shmem_startup_hook
为初始化代码提供了方便的位置,但并非严格要求将所有此类代码都放在此挂钩中。每个后端将在附加到共享内存后不久执行已注册的 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
回调函数对其进行初始化。如果段已由另一个后端分配和初始化,则此函数简单地将现有动态共享内存段附加到当前后端。
与服务器启动时预留的共享内存不同,无需获取 AddinShmemInitLock
或采取其他操作以避免在使用 GetNamedDSMSegment
预留共享内存时出现争用条件。此函数可确保仅一个后端分配和初始化段,并且所有其他后端都会收到指向已完全分配和已初始化的段的指针。
可在 PostgreSQL 源树中的 src/test/modules/test_dsm_registry/test_dsm_registry.c
中找到 GetNamedDSMSegment
的完整使用示例。
加载项可以在服务器启动时预留 LWLock。与服务器启动时预留的共享内存一样,加载项的共享库必须在 shared_preload_libraries 中指定它进行预加载,并且共享库应在其 _PG_init
函数中注册 shmem_request_hook
。此 shmem_request_hook
可通过调用来预留 LWLock
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这可确保 num_lwlocks
LWLock 数组可在名称 tranche_name
下使用。可以通过调用来获取指向此数组的指针
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
另一种更灵活的方法是在服务器启动后和 shmem_request_hook
之外获取 LWLock。要执行此操作,请首先通过调用来分配 tranche_id
int LWLockNewTrancheId(void)
接下来,初始化每个 LWLock,传递新的 tranche_id
作为参数
void LWLockInitialize(LWLock *lock, int tranche_id)
类似于共享内存,每个后端都应该确保仅有一个进程分配新的 tranche_id
并初始化每个新 LWLock。执行此操作的一种方法是在独占持有 AddinShmemInitLock
的情况下,仅在您的共享内存初始化代码中调用这些函数。如果使用 GetNamedDSMSegment
,通过在 init_callback
回调函数中调用这些函数,就足够避免竞争条件。
最后,每个使用 tranche_id
的后端都应通过调用使其与 tranche_name
关联
void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
可以在 PostgreSQL 源树中的 contrib/pg_prewarm/autoprewarm.c
中找到 LWLockNewTrancheId
、LWLockInitialize
和 LWLockRegisterTranche
的完整使用示例。
外接程序可以通过调用在等待事件类型 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)。这是必需的,因为后端错误会产生一个远距离的 longjmp()
,它无法正确展开具有非 POD 对象的 C++ 调用堆栈。
总之,最好将 C++ 代码放在 extern C
函数的墙后面,使这些函数与后端进行接口,同时避免异常、内存和调用堆栈泄漏。
如果您在文档中发现任何不正确的信息,不符合您对该特定功能的体验或需要进一步澄清,请使用 此表单 报告文档问题。