|
12.5. C 语言函数用户定义的函数可以用 C 写(或者是那些可以与 C 兼容的语言, 比如 C++).这样的函数是编译进可动态装载的对象的(也叫做共享库) 并且是由服务器根据需要装载的.动态装载的特性是 "C 语言" 函数和 "内部" 函数之间相互区别的地方 --- 实际的编码习惯 在两者之间实际上是一样的.(因此,标准的内部函数库为写用户定义 C 函数提供了大量最好的样例.) 目前对 C 函数有两种调用传统.新的"版本 1"的调用传统是通过 为该函数书写一个 PG_FUNCTION_INFO_V1() 宏来标识的,象下面演示的那样.缺少这个宏标识一个老风格的 ("版本 0")函数.两种风格里在 CREATE FUNCTION 里声明的都是 'C'. 现在老风格的函数已经废弃了,主要是因为移植性原因和缺乏功能, 不过出于兼容性原因,系统仍然支持它. 12.5.1. 动态装载当某个特定的可装载对象文件里的用户定义的函数第一次被后端会话调用 时,动态装载器把函数的目标码装载入内存。 因此,用于用户定义的 C 函数的 CREATE FUNCTION 必须为函数声明两部分信息: 可装载对象文件名字,和所声明的在那个目标文件里调用的函数的 C名字(联接符号).如果没有明确声明C名字,那么就假设它与SQL函数 名相同. 基于在 CREATE FUNCTION 命令中给出的名字, 下面的算法用于定位共享对象文件∶
如果这个顺序不管用,那么就给这个给出的名字附加上 平台相关的共享库文件名扩展(通常是 .so ), 然后再重新按照上面的过程来一便.如果还是失败,那么装载失败.
在任何情况下,在 CREATE FUNCTION 命令里给出的文件名是在系统表里按照文本记录的,因此, 如果需要再次装载,那么会再次运行这个过程.
我们建议使用与 $libdir 相对的目录或者 通过动态库路径定位共享库.这样,如果新版本安装在一个不同的 位置,那么就可以简化版本升级.
12.5.2. 基本类型的 C 语言函数表 Table 12-1 列出了被装载入 PostgreSQL 的 C 函数里需要的当作参数的 C 类型。 "定义在" 列给出了等效的 C 类型定义的实际的头文件 (实际的定义可能是在包含在列出的文件所包含的文件中. 我们建议用户只使用这里定义的接口.) 注意,你应该总是首先包括 postgres.h , 因为它声明了许多你需要的东西. Table 12-1. 与内建的 PostgreSQL 类型等效的 C 类型
PostgreSQL 内部把基本类型当作"一片内存"看待. 定义在某种类型上的用户定义函数实际上定义了 PostgreSQL 对(该数据类型) 可能的操作.也就是说, PostgreSQL 只是从磁盘读取和存储该数据类型, 而使用你定义的函数来输入,处理和输出数据.基本 类型可以有下面三种内部形态(格式)之一:
传递数值的类型的长度只能是1,2 或 4 字节. (还有 8 字节,如果 sizeof(Datum) 在你的机器上是 8 的话.). 你要仔细定义你的类型, 确保它们在任何体系平台上都是相同尺寸(字节). 例如, long 型是一个危险 的类型因为在一些机器上它是 4 字节而在另外一些机器上是 8 字节,而 int 型在大多数 Unix 机器上都是4字节的(尽管不是 在多数个人微机上).在一个 Unix 机器上的 integer 合理的实现可能是: /* 4-byte integer, passed by value */ typedef int integer;
另外,任何尺寸的定长类型都可以是传递引用型.例如,下面是一个 PostgreSQL 类型的实现: /* 16-byte structure, passed by reference */ typedef struct { double x, y; } Point;
只能使用指向这些类型的指针来在 PostgreSQL 函数里输入和输出. 要返回这样的类型的值,用 palloc() 分配正确数量的存储器,填充这些存储器,然后返回一个指向它的指针. (另外,你可以通过返回指针的方法返回一个与输入数据同类型的值. 但是, 绝对不要 修改传递引用的输入数值.) 最后,所有变长类型同样也只能通过传递引用的方法来传 递.所有变长类型必须以一个4字节长的长度域开始, 并且所有存储在该类型的数据必须放在紧接着长度域的存储空间里. 长度域是结构的全长(也就是说,包括长度域本身的长度). 我们可以用下面方法定义一个 text 类型: typedef struct { integer length; char data[1]; } text;
显然,上面声明的数据域的长度不足以存储任何可能的字串.因为在 C 中不可能声明变长度的结构, 所以我们倚赖这样的知识∶ C 编译器不会对数组 下标金星范围检查.我们只需要分配足够的空间,然后把数组当做已经 声明为合适长度的变量访问.(如果你不熟悉这个技巧,那么你在开始 深入 PostgreSQL 服务器编程之前可能需要 花点时间阅读一些 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 表示附加尺寸是用于变长类型的 更好的风格. 既然我们已经讨论了基本类型所有的可能结构, 我们便可以用实际的函数举一些例子. 12.5.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 */ double precision * add_one_double precision(double precision *arg) { double precision *result = (double precision *) palloc(sizeof(double precision)); *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 ' PGROOT /tutorial/funcs' LANGUAGE C WITH (isStrict); -- note overloading of SQL function name add_one() CREATE FUNCTION add_one(float8) RETURNS float8 AS ' PGROOT /tutorial/funcs', 'add_one_float8' LANGUAGE C WITH (isStrict); CREATE FUNCTION makepoint(point, point) RETURNS point AS ' PGROOT /tutorial/funcs' LANGUAGE C WITH (isStrict); CREATE FUNCTION copytext(text) RETURNS text AS ' PGROOT /tutorial/funcs' LANGUAGE C WITH (isStrict); CREATE FUNCTION concat_text(text, text) RETURNS text AS ' PGROOT /tutorial/funcs' LANGUAGE C WITH (isStrict);
这里的 PGROOT 代表 PostgreSQL 源代码的全路径. (更好的风格应该是在向搜索路径里增加 PGROOT /tutorial 之后,在 AS 子句里只使用 'funcs' , 不管怎样,我们都可以省略和系统相关的共享库扩展, 通常是 .so 或 .sl .) 请注意我们把函数声明为"strict"(严格),意思是说如果任何输入值为NULL, 那么系统应该自动假设一个NULL的结果.这样处理可以让我们避免在 函数代码里面检查NULL输入.如果不这样处理,我们就得明确检查NULL, 比如为每个传递引用的参数检查空指针.(对于传值类型的参数,我们 甚至没有办法检查!) 尽管这种老风格的调用风格用起来简单,它确不太容易移植;在一些系统上, 我们用这种方法传递比 int 小的数据类型就会碰到困难.而且,我们 没有很好的返回NULL结果的办法,也没有除了把函数严格化以外的处理 NULL参数的方法. 下面要讲的版本-1的方法则解决了这些问题. 12.5.4. C语言函数的版本-1调用风格版本-1调用风格依赖宏来消除大多数传递参数和结果的复杂性.版本-1风格函数的 C定义总是下面这样 Datum funcname(PG_FUNCTION_ARGS) 另外,下面的宏 PG_FUNCTION_INFO_V1(funcname); 也必须出现在同一个源文件里(通常就可以写在函数自身前面). 对那些"内部"-语言函数而言,不需要调用这个宏, 因为 PostgreSQL 目前假设内部函数都是版本-1. 不过,对于动态链接的函数,它是 必须 的. 在版本-1函数里, 每个实际参数都是用一个对应该参数的数据类型的 PG_GETARG_xxx() 宏 抓取的,结果是用返回类型的 PG_RETURN_xxx() 宏返回的. 下面是和上面一样的函数,但是是用新风格编的: #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); } /* By Reference, Fixed Length */ PG_FUNCTION_INFO_V1(add_one_double precision); Datum add_one_double precision(PG_FUNCTION_ARGS) { /* The macros for FLOAT8 hide its pass-by-reference nature */ double precision 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 命令和用于老风格的等效的命令一样. 猛地一看,版本-1的编码好象只是无目的地蒙人.但是它们的确给我们 许多改进,因为宏可以隐藏许多不必要的细节. 一个例子在 add_one_float8 的编码里,这里我们不再需要不停叮嘱自己 float8 是传递引用类型.另外一个例子是用于变长类型的宏 GETARG 隐藏 了抓取 "toasted" (烤炉)(压缩的或者超长的)值需要做的处理. 上面显示的老风格的 copytext 和 concat_text 函数在处理 toasted 的值的时候 实际上是错的,因为它们在处理输入时没有调用 pg_detoast_datum() . (用于老风格动态装载函数的句柄现在会处理这些细节, 不过与版本-1函数的所有可能性相比,它做的实在是不够充分.) 版本-1的函数另一个巨大的改进是对 NULL 输入和结果的处理. 宏 PG_ARGISNULL( n ) 允许一个函数测试每个输入 是否为 NULL (当然,这件事只是对那些没有声明为 "strict" 的函数有必要).因为如果有 PG_GETARG_ xxx () 宏,输入参数是从零开始计算的.请注意我们不应该执行 PG_GETARG_ xxx () , 除非有人声明了参数不是 NULL. 要返回一个 NULL 结果,执行一个 PG_RETURN_NULL() ,这样对严格的和不严格的函数 都有效. 版本-1的函数调用风格也令我们可能返回一 "套" 结果并且实现触发器函数和过程语言调用句柄.版本-1的代码 也比版本-0的更容易移植,因为它没有违反 ANSI C 对函数调用 协议的限制.更多的细节请参阅 源程序中的 src/backend/utils/fmgr/README . 12.5.5. 复合类型的 C 语言函数复合类型不象 C 结构那样有固定的布局. 复合类型的记录可能包含空(null)域. 另外,一个属于继承层次一部分的复合类 型可能和同一继承范畴的其他成员有不同的域/字段. 因此, PostgreSQL 提供一个过程接口用于从 C 里面访问复合类型.在 PostgreSQL 处理一个记录集时, 每条记录都将作为一个类型为 TUPLE(元组) 的不透明(opaque)的结构被传递给你的函数. 假设我们 为下面查询写一个函数 SELECT name, c_overpaid(emp, 1500) AS overpaid FROM emp WHERE name = 'Bill' OR name = 'Sam'; 在上面的查询里,我们可以这样定义 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编码,上面的东西会写成下面这样:*/ 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* 的传入函数 的参数,你想要的字段名称, 以及一个用以确定字段是否为空(null)的返回参数. GetAttributeByName 函数返回一个 Datum 值, 你可以用对应的 DatumGet XXX () 宏把它转换成合适的数据类型. 下面的命令让 PostgreSQL 知道 c_overpaid 函数: CREATE FUNCTION c_overpaid(emp, integer) RETURNS bool AS ' PGROOT /tutorial/obj/funcs' LANGUAGE C;
当然还有其他方法在 C 函数里构造新的记录或修改现有记录, 这些方法都太复杂,不适合在本手册里讨论. 参考后端的源代码获取例子. 12.5.6. 书写代码我们现在转到了书写编程语言函数的更艰难的阶段. 要注意:本手册此章的内容不会让你成为程序员. 在你尝试用 C 书写用于 PostgreSQL 的函数之前,你必须对 C 有很深的了解(包括对指针的使用和 malloc 存储器管理). 虽然可以用 C 以外的其他语 言如 FORTRAN 和 Pascal 书写用于 PostgreSQL 的共享函数,但通常很麻烦(虽然是完全可能的),因为其他语言并不遵循和 C 一样的 调用习惯 . 其他语言与 C 的传递参数和返回值的方式不一样. 因此我们假设你的编程语言函数是用 C 写的. 制作 C 函数的基本规则如下:
12.5.7. 编译和链接动态链接的函数在你能够使用由 C 写的 PostgreSQL 扩展函数之前,你必须 用一种特殊的方法编译和链接它们,这样才能生成可以被服务器 动态地装载的文件.准确地说,我们需要创建一个 共享库 . 如果需要更都的信息,那么你应该阅读你的操作系统的文档, 特别是 C 编译器, cc 和链接器, ld 的手册页. 另外, PostgreSQL 源代码里包含几个 可以运行的例子,它们在 contrib 目录里. 不过,如果你依赖这些例子,那么你就要把自己的模块做得和 PostgreSQL 源代码无关才行. 创建共享库和链接可执行文件类似:首先把源代码编译成目标文件, 然后把目标文件链接起来.目标文件需要创建成 位置无关码(position-independent code) ( PIC ),概念上就是在可执行程序装载它们的时候, 它们可以放在可执行程序的内存里的任何地方, (用于可执行文件的目标文件通常不是用这个方式编译的.) 链接动态库的命令包含特殊标志,与链接可执行文件的命令是有区别的. --- 至少理论上如此.在一些系统里的现实更恶心. 在下面的例子里,我们假设你的源程序代码在 foo.c 文件里并且将创建成名字叫 foo.so 的共享库.中介的对象文件将叫做 foo.o ,除非我们另外注明.一个共享库可以 包含多个对象文件,不过我们在这里只用一个.
生成的共享库文件然后就可以装载到 PostgreSQL 里面去了.在给 CREATE FUNCTION 命令声明文件名的时候,我们必须声明 共享库文件的名字而不是中间目标文件的名字.请注意你可以在 CREATE FUNCTION 命令上忽略 系统标准的共享库扩展 (通常是 .so 或 .sl ), 并且出于最佳的兼容性考虑也应该忽略. 回去看看 Section 12.5.1 获取有关服务器 预期在哪里找到共享库的信息. |