13.4. 编译(C)语言函

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

注意: 在第一次使用之后,一个动态装载的用户函数仍然停留在内存中, 因而在同一会话过程中对该函数的再次调用只是 简单的符号表查找。

声明目标文件的字符串(AS 子句里的字符串)应该是该函数目标文件的 完整路径,并用.单引号括起来。 如在 AS 子句里使用了 链接符号,链接符号也应该用单引号括起来, 并且就应该是 C 源代码里函数的名称。 在 Unix 系统里,命令 nm 会打印出一 个可动态装载的对象里的所有链接符号。

注意: Postgres 不会自动编译一个函数; 该函数必须在使用 CREATE FUNCTION 命令 之前编译。参阅下文获取额外信息。

目前C函数使用两种调用风格. "版本 1" 调用方法是用给调用函数写一个 PG_FUNCTION_INFO_V1() 宏来表示的,在下面有例子演示. 没有这样的宏表示是旧风格("版本 0")的函数. 两种方法在 CREATE FUNCTION 里声明的语言名字都是 'C'. 出于移植原因,以及缺乏功能,老风格的函数现在已经废弃了, 不过为了保持兼容,我们仍然支持它们.

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

下表列出了被装载入 Postgres 的 C 函数里需要的当作参数的 C 类型。 "定义在" 列给出了等效的 C 类型定义的实际的头文件 (在 .../src/backend/ 目录里)。 注意,你应该总是首先包括postgres.h, 然后它会包含 c.h

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

内建类型 C 类型 定义在
abstimeAbsoluteTimeutils/nabstime.h
boolboolinclude/c.h
box(BOX *)utils/geo-decls.h
bytea(bytea *)include/postgres.h
"char"charN/A
cidCIDinclude/postgres.h
datetime(DateTime *)include/c.h 或 include/postgres.h
int2int2 或 int16include/postgres.h
int2vector(int2vector *)include/postgres.h
int4int4 或 int32include/postgres.h
float4(float4 *)include/c.h 或 include/postgres.h
float8(float8 *)include/c.h 或 include/postgres.h
lseg(LSEG *)include/geo-decls.h
name(Name)include/postgres.h
oidoidinclude/postgres.h
oidvector(oidvector *)include/postgres.h
path(PATH *)utils/geo-decls.h
point(POINT *)utils/geo-decls.h
regprocregproc 或 REGPROCinclude/postgres.h
reltimeRelativeTimeutils/nabstime.h
text(text *)include/postgres.h
tidItemPointerstorage/itemptr.h
timespan(TimeSpan *)include/c.h 或 include/postgres.h
tintervalTimeIntervalutils/nabstime.h
xid(XID *)include/postgres.h

Postgres 内部把基本类型当作"一片内存"看待. 定义在某种类型上的用户定义函数实际上定义了 Postgres 对(该数据类型) 可能的操作.也就是说, Postgres 只是从磁盘读取和存储该数据类型, 而使用你定义的函数来输入,处理和输出数据.基本 类型可以有下面三种内部形态(格式)之一:

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

/* 4-byte integer, passed by value */
typedef int int4;

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

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

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

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

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

显然,上面的数据域不够存储任何可能的字串 -- 在 C中定义这么个结构是不可能的. 当处理变长类型时,我们必须仔细 分配正确的存储器数量并初始化长度域. 例如,如果我们想在一个 text 结构里存储 40 字节, 我们可能会使用象下面的代码 片段:

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

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

13.4.2. 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 并且编译成了共享目标, 我们可以用下面的命令为 Postgres 定义这些函数:

CREATE FUNCTION add_one(int4) RETURNS int4
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

-- note overloading of SQL function name add_one()
CREATE FUNCTION add_one(float8) RETURNS float8
     AS 'PGROOT/tutorial/funcs.so',
        'add_one_float8'
     LANGUAGE 'c' WITH (isStrict);

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);
                         
CREATE FUNCTION copytext(text) RETURNS text
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

这里的 PGROOT 代表 Postgres 源代码的全路径. 请注意这个路径取决于你的系统,共享对象的文件名可能不是以 .so结尾,而是.sl 或者其他的什么东西;请相应做修改.

请注意我们把函数声明为"strict"(严格),意思是说如果任何输入值为NULL, 那么系统应该自动假设一个NULL的结果.这样处理可以让我们避免在 函数代码里面检查NULL输入.如果不这样处理,我们就得明确检查NULL, 比如为每个传递引用的参数检查空指针.(对于传值类型的参数,我们 甚至没有办法检查!)

尽管这种老风格的调用风格用起来简单,它确不太容易移植;在一些系统上, 我们用这种方法传递比 int 小的数据类型就会碰到困难.而且,我们 没有很好的返回NULL结果的办法,也没有除了把函数严格化以外的处理 NULL参数的方法. 下面要讲的版本-1的方法则解决了这些问题.

13.4.3. C语言函数的版本-1调用风格

版本-1调用风格依赖宏来消除大多数传递参数和结果的复杂性.版本-1风格函数的 C定义总是下面这样

                Datum funcname(PG_FUNCTION_ARGS)
另外,下面的宏
                PG_FUNCTION_INFO_V1(funcname);
也必须出现在同一个源文件里(通常就可以写在函数自身前面). 对那些"内部"-语言函数而言,不需要调用这个宏, 因为 Postgres 目前假设内部函数都是版本 -1. 不过,对于动态链接的函数,它是必须的.

在版本-1函数里, 每个实际参数都是用一个对应该参数的数据类型的 PG_GETARG_xxx()宏 抓取的,结果是用返回类型的 PG_RETURN_xxx()宏返回的.

下面是和上面一样的函数,但是是用新风格编的:

#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 命令和用于老风格的等效的命令一样.

猛地一看,版本-1的编码好象只是无目的地蒙人.但是它们的确给我们 许多改进,因为宏可以隐藏许多不必要的细节. 一个例子在 add_one_float8 的编码里,这里我们不再需要不停叮嘱自己 float8 是传递引用类型.另外一个例子是用于变长类型的宏 GETARG 隐藏 了抓取"toasted"(烤炉)(压缩的或者超长的)值需要做的处理.上面显示的 老风格的 copytextconcat_text函数在处理 toasted 的值的时候 实际上是错的,因为它们在处理输入时没有调用 pg_detoast_datum(). (用于老风格动态装载函数的句柄现在会处理这些细节, 不过与版本-1函数的所有可能性相比,它做的实在是不够充分.)

版本-1的函数另一个巨大的改进是对 NULL 输入和结果的处理. 宏 PG_ARGISNULL(n) 允许一个函数测试每个输入 是否为 NULL (当然,这件事只是对那些没有声明为 “strict” 的函数有必要).因为如果有PG_GETARG_xxx() 宏,输入参数是从零开始计算的.要返回一个 NULL 结果,执行一个 PG_RETURN_NULL(),这样对严格的和不严格的函数 都有效.

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

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

复合类型不象 C 结构那样有固定的布局. 复合类型的记录可能包含空(null)域. 另外,一个属于继承层次一部分的复合类 型可能和同一继承范畴的其他成员有不同的域/字段. 因此, Postgres 提供一个过程接口用于从 C 里面访问复合类型.在 Postgres 处理一个记录集时, 每条记录都将作为一个类型为 TUPLE(元组) 的不透明(opaque)的结构被传递给你的函数. 假设我们 为下面查询写一个函数

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

/* 以版本-1编码,上面的东西会写成下面这样:*/

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

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

下面的查询让 Postgres 知道 c_overpaid 函数:

CREATE FUNCTION c_overpaid(emp, int4) 
RETURNS bool
AS 'PGROOT/tutorial/obj/funcs.so' 
LANGUAGE 'c';

当然还有其他方法在 C 函数里构造新的记录或修改现有记录, 这些方法都太复杂,不适合在本手册里讨论.

13.4.5. 书写代码

我们现在转到了书写编程语言函数的更艰难的阶段. 要注意:本手册此章的内容不会让你成为程序员. 在你尝试用C 书写用于 Postgres 的函数之前,你必须对 C 有很深的了解(包括对指针的使用和 malloc 存储器管理). 虽然可以用 C 以外的其他语 言如 FORTRANPascal 书写用于Postgres 的共享函数,但通常很麻烦(虽然是完全可能的),因为其他语言并不遵循和 C 一样的 调用习惯. 其他语言与C的传递参数和返回值的方式不一样. 因此我们假设你的编程语言函数是用 C 写的.

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

13.4.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

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

Digital Unix/Tru64 UNIX

PIC 是缺省,因此编译命令就是平常的那个. 带特殊选项的 ld 用于链接:

cc -c foo.c
ld -shared -expect_unresolved '*' -o foo.so foo.o
用 GCC 代替系统编译器时的过程是一样的;不需要特殊的选项.

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

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 制作共享库.它把平台之间的区别封装成 了一个通用的并且非常强大的接口.严肃的包还要求考虑有关库版本, 符号解析方法和一些其他的问题.

生成的共享库文件然后就可以装载到 Postgres里面去了.在给 CREATE FUNCTION 命令声明文件名的时候,我们必须声明 共享库文件的名字(以 .so 结尾)而不是简单的对象文件.

注意: 实际上,只要该文件是共享库文件,Postgres 并不在乎它的名字是什么.

CREATE FUNCTION 命令的路径必须是绝对路径 (也就是说,以 / 开头),指向一个 Postgres 服务器运行的机器上可见的目录. 相对路径实际上是可以用的,但是相对与数据库所在的目录(通常是 前端应用不可见的).显然,让路径相对于用户启动前端应用的目录 是没有意义的,因为服务器可能是运行在一台完全不同的机器上的! 运行 Postgres 服务器的用户 ID 必须 能够访问 CREATE FUNCTION 给出的路径并且可以 读取该共享库文件.(用户常犯的错误就是令该文件或者更高层目录 为 “postgres” 用户不可读和/或不可执行.)