四、编码规范
在我们转向更高级主题之前,我们将讨论一些PostgreSQL C语言函数的编码规则。虽然将非C语言编写的函数载入PostgreSQL是可能的,但这通常比较困难,因为诸如C++,FORTRAN,或者Pascal语言往往有着与C语言不同的调用约定。这意味着其他语言无法用C语言的方式在函数间传递参数和返回值。基于此,这里我们假设你的C语言函数是真真正正用C语言编写的。
编写、构建C函数的基本规则如下:
- 使用
pg_config --includedir-server
检查你或者你用户的系统中是否安装了PostgreSQL服务端开发头文件。 - 编译和链接可动态加载到PostgreSQL中的代码需要一些特定的标志。本文第五节会详细说明如何在特定的操作系统上完成此操作。
- 切记要为你的共享库定义“magic block”。
- 分配内存时,请使用PostgreSQL的函数
palloc
和pfree
替代C语言库函数malloc
和free
。通过palloc
分配的内存在每个事务完成后会自动释放以预防内存泄露。 - 使用结构前,一定要使用
memset
将结构的每个字节置0(或者在首次为它分配空间时使用palloc0
)。因为即使给结构的每个字段都赋了值,也有可能仍旧存在为对齐而产生的含有垃圾值的填充字节(结构中的空洞)。不这样做,就很难支持hash索引或hash join,因为只能使用数据结构的有效位去计算hash值。有时规划器也会通过逐位比较来判断常量是否相等,因此如果逻辑上相等的值逐位比较却不相等,就会得到不想要的规划结果,从而产生错误的执行计划。 postgres.h
中声明了一些PostgreSQL的内部类型,函数管理接口(PG_FUNCTION_ARGS
......)则在fmgr.h
中,因此程序至少要包含这两个头文件。基于可移植性考虑,最好首先包含postgres.h
,将其放在其他系统或用户头文件之前。包含postgres.h
会自动包含elog.h
和palloc.h
。- 目标文件里的符号名必须唯一,不能与已经加载到PostgreSQL服务中的其他可执行符号名冲突。如果收到与此相关的错误消息,必须重命名相关的函数或变量。
五、编译链接动态加载函数
要想使用C语言编写的PostgreSQL扩展函数,必须使用特定的方法编译链接生成一个可被动态加载到服务器中的文件。准确的说,需要创建一个共享库。
本小节不会介绍C编译器、cc、链接器ld,相关内容请参阅你操作系统文档中的手册页。PostgreSQL的源码中也提供了几个可工作的示例,位于contrib目录。这些例子需要可用的PostgreSQL源码。
创建共享库与链接可执行文件类似:先将源码编译成目标文件,然后将目标文件链接到一起。目标文件要创建成位置无关代码(position-independent code (PIC)),从概念上讲,可执行文件可以将它们加载到内存中的任意位置。(适用于可执行文件的目标文件通常不用这种方式编译)。链接共享库时会使用一些特定的选项以与链接可执行文件区分开来。
接下来的示例中,我们假设你的源码名为foo.c,创建的共享库名为foo.so。除非另有说明,中间目标文件名为foo.o。共享库可包含不止一个目标文件,这里我们仅使用一个。
FreeBSD
创建PIC的编译选项是 -fPIC
。创建共享库的编译器选项是 -shared
。
gcc-fPIC-c foo.c gcc-shared-o foo.so foo.o
Linux
创建PIC的编译选项是 -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
创建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
或者
gcc-fPIC-c foo.c gcc-G-o foo.so foo.o
生成好的共享库文件可被加载到。给 CREATE FUNCTION
命令指定的文件名必须是共享库的名称而非中间目标文件的。 CREATE FUNCTION
命令可以省略系统标准的共享库扩展名(通常是 .so
或 .sl
),省略掉扩展名会有更好的可移植性。
六、组合类型参数
组合类型不具有类似C语言结构那样的固定布局。组合类型实例可以有空字段。此外,属于继承层次结构一部分的组合类型可以具有与同一继承层次结构的其他成员不同的字段。因此,PostgreSQL提供了一个函数接口,用于从C语言访问组合类型的字段。
Suppose we want to write a function to answer the query:
SELECT name, c_overpaid(emp,1500)AS overpaid FROM emp WHERE name ='Bill'OR name ='Sam';
使用版本-1调用约定,定义 c_overpaid
如下:
/* for GetAttributeByName() */PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(c_overpaid); Datumc_overpaid(PG_FUNCTION_ARGS) { HeapTupleHeadert=PG_GETARG_HEAPTUPLEHEADER(0); int32limit=PG_GETARG_INT32(1); boolisnull; Datumsalary; 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
函数将其转换到正确的数据类型。如果设置了null标志,则返回值没有意义;在尝试对结果执行任何操作之前,请始终检查null标志。XXX
()
另外,GetAttributeByNum
通过列号而不是名字来选择目标属性。
以下命令声明供SQL调用的 c_overpaid
函数:
CREATE FUNCTION c_overpaid(emp,integer) RETURNS booleanAS'DIRECTORY/funcs','c_overpaid' LANGUAGE C STRICT;
这里使用 STRICT
可不必检查输入参数是否为空。
七、返回行(组合类型)
从C语言函数返回一行或组合类型值,可以使用一组提供了宏和函数的特定API以隐藏构造组合数据类型的复杂性。使用这些API需要在源码中包含:
构造组合类型(以后称为元组)有两种途径:从Datum值数组构造,或者从一个能被传递到元组列数据类型的输入转换函数的C字符串数组构造。不管用哪种方式,首先需要为元组结构获取或者构造一个 TupleDesc
描述符。当使用 Datums时,传递 TupleDesc
到BlessTupleDesc
,然后为每一行调用 heap_form_tuple
。当使用C字符串时,传递 TupleDesc
到 TupleDescGetAttInMetadata
,然后为每一行调用 BuildTupleFromCStrings
。在返回元组集合的场景,在函数首次调用时可一次完成全部的设置步骤。
有几个辅助函数可用于设置所需的 TupleDesc
。在大多数返回组合值的函数中建议如下调用:
TypeFuncClassget_call_result_type(FunctionCallInfofcinfo, Oid*resultTypeId, TupleDesc*resultTupleDesc)
将相同的 fcinfo
结构传递给调用函数本身(当然,这需要使用版本-1调用约定)。 resultTypeId
可以置空或者设置成一个局部变量地址用以接收函数的返回类型OID。 resultTupleDesc
可以是局部 TupleDesc
变量的地址。 TYPEFUNC_COMPOSITE
用于检查结果。这样做后, resultTupleDesc
已被需要的 TupleDesc
填充。(如果没有,可以报告错误“function returning record called in context that cannot accept type record”。)
获取TupleDesc
比较旧的已经废弃的函数是:
TupleDescRelationNameGetTupleDesc(constchar*relname)
用于获取命名relation行类型的 TupleDesc
。
TupleDescTypeGetTupleDesc(Oidtypeoid, List*colaliases)
用于获取基于一个类型的OID的 TupleDesc
。可用于获取一个基类型或者组合类型的 TupleDesc
。无法用于返回 record
的函数,也无法解析多态类型。
一旦有了 TupleDesc
,调用
TupleDescBlessTupleDesc(TupleDesctupdesc)
如果计划使用Datums。
可使用:
AttInMetadata*TupleDescGetAttInMetadata(TupleDesctupdesc)
如果计划使用C字符串。如果写一个返回集合的函数,可以保存这些函数的结果到 FuncCallContext
结构 — 分别使用 tuple_desc
或 attinmeta
字段。
当采用Datums,使用
HeapTupleheap_form_tuple(TupleDesctupdesc, Datum*values, bool*isnull)
用给定的采用Datum方式的用户数据构造 HeapTuple
。
当采用C字符串方式,使用
HeapTupleBuildTupleFromCStrings(AttInMetadata*attinmeta, char**values)
用给定的采用C字符串方式的用户数据构造 HeapTuple
。 values
是一个C字符串数组,每个数组表示返回行的一个属性。每一个C字符串都应该是属性数据类型输入函数所期望的形式。要给其中一个属性返回空值,需要将 values
数组中相应指针设置为 NULL
。每返回一行都需要调用此函数一次。
一旦从函数中构造好要返回的元组,它必须被转换成 Datum
。可使用
HeapTupleGetDatum(HeapTupletuple)
去转换 HeapTuple
到合法的Datum。如果函数返回单行,可直接返回些 Datum
,或在返回数据集的函数中做为当前行返回。
具体示例见下节。