33.7. C 语言函数 #
用户自定义函数可以用 C(或可与 C 兼容的语言,如 C++)编写。这样的函数被编译成动态可装载对象(也称共享库),由服务器按需装载。动态装载特性正是“C 语言”函数与“internal”函数的区别所在——两者的实际编码约定基本相同。(因此,标准的内部函数库是用户自定义 C 函数编码示例的丰富来源。)
目前 C 函数使用两种不同的调用约定。较新的“版本 1”调用约定通过为函数编写一个PG_FUNCTION_INFO_V1()宏来指示,如下文所示。没有这样的宏则表示这是一个旧式(“版本 0”)函数。无论哪种情况,CREATE FUNCTION中指定的语言名称都是C。旧式函数由于可移植性问题和功能缺乏,现在已被弃用,但出于兼容性原因仍然支持。
33.7.1. 动态载入 #
在一个会话中第一次调用一个特定可载入目标文件中的用户定义函数时,动态载入器会把那个目标文件载入到内存以便该函数被调用。因此用户定义的 C 函数的CREATE FUNCTION必须为该函数指定两块信息:可载入目标文件的名称,以及要在该目标文件中调用的特定函数的 C 名称(链接符号)。如果没有显式指定 C 名称,则它被假定为和 SQL 函数名相同。
下面的算法被用来基于CREATE FUNCTION
命令中给定的名称来定位共享目标文件:
如果这一序列不成功,就会把平台特定的共享库文件名扩展(通常是
.so)追加到给定名称后并再试这一序列。如果仍然失败,装载就会失败。
用于运行PostgreSQL服务器的用户 ID 必须能够通过要载入文件的路径。常见的错误是把文件或更高层的目录变得对postgres用户不可读或者不可执行。
在任何情况下,CREATE FUNCTION命令中给定的文件名会被原封不动地记录在系统目录中,这样如果需要再次载入该文件则会应用同样的过程。
注意
PostgreSQL不会自动编译 C 函数。在从CREATE FUNCTION命令中引用目标文件之前,它必须先被编译好。更多信息请见第 33.7.6 节。
在第一次使用之后,动态载入目标文件会保留在内存中。在同一个会话中,后续对该文件中函数的调用只需付出一次很小的符号表查找开销。如果需要强制重新载入一个目标文件(例如在重新编译之后),可以使用
LOAD命令或者开启一个新的会话。
建议通过相对于 $libdir 的路径,或者通过动态库路径来定位共享库。这样一来,如果新安装位于不同的位置,版本升级会更简单。$libdir 实际代表的目录可以通过命令 pg_config --pkglibdir 查出。
在PostgreSQL 7.2 版本之前,在
CREATE FUNCTION中只能指定目标文件的精确绝对路径。这种做法现在已被弃用,因为它会使函数定义变得不必要地不可移植。最好只指定不带路径和扩展名的共享库名,而让搜索机制来提供这些信息。
33.7.2. C 语言函数中的基础类型 #
要了解如何编写 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类型的实现示例:
/* 16 字节结构,传引用 */
typedef struct
{
double x, y;
} Point;
在PostgreSQL函数中传进或传出这种类型时,只能使用指向这种类型的指针。要返回这样一种类型的值,用
palloc分配正确的内存量,然后填充分配好的内存,并且返回一个指向该内存的指针。(你也可以通过返回指向某个输入值的指针来直接返回一个与返回值类型相同的输入值。但绝不要修改一个按引用传递的输入值的内容。)
最后,所有变长类型也必须按引用传递。所有变长类型都必须以一个正好 4 字节的长度字段开始,而要存储在该类型中的所有数据都必须位于紧随该长度字段之后的内存中。长度字段包含该结构的总长度,也就是说,它包含长度字段本身的尺寸。
作为一个例子,我们可以把类型 text 定义为如下形式:
typedef struct {
int4 length;
char data[1];
} text;显然,这里声明的 data 字段不足以容纳所有可能的字符串。由于在 C 中无法声明变长结构体,我们依赖于这样的知识:C 编译器不会对数组下标做范围检查。我们只需分配必要数量的空间,然后就像数组声明为正确长度那样去访问它。(这是一个常见的技巧,你可以在许多关于 C 的教科书中读到。)
在操作变长类型时,我们必须小心分配正确数量的内存,并正确设置长度字段。例如,如果我们想在一个 text
结构体中存储 40 字节,我们可以使用这样的代码片段:
#include "postgres.h" ... char buffer[40]; /* our source data */ ... text *destination = (text *) palloc(VARHDRSZ + 40); destination->length = VARHDRSZ + 40; memcpy(destination->data, buffer, 40); ...
VARHDRSZ 和 sizeof(int4)
一样,但是用宏 VARHDRSZ 来引用变长类型的额外开销的尺寸被认为是比较好的风格。
表 33.1说明了在编写使用
PostgreSQL 内置类型的 C 语言函数时,哪种 C 类型对应哪种 SQL 类型。“定义文件”列给出了需要包含的头文件,以获取类型定义。(实际的定义可能位于所列文件包含的其他文件中。建议用户坚持使用已定义的接口。)请注意,在任何源文件中都应该始终首先包含postgres.h,因为它声明了很多你反正都会用到的内容。
表 33.1. 内置 SQL 类型的等价 C 类型
| SQL 类型 | C 类型 | 定义文件 |
|---|---|---|
abstime | AbsoluteTime | utils/nabstime.h |
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 |
smallint (int2) | int2 或 int16 | postgres.h |
int2vector | int2vector* | postgres.h |
integer (int4) | int4 或 int32 | postgres.h |
real (float4) | float4* | postgres.h |
double precision (float8) | float8* | postgres.h |
interval | Interval* | utils/timestamp.h |
lseg | LSEG* | utils/geo_decls.h |
name | Name | postgres.h |
oid | Oid | postgres.h |
oidvector | oidvector* | postgres.h |
path | PATH* | utils/geo_decls.h |
point | POINT* | utils/geo_decls.h |
regproc | regproc | postgres.h |
reltime | RelativeTime | utils/nabstime.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* | utils/timestamp.h |
tinterval | TimeInterval | utils/nabstime.h |
varchar | VarChar* | postgres.h |
xid | TransactionId | postgres.h |
了解了基础类型所有可能的结构后,就可以看一些实际函数示例。
33.7.3. C 语言函数的调用约定版本 0
我们先介绍“旧风格”调用约定——虽然此方法现已弃用,但初学者更容易上手。在版本 0 方法中,C 函数的参数和结果就按普通 C 风格声明,只是要小心使用上所示的每种 SQL 数据类型的 C 表示。
下面是一些例子:
#include "postgres.h"
#include <string.h>
/* by value */
int
add_one(int arg)
{
return arg + 1;
}
/* by reference, fixed length */
float8 *
add_one_float8(float8 *arg)
{
float8 *result = (float8 *) palloc(sizeof(float8));
*result = *arg + 1.0;
return result;
}
Point *
makepoint(Point *pointx, Point *pointy)
{
Point *new_point = (Point *) palloc(sizeof(Point));
new_point->x = pointx->x;
new_point->y = pointy->y;
return new_point;
}
/* by reference, variable length */
text *
copytext(text *t)
{
/*
* VARSIZE is the total size of the struct in bytes.
*/
text *new_t = (text *) palloc(VARSIZE(t));
VARATT_SIZEP(new_t) = VARSIZE(t);
/*
* VARDATA is a pointer to the data region of the struct.
*/
memcpy((void *) VARDATA(new_t), /* destination */
(void *) VARDATA(t), /* source */
VARSIZE(t)-VARHDRSZ); /* how many bytes */
return new_t;
}
text *
concat_text(text *arg1, text *arg2)
{
int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
text *new_text = (text *) palloc(new_text_size);
VARATT_SIZEP(new_text) = new_text_size;
memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
return 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或.sl。)
注意我们把函数声明为“strict”(严格),意思是如果任何输入值为空,系统会自动假定结果为空。这样做可以避免在函数代码中检查空输入。否则,我们就必须显式检查空值,即检查每个传引用参数是否为空指针。(对于传值参数,我们甚至没有办法检查!)
尽管这种调用约定使用简单,但可移植性不好;在某些体系结构上,以这种方式传递小于int的数据类型会有问题。另外,也没有返回空结果的简单方法,除了把函数声明为严格函数之外,也没有其他处理空参数的简单方法。下面介绍的版本 1 约定克服了这些缺点。
33.7.4. C 语言函数的调用约定版本 1
版本-1 的调用约定依赖于宏来屏蔽传递参数和结果的大部分复杂性。版本-1 函数的 C 声明总是:
Datum funcname(PG_FUNCTION_ARGS)
此外,宏调用:
PG_FUNCTION_INFO_V1(funcname);
必须出现在同一个源文件中(按惯例会正好写在该函数本身之前)。这种宏调用不是internal语言函数所需要的,因为
PostgreSQL会假定所有内部函数都使用版本-1 调用约定。不过,对于动态载入函数是必需的。
在版本-1 函数中,每一个实参都使用对应于该参数数据类型的PG_GETARG_宏取得,结果要用对应于返回类型的xxx()PG_RETURN_宏返回。xxx()PG_GETARG_的参数是要取得的函数参数的编号,从零开始计。xxx()PG_RETURN_的参数是实际要返回的值。
xxx()
下面我们展示与上面相同的函数,以版本 1 风格编码:
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
/* 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);
}
/* b 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_P(0);
/*
* VARSIZE is the total size of the struct in bytes.
*/
text *new_t = (text *) palloc(VARSIZE(t));
VARATT_SIZEP(new_t) = VARSIZE(t);
/*
* VARDATA is a pointer to the data region of the struct.
*/
memcpy((void *) VARDATA(new_t), /* destination */
(void *) VARDATA(t), /* source */
VARSIZE(t)-VARHDRSZ); /* 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_P(0);
text *arg2 = PG_GETARG_TEXT_P(1);
int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
text *new_text = (text *) palloc(new_text_size);
VARATT_SIZEP(new_text) = new_text_size;
memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
PG_RETURN_TEXT_P(new_text);
}
这些函数的CREATE FUNCTION命令与版本 0 的等价形式相同。
乍看之下,版本 1 的编码约定似乎只是无谓的故弄玄虚。不过,它们确实提供了许多改进,因为宏可以隐藏不必要的细节。例如,在编码add_one_float8时,我们不再需要知道float8是传引用类型。另一个例子是,变长类型的GETARG宏允许更有效地取得“toasted”(压缩或外置)值。
版本-1 函数的一大改进是对空输入和结果的更好处理。宏PG_ARGISNULL(允许一个函数测试是否每一个输入为空值(当然,只需要在没有声明为“strict”的函数中这样做)。和n)PG_GETARG_宏一样,输入参数也是从零开始计数。注意应该在验证了一个参数不是空值之后才执行xxx()PG_GETARG_。要返回空值结果,应执行xxx()PG_RETURN_NULL(),它对严格的以及非严格的函数都有用。
新式接口中提供的其他选项是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 的函数调用约定可以返回集合结果(第 33.7.9 节)、实现触发器函数(第 35 章)和过程语言调用处理器(第 47 章)。版本-1 代码也比版本-0 更可移植,因为它不违反 C 标准中关于函数调用协议的限制。更多细节可见源代码发布中的src/backend/utils/fmgr/README。
33.7.5. 编写代码
在开始更高级的话题之前,我们应该讨论一下用于 PostgreSQL C 语言函数的编码规则。虽然有可能把不是 C 编写的函数载入到 PostgreSQL中,但即使能够做到,通常也很困难,因为其他语言(例如 C++、FORTRAN 或者 Pascal)通常不会遵循和 C 相同的调用约定。也就是说,其他语言不会以同样的方式在函数之间传递参数以及返回值。由于这个原因,我们会假定你的 C 语言函数确实是用 C 编写的。
编写和构建 C 语言函数的基本规则如下:
使用
pg_config --includedir-server查出 PostgreSQL 服务器头文件在你的系统上(或你的用户将要运行的系统上)安装于何处。这个选项是 PostgreSQL 7.2 新增的。对于 PostgreSQL 7.1,你应当使用选项--includedir。(pg_config遇到未知选项时会以非零状态退出。)对于 7.1 之前的版本,你只能去猜,但由于那是在现行调用约定引入之前,你不太可能还想要支持那些版本。为了让你的代码能够被 PostgreSQL 动态装入,编译和链接时总是需要特殊的选项。关于如何在特定操作系统上完成这件事,详见第 33.7.6 节。
分配内存时,使用 PostgreSQL 提供的
palloc和pfree,而不是相应的 C 库函数malloc和free。用palloc分配的内存会在每个事务结束时自动释放,从而避免内存泄漏。总是使用
memset将结构体的所有字节清零。如果不这么做,就很难支持哈希索引或哈希连接,因为那时你必须只挑出数据结构中真正有意义的位来计算哈希值。即使你初始化了结构体的所有字段,结构体中仍可能存在包含垃圾值的对齐填充字节(也就是结构体中的空洞)。PostgreSQL 的大多数内部类型都在
postgres.h中声明,而函数管理器接口(PG_FUNCTION_ARGS等)位于fmgr.h中,因此至少需要包含这两个文件。出于可移植性考虑,最好把postgres.h放在最前面,先于任何其他系统或用户头文件。包含postgres.h时,也会顺带为你包含elog.h和palloc.h。目标文件中定义的符号名不能彼此冲突,也不能与 PostgreSQL 服务器可执行文件中定义的符号冲突。如果你收到这类错误消息,就必须重命名相关函数或变量。
33.7.6. 编译和链接动态装载的函数 #
在你能够使用以 C 编写的 PostgreSQL 扩展函数之前,必须以特殊方式对它们进行编译和链接,以生成一个可由服务器动态装载的文件。更准确地说,需要创建一个共享库。
若想了解本节未涵盖的信息,你应阅读操作系统的文档,特别是 C 编译器
cc 和链接编辑器 ld 的手册页。此外,PostgreSQL 源代码在
contrib 目录中包含若干可用的示例。不过,如果你依赖这些示例,就会使你的模块依赖于 PostgreSQL
源代码是否可用。
创建共享库通常与链接可执行文件类似:先把源文件编译为目标文件,再把目标文件链接在一起。目标文件需要以位置无关代码(PIC),形式生成。从概念上讲,这意味着当它们被可执行文件装载时,可以放在内存中的任意位置。(面向可执行文件的目标文件通常不会这样编译。)链接共享库的命令中也包含一些特殊标志,用来把它与链接可执行文件的命令区分开来(至少理论上如此,某些系统上的实际做法要丑陋得多)。
在下面的示例中,我们假定你的源代码位于文件 foo.c 中,并将创建共享库 foo.so。除非另有说明,中间目标文件名为 foo.o。共享库可以包含多个目标文件,但这里我们只使用一个。
- BSD/OS
生成 PIC 的编译器选项是
-fpic。创建共享库时使用的链接器选项是-shared。gcc -fpic -c foo.c ld -shared -o foo.so foo.o
这从 BSD/OS 4.0 版起适用。
- 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
or:
gcc -fpic -c foo.c
and then:
ld -b -o foo.sl foo.o
与大多数其他系统不同,HP-UX 使用
.sl作为共享库扩展名。- IRIX
PIC 是默认行为,无须特殊的编译器选项。生成共享库时使用的链接器选项是
-shared。cc -c foo.c ld -shared -o foo.so foo.o
- Linux
生成 PIC 的编译器选项是
-fpic。在某些平台的某些情况下,如果-fpic不起作用,则必须使用-fPIC。更多信息请参阅 GCC 手册。创建共享库的编译器选项是-shared。完整示例如下:cc -fpic -c foo.c cc -shared -o foo.so foo.o
- MacOS X
下面是一个示例。它假定开发者工具已经安装。
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
使用 Sun 编译器时,生成 PIC 的编译器选项是
-KPIC;使用 GCC 时则是-fpic。要链接共享库,两种编译器都使用编译器选项-G,或者在使用 GCC 时改用-shared。cc -KPIC -c foo.c cc -G -o foo.so foo.o
or
gcc -fpic -c foo.c gcc -G -o foo.so foo.o
- Tru64 UNIX
PIC 是默认值,因此编译命令就是常规的那条。链接时需要使用带特殊选项的
ld。cc -c foo.c ld -shared -expect_unresolved '*' -o foo.so foo.o
使用 GCC 代替系统编译器时过程相同;不需要特殊选项。
- UnixWare
生成 PIC 的编译器选项,对于 SCO 编译器是
-K PIC,而-fpic则用于 GCC。链接共享库时,编译器选项对于 SCO 编译器是-G,对于 GCC 则是-shared。cc -K PIC -c foo.c cc -G -o foo.so foo.o
或者
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
提示
如果这些内容对你来说过于复杂,可以考虑使用 GNU Libtool,它通过统一接口隐藏了平台差异。
生成的共享库文件随后就可以装载到 PostgreSQL 中。在向 CREATE FUNCTION 命令指定文件名时,必须给出共享库文件名,而不是中间目标文件名。请注意,系统标准的共享库扩展名(通常是 .so 或 .sl)可以在 CREATE FUNCTION 命令中省略,并且通常也应省略,以获得最佳可移植性。
关于服务器期望在何处找到共享库文件,请回头参见第 33.7.1 节。
33.7.7. C 语言函数中的复合类型参数
复合类型没有像 C 结构体那样的固定布局。复合类型的实例可能包含空值字段。此外,继承层次中的复合类型可能具有和同一继承层次中其他成员不同的字段。因此,PostgreSQL提供了函数接口以便从 C 访问复合类型的字段。
假设我们想编写一个函数来回答查询
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
使用版本 0
调用约定,我们可以把
c_overpaid
定义为:
#include "postgres.h"
#include "executor/executor.h" /* for GetAttributeByName() */
bool
c_overpaid(TupleTableSlot *t, /* the current row of emp */
int32 limit)
{
bool isnull;
int32 salary;
salary = DatumGetInt32(GetAttributeByName(t, "salary", &isnull));
if (isnull)
return false;
return salary > limit;
}以版本 1 编码时,上面的函数形如:
#include "postgres.h"
#include "executor/executor.h" /* for GetAttributeByName() */
PG_FUNCTION_INFO_V1(c_overpaid);
Datum
c_overpaid(PG_FUNCTION_ARGS)
{
TupleTableSlot *t = (TupleTableSlot *) PG_GETARG_POINTER(0);
int32 limit = PG_GETARG_INT32(1);
bool isnull;
int32 salary;
salary = DatumGetInt32(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(salary > limit);
}
GetAttributeByName
是
PostgreSQL
的系统函数,返回指定行中的属性。它有三个参数:传给函数的
TupleTableSlot*
类型参数、所需属性的名称,以及一个返回参数(告知该属性是否为空)。GetAttributeByName
返回一个
Datum
值,你可以用适当的
DatumGet
宏把它转换为正确的数据类型。
XXX()
下面的命令在
SQL
中声明函数
c_overpaid:
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY/funcs', 'c_overpaid'
LANGUAGE C;
33.7.8. 从 C 语言函数中返回行(复合类型)
要从 C 语言函数中返回一行或一个复合类型值,可以使用一套特殊的 API,它通过一组宏和函数隐藏了构造复合数据类型时的大部分复杂性。要使用这套 API,源文件中必须包含:
#include "funcapi.h"
对返回复合数据类型(即行)的支持始于
AttInMetadata
结构。该结构持有从原始
C
字符串创建一行所需的各属性信息的数组。结构中包含的信息派生自
TupleDesc
结构,但存储它是为了避免在每次调用返回集合的函数时做冗余计算(见下一节)。对于返回集合的函数,AttInMetadata
结构应在第一次调用时计算一次并保存,供后续调用复用。AttInMetadata
还保存指向原始
TupleDesc
的指针。
typedef struct AttInMetadata
{
/* full TupleDesc */
TupleDesc tupdesc;
/* array of attribute type input function finfo */
FmgrInfo *attinfuncs;
/* array of attribute type typelem */
Oid *attelems;
/* array of attribute typmod */
int32 *atttypmods;
} AttInMetadata;
为帮助你填充此结构,有若干函数和一个宏可用。使用
TupleDesc RelationNameGetTupleDesc(const char *relname)
获取具名关系的
TupleDesc,或者
TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)
基于类型
OID 获取
TupleDesc。后者可用于获取基本类型或复合类型的
TupleDesc。然后
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
将返回一个指向
AttInMetadata
的指针,它基于给定的
TupleDesc
初始化。AttInMetadata
可与
C
字符串一起使用,产生格式正确的行值(内部称为元组)。
要返回元组,你必须基于
TupleDesc
创建一个元组槽。你可以使用
TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc)
初始化此元组槽,或通过其他(用户提供的)手段获得一个。需要元组槽来创建供函数返回的
Datum。同一槽位可以(而且应该)在每次调用时复用。
在构造好 AttInMetadata 结构之后,
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
可用于从
C
字符串形式的用户数据构建
HeapTuple。values
是一个
C
字符串数组,返回行的每个属性对应一个。每个
C
字符串都应具有该属性数据类型的输入函数所期望的形式。要为某个属性返回空值,应把
values
数组中相应的指针设为
NULL。对返回的每一行,都需要再次调用此函数。
只有当你的函数自然地以文本字符串计算要返回的值时,通过
TupleDescGetAttInMetadata 和
BuildTupleFromCStrings
构建元组才是方便的。如果你的代码自然地把值计算为一组
Datum
值,就应改用底层函数
heap_formtuple
把
Datum
值直接转换为元组。你仍然需要
TupleDesc 和
TupleTableSlot,但不需要
AttInMetadata。
在函数中构建好要返回的元组后,必须把它转换为
Datum。使用
TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple)
从元组和槽得到
Datum。如果你只打算返回单行,这个
Datum
可以直接返回;在返回集合的函数中,它可以用作当前的返回值。
下一节中会有一个示例。
33.7.9. 从 C 语言函数中返回集合 #
还有一种特殊的 API,为从 C 语言函数返回集合(多行)提供支持。集合返回函数必须遵循版本 1 调用约定。此外,与前述一样,源文件必须包含funcapi.h。
返回集合的函数(SRF)对它返回的每个项各调用一次。因此
SRF
必须保存足够的状态来记住自己正在做什么,并在每次调用时返回下一个项。结构
FuncCallContext
就是用来帮助控制这一过程的。在函数内,fcinfo->flinfo->fn_extra
用于跨调用保存指向
FuncCallContext
的指针。
typedef struct
{
/*
* 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.
*/
uint32 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.
*/
uint32 max_calls;
/*
* OPTIONAL pointer to result slot
*
* slot is for use when returning tuples (i.e., composite data types)
* and is not needed when returning base data types.
*/
TupleTableSlot *slot;
/*
* 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 needed 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;
} FuncCallContext;
SRF 会使用若干自动操纵 FuncCallContext 结构体(并期望通过 fn_extra 找到它)的函数和宏。使用:
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。
一个完整的伪代码示例如下:
Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
Datum result;
MemoryContext oldcontext;
further declarations as needed
if (SRF_IS_FIRSTCALL())
{
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
obtain slot
funcctx->slot = slot;
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 and just need to clean up: */
user code
SRF_RETURN_DONE(funcctx);
}
}
一个返回复合类型的简单 SRF 的完整示例如下:
PG_FUNCTION_INFO_V1(testpassbyval);
Datum
testpassbyval(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
int call_cntr;
int max_calls;
TupleDesc tupdesc;
TupleTableSlot *slot;
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_UINT32(0);
/* Build a tuple description for a __testpassbyval tuple */
tupdesc = RelationNameGetTupleDesc("__testpassbyval");
/* allocate a slot for a tuple with this tupdesc */
slot = TupleDescGetSlot(tupdesc);
/* assign slot to function context */
funcctx->slot = slot;
/*
* 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;
slot = funcctx->slot;
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 storage in our slot.
* 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 = TupleGetDatum(slot, 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 __testpassbyval AS (f1 integer, f2 integer, f3 integer);
CREATE OR REPLACE FUNCTION testpassbyval(integer, integer) RETURNS SETOF __testpassbyval
AS 'filename', 'testpassbyval'
LANGUAGE C IMMUTABLE STRICT;
源码发布中的 contrib/tablefunc 目录包含更多返回集合函数的示例。
33.7.10. 多态参数和返回类型
C 语言函数可以声明为接受和返回多态类型
anyelement 和 anyarray。关于多态函数的更详细解释,见第 33.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 则是从零开始计。
例如,假设我们想要写一个接收一个任意类型元素并且返回一个该类型的一维数组的函数:
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;
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");
/* 得到所提供的元素 */
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, 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 STRICT;
注意这里使用了STRICT;这一点至关重要,因为代码并没有费心去测试空值输入。