33.7. C 语言函数

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

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

33.7.1. 动态装载

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

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

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

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

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

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

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

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

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

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

注意: 在第一次使用之后,在内存中动态装载了一个目标文件。 在同一次会话中的后继的函数调用将只会产生很小的符号表查询的过热。 如果你需要强制对象文件的重载,比如你重新编译了该文件,那么可以使用 LOAD 命令或者开始一次新的会话。

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

PostgreSQL 版本 7.2 之前, 我们只能在 CREATE FUNCTION 中声明目标文件的准确的绝对路径。 目前这个方法已经过时了,因为这样令函数定义毫无意义的不可移植。 最好是只声明共享库的名字,不带路径,也没有扩展名。然后让搜索机制提供那些信息。

33.7.2. 基本类型的 C 语言函数

要知道如何写 C 语言的函数,你需要知道 PostgreSQL 在内部是如何表现基本数据类型的,以及它们是如何传入函数以及传出函数的。 PostgreSQL 内部把基本类型当作"一片内存"看待。 定义在某种类型上的用户定义函数实际上定义了 PostgreSQL 对(该数据类型)可能的操作。 也就是说,PostgreSQL 只是从磁盘读取和存储该数据类型, 而使用你定义的函数来输入,处理和输出数据。基本类型可以有下面三种内部形态(格式)之一:

传递数值的类型的长度只能是1,2 或 4 字节。 (还有 8 字节,如果 sizeof(Datum) 在你的机器上是 8 的话。)。 你要仔细定义你的类型,确保它们在任何体系平台上都是相同尺寸(字节)。 例如,long 型是一个危险的类型因为在一些机器上它是 4 字节而在另外一些机器上是 8 字节, 而 int型在大多数 Unix 机器上都是4字节的。 在一个 Unix 机器上的 integer 合理的实现可能是:

/* 4-字节整数,传值 */
typedef int integer;

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

/* 16-字节结构,传递引用 */
typedef struct
{
    double  x, y;
} Point;

只能使用指向这些类型的指针来在 PostgreSQL 函数里传入和传出数据。 要返回这样的类型的值,用 palloc() 分配正确数量的存储器,填充这些存储器,然后返回一个指向它的指针。 (另外,你可以通过返回指针的方法返回一个与输入数据同类型的值。 但是,绝对不要 修改传递引用的输入数值。)

最后,所有变长类型同样也只能通过传递引用的方法来传递。 所有变长类型必须以一个正好 4 字节长的长度域开始, 并且所有存储在该类型的数据必须放在紧接着长度域的存储空间里。 长度域是结构的全长,也就是说,包括长度域本身的长度。

比如,我们可以用下面方法定义一个 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);
...

VARHDRSZsizeof(int4) 一样, 但是我们认为用宏 VARHDRSZ 表示附加尺寸是用于变长类型的更好的风格。

Table 33-1 列出了书写一个使用了 PostgreSQL 内置类型的 C 函数里需要的知道的哪个 SQL 类型对应哪个 C 类型。 "定义在" 列给出了需要包含以获取该类型定义的头文件(实际的定义可能是在包含在列出的文件所包含的文件中。 我们建议用户只使用这里定义的接口。) 注意,你应该总是首先包括 postgres.h, 因为它声明了许多你需要的东西。

Table 33-1. 与内建的类型等效的 C 类型

内建类型 C 类型 定义在
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)int2int16postgres.h
int2vectorint2vector*postgres.h
integer (int4)int4int32postgres.h
real (float4)float4*postgres.h
double precision (float8)float8*postgres.h
intervalInterval*utils/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*utils/timestamp.h
tintervalTimeIntervalutils/nabstime.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

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

33.7.3. C 语言函数的版本-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的方法则解决了这些问题。

33.7.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()宏返回的。 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_double precision);

Datum
add_one_float8 precision(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 隐藏了抓取"toasted"(烤炉)(压缩的或者超长的)值需要做的处理。

版本-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指定。storagetypeplainexternalextendedmain 之一。)

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

33.7.5. 书写代码

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

书写和制作 C 函数的基本规则如下:

33.7.6. 编译和链接动态链接的函数

在你能够使用由 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 的标志是-KPIC GCC-fpic. 链接共享库时 SCO 编译器用 -GGCC-shared

cc -K PIC -c foo.c
cc -G -o foo.so foo.o

or

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

技巧: 如果你觉得这些步骤实在太复杂,那么你应该考虑使用 GNU Libtool,它把平台的差异隐藏在了一个统一的接口里。

生成的共享库文件然后就可以装载到 PostgreSQL里面去了.在给 CREATE FUNCTION 命令声明文件名的时候,我们必须声明 共享库文件的名字而不是中间目标文件的名字.请注意你可以在 CREATE FUNCTION 命令上忽略 系统标准的共享库扩展 (通常是.so.sl), 并且出于最佳的兼容性考虑也应该忽略.

回去看看 Section 33.7.1 获取有关服务器 预期在哪里找到共享库的信息.

33.7.7. 复合类型的 C 语言函数

复合类型不象 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(TupleTableSlot *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)
{
    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);
    /* 另外,我们可能更希望将 PG_RETURN_NULL() 用在空薪水上 */

    PG_RETURN_BOOL(salary > limit);
}

GetAttributeByNamePostgreSQL 系统函数, 用来返回当前记录的字段。它有三个参数:类型为 TupleTableSlot* 的传入函数 的参数,你想要的字段名称, 以及一个用以确定字段是否为空(null)的返回参数。 GetAttributeByName 函数返回一个Datum值, 你可以用对应的 DatumGetXXX() 宏把它转换成合适的数据类型。

下面的命令在 SQL 里声明 c_overpaid 函数:

CREATE FUNCTION c_overpaid(emp, integer) 
	RETURNS bool
	AS 'DIRECTORY/funcs', 'c_overpaid'
	LANGUAGE C;

33.7.8. 从 C 函数里返回行(复合类型)

要从一个 C 语言函数里返回一个行或者一个复合类型的数值,我们可以使用一个特殊的 API, 它提供了许多宏和函数来消除大多数制作复合数据类型的复杂性。 要使用该 API,源代码必须包含:

#include "funcapi.h"

支持返回复合数据类型(或者行)是从 AttInMetadata 结构开始的。这个结构保存着从一个裸的 C 字串里创建一行所需要的各字段信息的数组。 这里装载的信息是源自 TupleDesc 的,它存储在这里是为了避免每次调用返回结果集的函数(见下节)的额外的计算开销。 如果是一个返回结果集的函数, 那么在第一次调用函数的时候应该计算 AttInMetadata 结构一次, 然后保存起来为后面使用。AttInMetadata 还保存一个指向原始的 TupleDesc 的指针。

typedef struct AttInMetadata
{
    /* 完整的 TupleDesc */
    TupleDesc       tupdesc;

    /* 输入函数属性 finfo 类型数组 */
    FmgrInfo       *attinfuncs;

    /* 属性 typelem 类型数组 */
    Oid            *attelems;

    /* 属性 typmod 类型数组 */
    int32        *atttypmods;
}     AttInMetadata;

为了帮助你填充这个结构,我们定义了一些函数和宏。用

TupleDesc RelationNameGetTupleDesc(const char *relname)

基于一个名字获取 TupleDesc,或者

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

一个基于一个类型 OID 获取TupleDesc。 它可以用于为一个基础(标量)类型或者复合(关系)类型获取一个 TupleDesc。然后

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

将返回一个指向 AttInMetadata 的指针,基于给出的 TupleDesc 做了初始化。AttInMetadata 可以用于和 C 字串连接获取一个合适格式化的行(内部叫元组)。

要返回一个元组,你必须创建一个基于 TupleDesc 的元组槽。你可以用

TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc)

来初始化这个元组槽,或者通过其它方法(用户提供的)获取一个。 这个元组槽是用来创建一个函数返回的 Datum 用的。 每次调用都可以(也应该)复用这个槽位。

在构造完一个 AttInMetadata 结构以后, 我们可以用

HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)

制作一个 HeapTuple,以 C 字串的形式给出用户数据。 "values" 是一个 C 字串的数组,返回行的每个字段对应其中一个。 每个 C 字串都应该是字段数据类型的输入函数预期的形式。为了从其中一个字段中返回一个空值, values 数组中对应的指针应该设置为 NULL。这个函数将会需要为你返回的每个元组调用一次。

通过 TupleDescGetAttInMetadataBuildTupleFromCStrings 制作一个元组只有在你的函数计算作为字串返回的数值的时候才比较方便。 如果你的代码需要把值当作一个 Datum 的集合进行计算, 你应该使用下层的 heap_formtuple 过程把 Datum 直接转换成一个元组。 你仍然需要 TupleDesc 和一个 TupleTableSlot, 但是不需要 AttInMetadata 了。

一旦你制作了一个从你的函数中返回的元组,那么该元组必须转换成一个 Datum。使用

TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple)

从一个给出的元组和一个槽位中获取一个 Datum。 如果你想只返回一行,那么这个 Datum 可以用于直接返回, 或者是它可以用作在一个返回集合的函数里的当前返回值。

例子在下面给出。

33.7.9. 从 C 语言函数里返回集合

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

    /*
     * 指向结果槽位的可选指针
     *
     * 槽位是在返回元组的时候使用的(也就是说,返回复合数据类型)
     * 如果返回基本类型(也就是说,标量),是不需要的
     */
    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;
} 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
            获取槽位
            funcctx->slot = slot;
        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(testpassbyval);

 Datum
 testpassbyval(PG_FUNCTION_ARGS)
 {
     FuncCallContext     *funcctx;
     int                  call_cntr;
     int                  max_calls;
     TupleDesc            tupdesc;
     TupleTableSlot       *slot;
     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);

         /*
          * 为 __testpassbyval 元组制作一个元组描述
          */
         tupdesc = RelationNameGetTupleDesc("__testpassbyval");

         /* 用这个 tupdesc 为一个元组分配槽位 */
         slot = TupleDescGetSlot(tupdesc);

         /* 将槽位赋予给函数环境 */
         funcctx->slot = slot;

         /*
          * 生成稍后从裸 C 字串生成元组的属性元数据
          * 
          */
         attinmeta = TupleDescGetAttInMetadata(tupdesc);
         funcctx->attinmeta = attinmeta;

         MemoryContextSwitchTo(oldcontext);
     }

     /* 每次函数调用都要做的事情 */
     funcctx = SRF_PERCALL_SETUP();

     call_cntr = funcctx->call_cntr;
     max_calls = funcctx->max_calls;
     slot = funcctx->slot;
     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 = TupleGetDatum(slot, 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 __testpassbyval AS (f1 integer, f2 integer, f3 integer);

 CREATE OR REPLACE FUNCTION testpassbyval(integer, integer) RETURNS setof __testpassbyval
   AS 'filename','testpassbyval' LANGUAGE 'c' IMMUTABLE STRICT;
 

参阅源码发布包里的 contrib/tablefunc 获取更多有关返回集合的函数的例子。

33.7.10. 多态参数和返回类型

C 语言函数可以声明为接受和返回多态的类型 anyelementanyarray。 参阅 Section 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 为基的。

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

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;
    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");

    /* 获取提供的元素 */
    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, 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 STRICT;

请注意使用 STRICT;这一点非常重要,因为代码没有认真测试空输入。