Skip to content

38.10. C 语言函数#

38.10.1. 动态加载
38.10.2. C 语言函数中的基本类型
38.10.3. 版本 1 调用约定
38.10.4. 编写代码
38.10.5. 编译和链接动态加载函数
38.10.6. 复合类型参数
38.10.7. 返回行(复合类型)
38.10.8. 返回集
38.10.9. 多态参数和返回类型
38.10.10. 共享内存和 LWLocks
38.10.11. 使用 C++ 进行扩展

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

目前仅一种调用约定用于 C 函数(“版本 1”)。通过为函数编写PG_FUNCTION_INFO_V1()宏调用来表示对该调用约定的支持,如下所示。

38.10.1. 动态加载#

在会话中首次调用特定可加载对象文件中的用户定义函数时,动态加载器将该对象文件加载到内存中,以便可以调用该函数。因此,用户定义 C 函数的CREATE FUNCTION必须为该函数指定两条信息:可加载对象文件的名称以及该对象文件中要调用的特定函数的 C 名称(链接符号)。如果未明确指定 C 名称,则假定它与 SQL 函数名称相同。

以下算法用于基于CREATE FUNCTION命令中给出的名称查找共享对象文件

  1. 如果名称是绝对路径,则加载给定的文件。

  2. 如果名称以字符串 $libdir 开头,则该部分将被 PostgreSQL 包库目录名称替换,该名称在构建时确定。

  3. 如果名称不包含目录部分,则在由配置变量 dynamic_library_path 指定的路径中搜索文件。

  4. 否则(文件未在路径中找到,或包含非绝对目录部分),动态加载程序将尝试按给定名称获取名称,这很可能会失败。(依赖于当前工作目录不可靠。)

如果此序列不起作用,则将特定于平台的共享库文件名扩展名(通常为.so)附加到给定名称,并再次尝试此序列。如果也失败,则加载将失败。

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

运行PostgreSQL服务器的用户 ID 必须能够遍历要加载的文件的路径。使文件或更高级别的目录对postgres用户不可读和/或不可执行是一个常见的错误。

在任何情况下,在CREATE FUNCTION命令中给出的文件名都会在系统目录中按字面记录,因此如果需要再次加载文件,则会应用相同的过程。

注意

PostgreSQL不会自动编译 C 函数。必须在CREATE FUNCTION命令中引用对象文件之前对其进行编译。有关其他信息,请参见第 38.10.5 节

为了确保不会将动态加载的对象文件加载到不兼容的服务器中,PostgreSQL会检查文件是否包含内容适当的“魔术块”。这允许服务器检测明显的兼容性问题,例如为不同主要版本的PostgreSQL编译的代码。要包含魔术块,请在包含头文件fmgr.h之后,在模块源文件(且仅一个)中编写以下内容

PG_MODULE_MAGIC;

首次使用后,动态加载的对象文件将保留在内存中。在同一会话中对该文件中的函数的后续调用将仅产生符号表查找的小开销。如果您需要强制重新加载对象文件,例如在重新编译后,请开始一个新的会话。

或者,动态加载的文件可以包含一个初始化函数。如果文件包含名为_PG_init的函数,则该函数将在加载文件后立即被调用。该函数不接收任何参数,并且应该返回 void。目前没有办法卸载动态加载的文件。

38.10.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 中约定int*XX*表示*XX*。因此,还要注意 C 类型int8的大小为 1 字节。SQL 类型int8在 C 中称为int64。另请参见表 38.2。)

另一方面,任何大小的固定长度类型都可以按引用传递。例如,以下是PostgreSQL类型的示例实现

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

只有在将它们传入和传出PostgreSQL函数时,才能使用指向此类类型的指针。要返回此类类型的变量,请使用palloc分配合适数量的内存,填写已分配的内存,并返回指向它的指针。(此外,如果您只想返回与您的一个输入参数(具有相同数据类型)相同的值,则可以跳过额外的palloc,只需返回指向输入值的指针即可。)

最后,所有变长类型也必须按引用传递。所有变长类型都必须以一个恰好为 4 个字节的不透明长度字段开头,该字段将由SET_VARSIZE设置;切勿直接设置此字段!要存储在该类型中的所有数据都必须位于该长度字段紧随其后的内存中。长度字段包含结构的总长度,也就是说,它包括长度字段本身的大小。

另一个重要的一点是避免在数据类型值中留下任何未初始化的位;例如,注意将结构中可能存在的任何对齐填充字节清零。如果没有这样做,则数据类型的逻辑等效常量可能会被规划器视为不相等,从而导致低效(尽管不正确)的计划。

警告

切勿修改按引用传递的输入值的内容。如果您这样做,则可能会损坏磁盘上的数据,因为给您的指针可能直接指向磁盘缓冲区。此规则的唯一例外在第 38.12 节中进行了说明。

例如,我们可以将类型text定义如下

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

[FLEXIBLE_ARRAY_MEMBER]表示数据部分的实际长度未由该声明指定。

在操作变长类型时,我们必须小心地分配正确数量的内存并正确设置长度字段。例如,如果我们想在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);
...

VARHDRSZsizeof(int32)相同,但使用宏VARHDRSZ来引用可变长度类型的开销大小被认为是良好的风格。此外,长度字段必须使用SET_VARSIZE宏设置,而不是通过简单的赋值。

PostgreSQL的许多内置 SQL 数据类型对应的 C 类型如表 38.2所示。“定义于”列给出了获取类型定义所需的需要包含的头文件。(实际定义可能在由所列文件包含的不同文件中。建议用户坚持使用已定义的接口。)请注意,您应该始终在任何服务器代码源文件中首先包含postgres.h,因为它声明了许多您无论如何都需要的内容,并且因为首先包含其他头文件可能会导致可移植性问题。

表 38.2。内置 SQL 类型对应的 C 类型

SQL 类型C 类型定义于
booleanboolpostgres.h(可能是编译器内置)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(编译器内置)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
float4 (real)float4postgres.h
float8 (double precision)float8postgres.h
int2 (smallint)int16postgres.h
int4 (integer)int32postgres.h
int8 (bigint)int64postgres.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
numericNumericutils/numeric.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocRegProcedurepostgres.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
时间戳时间戳datatype/timestamp.h
带时区的timestampTimestampTzdatatype/timestamp.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

现在我们已经了解了所有基本类型的可能结构,我们可以展示一些真实函数的示例。

38.10.3. 版本 1 调用约定#

版本 1 调用约定依赖于宏来抑制传递参数和结果的大部分复杂性。版本 1 函数的 C 声明始终为

Datum funcname(PG_FUNCTION_ARGS)

此外,宏调用

PG_FUNCTION_INFO_V1(funcname);

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

在版本 1 函数中,每个实际参数都使用PG_GETARG_*xxx*()宏获取,该宏对应于参数的数据类型。(在非严格函数中,需要使用PG_ARGISNULL()对参数的空值进行先前的检查;见下文。)使用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"
#include "varatt.h"

PG_MODULE_MAGIC;

/* 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_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);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_PP(0);
    text  *arg2 = PG_GETARG_TEXT_PP(1);
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    int32 new_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 integer
     AS 'DIRECTORY/funcs', 'add_one'
     LANGUAGE C STRICT;

-- note overloading of SQL function name "add_one"
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教程目录,其中包含本节中使用的示例的代码)。(更好的样式是在AS子句中仅使用'funcs',在将DIRECTORY*添加到搜索路径之后。无论如何,我们都可以省略共享库的系统特定扩展名,通常为.so。)

请注意,我们已将函数指定为“严格”,这意味着如果任何输入值为空,系统应自动假定结果为空。通过这样做,我们避免在函数代码中检查空输入。如果没有这样做,我们将不得不使用PG_ARGISNULL()显式检查空值。

PG_ARGISNULL(*n*)允许函数测试每个输入是否为 null。(当然,仅在未声明为“strict”的函数中执行此操作时才需要这样做。)与PG_GETARG_*xxx*()宏一样,输入参数从零开始计数。请注意,在验证参数不为 null 之前,应避免执行PG_GETARG_*xxx*()。要返回 null 结果,请执行PG_RETURN_NULL();这在严格和非严格函数中均有效。

乍一看,与使用纯C调用约定相比,版本 1 编码约定似乎只是毫无意义的晦涩难懂。但是,它们确实允许我们处理NULLable 参数/返回值和“toasted”(压缩或离线)值。

版本 1 接口提供的其他选项是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*指定列的存储类型。*storagetype*是plainexternalextendedmain之一。)

最后,版本 1 函数调用约定可以返回集合结果(第 38.10.8 节)并实现触发器函数(第 39 章)和过程语言调用处理程序(第 58 章)。有关更多详细信息,请参阅源代码分发中的src/backend/utils/fmgr/README

38.10.4. 编写代码#

在讨论更高级的主题之前,我们应该讨论一些PostgreSQLC 语言函数的编码规则。虽然可以将用 C 以外的语言编写的函数加载到PostgreSQL中,但这通常很困难(如果可能的话),因为其他语言(如 C++、FORTRAN 或 Pascal)通常不遵循与 C 相同的调用约定。也就是说,其他语言不会以相同的方式在函数之间传递参数和返回值。因此,我们将假设您的 C 语言函数实际上是用 C 编写的。

编写和构建 C 函数的基本规则如下

  • 使用 pg_config --includedir-server 找出 PostgreSQL 服务器头文件在您的系统(或您的用户将运行的系统)上安装的位置。

  • 编译和链接您的代码以便可以将其动态加载到 PostgreSQL 中始终需要特殊标志。请参阅 第 38.10.5 节,了解如何针对您的特定操作系统执行此操作的详细说明。

  • 请记住为您的共享库定义一个 魔块,如 第 38.10.1 节 中所述。

  • 分配内存时,使用 PostgreSQL 函数 pallocpfree,而不是相应的 C 库函数 mallocfree。由 palloc 分配的内存将在每个事务结束时自动释放,从而防止内存泄漏。

  • 始终使用 memset 将结构的字节清零(或首先使用 palloc0 分配它们)。即使您为结构的每个字段分配,也可能存在包含垃圾值的对其填充(结构中的空洞)。如果没有此操作,则很难支持哈希索引或哈希联接,因为您必须只选出数据结构的重要位来计算哈希。规划器有时也依赖于通过按位相等比较常量,因此如果逻辑等价的值在按位上不相等,您可能会得到不良的规划结果。

  • 大多数内部 PostgreSQL 类型在 postgres.h 中声明,而函数管理器接口(PG_FUNCTION_ARGS 等)在 fmgr.h 中,因此您至少需要包含这两个文件。出于可移植性原因,最好在任何其他系统或用户头文件之前 首先 包含 postgres.h。包含 postgres.h 还将为您包含 elog.hpalloc.h

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

38.10.5. 编译和链接动态加载函数#

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

有关本节中未包含的信息,您应该阅读操作系统的文档,尤其是 C 编译器cc和链接编辑器ld的手册页。此外,PostgreSQL源代码在contrib目录中包含几个工作示例。但是,如果您依赖这些示例,则会使您的模块依赖于PostgreSQL源代码的可用性。

创建共享库通常类似于链接可执行文件:首先将源文件编译成目标文件,然后将目标文件链接在一起。目标文件需要作为位置无关代码(PIC) 创建,从概念上讲,这意味着它们在可执行文件加载时可以放在内存中的任意位置。(通常不会以这种方式编译用于可执行文件的目标文件。)链接共享库的命令包含特殊标志,以将其与链接可执行文件区分开来(至少在理论上是这样——在某些系统上,做法要丑陋得多)。

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

FreeBSD

创建 的编译器标志是 -fPIC。要创建共享库,编译器标志是 -shared

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

这适用于 FreeBSD 的 3.0 版本。

Linux

创建 的编译器标志是 -fPIC。创建共享库的编译器标志是 -shared。一个完整的示例如下所示

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o
macOS

这里有一个示例。它假设已安装开发者工具。

cc -c foo.c
cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o
NetBSD

创建 的编译器标志是 -fPIC。对于 系统,使用带有标志 -shared 的编译器来链接共享库。在较旧的非 ELF 系统上,使用 ld -Bshareable

gcc -fPIC -c foo.c
gcc -shared -o foo.so foo.o
OpenBSD

创建 的编译器标志是 -fPIC。使用 ld -Bshareable 来链接共享库。

gcc -fPIC -c foo.c
ld -Bshareable -o foo.so foo.o
Solaris

使用 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

提示

如果您觉得这太复杂,应考虑使用GNU Libtool,它将平台差异隐藏在统一界面之后。

然后可以将生成的共享库文件加载到PostgreSQL中。向CREATE FUNCTION命令指定文件名时,必须提供共享库文件名,而不是中间对象文件。请注意,系统标准共享库扩展名(通常为.so.sl)可以从CREATE FUNCTION命令中省略,并且通常应省略以获得最佳可移植性。

请参阅第 38.10.1 节,了解服务器期望在何处找到共享库文件。

38.10.6. 复合类型参数#

复合类型没有像 C 结构那样的固定布局。复合类型的实例可以包含空字段。此外,作为继承层次结构一部分的复合类型可以具有与同一继承层次结构中的其他成员不同的字段。因此,PostgreSQL提供了一个函数接口,用于从 C 访问复合类型的字段。

假设我们要编写一个函数来回答查询

SELECT name, c_overpaid(emp, 1500) AS overpaid
    FROM emp
    WHERE name = 'Bill' OR name = 'Sam';

使用版本 1 调用约定,我们可以将c_overpaid定义为

#include "postgres.h"
#include "executor/executor.h"  /* for GetAttributeByName() */

PG_MODULE_MAGIC;

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_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByName是PostgreSQL系统函数,用于从指定行中返回属性。它有三个参数:传递到函数中的HeapTupleHeader类型参数、所需属性的名称以及一个返回参数,该参数指示属性是否为空。GetAttributeByName返回一个Datum值,您可以使用适当的DatumGet*XXX*()函数将其转换为适当的数据类型。请注意,如果空标志已设置,则返回值没有意义;在尝试对结果执行任何操作之前,务必始终检查空标志。

还有GetAttributeByNum,它按列号而不是按名称选择目标属性。

以下命令在 SQL 中声明函数c_overpaid

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

请注意,我们使用了STRICT,这样我们不必检查输入参数是否为 NULL。

38.10.7. 返回行(复合类型)#

要从 C 语言函数返回行或复合类型值,可以使用一个特殊的 API,它提供宏和函数来隐藏构建复合数据类型的大部分复杂性。要使用此 API,源文件必须包含

#include "funcapi.h"

有两种方法可以构建复合数据值(以下称为“元组”):可以从 Datum 值数组构建,也可以从 C 字符串数组构建,该数组可以传递给元组列数据类型的输入转换函数。在任何一种情况下,您首先需要获取或构造元组结构的TupleDesc描述符。在使用 Datum 时,将TupleDesc传递给BlessTupleDesc,然后为每一行调用heap_form_tuple。在使用 C 字符串时,将TupleDesc传递给TupleDescGetAttInMetadata,然后为每一行调用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)

如果你计划使用数据,或者

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

如果你计划使用 C 字符串。如果你正在编写返回集合的函数,则可以将这些函数的结果保存在FuncCallContext结构中 — 分别使用tuple_descattinmeta字段。

使用数据时,请使用

HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)

根据数据形式的用户数据构建HeapTuple

使用 C 字符串时,请使用

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

根据 C 字符串形式的用户数据构建HeapTuple。*values是一个 C 字符串数组,每个字符串对应于返回行的一个属性。每个 C 字符串应采用属性数据类型输入函数所期望的形式。为了对其中一个属性返回 null 值,values*数组中的相应指针应设置为NULL。对于你返回的每一行都需要再次调用此函数。

一旦你构建了一个从函数返回的元组,它必须转换为Datum。使用

HeapTupleGetDatum(HeapTuple tuple)

HeapTuple转换为有效的 Datum。如果你打算只返回一行,则可以直接返回此Datum,或者它可以用作集合返回函数中的当前返回值。

示例出现在下一节中。

38.10.8. 返回集#

C 语言函数有两种返回集(多行)的选项。在一种方法中,称为ValuePerCall模式,重复调用返回集函数(每次传递相同的参数),并且它在每次调用时返回一行新数据,直到它没有更多行要返回并通过返回 NULL 来发出信号。因此,返回集函数 (SRF) 必须跨调用保存足够的状态来记住它正在做什么并在每次调用时返回正确的下一项。在另一种方法中,称为Materialize模式,SRF 填充并返回一个包含其整个结果的元组存储对象;然后只对整个结果进行一次调用,并且不需要调用间状态。

在使用 ValuePerCall 模式时,请务必记住,不能保证查询完全运行;也就是说,由于LIMIT等选项,执行器可能会在获取所有行之前停止对返回集函数的调用。这意味着在最后一次调用中执行清理活动是不安全的,因为这可能永远不会发生。建议对需要访问外部资源(例如文件描述符)的函数使用 Materialize 模式。

本节的其余部分记录了一组帮助程序宏,这些宏通常用于(但不是必须用于)使用 ValuePerCall 模式的 SRF。可以在src/backend/utils/fmgr/README中找到有关 Materialize 模式的其他详细信息。此外,PostgreSQL源发行版中的contrib模块包含许多使用 ValuePerCall 和 Materialize 模式的 SRF 示例。

要使用此处描述的 ValuePerCall 支持宏,请包含funcapi.h。这些宏与结构FuncCallContext一起使用,该结构包含需要跨调用保存的状态。在调用 SRF 中,fcinfo->flinfo->fn_extra用于在调用中保存指向FuncCallContext的指针。这些宏在首次使用时会自动填充该字段,并期望在后续使用中在那里找到相同的指针。

typedef struct FuncCallContext
{
    /*
     * 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.
     */
    uint64 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.
     */
    uint64 max_calls;

    /*
     * OPTIONAL pointer to miscellaneous user-provided context information
     *
     * user_fctx is for use as a pointer to your own data to retain
     * arbitrary context information between calls of your function.
     */
    void *user_fctx;

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

    /*
     * memory context used for structures that 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 reused across multiple calls
     * of the SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * OPTIONAL pointer to struct containing tuple description
     *
     * tuple_desc is for use when returning tuples (i.e., composite data types)
     * and is only needed if you are going to build the tuples with
     * heap_form_tuple() rather than with BuildTupleFromCStrings().  Note that
     * the TupleDesc pointer stored here should usually have been run through
     * BlessTupleDesc() first.
     */
    TupleDesc tuple_desc;

} FuncCallContext;

使用此基础架构的SRF要使用的宏是

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。使用funcctx->user_fctx来保存对任何此类跨调用数据结构的指针。(您在multi_call_memory_ctx中分配的数据将在查询结束时自动消失,因此也不必手动释放该数据。)

警告

虽然函数的实际参数在调用之间保持不变,但如果您在瞬态上下文中对参数值进行解冻(通常由PG_GETARG_*xxx*宏透明地完成),则解冻的副本将在每个周期中被释放。因此,如果您在user_fctx中保留对这些值引用的,则必须在解冻后将它们复制到multi_call_memory_ctx中,或确保仅在该上下文中对这些值进行解冻。

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

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    further declarations as needed

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* One-time setup code appears here: */
        user code
        if returning composite
            build TupleDesc, and perhaps AttInMetadata
        endif returning composite
        user code
        MemoryContextSwitchTo(oldcontext);
    }

    /* Each-time setup code appears here: */
    user code
    funcctx = SRF_PERCALL_SETUP();
    user 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 code
        obtain result Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Here we are done returning items, so just report that fact. */
        /* (Resist the temptation to put cleanup code here.) */
        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;

    /* 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_INT32(0);

        /* Build a tuple descriptor for our result type */
        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")));

        /*
         * 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;
    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 building the returned tuple.
         * This should be an array of C strings which will
         * be processed later by the type input 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 = HeapTupleGetDatum(tuple);

        /* clean up (this is not really 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 __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类型。

38.10.9. 多态参数和返回类型#

C 语言函数可以被声明为接受和返回第 38.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 "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");

    /* get the provided element, being careful in case it's NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* we have one dimension */
    ndims = 1;
    /* and one element */
    dims[0] = 1;
    /* and lower bound is 1 */
    lbs[0] = 1;

    /* get required info about the element type */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* now build the array */
    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标记后,实现该行为(如果需要)。

38.10.10. 共享内存和 LW 锁#

加载项可以在服务器启动时保留 LW 锁和共享内存分配。加载项的共享库必须通过在shared_preload_libraries中指定它来预加载。共享库应在其_PG_init函数中注册一个shmem_request_hook。此shmem_request_hook可以保留 LW 锁或共享内存。通过调用以下内容保留共享内存

void RequestAddinShmemSpace(int size)

从您的shmem_request_hook

通过调用

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

shmem_request_hook预留 LWLock。这将确保num_lwlocksLWLock 数组在tranche_name名称下可用。使用GetNamedLWLockTranche获取指向此数组的指针。

可以在PostgreSQL源树中的contrib/pg_stat_statements/pg_stat_statements.c中找到shmem_request_hook的示例。

为避免可能的竞争条件,每个后端都应在连接到共享内存并对其分配进行初始化时使用 LWLockAddinShmemInitLock,如下所示

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->locks = GetNamedLWLockTranche("my tranche name");
        }
        LWLockRelease(AddinShmemInitLock);
}

38.10.11. 使用 C++ 扩展#

虽然PostgreSQL后端是用 C 编写的,但如果遵循以下准则,则可以用 C++ 编写扩展

  • 后端访问的所有函数都必须向后端提供 C 接口;这些 C 函数随后可以调用 C++ 函数。例如,后端访问的函数需要 extern C 链接。对于在后端和 C++ 代码之间作为指针传递的任何函数,这也是必需的。

  • 使用适当的取消分配方法释放内存。例如,大多数后端内存使用 palloc() 分配,因此使用 pfree() 释放它。在这种情况下使用 C++ delete 将失败。

  • 防止异常传播到 C 代码(在所有 extern C 函数的顶层使用 catch-all 块)。即使 C++ 代码没有明确抛出任何异常,这也必需,因为内存不足等事件仍可能抛出异常。必须捕获任何异常并将适当的错误传递回 C 接口。如果可能,请使用 -fno-exceptions 编译 C++ 以完全消除异常;在这种情况下,您必须检查 C++ 代码中的故障,例如,检查 new() 返回的 NULL。

  • 如果从 C++ 代码调用后端函数,请确保 C++ 调用堆栈仅包含普通旧数据结构 ()。这是必需的,因为后端错误会生成一个远程 longjmp(),该远程 longjmp() 无法正确展开包含非 POD 对象的 C++ 调用堆栈。

总之,最好将 C++ 代码放在extern C函数的墙后面,这些函数与后端接口,并避免异常、内存和调用堆栈泄漏。