本文编译自PostgreSQL官方文档第38.10 C-Language Functions
可以用C(或者与C兼容,比如C++)语言编写用户自定义函数(User-defined functions)。这些函数被编译到动态可加载目标文件(也称为共享库)中并被守护进程加载到服务中。“C语言函数”与“内部函数”的区别就在于动态加载这个特性,二者的实际编码约定本质上是相同的(因此,标准的内部函数库为用户自定义C语言函数提供了丰富的示例代码)。
当前C语言函数只有一种调用约定(“Version 1”)。通过为函数编写PG_Function_INFO_V1()宏调用来表示对该调用约定的支持,后面会举例说明。
一、动态加载
首次在会话中调用位于特定可加载目标文件中的用户用定义函数。当使用CREATE FUNCTION命令为用户自定义C函数创建SQL函数时必须为该函数指定两条信息:可加载目标文件名以及位于目标文件中的欲调用的C语言函数名(链接符号)。如果没有给出C语言函数名,则假设与SQL函数名相同。
CREATE FUNCTION命令使用以下逻辑定位给定名字的共享目标文件:
- 如果名字是绝对路径,加载该文件;
- 如果名字以libdir开始,libdir开始,libdir被替换成构建PostgreSQL时指定的库目录名;
- 如果名字中未包含目录,则在配置变量dynamic_library_path指定的目录中搜索;
- 其他情况(在指定路径中未发现文件或者文件名包含非绝对路径),动态加载器尝试按给定的名字加载,这很可能会失败(依赖当前工作目录是不可靠的。)
如果按以上顺序依旧无法正常加载,则给指定的名字加上平台特定的动态加载库的扩展名(通常是.so),然后按上述顺序再试一次。如果依旧失败,则整个加载过程失败。
推荐将可加载目标文件存放在libdir或者动态库路径指定的目录中。如果新安装位于不同的位置,可简化版本升级。可使用命令pg_config --pkglibdir找到libdir实际指向的目录。
运行PostgreSQL服务的用户必须对要加载的文件的路径有遍历的权限。postgres用户对文件或更高级别的目录没有可读/或可执行权限是一个常见的错误。
在任何情况下,CREATE FUNCTION命令中给定的文件名都会记录在系统目录(catalog)中,如果需要再次加载该文件,执行应用相同的过程即可。
PostgreSQL会检查动态可加载目标文件中是否包含特定内容的“magic block”以确保其不被加载到不兼容的服务中。这允许服务器检测明显的不兼容,比如代码使用不同的PostgreSQL主版本编译。要加入magic block,只需在代码的#include "fmgr.h"头文件的下面加入以下代码(只能加一次):
PG_MODULE_MAGIC;
首次使用动态可加载目标文件后,它将被保留在内存中。以后在同一会话中调用该文件中的函数时,仅需非常小的符号表查找开销。如果你需要强制重新加载目标文件(比如在重新编译后),开户一个新会话即可。
动态可加载文件可包含一个初始化函数(可选)。如果文件包含一个名为_PG_init的函数,完成加载后该函数将立即被调用。该函数不带参数也不返回任何结果。目前暂无方法卸载动态可加载文件。
二、C语言函数中的基类型
要了解如何写C语言函数,你需要了解在PostgreSQL内部如何表示基本数据类型并如何在函数间传递它们。在内部,PostgreSQL将基本类型视为“内存blob”。你在类型上定义的用户定义函数反过来定义了PostgreSQL可以对其进行操作的方式。也就是说,PostgreSQL只从磁盘存储和获取数据,然后使用你的用户自定义函数去输入、处理、输出数据。
基本类型可具有以下三种内部格式之一:
- 按值传递,定长
- 按引用传递,定长
- 按引用传递,变长
按值类型的长度只能是1、2、4字节(如果在你的机器上sizeof(Datum)=8,也可为8字节)。你必须仔细定义你的类型以保证它们在所有的架构下长度都相同。比如:long类型就很危险,因为它在有些机器上是4字节但在另一类机器上又是8字节,而在大多数Unix机器上,int类型都是4字节。在Unix机器上,可像这样合理实现int4类型:
/* 4-byte integer, passed by value */typedefintint4;
(实际上,PostgreSQL C代码调用此类型int32,因为在C中有个约定,intXX表示XX位。因些还要注意,C类型int8的长度是1字节而SQL类型的int8在C中是int64。)
另一方面,任何大小的定长类型都可以通过引用传递。下面是一个PostgreSQL类型的简单实现的例子:
/* 16-byte structure, passed by reference */typedefstruct{ doublex, y; } Point;
当向PostgreSQL函数传入传出该类型时,只能使用指向该类型的指针。要返回该类型,要使用palloc分配正确大小的内存,接着填充分配好的内存,然后返回指向它的指针。(当然,如果你只是想返回与你输入的某个参数具有相同数据类型的值,你可以跳过额外的palloc,仅返回指向输入值的指针。)。
Finally, 所有可变长类型必须通过引用传递。所有的可变长类型必须以一个确切的4字节的长度字段开头,该字段通过SET_VARSIZE设置;永远不要直接对该字段赋值!在内存中,所有保存在该类型中的数据必须紧跟在长度字段之后。长度字段包含结构的总长度,长度字段自身的大小也包含其中。
另一个重点是要避免在数据类型值中有任何未初始化的数据位;例如,注意将结构中可能存在的任何对齐填充字节清零。如果没有这一点,规划器可能会认为数据类型的逻辑等效常量不相等,从而导致执行计划效率低下(尽管不是不正确)。
举个例子,我们可以定义像下面这样定义 text
类型:
typedefstruct { int32length; chardata[FLEXIBLE_ARRAY_MEMBER]; } text;
[FLEXIBLE_ARRAY_MEMBER]注解表示数据部分的实际长度不在此定义是指定。
当维护可变长类型时,我们必须仔细分配合适数量的内存并正确设置长度字段。假如你想保存40个字节到text结构中,需要使用如下代码片段:
... charbuffer[40]; /* our source data */... text*destination= (text*) palloc(VARHDRSZ+40); SET_VARSIZE(destination, VARHDRSZ+40); memcpy(destination->data, buffer, 40); ...
VARHDRSZ与sizeof(int32)相同,但使用宏VARHDRS4来引用可变长度类型的开销大小被认为是一种不错的风格。此外,长度字段必须使用SET_VARSIZE宏设置,而不是通过简单的赋值。
下表显示了与PostgreSQL的许多内置SQL数据类型相对应的C类型。“Defined In”列给出了必须被包含以取得类型定义的头文件。(实际定义可能位于下表列出的不同文件中。推荐用户只会用已定义的接口)。在服务端代码文件中,你应该总是首先包含 postgres.h
,因为它声明了许多你无论如何都需要的内容,并且先包含其他头文件可能会导致可移植性问题。
内置SQL类型的等效C类型
SQL Type | C Type | Defined In |
boolean |
bool |
postgres.h (maybe compiler built-in) |
box |
BOX* |
utils/geo_decls.h |
bytea |
bytea* |
postgres.h |
"char" |
char |
(compiler built-in) |
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 |
现在我们已经讨论了基本类型的所有可能结构,接下来展示一些真实函数的示例。
三、版本1(Version 1)调用约定
版本1调用约定依赖于宏来抑制传递参数和结果的复杂性。版本1的C函数总是像下面这样声明:
Datumfuncname(PG_FUNCTION_ARGS)
与此同时,宏调用:
PG_FUNCTION_INFO_V1(funcname);
必须出现在相同的源文件中(我们约定总是将其放在函数声明之前)。内置语言函数不需要此宏调用,因为PostgreSQL假设所有的内置函数都使用版本1调用约定。仅动态加载的函数有此要求。
在版本1的函数中,使用对应到参数实际的数据类型的PG_GETARG_xxx()宏取得参数。(在非严格模式,需要先使用PG_ARGISNULL()宏检查参数是否为空,见下面的例子)。对于指定类型,使用PG_RETURN_xxx() 宏返回结果。PG_GETARG_xxx() 宏根据函数参数的编号获取相应的参数值,编号从0开始计数。PG_RETURN_xxx() 返回实际的参数值。
下面是一些使用版本-1调用约定的例子:
PG_MODULE_MAGIC; /* 传值 */PG_FUNCTION_INFO_V1(add_one); Datumadd_one(PG_FUNCTION_ARGS) { int32arg=PG_GETARG_INT32(0); PG_RETURN_INT32(arg+1); } /* 传引用,定长 */PG_FUNCTION_INFO_V1(add_one_float8); Datumadd_one_float8(PG_FUNCTION_ARGS) { /* The macros for FLOAT8 hide its pass-by-reference nature. */float8arg=PG_GETARG_FLOAT8(0); PG_RETURN_FLOAT8(arg+1.0); } PG_FUNCTION_INFO_V1(makepoint); Datummakepoint(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); } /* 传引用,变长 */PG_FUNCTION_INFO_V1(copytext); Datumcopytext(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(VARDATA(new_t), /* destination */VARDATA_ANY(t), /* source */VARSIZE_ANY_EXHDR(t)); /* how many bytes */PG_RETURN_TEXT_P(new_t); } PG_FUNCTION_INFO_V1(concat_text); Datumconcat_text(PG_FUNCTION_ARGS) { text*arg1=PG_GETARG_TEXT_PP(0); text*arg2=PG_GETARG_TEXT_PP(1); int32arg1_size=VARSIZE_ANY_EXHDR(arg1); int32arg2_size=VARSIZE_ANY_EXHDR(arg2); int32new_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 integerAS'DIRECTORY/funcs','add_one' LANGUAGE C STRICT;-- note overloading of SQL function name "add_one"CREATE FUNCTION add_one(doubleprecision) RETURNS doubleprecisionAS'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 textAS'DIRECTORY/funcs','copytext' LANGUAGE C STRICT;CREATE FUNCTION concat_text(text,text) RETURNS textAS'DIRECTORY/funcs','concat_text' LANGUAGE C STRICT;
这里,DIRECTORY是存放共享库文件的目录(PostgreSQL实例的tutorial目录中包含有本节示例的代码)。(比较好的风格是在AS子句中仅使用 “funcs”,把DIRECTORY添加到搜索路径中。在任何情况下都要确保使用系统标准的共享库的扩展名,通常是 .so)。
注意:我们指定函数为“strict”,意味着如果任意输入值为空,系统将自动假设结果为空。这样做,我们可以避免在函数代码中检查空输入。如果不这样做,我们必须显示的在函数代码中使用PG_ARGISNULL()去检查空值。
PG_ARGISNULL(n)宏允许函数测试每个输入是否为空。(当然,只有在没有声明为“strict”的函数中才需要这样做)。对于PG_GETARG_xxx() 系列宏,输入参数从0开始计数。注意:在验证参数不为null之前,应该避免执行PG_GETARG_xxx()。要返回空结果,执行 PG_RETURN_NULL();在严格和非严格函数中都可工作。
乍一看,与使用普通C调用约定相比,版本1编码约定可能毫无意义和晦涩。然而,它们确实允许我们处理NULLable参数/返回值和“toasted”(压缩或离线)值。
版本1接口提供的其他选项是PG_GETARG_xxx()宏的两个变体。其中第一个参数PG_GETARG_xxx_COPY()保证返回指定参数的副本,该副本可以安全地写入。(普通宏有时会返回一个指针,该指针指向物理存储在表中的值,不能写入该值。使用PG_GETARG_xxx_COPY()宏可以保证结果是可写的。)第二个变体由PG_GETARG_xxx_SLICE()宏组成,该宏具有三个参数。第一个是函数参数的编号(如上所示)。第二个和第三个是要返回的段的偏移量和长度。偏移量从零开始计数,负长度要求返回值的其余部分。在存储类型为“外部”的情况下,这些宏可以更有效地访问大值的部分。(可以使用ALTER TABLE tablename ALTER column colname SET storage storagetype指定列的存储类型。存储类型是plain、external、extended、main中的一个)。
最后,版本1函数调用约定使返回集合结果、实现触发器函数、过程语言调用处理程序成为可能。有关更多详细信息,请参阅源代码发行版中的src/backend/utils/fmgr/README。