↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1
历史版本PostgreSQL 7.3 已于 2007 年 11 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本。

9.5. C 语言函数 #

用户自定义函数可以用 C(或可与 C 兼容的语言,如 C++)编写。这样的函数被编译成动态可装载对象(也称共享库),由服务器按需装载。动态装载特性正是“C 语言”函数与“internal”函数的区别所在——两者的实际编码约定基本相同。(因此,标准的内部函数库是用户自定义 C 函数编码示例的丰富来源。)

目前 C 函数使用两种不同的调用约定。 较新的“版本 1”调用约定通过为函数写一个 PG_FUNCTION_INFO_V1() 宏调用来表明,如下所示。没有这样的宏则表示是旧风格(“版本 0”)函数。无论哪种情况,CREATE FUNCTION 中指定的语言名都是 C。旧风格函数由于可移植性问题和功能欠缺现已弃用,但出于兼容性原因仍受支持。

9.5.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 不会自动编译 C 函数。对象文件必须在被 CREATE FUNCTION 命令引用之前编译好。更多信息见 第 9.5.8 节。

注意

动态装载的对象文件在第一次使用后会保留在内存中。同一会话中以后对该文件中函数的调用只会产生符号表查找这一很小的开销。如果你需要强制重新装载一个对象文件(例如重新编译之后),可以使用 LOAD 命令或开始一个新会话。

建议通过相对于 $libdir 的路径,或者通过动态库路径来定位共享库。这样一来,如果新安装位于不同的位置,版本升级会更简单。$libdir 实际代表的目录可以通过命令 pg_config --pkglibdir 查出。

注意

在 PostgreSQL 7.2 版之前,CREATE FUNCTION 中只能指定对象文件的精确绝对路径。这种做法现已弃用,因为它使函数定义产生不必要的不可移植性。最好只指定不带路径和扩展名的共享库名,让搜索机制去提供那些信息。

9.5.2. C 语言函数中的基础类型

表 9.1 给出了将要装载到 PostgreSQL 中的 C 函数的参数所需的 C 类型。“定义于”列给出为获得类型定义而需要包含的头文件。(实际定义可能在所列文件包含的另一个文件中。建议用户坚持使用已定义的接口。)注意,你应该始终包含 postgres.h 放在任何源文件的最前面,因为它声明了许多你反正需要的东西。

表 9.1. 内置 PostgreSQL 类型的等价 C 类型

SQL 类型 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

在内部,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 会自动把事情安排好,使整数类型真正具有其宣称的大小。

另一方面,任意尺寸的定长类型都可以通过引用传递。例如,这里有一种 PostgreSQL类型的实现示例:

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

在 PostgreSQL 函数中传入和传出这类类型时只能使用指向它们的指针。要返回这种类型的值,用 palloc() 分配适量内存,填充所分配的内存,并返回指向它的指针。(或者,你也可以通过返回同类型输入值的指针来返回该输入值。但绝不要修改传引用输入值的内容。)

最后,所有变长类型也必须以传引用方式传递。所有变长类型都必须以一个恰好 4 字节的长度字段开始,而该类型中要存储的所有数据都位于紧随该长度字段之后的内存中。长度字段是结构的总长度(即它包含长度字段本身的大小)。我们可以这样定义 text 类型:

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

显然,这里声明的 data 字段不足以容纳所有可能的字符串。由于在 C 中无法声明变大小的结构,我们依赖这样一个知识:C 编译器不会对数组下标做范围检查。我们只分配所需数量的空间,然后就像数组声明了正确长度一样去访问它。(如果你对这个技巧不熟悉,可能需要先花些时间读一本 C 编程入门教科书,再深入研究 PostgreSQL 服务器编程。)操作变长类型时,我们必须小心分配正确数量的内存并正确设置长度字段。例如,如果我们想在 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 来引用变长类型的 额外开销的尺寸被认为是比较好的风格。

了解了基础类型所有可能的结构后,就可以看一些实际函数示例。

9.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 */

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;
}

/* 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(int4) RETURNS int4
     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。)

注意我们把这些函数指定为“严格”的,意思是如果任何输入值为 NULL,系统就应自动假定结果为 NULL。这样做就避免了在函数代码中检查 NULL 输入。不这样做的话,我们就得显式检查空值,例如为每个传引用参数检查空指针。(对传值参数,我们甚至没有办法检查!)

虽然这个调用约定使用简单,但可移植性不好;在某些体系结构上,以这种方式传递小于 int 的数据类型存在问题。此外,也没有返回 NULL 结果的简单方法,除了把函数声明为严格之外也无法以任何方式处理 NULL 参数。接下来介绍的版本 1 约定克服了这些缺点。

9.5.4. C 语言函数的版本 1 调用约定

版本-1 的调用约定依赖于宏来屏蔽传递参数和结果的大部分复杂性。版本-1 函数的 C 声明总是:

Datum funcname(PG_FUNCTION_ARGS)

此外,宏调用:

PG_FUNCTION_INFO_V1(funcname);

必须出现在同一个源文件中(按惯例写在函数本身之前)。对 internal 语言的函数不需要此宏调用,因为 PostgreSQL 目前假定所有内部函数都是版本 1。但对动态装载的函数它是必需的。

在版本 1 函数中,每个实际参数用与该参数数据类型对应的 PG_GETARG_xxx() 宏取得,结果用与返回类型对应的 PG_RETURN_xxx() 宏返回。

下面我们展示与上面相同的函数,以版本 1 风格编码:

#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_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* The macros for FLOAT8 hide its pass-by-reference nature */
    float8   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命令与版本 0 的等价形式相同。

乍一看,版本 1 编码约定可能显得像无谓的故弄玄虚。但它们确实提供了不少改进,因为宏可以隐藏不必要的细节。例如,在编写 add_one_float8 时,我们不再需要知道 float8 是传引用类型。另一个例子是,变长类型的 GETARG 宏隐藏了处理获取“经 TOAST 处理”(压缩或行外存储)值的需要。上面所示的旧风格 copytext 和 concat_text 函数在存在经 TOAST 处理的值时实际上是错误的,因为它们没有对其输入调用 pg_detoast_datum()。(旧风格动态装载函数的处理器目前会处理这一细节,但效率低于版本 1 函数所能达到的水平。)

版本 1 函数的一大改进是对 NULL 输入和结果的更好处理。宏 PG_ARGISNULL(n) 允许函数测试每个输入是否为 NULL(当然,只有在未声明为“严格”的函数中才有必要这样做)。与 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 指定。存储类型是 plain、external、extended 或 main 之一。)

版本 1 函数调用约定使得返回“集合”结果、实现触发器函数和过程语言调用处理器成为可能。版本 1 代码也比版本 0 更具可移植性,因为它不违反 ANSI C 对函数调用协议的限制。更多细节见源码发行包中的 src/backend/utils/fmgr/README。

9.5.5. C 语言函数中的复合类型

复合类型没有像 C 结构那样的固定布局。复合类型的实例可能包含空字段。此外,属于某个继承层次的复合类型可能具有与同一继承层次其他成员不同的字段。因此,PostgreSQL 提供了一个从 C 访问复合类型字段的过程式接口。当 PostgreSQL 处理一组行时,每一行都会作为一个 TUPLE 类型的不透明结构传入你的函数。假设我们想编写一个函数来回答查询

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;
}

/* In version-1 coding, the above would look like this: */

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* 类型参数、所需属性的名称,以及一个返回参数(告知该属性是否为空)。GetAttributeByName 返回一个 Datum 值,你可以用适当的 DatumGetXXX() 宏把它转换为正确的数据类型。

下面的命令让 PostgreSQL 知道 c_overpaid 函数:

CREATE FUNCTION c_overpaid(emp, int4) 
RETURNS bool
AS 'PGROOT/tutorial/funcs' 
LANGUAGE C;

9.5.6. 表函数 API

表函数 API 帮助创建用户自定义的 C 语言表函数(第 9.7 节)。表函数是产生一组行的函数,这些行由基本(标量)数据类型或复合(多列)数据类型组成。该 API 分为两个主要部分:对返回复合数据类型的支持,以及对返回多行(返回集合的函数即 SRF)的支持。

表函数 API 依靠宏和函数来屏蔽构建复合数据类型和返回多个结果的大部分复杂度。表函数必须遵循上面所述的版本 1 调用约定。此外,源文件必须包含:

#include "funcapi.h"

9.5.6.1. 返回行(复合类型)

表函数 API 对返回复合数据类型(即行)的支持始于 AttInMetadata 结构。该结构持有从原始 C 字符串创建一行所需的各属性信息的数组。它还保存指向 TupleDesc 的指针。这里携带的信息派生自 TupleDesc,但存储在此是为了避免每次调用表函数时的冗余 CPU 开销。对于返回集合的函数,AttInMetadata 结构应在第一次调用时计算一次并保存,供后续调用复用。

typedef struct AttInMetadata
{
    /* full TupleDesc */
    TupleDesc       tupdesc;

    /* array of attribute type input function finfo */
    FmgrInfo       *attinfuncs;

    /* array of attribute type typelem */
    Oid            *attelems;

    /* array of attribute 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)

可用于从 C 字符串形式的用户数据构建 HeapTuple。"values" 是一个 C 字符串数组,返回元组的每个属性对应一个。每个 C 字符串都应具有该属性数据类型的输入函数所期望的形式。要为某个属性返回空值,应把 values 数组中相应的指针设为 NULL。对返回的每个元组,都需要再次调用此函数。

只有当你的函数自然地以文本字符串计算要返回的值时,通过 TupleDescGetAttInMetadata 和 BuildTupleFromCStrings 构建元组才是方便的。如果你的代码自然地把值计算为一组 Datum,就应改用底层例程 heap_formtuple 把 Datum 直接转换为元组。你仍然需要 TupleDesc 和 TupleTableSlot,但不需要 AttInMetadata。

在函数中构建好要返回的元组后,必须把它转换为 Datum。使用

TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple)

从元组和槽得到 Datum。如果你只打算返回单行,这个 Datum 可以直接返回;在返回集合的函数中,它可以用作当前的返回值。

下面会有一个示例。

9.5.6.2. 返回集合

返回集合的函数(SRF)通常对它返回的每个项各调用一次。因此 SRF 必须保存足够的状态来记住自己正在做什么,并在每次调用时返回下一个项。表函数 API 提供了 FuncCallContext 结构来帮助控制这一过程。fcinfo->flinfo->fn_extra 用于跨调用保存指向 FuncCallContext 的指针。

typedef struct
{
    /*
     * Number of times we've been called before.
     * 
     * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and
     * incremented for you every time SRF_RETURN_NEXT() is called.
     */
    uint32 call_cntr;

    /*
     * OPTIONAL maximum number of calls
     *
     * max_calls is here for convenience ONLY and setting it is OPTIONAL.
     * If not set, you must provide alternative means to know when the
     * function is done.
     */
    uint32 max_calls;

    /*
     * OPTIONAL pointer to result slot
     * 
     * slot is for use when returning tuples (i.e. composite data types)
     * and is not needed when returning base (i.e. scalar) data types.
     */
    TupleTableSlot *slot;

    /*
     * OPTIONAL pointer to misc user provided context info
     * 
     * user_fctx is for use as a pointer to your own struct to retain
     * arbitrary context information between calls for your function.
     */
    void *user_fctx;

    /*
     * OPTIONAL pointer to struct containing arrays of attribute type input
     * metainfo
     * 
     * attinmeta is for use when returning tuples (i.e. composite data types)
     * and is not needed when returning base (i.e. scalar) data types. It
     * is ONLY needed if you intend to use BuildTupleFromCStrings() to create
     * the return tuple.
     */
    AttInMetadata *attinmeta;

    /*
     * memory context used for structures which must live for multiple calls
     *
     * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used
     * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory
     * context for any memory that is to be re-used across multiple calls
     * of the 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 时当前的内存上下文是一个会在调用之间被清理的瞬态上下文。这意味着你不需要对每个 palloc 的内存调用 pfree;它反正会消失。但如果你想分配任何要跨调用存活的数据结构,就需要把它们放到别处 。multi_call_memory_ctx 所引用的内存上下文是任何需要存活到 SRF 运行结束的数据的合适位置。在大多数情况下,这意味着你应该在第一次调用设置期间切换到 multi_call_memory_ctx。

一个完整的伪代码示例如下:

Datum
my_Set_Returning_Function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    MemoryContext     oldcontext;
    [user defined declarations]

    if (SRF_IS_FIRSTCALL())
    {
        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* one-time setup code appears here: */
        [user defined code]
        [if returning composite]
            [build TupleDesc, and perhaps AttInMetadata]
            [obtain slot]
            funcctx->slot = slot;
        [endif returning composite]
        [user defined code]
        MemoryContextSwitchTo(oldcontext);
    }

    /* each-time setup code appears here: */
    [user defined code]
    funcctx = SRF_PERCALL_SETUP();
    [user defined code]

    /* this is just one way we might test whether we are done: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* here we want to return another item: */
        [user defined code]
        [obtain result Datum]
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* here we are done returning items, and just need to clean up: */
        [user defined code]
        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;

     /* stuff done only on the first call of the function */
     if (SRF_IS_FIRSTCALL())
     {
        MemoryContext   oldcontext;

        /* create a function context for cross-call persistence */
        funcctx = SRF_FIRSTCALL_INIT();

        /* switch to memory context appropriate for multiple function calls */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* total number of tuples to be returned */
        funcctx->max_calls = PG_GETARG_UINT32(0);

        /*
         * Build a tuple description for a __testpassbyval tuple
         */
        tupdesc = RelationNameGetTupleDesc("__testpassbyval");

        /* allocate a slot for a tuple with this tupdesc */
        slot = TupleDescGetSlot(tupdesc);

        /* assign slot to function context */
        funcctx->slot = slot;

        /*
         * Generate attribute metadata needed later to produce tuples from raw
         * C strings
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* stuff done on every call of the function */
    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)    /* do when there is more left to send */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Prepare a values array for storage in our slot.
         * This should be an array of C strings which will
         * be processed later by the appropriate "in" functions.
         */
        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));

        /* build a tuple */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* make the tuple into a datum */
        result = TupleGetDatum(slot, tuple);

        /* Clean up (this is not actually necessary) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

         SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* do when there is no more left */
    {
         SRF_RETURN_DONE(funcctx);
    }
}

配套的 SQL 代码为

CREATE TYPE __testpassbyval AS (f1 int4, f2 int4, f3 int4);

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

更多表函数的示例参见 contrib/tablefunc。

9.5.7. 编写代码

现在我们转向编写编程语言函数这一更困难的任务。请注意:本手册的这一部分不会使你成为程序员。在尝试为 PostgreSQL 编写 C 函数之前,你必须对 C(包括指针的使用)有良好的理解。虽然也许可以把用 C 以外语言编写的函数装载到 PostgreSQL,但这通常很困难(即使在可能的时候),因为其他语言(如 FORTRAN 和 Pascal)往往不遵循与 C 相同的调用约定。也就是说,其他语言在函数之间传递参数和返回值的方式不同。因此,我们将假定你的编程语言函数是用 C 编写的。

构建 C 函数的基本规则如下:

  • 使用 pg_config --includedir-server 找出 PostgreSQL 服务器头文件安装在你的系统(或你的用户将要运行的系统)上的位置。此选项是 PostgreSQL 7.2 新增的。对 PostgreSQL 7.1,应使用选项 --includedir。(如果遇到未知选项,pg_config 会以非零状态退出。)对 7.1 之前的版本你只能猜测,但由于那是在当前调用约定引入之前,你不太可能想支持那些版本。

  • 分配内存时,使用 PostgreSQL 的例程 palloc 和 pfree,而不是相应的 C 库例程 malloc 和 free。palloc 分配的内存会在每个事务结束时自动释放,从而防止内存泄漏。

  • 总是使用 memset 或 bzero 把结构体的字节清零。有几个例程(如哈希访问方法、哈希连接和排序算法)会计算结构体中原始位的函数。即使你初始化了结构体的所有字段,结构体中仍可能存在若干包含垃圾值的对齐填充字节(结构体中的空洞)。

  • PostgreSQL 的大多数内部类型都在 postgres.h 中声明,而函数管理器接口(PG_FUNCTION_ARGS 等)位于 fmgr.h 中,因此至少需要包含这两个文件。出于可移植性考虑,最好把 postgres.h 放在 最前面,先于任何其他系统或用户头文件。包含 postgres.h 时,也会顺带为你包含 elog.h 和 palloc.h。

  • 对象文件中定义的符号名不得相互冲突,也不得与 PostgreSQL 服务器可执行文件中定义的符号冲突。如果收到此类错误消息,你必须重命名你的函数或变量。

  • 编译和链接你的目标代码使其能被动态装载到 PostgreSQL 总是需要特殊的标志。关于如何针对你的特定操作系统进行操作,参见 第 9.5.8 节 的详细说明。

9.5.8. 编译和链接动态装载的函数 #

在你能够使用以 C 编写的 PostgreSQL 扩展函数之前, 必须以特殊方式对它们进行编译和链接,以生成一个可由服务器动态装载的文件。 更准确地说,需要创建一个共享库。

若想了解本节未涵盖的信息,你应阅读操作系统的文档,特别是 C 编译器 cc 和链接编辑器 ld 的手册页。 此外,PostgreSQL 源代码在 contrib 目录中包含若干可用的示例。 不过,如果你依赖这些示例,就会使你的模块依赖于 PostgreSQL 源代码是否可用。

创建共享库通常与链接可执行文件类似:先把源文件编译为目标文件, 再把目标文件链接在一起。目标文件需要以位置无关代码 (PIC)形式生成。 从概念上讲,这意味着当它们被可执行文件装载时,可以放在内存中的任意位置。 (面向可执行文件的目标文件通常不会这样编译。) 链接共享库的命令中也包含一些特殊标志,用来把它与链接可执行文件的命令区分开来 (至少理论上如此,某些系统上的实际做法要丑陋得多)。

在下面的示例中,我们假定你的源代码位于文件 foo.c 中, 并将创建共享库 foo.so。除非另有说明, 中间目标文件名为 foo.o。共享库可以包含多个目标文件, 但这里我们只使用一个。

BSD/OS

生成 PIC 的编译器选项是 -fpic。创建共享库时使用的链接器选项是 -shared。

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

这从 BSD/OS 4.0 版起适用。

FreeBSD

生成 PIC 的编译器选项是 -fpic。创建共享库时使用的编译器选项是 -shared。

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

这从 FreeBSD 3.0 版起适用。

HP-UX

生成 PIC 的系统编译器选项是 +z。使用 GCC 时则是 -fpic。用于共享库的链接器选项是 -b。因此:

cc +z -c foo.c

or:

gcc -fpic -c foo.c

and then:

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

使用 Sun 编译器时,生成 PIC 的编译器选项是 -KPIC;使用 GCC 时则是 -fpic。要链接共享库,两种编译器都使用编译器 选项 -G,或者在使用 GCC 时 改用 -shared。

cc -KPIC -c foo.c
cc -G -o foo.so foo.o

or

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

生成 PIC 的编译器选项,对于 SCO 编译器是 -K PIC,而 -fpic 则用于 GCC。 链接共享库时,编译器选项对于 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 命令指定文件名时, 必须给出共享库文件名,而不是中间目标文件名。 请注意,系统标准的共享库扩展名(通常是 .so 或 .sl) 可以在 CREATE FUNCTION 命令中省略,并且通常也应省略,以获得最佳可移植性。

关于服务器期望在何处找到共享库文件,请回头参见 第 9.5.1 节。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。