用户定义的函数可以用 C 写(或者是与C兼容的语言,比如C++)。这样的函数被编译进动态加载对象(共享库)并且由服务器根据需要加载。动态加载的特性是"C 语言函数"和"内部函数"之间的区别,不过,实际的编码习惯在两者之间实际上是一样的。因此,标准的内部函数库为写用户定义C函数提供了大量最好的样例。
目前对 C 函数有两种调用约定。新的"版本-1"的调用约定是通过为该函数书写一个 PG_FUNCTION_INFO_V1() 宏来标识的,像下面演示的那样。缺少这个宏表示一个老风格的("版本-0")函数。两种风格里在 CREATE FUNCTION 里声明的都是 C 。现在老风格的函数已经废弃了,主要是因为移植性原因和缺乏功能,不过出于兼容性原因,系统仍然支持它。
当用户定义的函数第一次被服务器会话调用时,动态加载器才把可加载对象文件里的函数目标码加载进内存。因此,用于用户定义 C 函数的 CREATE FUNCTION 必须为函数声明两个信息:可加载对象文件名、在目标文件里调用的 C 函数名(连接符号)。如果没有明确声明 C 函数名,那么就假设它与 SQL 函数名相同。
基于在 CREATE FUNCTION 命令中给出的名字,下面的算法用于定位共享对象文件:
如果名字是一个绝对路径,则加载给出的文件。
如果名字以字符串 $libdir 开头,那么该部分将被 PostgreSQL 库目录名代替,该目录是在编译时确定的。
如果名字不包含目录部分,那么在配置参数 dynamic_library_path 声明的路径里查找。
如果没有在路径里找到该文件,或者它包含一个非绝对目录部分,那么动态加载器就会试图直接拿这个名字来加载,这样几乎可以肯定是要失败的(依靠当前工作目录是不可靠的)。
如果这个顺序不管用,那么就给这个名字加上平台相关的共享库文件扩展名(通常是 .so),然后再重新按照上面的过程找一遍。如果还是失败,那么加载失败。
建议使用相对于 $libdir 的目录或者通过动态库路径定位共享库。这样,如果新版本安装在一个不同的位置,那么就可以简化版本升级。$libdir 的实际目录位置可以用 pg_config --pkglibdir 命令找到。
运行 PostgreSQL 服务器的用户必须可以遍历路径到达想加载的文件。一个常见的错误就是把该文件或者一个高层目录的权限设置为 postgres 用户不可读和/或不能执行。
在任何情况下,在 CREATE FUNCTION 命令里给出的文件名是在系统表里按照文本记录的,因此,如果需要再次加载,那么会再次运行这个过程。
【注意】PostgreSQL 不会自动编译 C 函数;在使用 CREATE FUNCTION 命令之前你必须编译它。参阅节33.9.6获取更多信息。
为了确保不会错误加载共享库文件,从 PostgreSQL 8.2 开始将检查那个文件的"magic block"以确保版本兼容性。要包含"magic block",请在包含了 fmgr.h 头文件之后,将下面的内容写进一个(也只能是一个)模块的源代码文件中:
#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif
如果不打算兼容 8.2 之前的版本,#ifdef 测试也可以省略。
动态加载对象文件在首次使用之后将一直滞留在内存中。在同一个会话中的下一次调用将只需查找符号表的很小开销。如果你想强制重新加载(比如重新编译之后),可以使用 LOAD 命令或者重新开始一个新的会话。
动态加载文件也可以包含初始化函数和结束函数。如果包含一个名为 _PG_init
的函数,那么该函数将在该文件被加载后立即执行,该函数不能接受任何参数并且必须返回 void 。如果包含一个名为 _PG_fini
的函数,那么该函数将在该文件即将被卸载前执行,同样,该函数不能接受任何参数并且必须返回 void 。需要注意的是 _PG_fini
仅在该文件即将被卸载前执行而不是在会话结束的时候执行。目前,仅在明确使用 LOAD 命令重新加载文件的时候才会导致卸载先前加载的文件。
要知道如何写 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 类型的实现:
/* 16-字节结构,传递引用 */ typedef struct { double x, y; } Point;
只能使用指向这些类型的指针在 PostgreSQL 函数里传入和传出数据。要返回这样类型的值,用 palloc 分配正确数量的内存,填充这些内存,然后返回一个指向它的指针。如果只是想返回和输入参数类型与数值都相同的数值,可以忽略额外的 palloc ,只要返回指向输入数值的指针就行。
最后,所有变长类型同样也只能通过引用来传递。所有变长类型必须以一个 4 字节的长度域开始,并且所有存储在该类型中的数据必须放在紧接着长度域的存储空间里。长度域是结构的全长,也就是说,包括长度域本身的长度。
警告 |
绝对不要修改一个引用传递的输入值,否则很可能破坏磁盘上的数据。因为指针很可能直接指向一个磁盘缓冲区。这条规则的唯一例外在节33.10里。 |
比如,我们可以用下面的方法定义一个 text 类型:
typedef struct { int4 length; char data[1]; } text;
显然,上面声明的数据域长度不足以存储任何可能的字符串。因为在 C 中不可能声明变长结构,所以我们倚赖这样的知识:C 编译器不会对数组下标进行范围检查。只需要分配足够的空间,然后把数组当做已经声明为合适长度的变量访问。这是一个常用的技巧,你可以在许多 C 教科书中读到。
当处理变长类型时,必须仔细分配正确的内存数量并正确设置长度域。例如,如果想在一个 text 结构里存储 40 字节,我们可能会使用像下面的代码片段:
#include "postgres.h" ... char buffer[40]; /* 我们的源数据 */ ... text *destination = (text *) palloc(VARHDRSZ + 40); destination->length = VARHDRSZ + 40; memcpy(destination->data, buffer, 40); ...
VARHDRSZ 等价于 sizeof(int4) ,但是我们认为用宏 VARHDRSZ 表示附加尺寸是用于变长类型的更好风格。
表33-1列出了书写使用 PostgreSQL 内置类型的 C 函数里需要知道的 SQL 类型与 C 类型的对应关系。"定义在"列给出了需要包含以获取该类型定义的头文件。注意,你应该总是首先包括 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 |
既然我们已经讨论了基本类型所有可能的结构,我们便可以用实际的函数举一些例子。
先提供现在已经不提倡了的"老风格",因为比较容易迈出第一步。此风格 C 函数的参数和结果用普通 C 风格声明,但是要小心使用上面显示的 SQL 数据类型的 C 表现形式。
下面是一些例子:
#include "postgres.h" #include <string.h> /* 传递数值 */ int add_one(int arg) { return arg + 1; } /* 传递引用,定长 */ 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; } /* 传递引用,变长 */ text * copytext(text *t) { /* * VARSIZE 是结构以字节计的总长度 */ text *new_t = (text *) palloc(VARSIZE(t)); VARATT_SIZEP(new_t) = VARSIZE(t); /* * VARDATA 是结构中一个指向数据区的指针 */ 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; -- 注意:重载了名字为 add_one() 的 SQL 函数 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 或 .sl 。
请注意我们把函数声明为"strict"(严格),意思是说如果任何输入值为 NULL ,那么系统应该自动假设一个 NULL 的结果。这样处理可以让我们避免在函数代码里面检查 NULL 输入。如果不这样处理,我们就得明确检查 NULL ,比如为每个传递引用的参数检查空指针。对于传值类型的参数,我们甚至没有办法检查!
尽管这种老调用风格用起来简单,但它却不太容易移植;在一些系统上,用这种方法传递比 int 小的数据类型就会碰到困难。而且,我们没有很好的返回 NULL 结果的办法,也没有除了把函数严格化以外的处理 NULL 参数的方法。下面要讲的新方法则解决了这些问题。
版本-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()
接受要抓取的函数参数的编号(从 0 开始)作为其参数。PG_RETURN_xxx()
接受实际要返回的数值为自身的参数。
下面是和上面一样的函数,但是是用版本-1 风格编写的:
#include "postgres.h" #include <string.h> #include "fmgr.h" /* 传递数值 */ PG_FUNCTION_INFO_V1(add_one); Datum add_one(PG_FUNCTION_ARGS) { int32 arg = PG_GETARG_INT32(0); PG_RETURN_INT32(arg + 1); } /* 传递引用,定长 */ PG_FUNCTION_INFO_V1(add_one_float8); Datum add_one_float8(PG_FUNCTION_ARGS) { /* 用于 FLOAT8 的宏,隐藏其传递引用的本质 */ float8 arg = PG_GETARG_FLOAT8(0); PG_RETURN_FLOAT8(arg + 1.0); } PG_FUNCTION_INFO_V1(makepoint); Datum makepoint(PG_FUNCTION_ARGS) { /* 这里,我们没有隐藏 Point 的传递引用的本质 */ 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); } /* 传递引用,变长 */ PG_FUNCTION_INFO_V1(copytext); Datum copytext(PG_FUNCTION_ARGS) { text *t = PG_GETARG_TEXT_P(0); /* * VARSIZE 是结构以字节计的总长度 */ text *new_t = (text *) palloc(VARSIZE(t)); VARATT_SIZEP(new_t) = VARSIZE(t); /* * VARDATA 是结构中指向数据区的一个指针 */ memcpy((void *) VARDATA(new_t), /* 目的 */ (void *) VARDATA(t), /* 源 */ VARSIZE(t) - VARHDRSZ); /* 多少字节 */ 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 命令和用于老风格的等效命令一样。
猛一看,版本-1 的编码好像只是无目的地蒙人。但是它的确给我们许多改进,因为宏可以隐藏许多不必要的细节。一个例子在 add_one_float8
的编码里,这里我们不再需要不停叮嘱自己 float8 是传递引用类型。另外一个例子是用于变长类型的宏 GETARG 隐藏了抓取"非常规"(压缩的或者超长的)值需要做的处理。
版本-1 的函数另一个巨大的改进是对 NULL 输入和结果的处理。宏 PG_ARGISNULL(n)
允许一个函数测试每个输入是否为 NULL ,当然,这只是对那些没有声明为"strict"的函数有必要。因为如果有 PG_GETARG_xxx()
宏,输入参数是从零开始计算的。请注意我们不应该执行 PG_GETARG_xxx()
,除非有人声明了参数不是 NULL 。要返回一个 NULL 结果,可以执行一个 PG_RETURN_NULL()
,这样对严格的和不严格的函数都有效。
在新风格的接口中提供的其它选项是 PG_GETARG_xxx()
宏的两个变种。第一个变体 PG_GETARG_xxx_COPY()
保证返回一个指定参数的副本,该副本是可以安全地写入的。普通的宏有时候会返回一个指向物理存储在表中的某值的指针,因此我们不能写入该指针。用 PG_GETARG_xxx_COPY()
宏保证获取一个可写的结果。第二个变体由 PG_GETARG_xxx_SLICE()
宏组成,它接受三个参数。第一个是参数的个数(与上同)。第二个和第三个是要返回的偏移量和数据段的长度。偏移是从零开始计算的,一个负数的长度则要求返回该值的剩余长度的数据。这些过程提供了访问大数据值的中一部分的更有效方法,特别是数据的存储类型是"external"的时候。一个字段的存储类型可以用 ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype 指定。storagetype 是 plain, external, extended, main 之一。
版本-1 的函数调用风格也令我们可能返回一"套"结果(节33.9.10)并且实现触发器函数(章34)和过程语言调用处理器(章47)。版本-1 的代码也更容易移植,因为它没有违反 C 标准对函数调用协议的限制。更多的细节请参阅源程序中的 src/backend/utils/fmgr/README 文件。
在转到更深的话题之前,先要讨论一些 PostgreSQL C 语言函数的编码规则。虽然可以用 C 以外的其它语言(C++, FORTRAN, Pascal)书写用于 PostgreSQL 的共享函数,但通常都很麻烦,因为它们并不遵循 C 的调用习惯。也就是说,其它语言与 C 的传递参数和返回值的方式不一样。因此假设你的编程语言函数是用 C 写的。
书写和编译 C 函数的基本规则如下:
使用 pg_config --includedir-server 找出 PostgreSQL 服务器的头文件安装位置。
把你的代码编译成可以动态装入 PostgreSQL 的库文件总是需要一些特殊的标记。参阅节33.9.6获取如何在你的平台上做这件事的详细说明。
按照节33.9.1的指示为你的共享库定义一个"magic block"。
当分配内存时,用 PostgreSQL 的 palloc
和 pfree
函数取代相应的 C 库函数 malloc
和 free
。用 palloc
分配的内存在每个事务结束时会自动释放,避免了内存泄露。
记得用 memset
对结构字节清零。如果不这么做,就很难支持 Hash 索引和 Hash 连接,因为必须从数据结构中选出最具特征的位来计算 Hash 。即使你初始化了结构的所有域,仍然有可能有几个对齐字节(结构中的洞)含有垃圾值。
大多数的 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 。
gcc -fpic -c foo.c ld -shared -o foo.so foo.o
上面方法适用于 4.0 版本的 BSD/OS 。
创建 PIC 的编译器标志是 -fpic 。创建共享库的链接器标志是 -shared 。
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
上面方法适用于 3.0 版本的 FreeBSD 。
创建 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 作为共享库扩展名,和其它大部分系统不同。
PIC 是缺省,不需要使用特殊的编译器选项。创建共享库的链接器标志是 -shared 。
cc -c foo.c ld -shared -o foo.so foo.o
创建 PIC 的编译器标志是 -fpic 。在某些平台上则是 -fPIC 。参考 GCC 手册获取更多信息。创建共享库的编译器标志是 -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
用 Sun 编译器时创建 PIC 的编译器标志是 -KPIC ;用 GCC 编译器时创建 PIC 的编译器标志是 -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
PIC 是缺省,不需要使用特殊的编译器选项。带特殊选项的 ld 用于链接:
cc -c foo.c ld -shared -expect_unresolved '*' -o foo.so foo.o
用 GCC 代替系统编译器时的过程是一样的;不需要特殊的选项。
用 SCO 编译器时创建 PIC 的编译器标志是 -K PIC ;用 GCC 编译器时创建 PIC 的编译器标志是 -fpic 。链接共享库时 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 命令声明文件名的时候,必须声明共享库文件的名字而不是中间目标文件的名字。请注意你可以在 CREATE FUNCTION 命令上忽略系统标准的共享库扩展名(通常是 .so 或 .sl),并且出于最佳的兼容性考虑也应该忽略。
回头看看节33.9.1获取有关服务器预期在哪里找到共享库的信息。
如果你打算发布你的 PostgreSQL 扩展模块,那么给它们设置一个可移植的编译系统可能会相当困难。因此 PostgreSQL 提供了一个 PGXS 架构用于扩展的编译,这样,简单的扩展模块就可以在一个已经安装了的服务器上编译了。这个架构并不打算实现一个统一的编译所有与 PostgreSQL 相关的软件的架构;它只是用于自动化那些简单的服务器扩展模块的编译。对于更复杂的包,还是需要书写你自己的编译系统。
要使用该架构就必须写一个简单的 makefile并在其中设置一些变量,在结尾包括全局的 PGXS makefile 。下面是一个编译 isbn_issn 的例子,它包含一个共享库、一个 SQL 脚本、一个文档。
MODULES = isbn_issn DATA_built = isbn_issn.sql DOCS = README.isbn_issn PGXS := $(shell pg_config --pgxs) include $(PGXS)
最后两行应该总是一样的。你应该在文件的前面赋予变量或者增加自定义的 make 规则。
可以设置下列变量:
一个需要从同一个根的源代码上制作的共享对象的列表(不要在这个列表里包含后缀)
安装到 prefix/share/contrib 的随机文件
需要首先制作并安装到 prefix/share/contrib 里面的随机文件
安装到 prefix/doc/contrib 里面的随机文件
安装到 prefix/bin 里面的脚本文件(非二进制)
需要首先制作并安装到 prefix/bin 里面的脚本文件(非二进制)
回归测试案例的列表(没有后缀)
或者最多声明下面两个之一:
一个需要制作的二进制文件(在 OBJS 里面列出目标文件)
一个需要制作的共享对象(在 OBJS 里列出目标文件)
还可以设置下列变量:
在 make clean 里删除的额外文件
将增加到 CPPFLAGS
将增加到 PROGRAM 链接行里
将增加到 MODULE_big 连接行里
把这个 makefile 以 Makefile 为名保存在扩展的目录里。然后就可以运行 make 来编译,接着用 make install 来安装你的模块。这个扩展是为 pg_config 命令在你的路径里找到的第一个 PostgreSQL 编译和安装的。
在 REGRESS 变量中列出的脚本用于对你的模块进行回归测试,要使测试能够运行必须在你的扩展模块的目录下面建立一个 sql/ 子目录,并在其中为期望运行的每组测试放一个文件扩展名为 .sql 的文件,这些文件不应当包含在 REGRESS 列表中。对每个测试都必须在 expected/ 子目录中包含一个扩展名为 .out 的期望结果文件。make installcheck 将会运行测试,将输出结果与预设的期望结果文件对比,差异将按照 diff -c 格式写入 regression.diffs 文件。需要注意的是,企图运行一个丢失了期望结果文件的测试将被报告为"trouble",所以请务必确保所有期望结果文件都存在。
【提示】创建期望结果文件最简单的办法是先创建一个空文件,然后小心的检查 results/目录中的测试运行结果,确保正确以后将其复制到 expected/ 子目录中。
复合类型不像 C 结构那样有固定的布局。复合类型的实例可能包含空(NULL)字段。另外,一个属于继承层次一部分的复合类型可能和同一继承范畴的其它成员有不同的域/字段。因此,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" /* 使用 GetAttributeByName() */ bool c_overpaid(HeapTupleHeader t, /* 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" /* 使用 GetAttributeByName() */ 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); /* 另外,可能更希望将 PG_RETURN_NULL() 用在 null 薪水上 */ PG_RETURN_BOOL(DatumGetInt32(salary) > limit); }
GetAttributeByName
是 PostgreSQL 系统函数,用来返回当前记录的字段。它有三个参数:类型为 HeapTupleHeader 的传入函数的参数、你想要的字段名称、一个确定字段是否为 NULL 的返回参数。GetAttributeByName
函数返回一个 Datum 值,你可以用对应的 DatumGetXXX()
宏把它转换成合适的数据类型。请注意,如果设置了 NULL 标志,那么返回值是无意义的,在准备对结果做任何处理之前,总是要先检查 NULL 标志。
还有一个 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 描述符。在使用 Datums 的时候,你给 BlessTupleDesc
传递这个 TupleDesc 然后为每行调用 heap_form_tuple
。在使用 C 字符串的时候,你给 TupleDescGetAttInMetadata
传递 TupleDesc ,然后为每行调用 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 ,如果你想使用 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)
制作一个 HeapTuple ,它把数据以 Datum 的形式交给用户。
在使用 C 字符串的时候,用
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
制作一个 HeapTuple ,以 C 字符串的形式给出用户数据。values 是一个 C 字符串的数组,返回行的每个字段对应其中一个。每个 C 字符串都应该是字段数据类型的输入函数预期的形式。为了从其中一个字段中返回一个 NULL ,values 数组中对应的指针应该设置为 NULL 。这个函数将会需要为你返回的每个行调用一次。
一旦你制作了一个从你的函数中返回的行,那么该行必须转换成一个 Datum 。使用
HeapTupleGetDatum(HeapTuple tuple)
把一个 HeapTuple 转换为一个有效的 Datum 。如果你想只返回一行,那么这个 Datum 可以用于直接返回,或者是它可以用作在一个返回集合的函数里的当前返回值。
例子在下面给出。
还有一个特殊的 API 用于提供从 C 语言函数中返回集合(多行)。一个返回集合的函数必须遵循版本-1 的调用方式。同样,源代码必须包含 funcapi.h ,就像上面说的那样。
一个返回集合的函数(SRF)通常为它返回的每个项都调用一次。因此 SRF 必须保存足够的状态用于记住它正在做的事情以及在每次调用的时候返回下一个项。表函数 API 提供了 FuncCallContext 结构用于帮助控制这个过程。fcinfo->flinfo->fn_extra 用于保存一个跨越多次调用的指向 FuncCallContext 的指针。
typedef struct { /* * 前面已经被调用的次数 * 初始的时候,call_cntr 被 SRF_FIRSTCALL_INIT() 置为 0,并且每次你调用 SRF_RETURN_NEXT() 的时候都递增 */ uint32 call_cntr; /* * 可选的最大调用数量 * 这里的 max_calls 只是为了方便,设置它也是可选的 * 如果没有设置,你必须提供可选的方法来知道函数何时结束 */ uint32 max_calls; /* * 指向结果槽位的可选指针 * 这个数据类型已经过时,只用于向下兼容。也就是那些使用已废弃的 TupleDescGetSlot() 的用户定义 SRF */ TupleTableSlot *slot; /* * 可选的指向用户提供的杂项环境信息的指针 * user_fctx 用做一个指向你自己的结构的指针,包含任意提供给你的函数的调用间的环境信息 */ void *user_fctx; /* * 可选的指向包含属性类型输入元信息的结构数组的指针 * attinmeta 用于在返回行的时候(也就是说返回复合数据类型) * 在只返回基本(也就是标量)数据类型的时候并不需要。 * 只有在你准备用 BuildTupleFromCStrings() 创建返回行的时候才需要它 */ AttInMetadata *attinmeta; /* * 用于必须在多次调用间存活的结构的内存环境 * * multi_call_memory_ctx 是由 SRF_FIRSTCALL_INIT() 为你设置的,并且由 SRF_RETURN_DONE() 用于清理。 * 它是用于存放任何需要跨越多次调用 SRF 之间重复使用的内存 */ MemoryContext multi_call_memory_ctx; /* * 可选的指针,指向包含行描述的结构 * * tuple_desc 用于返回行(也就是说复合数据类型)并且只是在你想使用 heap_form_tuple() 而不是 BuildTupleFromCStrings() 制作行的时候需要。 * 请注意这里存储的 TupleDesc 指针通常应该先用 BlessTupleDesc() 处理。 */ TupleDesc tuple_desc; } 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 被调用时的内存环境是一个临时环境,在调用之间将会被清理掉。这意味着你不需要 pfree
所有你 palloc
的东西;它会自动消失的。不过,如果你想分配任何跨越调用存在的数据结构,那你就需要把它们放在其它什么地方。被 multi_call_memory_ctx 引用的环境适合用于保存那些需要直到 SRF 结束前都存活的数据。在大多数情况下,这意味着你在第一次调用设置的时候应该切换到 multi_call_memory_ctx 。
一个完整的伪代码例子看起来像下面这样:
Datum my_set_returning_function(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; Datum result; MemoryContext oldcontext; 更多的声明 if (SRF_IS_FIRSTCALL()) { funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* 这里放出现一次的设置代码: */ 用户定义代码 if 返回复合 制作 TupleDesc 以及可能还有 AttInMetadata endif 返回复合 用户定义代码 MemoryContextSwitchTo(oldcontext); } /* 每次都执行的设置代码在这里出现: */ 用户定义代码 funcctx = SRF_PERCALL_SETUP(); 用户定义代码 /* 这里只是用来测试是否完成的一个方法: */ if (funcctx->call_cntr < funcctx->max_calls) { /* 这里想返回另外一个条目: */ 用户代码 获取结果 Datum SRF_RETURN_NEXT(funcctx, result); } else { /* 这里完成返回条目的工作了,只需要清理就 OK 了: */ 用户代码 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 类型。
参阅源码发布包里的 contrib/tablefunc 获取更多有关返回集合的函数的例子。
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 是以 0 为基的。get_call_result_type
也可以替代 get_fn_expr_rettype
。
比如,假设想写一个函数接受任意类型的一个元素,并且返回该类型的一个一维数组:
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); /* 维数是 1 */ 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;
插件可能保留 LWLocks 并在服务器启动时分配共享内存。插件的共享库必须通过指定 shared_preload_libraries 的方法预先加载。共享内存可以通过在 _PG_init
函数中调用
void RequestAddinShmemSpace(int size)
保留。
LWLocks 可以通过在 _PG_init
中调用
void RequestAddinLWLocks(int n)
保留。
为了避免可能出现的竞争条件,每一个后端都应当在连接并初始化其共向内存时使用 LWLock AddinShmemInitLock
,示范如下:
static mystruct *ptr = NULL; if (!ptr) { bool found; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); ptr = ShmemInitStruct("my struct name", size, &found); if (!ptr) elog(ERROR, "out of shared memory"); if (!found) { initialize contents of shmem area; acquire any requested LWLocks using: ptr->mylockid = LWLockAssign(); } LWLockRelease(AddinShmemInitLock); }