问题报告 纠错本页面

35.9. C-语言函数

用户定义的函数可以用 C 写(或者是与C兼容的语言,比如C++)。 这样的函数被编译进动态加载对象(共享库)并且由服务器根据需要加载。 动态加载的特性是"C 语言函数""内部函数"之间的区别—不过, 实际的编码习惯在两者之间实际上是一样的。因此, 标准的内部函数库为写用户定义C函数提供了大量最好的样例。

目前对 C 函数有两种调用约定。新的"版本-1"的调用约定是通过为该函数书写一个 PG_FUNCTION_INFO_V1()宏来标识的,像下面演示的那样。缺少这个宏表示一个 老风格的("版本-0")函数。两种风格里在CREATE FUNCTION里声明的都是C。 现在老风格的函数已经废弃了,主要是因为移植性原因和缺乏功能, 不过出于兼容性原因,系统仍然支持它。

35.9.1. 动态加载

当用户定义的函数第一次被服务器会话调用时, 动态加载器才把可加载对象文件里的函数目标码加载进内存。 因此,用于用户定义 C 函数的CREATE FUNCTION必须为函数声明两个信息: 可加载对象文件名、在目标文件里调用的 C 函数名(连接符号)。 如果没有明确声明 C 函数名,那么就假设它与 SQL 函数名相同。

基于在CREATE FUNCTION命令中给出的名字, 下面的算法用于定位共享对象文件:

  1. 如果名字是一个绝对路径,则加载给出的文件。

  2. 如果名字以字符串$libdir开头, 那么该部分将被PostgreSQL库目录名代替, 该目录是在编译时确定的。

  3. 如果名字不包含目录部分, 那么在配置参数dynamic_library_path. 声明的路径里查找。

  4. 如果没有在路径里找到该文件,或者它包含一个非绝对目录部分, 那么动态加载器就会试图直接拿这个名字来加载, 这样几乎可以肯定是要失败的(依靠当前工作目录是不可靠的)。

如果这个顺序不管用,那么就给这个名字加上平台相关的共享库文件扩展名(通常是.so), 然后再重新按照上面的过程找一遍。如果还是失败,那么加载失败。

建议使用相对于$libdir的目录或者通过动态库路径定位共享库。 这样,如果新版本安装在一个不同的位置,那么就可以简化版本升级。 $libdir的实际目录位置可以 用pg_config --pkglibdir命令找到。

运行PostgreSQL服务器的用户ID 必须可以遍历路径到达想加载的文件。 一个常见的错误就是把该文件或者一个高层目 录的权限设置为postgres用户不可读和/或不能执行。

在任何情况下,在CREATE FUNCTION命令里给出的文件 名是在系统表里按照文本记录的, 因此,如果需要再次加载,那么会再次运行这个过程。

注意: PostgreSQL不会自动编译 C 函数; 在使用CREATE FUNCTION命令之前你必须编译它。 参阅第 35.9.6 节获取更多信息。

为了确保不会错误加载共享库文件,从PostgreSQL 开始将检查那个文件的"magic block",这允许服务器以检查明显的不兼容性。 比如不同版本PostgreSQL的编译代码。 magic block需要被作为PostgreSQL 8.2。 为了包含"magic block", 请在包含了fmgr.h头文件之后, 将下面的内容写进一个(也只能是一个)模块的源代码文件中:

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

如果不打算兼容8.2 PostgreSQL之前的版本, #ifdef测试也可以省略。

动态加载对象文件在首次使用之后将一直滞留在内存中。 在同一个会话中的下一次调用将只需查找符号表的很小开销。 如果你想强制重新加载(比如重新编译之后), 可以重新开始一个新的会话。

动态加载文件也可以包含初始化函数和结束函数。 如果包含一个名为_PG_init的函数,那么该函数将在该文件被加载后立即执行, 该函数不能接受任何参数并且必须返回 void 。 如果包含一个名为_PG_fini的函数,那么该函数将在该文件即将被卸载前执行, 同样,该函数不能接受任何参数并且必须返回 void 。 需要注意的是_PG_fini仅在该文件即将被卸载前执行而不是在会话结束的时候执行。 目前,卸载被禁止并且将不会发生,但是这可能在将来改变。

35.9.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 C代码调用此int32类型, 因为它是C中的惯例,intXX 意味着XX bits。 注意因此C类型int8的大小是1字节。SQL类型int8被称为C中int64。参见 表 35-1

另外,任何尺寸的定长类型都可以是传递引用型。例如, 下面是一个PostgreSQL类型的实现:

/* 16-byte structure, passed by reference */
typedef struct
{
    double  x, y;
} Point;

只能使用指向这些类型的指针在PostgreSQL函数里传入和传出数据。 要返回这样类型的值,用palloc分配正确数量的内存,填充这些内存, 然后返回一个指向它的指针。如果只是想返回和输入参数类型与数值都相同的数值, 可以忽略额外的palloc,只要返回指向输入数值的指针就行。

最后,所有变长类型同样也只能通过引用来传递。 所有变长类型必须以一个4字节的长度域开始,通过SET_VARSIZE设置, 没有直接设置这个字段! 所有存储在该类型中的数据必须放在紧接着长度域的存储空间里。 长度域是结构的全长,也就是说,包括长度域本身的长度。

另外一个重要的点是避免数据类型值中留下未初始化的位;比如,请注意任何对齐填充字节 溢出的零可能出现在结构体中。没有这些,你的数据类型的逻辑上等价常量可能被规划器看做是 不平等的,导致低效(虽然是不正确的)规划。

警告

绝对不要修改一个引用传递的输入值,否则很可能破坏磁盘上的数据。 因为指针很可能直接指向一个磁盘缓冲区。 这条规则的唯一例外在第 35.10 节里。

比如,我们可以用下面的方法定义一个text类型:

typedef struct {
    int32 length;
    char data[1];
} text;

显然,上面声明的数据域长度不足以存储任何可能的字符串。 因为在C中不可能声明变长结构,所以我们倚赖这样的知识: C编译器不会对数组下标进行范围检查。只需要分配足够的空间, 然后把数组当做已经声明为合适长度的变量访问。这是一个常用的技巧, 你可以在许多C教科书中读到。

当处理变长类型时,必须仔细分配正确的内存数量并正确设置长度域。 例如,如果想在一个text结构里存储40字节, 我们可能会使用像下面的代码片段:

#include "postgres.h"
...
char buffer[40]; /* our source data */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ等价于sizeof(int32), 但是我们认为用宏VARHDRSZ表示附加尺寸是用于变长类型的更好风格。 同时,该长度字段必须使用SET_VARSIZE宏设置, 而不是简单的赋值。

表 35-1列出了书写 使用PostgreSQL内置类型的 C 函数里需要知道的 SQL 类型与 C 类型的对应关系。"定义在" 列给出了需要包含以获取该类型定义的头文件。 实际定义可能在列表文件中包含的不同文件中。我们建议用户坚持定义的接口。 注意,你应该总是首先包括postgres.h, 因为它声明了许多你需要的东西。

表 35-1. 与内建SQL类型等效的C类型

SQL Type C Type Defined In
abstimeAbsoluteTimeutils/nabstime.h
booleanboolpostgres.h (可能编译器内置)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(编译器内置)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
smallint (int2)int16postgres.h
int2vectorint2vector*postgres.h
integer (int4)int32postgres.h
real (float4)float4*postgres.h
double precision (float8)float8*postgres.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocregprocpostgres.h
reltimeRelativeTimeutils/nabstime.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
timestampTimestamp*datatype/timestamp.h
tintervalTimeIntervalutils/nabstime.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

既然我们已经讨论了基本类型所有可能的结构, 我们便可以用实际的函数举一些例子。

35.9.3. 版本-0调用约定

先提供现在已经不提倡了的"老风格"—因为比较容易迈出第一步。 在版本-0方法中,此风格 C 函数的参数和结果用普通 C 风格声明, 但是要小心使用上面显示的 SQL 数据类型的 C 表现形式。

下面是一些例子:

#include "postgres.h"
#include <string.h>
#include "utils/geo_decls.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/* 传递数值 */
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));
    SET_VARSIZE(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);

    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);
    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约定,下面要讲的新方法则解决了这些问题。

35.9.4. 版本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()接受要抓取的函数参数的编号 (从 0 开始)作为其参数。PG_RETURN_xxx() 接受实际要返回的数值为自身的参数。

下面是和上面一样的函数,但是使用版本-1风格编写的:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/*传递数值*/

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命令和版本-0等效命令一样。

猛一看,版本-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指定。 storagetypeplainexternalextended,或者main之一。

版本-1 的函数调用风格也令我们可能返回一"套"结果(第 35.9.9 节) 并且实现触发器函数(第 36 章)和过程语言调用处理器 (第 51 章)。 版本-1的代码也更容易移植,因为它没有违反C标准对函数调用协议的限制。 更多的细节请参阅源程序中的src/backend/utils/fmgr/README文件。

35.9.5. 书写代码

在转到更深的话题之前,先要讨论一些PostgreSQL C语言函数的编码规则。 虽然可以用C以外的其它语言书写用于 PostgreSQL的共享函数, 但通常都很麻烦(当它可能的时候),因为其他语言, 比如C++, FORTRAN或者Pascal并不遵循C的调用习惯。 也就是说,其它语言在函数之间的传递参数和返回值的方式不一样。 因此假设你的C-编程语言函数是用C写的。

书写和编译C函数的基本规则如下:

35.9.6. 编译和链接动态加载的函数

在能够使用由 C 写的PostgreSQL扩展函数之前, 必须用一种特殊的方法编译和链接它们,这样才能生成可以被服务器动态加载的文件。 准确地说是需要创建一个共享库

如果需要更多信息,那么你应该阅读操作系统的文档,特别是 C 编译器(cc) 和连接器(ld)的文档。另外,PostgreSQL 源代码里包含几个可以运行的例子,它们在contrib目录里。不过, 如果你依赖这些例子,那么你的模块将依赖于PostgreSQL源代码的可用性。

创建共享库和链接可执行文件类似:首先把源代码编译成目标文件,然后把目标文件链接起来。 目标文件需要创建成位置无关码(PIC), 也就是在可执行程序加载它们的时候, 它们可以被放在可执行程序内存里的任何地方(用于可执行文件的目标文件通常不是用这个方式编译的), 链接动态库的命令包含特殊标志,与链接可执行文件的命令是有区别的(至少理论上如此,不过现实未必)。

在下面的例子里,假设你的源程序代码在foo.c文件里,并且我们要创建 foo.so的共享库。中介的对象文件将叫做foo.o (除非另外注明)。虽然一个共享库可以包含多个对象文件,但是在这里只用一个。

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

Mac OS 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编译器时创建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

Tru64 UNIX

PIC是缺省,不需要使用特殊的编译器选项。带特殊选项的ld用于链接:

cc -c foo.c
ld -shared -expect_unresolved '*' -o foo.so foo.o

用 GCC 代替系统编译器时的过程是一样的;不需要特殊的选项。

UnixWare

用 SCO 编译器时创建PIC的编译器标志是-K PIC; 用GCC编译器时创建PIC的编译器标志是 -fpic。链接共享库时 SCO 编译器用-GGCC 使用-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),并且出于最佳的兼容性考虑也应该忽略。

回头看看第 35.9.1 节获取有关服务器预期在哪里找到共享库的信息。

35.9.7. 复合类型参数

复合类型不像 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"  /* for GetAttributeByName() */

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

bool
c_overpaid(HeapTupleHeader 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() */

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

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);
<!--
    /* Alternatively, we might prefer to do PG_RETURN_NULL() for null salary. */
-->

    /*另外,可能更希望将PG_RETURN_NULL()用在null薪水上*/

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByNamePostgreSQL系统函数, 用来返回当前记录的字段。它有三个参数:类型为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。

35.9.8. 返回行(复合类型)

要从一个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,那么调用:

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

如果你想使用Datum,或者:

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

如果你想使用C字符串。如果你在写一个返回集合的函数, 那么你可以把这些函数的结果保存在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 可以用于直接返回, 或者是它可以用作在一个返回集合的函数里的当前返回值。

例子在下面给出。

35.9.9. 返回集合

还有一个特殊的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;
    
更多的声明

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        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)
    {
    

/* 这里想返回另外一个条目: */
        用户代码
        获取结果

        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 获取更多有关返回集合的函数的例子。

35.9.10. 多态参数和返回类型

C 语言函数可以声明为接受和返回多态的类型anyelementanyarrayanynonarray, anyenumanyrange。 参阅第 35.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。 还有get_fn_expr_variadic用于找出是否调用包含明确的VARIADIC关键字。 对于VARIADIC "any"函数是最有用的,正如下面所述。

比如,假设想写一个函数接受任意类型的一个元素,并且返回该类型的一个一维数组:

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;
/* 有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;

有一个变种多态性,仅适用于C语言函数:他们可以声明采取类型 "any"的参数。(注意:这个类型名称必须是双引号, 因为它同时也是一个SQL的保留字)。类似于anyelement除了它并不限制不同"any" 参数是相同类型,也没有帮助确定该函数的结果类型。一个C语言的函数也可以声明最后的参数为VARIADIC "any"。 这将匹配一个或多个任意类型的实参(不一定是相同的类型)。 这些参数被收集到一个数组中如发生正常的可变参数函数; 他们会分别被传递到函数中。PG_NARGS()宏和 上面描述的方法必须被用来确定实际参数数目 以及使用此功能时的类型。同时,这个函数的用户可能希望在函数调用中使用VARIADIC关键字, 以期望函数把数组元素看作单独的参数。函数本身必须实现 想要的操作,使用get_fn_expr_variadic之后 检测实际参数被标记为VARIADIC

35.9.11. 转换函数

一些函数的调用可以在规划中基于函数的属性特性被简化。比如, int4mul(n, 1)可简化为n。 为了定义函数-特定优化,写transform function并将其OID放入 基函数的pg_proc项的protransform字段中, 转换函数必须有SQL签名protransform(internal) RETURNS internal。 参数,其实FuncExpr *是代表调用基函数的一个虚拟节点。 如果表达式树的变换函数的研究证明简化的表达式树可以替代所有 可能的具体调用其表示建立并且返回简单的表达式。 否则,返回NULL指针(不是SQL null)。

我们不做任何保证, PostgreSQL不会调用这种情况下的主要函数以简化转换函数。 确保在简化的表达式以及实际调用主要函数之间的严格等价性。

当前,这个设施在SQL水平上不暴露给用户,出于安全考虑。因此只有实践中用于优化内置函数。

35.9.12. 共享内存和LWLocks

插件可能保留 LWLocks 并在服务器启动时分配共享内存。 插件的共享库必须通过指定shared_preload_libraries的方法预先加载。

void RequestAddinShmemSpace(int size)

共享内存可以通过在_PG_init函数中调用。

LWLocks通过调用进行预留:

void RequestAddinLWLocks(int n)

来自_PG_init

为了避免可能的竞争条件,当连接并且初始化共享内存分配时, 每个后端应该使用LWLock AddinShmemInitLock,如下所示:

static mystruct *ptr = NULL;

if (!ptr)
{
        bool    found;

        LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
        ptr = ShmemInitStruct("my struct name", size, &found);
        if (!found)
        {
                initialize contents of shmem area;
                acquire any requested LWLocks using:
                ptr->mylockid = LWLockAssign();
        }
        LWLockRelease(AddinShmemInitLock);
}

35.9.13. 使用C++的可扩展性

尽管PostgreSQL后端以C写入,如果伴随这些准则, 在C++中写入扩展是可能的:

总之,把C++代码放在与后端接口的extern C函数之后是最好的, 并且避免异常,内存以及调用堆栈泄露。