比如,我们可以用下面方法定义一个 text 类型:
typedef struct {
integer 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 表示附加尺寸是用于变长类型的更好的风格。
表 34-1 列出了书写一个使用了 PostgreSQL 内置类型的 C 函数里需要的知道的哪个 SQL 类型对应哪个 C 类型。"定义在" 列给出了需要包含以获取该类型定义的头文件(实际的定义可能是在包含在列出的文件所包含的文件中。我们建议用户只使用这里定义的接口。)注意,你应该总是首先包括 postgres.h,因为它声明了许多你需要的东西。
| 内建类型 | 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 |
既然我们已经讨论了基本类型所有的可能结构,我们便可以用实际的函数举一些例子。
语言函数的版本-0 调用风格
我们先提供"老风格"的调用风格 — 尽管这种做法现在已经不提倡了,但它还是比较容易迈出第一步。在版本-0方法里,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), /* 目的 */
(void *) VARDATA(t), /* 源 */
VARSIZE(t)-VARHDRSZ); /* 多少字节 */
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 输入。如果不这样处理,我们就得明确检查空值,比如为每个传递引用的参数检查空指针。(对于传值类型的参数,我们甚至没有办法检查!)
尽管这种老风格的调用风格用起来简单,它确不太容易移植;在一些系统上,我们用这种方法传递比 int 小的数据类型就会碰到困难。而且,我们没有很好的返回 NULL 结果的办法,也没有除了把函数严格化以外的处理 NULL 参数的方法。下面要讲的版本-1的方法则解决了这些问题。
C 语言函数的版本-1调用风格
版本-1 调用风格依赖宏来消除大多数传递参数和结果的复杂性。版本-1 风格函数的 C 定义总是下面这样
Datum funcname(PG_FUNCTION_ARGS)
另外,下面的宏
PG_FUNCTION_INFO_V1(funcname);
也必须出现在同一个源文件里(通常就可以写在函数自身前面)。 对那些内部-语言函数而言,不需要调用这个宏, 因为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));
SET_VARSIZE(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);
SET_VARSIZE(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 隐藏了抓取"toasted"(烤炉)(压缩的或者线外存储的)值需要做的处理。
版本-1的函数另一个巨大的改进是对 NULL 输入和结果的处理。 宏 PG_ARGISNULL(n) 允许一个函数测试每个输入是否为 NULL (当然,这件事只是对那些没有声明为 "strict" 的函数有必要)。 因为如果有PG_GETARG_xxx() 宏,输入参数是从零开始计算的。请注意,除非我们检验过参数不是 NULL,否则我们不应该执行 PG_GETARG_xxx()。 要返回一个 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的函数调用风格也让我们可能返回一个结果集 (Section 34.9.10)并且实现触发器函数(Chapter 35)和过程语言调用句柄(Chapter 48)。 版本-1 的代码也比版本-0的更容易移植,因为它没有违反 C 标准对函数调用协议的限制。更多的细节请参阅源程序中的src/backend/utils/fmgr/README。
书写代码
在我们转到更深的话题之前,我们要先讨论一些PostgreSQLC 语言函数的编码规则。虽然可以用 C 以外的其他语言如书写用于PostgreSQL的共享函数,但通常很麻烦(虽然是完全可能的),因为其他像 C++,FORTRAN,或者 Pascal 这样的语言并不遵循和 C 的调用习惯。也就是说,其他语言与C的传递参数和返回值的方式不一样。因此我们假设你的编程语言函数是用 C 写的。
书写和制作 C 函数的基本规则如下:
- 使用 pg_config --includedir-server 找出PostgreSQL服务器的头文件在你的系统(或者是你的用户所运行的系统)中的安装位置。
- 编译和连接你的代码,使之可以动态装在到 PostgreSQL 中,这总是需要一些特殊的标志。参阅 34.9.6 节获取如何在你的操作系统里完成这个任务的细节。
- 记得为你的共享库定义一个“magic block”(标识块),如我们在34.9.1节说的那样。
- 当分配存储器时,用 PostgreSQL 的函数palloc和pfree取代相应的 C 库函数malloc和free。用palloc分配的存储器在每个事务结束时会自动释放,避免了内存泄露。
- 用memset对你的结构里的字节清零。如果不这么做,就很难支持 hash 索引和 hash 连接,因为我们必须从自己的数据结构中选出最具特征的位来计算 hash。即使你初始化了你的结构的所有域,仍然有可能有几个对齐字节(结构中的洞)含有垃圾值。
- 大多数的 PostgreSQL 内部类型定义在postgres.h,而函数管理器接口(PG_FUNCTION_ARGS等等。)都在fmgr.h,所以你至少应该包括这两个文件。出于移植性原因,最好先包括postgres.h,然后再包括其它系统或者用户头文件。包含了postgres.h将自动包含c.h,elog.h和 palloc.h。
- 在目标文件里定义的符号一定不能相互冲突,也不能和定义在 PostgreSQL 服务器可执行代码中的符号名字冲突。如果你看到了与此相关的错误信息,那么你必须给你的函数或者变量重命名。
编译和链接动态链接的函数
在你能够使用由 C 写 PostgreSQL 扩展函数之前, 你必须用一种特殊的方法编译和链接它们,这样才能生成可以被服务器动态地装载的文件。 准确地说,我们需要创建一个共享库。
如果需要超出本节所包含范围的信息,那么你应该阅读你的操作系统的文档, 特别是 C 编译器cc和链接器ld的手册页。另外,PostgreSQL 源代码里包含几个可以运行的例子,它们在contrib目录里。不过,如果你依赖这些例子,那么你自己的模块就会需要 PostgreSQL 源代码的存在才能编译了。
创建共享库和链接可执行文件类似:首先把源代码编译成目标文件,然后把目标文件链接起来。目标文件需要创建成位置无关码(position-independent code/PIC),概念上就是在可执行程序装载它们的时候,它们可以放在内存里的任何地方,(用于可执行文件的目标文件通常不是用这个方式编译的。)链接动态库的命令包含特殊标志,与链接可执行文件的命令是有区别的。(至少理论上如此 — 在一些系统里会更恶心。)
在下面的例子里,我们假设你的源程序代码在foo.c文件里并且将创建成名字叫foo.so的共享库。,除非我们另外注明,否则中介的对象文件将叫做foo.o。一个共享库可以包含多个对象文件,不过我们在这里只用一个。
BSD/OS
创建 PIC 的编译器标志是 -fpic。创建共享库的链接器标志是 -shared。
gcc -fpic -c foo.c ld -shared -o foo.so foo.o
上面方法适用于版本 4.0 的 BSD/OS。
FreeBSD
创建 PIC 的编译器标志是 -fpic'。创建共享库的链接器标志是 -shared。
gcc -fpic -c foo.c gcc -shared -o foo.so foo.o
上面方法适用于版本 3.0 的 FreeBSD.
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 做共享库扩展,和其它大部分系统不同。
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
创建 PIC 的编译器命令是用 Sun 编译器时为-KPIC而用 GCC 时为-fpic。链接共享库时两个编译器都可以用-G或者用 GCC 时还可以是-shared。
cc -KPIC -c foo.c cc -G -o foo.so foo.o
或
gcc -fpic -c foo.c gcc -G -o foo.so foo.o
Tru64 UNIX
PIC 是缺省,因此编译命令就是平常的那个。 带特殊选项的ld用于链接:
cc -c foo.c ld -shared -expect_unresolved '*' -o foo.so foo.o
用 GCC 代替系统编译器时的过程是一样的;不需要特殊的选项。
UnixWare
SCO 编译器创建 PIC 的标志是-K PIC,GCC 是 -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),并且出于最佳的兼容性考虑也应该忽略。
回去看看 Section 34.9.1 获取有关服务器预期在哪里找到共享库的信息。
扩展的制作架构
如果你在考虑发布你的PostgreSQL扩展模块,那么给他们设置一个可移植的制作系统可能会相当困难。因此PostgreSQL安装提供了一个用于扩展的制作架构,叫做 PGXS,这样,简单的扩展模块可以在一个已经安装了的服务器上制作了。请注意这个架构并不是企图用于实现一个统一的、可以用于制作所有与PostgreSQL相关的软件的架构;它只是用于自动化那些简单的服务器扩展模块的制作。对于更复杂的包,你还是需要书写自己的制作系统。
要在你的扩展中使用该架构,你必须写一个简单的 makefile。在该makefile里,你需要设置一些变量并且最后包括全局的 PGXS makefile。 下面是一个制作一个包含一个共享库,一个 SQL 脚本,和一个文档文本文件的叫做isbn_issn的例子:
MODULES = isbn_issn DATA_built = isbn_issn.sql DOCS = README.isbn_issn PGXS := $(shell pg_config --pgxs) include $(PGXS)
最后两行应该总是一样的。在文件的前面,你赋予变量或者增加客户化的 make 规则。
