37.10. C 语言函数 #
用户定义的函数可以用 C 编写(或者可以与 C 兼容的语言,例如 C++)。这类函数被编译成动态载入对象(也被称为共享库)并且由服务器在需要时载入。动态载入是把“C 语言”函数和“内部”函数区分开的特性 — 两者真正的编码习惯实际上是一样的(因此,标准内部函数库为用户定义的 C 函数提供了丰富的编码示例)。
当前仅有一种调用约定被用于 C 函数(“版本 1”)。如下文所示,为函数编写一个 PG_FUNCTION_INFO_V1() 宏就能指示对该调用约定的支持。
37.10.1. 动态载入 #
在一个会话中第一次调用一个特定可载入目标文件中的用户定义函数时,动态载入器会把那个目标文件载入到内存以便该函数被调用。因此用户定义的 C 函数的 CREATE FUNCTION 必须为该函数指定两块信息:可载入目标文件的名称,以及要在该目标文件中调用的特定函数的 C 名称(链接符号)。如果没有显式指定 C 名称,则它被假定为和 SQL 函数名相同。
下面的算法被用来基于 CREATE FUNCTION
命令中给定的名称来定位共享目标文件:
如果名称是一个绝对路径,则载入给定的文件。
如果该名称不包含目录部分,会在配置变量 dynamic_library_path 指定的路径中搜索该文件。
否则(在该路径中没找到该文件,或者它包含一个非绝对目录),动态载入器将尝试接受给定的名称,这很可能会失败(依赖当前工作目录是不可靠的)。
如果这个序列不起作用,会把平台相关的共享库文件名扩展(通常是
.so)追加到给定的名称并且再次尝试上述的过程。如果还是失败,则载入失败。
建议通过相对于 $libdir 的路径,或者通过动态库路径来定位共享库。这样一来,如果新安装位于不同的位置,版本升级会更简单。$libdir 实际代表的目录可以通过命令 pg_config --pkglibdir 查出。
用于运行 PostgreSQL 服务器的用户 ID 必须能够通过要载入文件的路径。常见的错误是把文件或更高层的目录变得对 postgres 用户不可读或者不可执行。
在任何情况下,CREATE FUNCTION 命令中给定的文件名会被原封不动地记录在系统目录中,这样如果需要再次载入该文件则会应用同样的过程。
注意
PostgreSQL 不会自动编译 C 函数。在从 CREATE FUNCTION 命令中引用目标文件之前,它必须先被编译好。更多信息请见第 37.10.5 节。
为确保动态装载的目标文件不会被加载到不兼容的服务器中,PostgreSQL 会检查该文件是否包含内容正确的“魔数块”。这样服务器就能检测出明显的不兼容情况,例如代码是针对另一个 PostgreSQL 主版本编译的。要加入魔数块,请在模块某一个(且只能一个)源文件中,在包含头文件 fmgr.h 之后写入:
PG_MODULE_MAGIC;
在第一次使用之后,动态载入目标文件会保留在内存中。在同一个会话中,后续对该文件中函数的调用只需付出一次很小的符号表查找开销。如果需要强制重新载入一个目标文件(例如在重新编译之后),就需要开启一个新的会话。
动态装载的文件可以选择包含初始化和终结函数。如果文件包含一个名为 _PG_init 的函数,该函数会在文件装载后立即被调用。该函数不接收参数,并且应返回 void。如果文件包含一个名为 _PG_fini 的函数,该函数会在文件卸载前立即被调用。同样,该函数不接收参数,并且应返回 void。注意,_PG_fini 只会在卸载文件时被调用,而不会在进程终止时被调用。(目前,卸载已被禁用,永远不会发生,但将来可能会改变。)
37.10.2. C 语言函数中的基础类型 #
要了解如何编写 C 语言函数,你需要了解 PostgreSQL 如何在内部表达基础类型以及如何与函数传递它们。在内部,PostgreSQL 把基础类型视为一块“内存数据块”。你为该类型定义的用户自定义函数,决定了 PostgreSQL 如何操作它。也就是说,PostgreSQL 只负责把数据存到磁盘、再从磁盘取回,而数据的输入、处理和输出则依赖你定义的这些函数。
基础类型可以有三种内部格式之一:
传值,定长
传引用,定长
传引用,变长
传值类型在长度上只能是 1、2 或 4 字节(如果你的机器上
sizeof(Datum) 是 8,则还有 8 字节)。你应当小心地定义你的类型以便它们在所有的架构上都是相同的尺寸(字节)。例如,long 类型很危险,因为它在某些机器上是 4 字节但在另外一些机器上是 8 字节,而 int 类型在大部分 Unix 机器上都是 4 字节。在 Unix 机器上 int4 类型的一种合理实现可能是:
/* 4 字节整数,传值 */ typedef int int4;
(实际的 PostgreSQL C 代码会把这种类型称为 int32,因为
C 中的习惯是 int
表示 XXXX 位。注意因此还有尺寸为 1 字节的 C 类型 int8。SQL 类型
int8 在 C 中被称为 int64。另见表 37.2)。
另一方面,任意尺寸的定长类型都可以通过引用传递。例如,这里有一种 PostgreSQL 类型的实现示例:
/* 16 字节结构,传引用 */
typedef struct
{
double x, y;
} Point;
在 PostgreSQL 函数中传进或传出这种类型时,只能使用指向这种类型的指针。要返回这样一种类型的值,用
palloc 分配正确的内存量,然后填充分配好的内存,并且返回一个指向该内存的指针(还有,如果只想返回与具有相同数据类型的一个输入参数相同的值,可以跳过额外的 palloc 并且返回指向该输入值的指针)。
最后,所有变长类型必须也以引用的方式传递。所有变长类型必须用一个正好 4 字节的不透明长度字段开始,该字段会由 SET_VARSIZE
设置,绝不要直接设置该字段!所有要被存储在该类型中的数据必须在内存中接着该长度字段的后面存储。长度字段包含该结构体的总长度,也就是包括长度字段本身的尺寸。
另一个重点是要避免在数据类型值中留下未被初始化的位。例如,要注意把可能存在于结构体中的任何对齐填充字节置零。如果不这样做,你的数据类型的逻辑等价常量可能会被规划器认为是不等的,进而导致低效的(不过还是正确的)计划。
警告
绝不要修改通过引用传递的输入值的内容。如果这样做很可能会破坏磁盘上的数据,因为给出的指针可能直接指向一个磁盘缓冲区。这条规则唯一的例外在第 37.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 宏来设置长度字段,而不是用简单的赋值来设置。
表 37.2 展示了
PostgreSQL 中许多内置 SQL 数据类型所对应的 C 类型。“定义文件”列给出了需要包含的头文件,以获取类型定义。(实际的定义可能位于所列文件包含的其他文件中。建议用户坚持使用已定义的接口。)请注意,在服务器代码的任何源文件中,都应该始终首先包含 postgres.h,因为它声明了很多你反正都会用到的内容,而且先包含其他头文件可能会带来可移植性问题。
表 37.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 |
了解了基础类型所有可能的结构后,就可以看一些实际函数示例。
37.10.3. 版本 1 的调用约定
版本-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 调用约定的示例:
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.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((void *) VARDATA(new_t), /* destination */
(void *) 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”,这意味着如果有任何输入值为空值,系统应该自动假定结果为空值。通过这种做法,我们避免在函数代码中检查空值输入。如果不这样做,我们必须使用 PG_ARGISNULL() 明确地检查空值输入。
宏 PG_ARGISNULL( 允许一个函数测试是否每一个输入为空值(当然,只需要在没有声明为“strict”的函数中这样做)。和 n)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_ 宏有三个参数。第一个是函数参数的编号(如上文)。第二个和第三个是要被返回的段的偏移量和长度。偏移量从零开始计算,而负值的长度则表示要求返回该值的剩余部分。当大型值的存储类型为“external”时,这些宏提供了访问这些大型值的部分内容的更有效方法(列的存储类型可以使用 xxx_SLICE()ALTER TABLE 来指定。tablename ALTER COLUMN colname SET STORAGE storagetypestoragetype 取 plain、external、extended 或者 main)。
最后,版本-1 的函数调用约定可以返回集合结果(第 37.10.8 节)、实现触发器函数(第 38 章)和过程语言调用处理器(第 55 章)。更多细节可见源代码发布中的 src/backend/utils/fmgr/README。
37.10.4. 编写代码
在开始更高级的话题之前,我们应该讨论一下用于 PostgreSQL C 语言函数的编码规则。虽然有可能把不是 C 编写的函数载入到 PostgreSQL 中,但即使能够做到,通常也很困难,因为其他语言(例如 C++、FORTRAN 或者 Pascal)通常不会遵循和 C 相同的调用约定。也就是说,其他语言不会以同样的方式在函数之间传递参数以及返回值。由于这个原因,我们会假定你的 C 语言函数确实是用 C 编写的。
编写和构建 C 语言函数的基本规则如下:
使用
pg_config --includedir-server查出 PostgreSQL 服务器头文件在你的系统上(或你的用户将要运行的系统上)安装于何处。为了让你的代码能够被 PostgreSQL 动态装入,编译和链接时总是需要特殊的选项。关于如何在特定操作系统上完成这件事,详见第 37.10.5 节。
记得按照第 37.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 服务器可执行文件中定义的符号冲突。如果你收到这类错误消息,就必须重命名相关函数或变量。
37.10.5. 编译和链接动态装载函数 #
在你能够使用以 C 编写的 PostgreSQL 扩展函数之前,必须以特殊方式对它们进行编译和链接,以生成一个可由服务器动态装载的文件。更准确地说,需要创建一个共享库。
若想了解本节未涵盖的信息,你应阅读操作系统的文档,特别是 C 编译器
cc 和链接编辑器 ld 的手册页。此外,PostgreSQL 源代码在
contrib 目录中包含若干可用的示例。不过,如果你依赖这些示例,就会使你的模块依赖于 PostgreSQL
源代码是否可用。
创建共享库通常与链接可执行文件类似:先把源文件编译为目标文件,再把目标文件链接在一起。目标文件需要以位置无关代码(PIC)形式生成。从概念上讲,这意味着当它们被可执行文件装载时,可以放在内存中的任意位置。(面向可执行文件的目标文件通常不会这样编译。)链接共享库的命令中也包含一些特殊标志,用来把它与链接可执行文件的命令区分开来(至少理论上如此,某些系统上的实际做法要丑陋得多)。
在下面的示例中,我们假定你的源代码位于文件 foo.c 中,并将创建共享库 foo.so。除非另有说明,中间目标文件名为 foo.o。共享库可以包含多个目标文件,但这里我们只使用一个。
- FreeBSD
生成 PIC 的编译器选项是
-fPIC。创建共享库时使用的编译器选项是-shared。gcc -fPIC -c foo.c gcc -shared -o foo.so foo.o
这从 FreeBSD 3.0 版起适用。
- HP-UX
生成 PIC 的系统编译器选项是
+z。使用 GCC 时则是-fPIC。用于共享库的链接器选项是-b。因此:cc +z -c foo.c
或者:
gcc -fPIC -c foo.c
然后:
ld -b -o foo.sl foo.o
HP-UX 使用
.sl作为共享库扩展名,这与大多数其他系统不同。- 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 命令中省略,并且通常也应省略,以获得最佳可移植性。
关于服务器期望在何处找到共享库文件,请回头参见第 37.10.1 节。
37.10.6. 复合类型参数
复合类型没有像 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" /* 用于 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);
/* 另外,我们可能更想对空 salary 用 PG_RETURN_NULL() 。*/
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。
37.10.7. 返回行(复合类型)
要从 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 可以被直接返回,在一个集合返回函数中它也可以被当做当前的返回值。
下一节中会有一个示例。
37.10.8. 返回集合 #
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
{
/*
* 本次调用以前已经被调用过多少次
*
* SRF_FIRSTCALL_INIT() 会为你把 call_cntr 初始化为 0,
* 并且在每次调用 SRF_RETURN_NEXT() 时增加。
*/
uint64 call_cntr;
/*
* 可选:最大调用次数
*
* 这里的 max_calls 只是为了方便,设置它是可选的。
* 如果没有设置,你必须提供替代的方法来了解函数什么时候做完。
*/
uint64 max_calls;
/*
* 可选:指向用户提供的上下文信息的指针
*
* user_fctx 是一个指向你自己的数据的指针,它可用来在函数的多次
* 调用之间保存任意的上下文信息。
*/
void *user_fctx;
/*
* 可选:指向包含属性类型输入元数据的结构的指针
*
* attinmeta 被用在返回元组(即复合数据类型)时,在返回基础类型
* 时不会使用。只有想用BuildTupleFromCStrings()创建返回元组时才需要它。
*/
AttInMetadata *attinmeta;
/*
* 用于保存必须在多次调用间都存在的结构的内存上下文
*
* SRF_FIRSTCALL_INIT() 会为你设置 multi_call_memory_ctx,并且由
* SRF_RETURN_DONE() 来清理。对于任何需要在 SRF 的多次调用间都
* 存在的内存来说,它是最合适的内存上下文。
*/
MemoryContext multi_call_memory_ctx;
/*
* 可选:指向包含元组描述的结构的指针
*
* tuple_desc 被用在返回元组(即复合数据类型)时,并且只有在用
* heap_form_tuple() 而不是 BuildTupleFromCStrings() 构建元组时才需要它。
* 注意这里存储的 TupleDesc 指针通常已经被先运行过 BlessTupleDesc()。
*/
TupleDesc tuple_desc;
} FuncCallContext;
使用此基础结构的 SRF 将使用的宏是:
SRF_IS_FIRSTCALL()
来判断你的函数是否是第一次被调用。在第一次调用时(只能在第一次调用时)使用:
SRF_FIRSTCALL_INIT()
初始化 FuncCallContext。在每次函数调用时,包含第一次,调用:
SRF_PERCALL_SETUP()
设置使用 FuncCallContext。
如果你的函数在当前调用中有数据要返回,请使用:
SRF_RETURN_NEXT(funcctx, result)
把它返回给调用者(result 必须是类型 Datum,可以是一个单一值或者按上文所述准备好的元组)。最后,当函数完成了数据返回后,可使用:
SRF_RETURN_DONE(funcctx)
来清理并且结束 SRF。
调用 SRF 时当前所处的内存上下文是一个瞬时上下文,它会在两次调用之间被清空。这意味着,你不必对用 palloc 分配的所有东西都调用 pfree,因为它们反正会自动释放。不过,如果你需要分配在多次调用之间持续存在的数据结构,就必须把它们放到别处。对于任何需要一直存活到 SRF 运行结束的数据,multi_call_memory_ctx 所指向的内存上下文就是合适的位置。在大多数情况下,这意味着你应当在做首次调用初始化时切换到 multi_call_memory_ctx。可以使用 funcctx->user_fctx 保存指向这类跨调用数据结构的指针。(在 multi_call_memory_ctx 中分配的数据会在查询结束时自动消失,因此同样无需手工释放。)
警告
虽然函数的实参在多次调用之间保持不变,但如果在瞬时上下文中反 TOAST 了参数(通常由
PG_GETARG_
宏完成),那么被反 TOAST 的拷贝将在每次循环中被释放。相应地,如果你把这些值的引用保存在 xxxuser_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);
/* 这里是一次性设置代码: */
user code
if returning composite
build TupleDesc, and perhaps AttInMetadata
endif returning composite
user code
MemoryContextSwitchTo(oldcontext);
}
/* 这里是每一次都要做的设置代码: */
user code
funcctx = SRF_PERCALL_SETUP();
user code
/* 这里只是一种测试是否执行完的方法: */
if (funcctx->call_cntr < funcctx->max_calls)
{
/* 这里返回另一个项: */
user code
obtain result Datum
SRF_RETURN_NEXT(funcctx, result);
}
else
{
/* 这里已经完成了项的返回,所以只报告事实。 */
/* (不要将清理代码放在这里。) */
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;
/* 仅在函数的第一次调用时执行的操作 */
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
/* 为跨调用持久性创建函数上下文 */
funcctx = SRF_FIRSTCALL_INIT();
/* 切换到适合多次函数调用的内存上下文 */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* 要返回的元组总数 */
funcctx->max_calls = PG_GETARG_UINT32(0);
/* 为我们的结果类型构建一个元组描述符 */
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")));
/*
* 生成后续从原始C字符串生成元组所需的属性元数据
*/
attinmeta = TupleDescGetAttInMetadata(tupdesc);
funcctx->attinmeta = attinmeta;
MemoryContextSwitchTo(oldcontext);
}
/* 每次函数调用时执行的操作 */
funcctx = SRF_PERCALL_SETUP();
call_cntr = funcctx->call_cntr;
max_calls = funcctx->max_calls;
attinmeta = funcctx->attinmeta;
if (call_cntr < max_calls) /* 当还有更多要发送时执行 */
{
char **values;
HeapTuple tuple;
Datum result;
/*
* 为构建返回的元组准备一个值数组。
* 这应该是一个由后续类型输入函数处理的C字符串数组。
*/
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));
/* 构建一个元组 */
tuple = BuildTupleFromCStrings(attinmeta, values);
/* 将元组转换为 datum */
result = HeapTupleGetDatum(tuple);
/* 清理(这实际上并不是必要的) */
pfree(values[0]);
pfree(values[1]);
pfree(values[2]);
pfree(values);
SRF_RETURN_NEXT(funcctx, result);
}
else /* 当没有更多要发送时执行 */
{
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 类型。
37.10.9. 多态参数和返回类型
可以声明 C 语言函数来接受和返回第 37.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");
/* 得到提供的元素,小心它为 NULL 的情况 */
isnull = PG_ARGISNULL(0);
if (isnull)
element = (Datum) 0;
else
element = PG_GETARG_DATUM(0);
/* 只有一个维度 */
ndims = 1;
/* 和一个元素 */
dims[0] = 1;
/* 且下界是 1 */
lbs[0] = 1;
/* 得到该元素类型所需的信息 */
get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
/* 现在构建数组 */
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 后自行实现它。
37.10.10. 共享内存与 LWLocks #
附加模块可以在服务器启动时预留 LWLock 和一块共享内存。附加模块的共享库必须通过在以下参数中指定它来预装载:shared_preload_libraries。共享内存可以通过下面的调用来预留:
void RequestAddinShmemSpace(int size)
该调用应放在你的 _PG_init 函数中。
LWLock 可以通过下面的调用来预留:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
该调用应放在 _PG_init 中。这样可以确保一个包含 num_lwlocks 个 LWLock 的数组可通过以下名称访问:tranche_name。使用 GetNamedLWLockTranche 可以取得指向该数组的指针。
为避免可能的竞争条件,每个后端都应使用 LWLock AddinShmemInitLock 来连接和初始化分配给它的共享内存,如下所示:
static mystruct *ptr = NULL;
if (!ptr)
{
bool found;
LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
initialize contents of shmem area;
acquire any requested LWLocks using:
ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);
}
37.10.11. 把 C++ 用于可扩展性 #
尽管 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++ 调用栈只包含传统 C 风格的数据结构(POD)。这是必要的,因为后端错误会产生远距离的
longjmp(),它无法正确地展开具有非 POD 对象的 C++ 调用栈。
总之,最好把 C++ 代码放在与后端交互的 extern C 函数之后,并且避免异常、内存和调用栈泄漏。