PostgreSQL SQL扩展 ---- C语言函数(一)

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 可以用C(或者与C兼容,比如C++)语言编写用户自定义函数(User-defined functions)。这些函数被编译到动态可加载目标文件(也称为共享库)中并被守护进程加载到服务中。“C语言函数”与“内部函数”的区别就在于动态加载这个特性,二者的实际编码约定本质上是相同的(因此,标准的内部函数库为用户自定义C语言函数提供了丰富的示例代码)

本文编译自PostgreSQL官方文档第38.10 C-Language Functions

可以用C(或者与C兼容,比如C++)语言编写用户自定义函数(User-defined functions)。这些函数被编译到动态可加载目标文件(也称为共享库)中并被守护进程加载到服务中。“C语言函数”与“内部函数”的区别就在于动态加载这个特性,二者的实际编码约定本质上是相同的(因此,标准的内部函数库为用户自定义C语言函数提供了丰富的示例代码)。

当前C语言函数只有一种调用约定(“Version 1”)。通过为函数编写PG_Function_INFO_V1()宏调用来表示对该调用约定的支持,后面会举例说明。

一、动态加载

首次在会话中调用位于特定可加载目标文件中的用户用定义函数。当使用CREATE FUNCTION命令为用户自定义C函数创建SQL函数时必须为该函数指定两条信息:可加载目标文件名以及位于目标文件中的欲调用的C语言函数名(链接符号)。如果没有给出C语言函数名,则假设与SQL函数名相同。

CREATE FUNCTION命令使用以下逻辑定位给定名字的共享目标文件:

  1. 如果名字是绝对路径,加载该文件;
  2. 如果名字以libdirlibdir开始,libdir被替换成构建PostgreSQL时指定的库目录名;
  3. 如果名字中未包含目录,则在配置变量dynamic_library_path指定的目录中搜索;
  4. 其他情况(在指定路径中未发现文件或者文件名包含非绝对路径),动态加载器尝试按给定的名字加载,这很可能会失败(依赖当前工作目录是不可靠的。)

如果按以上顺序依旧无法正常加载,则给指定的名字加上平台特定的动态加载库的扩展名(通常是.so),然后按上述顺序再试一次。如果依旧失败,则整个加载过程失败。

推荐将可加载目标文件存放在libdir或者动态库路径指定的目录中。如果新安装位于不同的位置,可简化版本升级。可使用命令pg_config --pkglibdir找到libdir实际指向的目录。

运行PostgreSQL服务的用户必须对要加载的文件的路径有遍历的权限。postgres用户对文件或更高级别的目录没有可读/或可执行权限是一个常见的错误。

在任何情况下,CREATE FUNCTION命令中给定的文件名都会记录在系统目录(catalog)中,如果需要再次加载该文件,执行应用相同的过程即可。

PostgreSQL会检查动态可加载目标文件中是否包含特定内容的“magic block”以确保其不被加载到不兼容的服务中。这允许服务器检测明显的不兼容,比如代码使用不同的PostgreSQL主版本编译。要加入magic block,只需在代码的#include "fmgr.h"头文件的下面加入以下代码(只能加一次):

PG_MODULE_MAGIC;

首次使用动态可加载目标文件后,它将被保留在内存中。以后在同一会话中调用该文件中的函数时,仅需非常小的符号表查找开销。如果你需要强制重新加载目标文件(比如在重新编译后),开户一个新会话即可。

动态可加载文件可包含一个初始化函数(可选)。如果文件包含一个名为_PG_init的函数,完成加载后该函数将立即被调用。该函数不带参数也不返回任何结果。目前暂无方法卸载动态可加载文件。

二、C语言函数中的基类型

要了解如何写C语言函数,你需要了解在PostgreSQL内部如何表示基本数据类型并如何在函数间传递它们。在内部,PostgreSQL将基本类型视为“内存blob”。你在类型上定义的用户定义函数反过来定义了PostgreSQL可以对其进行操作的方式。也就是说,PostgreSQL只从磁盘存储和获取数据,然后使用你的用户自定义函数去输入、处理、输出数据。

基本类型可具有以下三种内部格式之一:

  • 按值传递,定长
  • 按引用传递,定长
  • 按引用传递,变长

按值类型的长度只能是1、2、4字节(如果在你的机器上sizeof(Datum)=8,也可为8字节)。你必须仔细定义你的类型以保证它们在所有的架构下长度都相同。比如:long类型就很危险,因为它在有些机器上是4字节但在另一类机器上又是8字节,而在大多数Unix机器上,int类型都是4字节。在Unix机器上,可像这样合理实现int4类型:

/* 4-byte integer, passed by value */typedefintint4;

(实际上,PostgreSQL C代码调用此类型int32,因为在C中有个约定,intXX表示XX位。因些还要注意,C类型int8的长度是1字节而SQL类型的int8在C中是int64。)

另一方面,任何大小的定长类型都可以通过引用传递。下面是一个PostgreSQL类型的简单实现的例子:

/* 16-byte structure, passed by reference */typedefstruct{
doublex, y;
} Point;

当向PostgreSQL函数传入传出该类型时,只能使用指向该类型的指针。要返回该类型,要使用palloc分配正确大小的内存,接着填充分配好的内存,然后返回指向它的指针。(当然,如果你只是想返回与你输入的某个参数具有相同数据类型的值,你可以跳过额外的palloc,仅返回指向输入值的指针。)。

Finally, 所有可变长类型必须通过引用传递。所有的可变长类型必须以一个确切的4字节的长度字段开头,该字段通过SET_VARSIZE设置;永远不要直接对该字段赋值!在内存中,所有保存在该类型中的数据必须紧跟在长度字段之后。长度字段包含结构的总长度,长度字段自身的大小也包含其中。

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

举个例子,我们可以定义像下面这样定义 text 类型:

typedefstruct {
int32length;
chardata[FLEXIBLE_ARRAY_MEMBER];
} text;

[FLEXIBLE_ARRAY_MEMBER]注解表示数据部分的实际长度不在此定义是指定。

当维护可变长类型时,我们必须仔细分配合适数量的内存并正确设置长度字段。假如你想保存40个字节到text结构中,需要使用如下代码片段:

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

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

下表显示了与PostgreSQL的许多内置SQL数据类型相对应的C类型。Defined In”列给出了必须被包含以取得类型定义的头文件。(实际定义可能位于下表列出的不同文件中。推荐用户只会用已定义的接口)。在服务端代码文件中,你应该总是首先包含 postgres.h ,因为它声明了许多你无论如何都需要的内容,并且先包含其他头文件可能会导致可移植性问题。


内置SQL类型的等效C类型

SQL Type C Type Defined In
boolean bool postgres.h (maybe compiler built-in)
box BOX* utils/geo_decls.h
bytea bytea* postgres.h
"char" char (compiler built-in)
character BpChar* postgres.h
cid CommandId postgres.h
date DateADT utils/date.h
float4 (real) float4 postgres.h
float8 (double precision) float8 postgres.h
int2 (smallint) int16 postgres.h
int4 (integer) int32 postgres.h
int8 (bigint) int64 postgres.h
interval Interval* datatype/timestamp.h
lseg LSEG* utils/geo_decls.h
name Name postgres.h
numeric Numeric utils/numeric.h
oid Oid postgres.h
oidvector oidvector* postgres.h
path PATH* utils/geo_decls.h
point POINT* utils/geo_decls.h
regproc RegProcedure postgres.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 datatype/timestamp.h
timestamp with time zone TimestampTz datatype/timestamp.h
varchar VarChar* postgres.h
xid TransactionId postgres.h

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

三、版本1(Version 1)调用约定

版本1调用约定依赖于宏来抑制传递参数和结果的复杂性。版本1的C函数总是像下面这样声明:

Datumfuncname(PG_FUNCTION_ARGS)

与此同时,宏调用:

PG_FUNCTION_INFO_V1(funcname);

必须出现在相同的源文件中(我们约定总是将其放在函数声明之前)。内置语言函数不需要此宏调用,因为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;
/* 传值 */PG_FUNCTION_INFO_V1(add_one);
Datumadd_one(PG_FUNCTION_ARGS)
{
int32arg=PG_GETARG_INT32(0);
PG_RETURN_INT32(arg+1);
}
/* 传引用,定长 */PG_FUNCTION_INFO_V1(add_one_float8);
Datumadd_one_float8(PG_FUNCTION_ARGS)
{
/* The macros for FLOAT8 hide its pass-by-reference nature. */float8arg=PG_GETARG_FLOAT8(0);
PG_RETURN_FLOAT8(arg+1.0);
}
PG_FUNCTION_INFO_V1(makepoint);
Datummakepoint(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);
}
/* 传引用,变长 */PG_FUNCTION_INFO_V1(copytext);
Datumcopytext(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);
Datumconcat_text(PG_FUNCTION_ARGS)
{
text*arg1=PG_GETARG_TEXT_PP(0);
text*arg2=PG_GETARG_TEXT_PP(1);
int32arg1_size=VARSIZE_ANY_EXHDR(arg1);
int32arg2_size=VARSIZE_ANY_EXHDR(arg2);
int32new_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 integerAS'DIRECTORY/funcs','add_one'     LANGUAGE C STRICT;-- note overloading of SQL function name "add_one"CREATE FUNCTION add_one(doubleprecision) RETURNS doubleprecisionAS'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 textAS'DIRECTORY/funcs','copytext'     LANGUAGE C STRICT;CREATE FUNCTION concat_text(text,text) RETURNS textAS'DIRECTORY/funcs','concat_text'     LANGUAGE C STRICT;

这里,DIRECTORY是存放共享库文件的目录(PostgreSQL实例的tutorial目录中包含有本节示例的代码)。(比较好的风格是在AS子句中仅使用 “funcs”,把DIRECTORY添加到搜索路径中。在任何情况下都要确保使用系统标准的共享库的扩展名,通常是 .so)。

注意:我们指定函数为“strict”,意味着如果任意输入值为空,系统将自动假设结果为空。这样做,我们可以避免在函数代码中检查空输入。如果不这样做,我们必须显示的在函数代码中使用PG_ARGISNULL()去检查空值。

PG_ARGISNULL(n)宏允许函数测试每个输入是否为空。(当然,只有在没有声明为“strict”的函数中才需要这样做)。对于PG_GETARG_xxx() 系列宏,输入参数从0开始计数。注意:在验证参数不为null之前,应该避免执行PG_GETARG_xxx()。要返回空结果,执行 PG_RETURN_NULL();在严格和非严格函数中都可工作。

乍一看,与使用普通C调用约定相比,版本1编码约定可能毫无意义和晦涩。然而,它们确实允许我们处理NULLable参数/返回值和“toasted”(压缩或离线)值。

版本1接口提供的其他选项是PG_GETARG_xxx()宏的两个变体。其中第一个参数PG_GETARG_xxx_COPY()保证返回指定参数的副本,该副本可以安全地写入。(普通宏有时会返回一个指针,该指针指向物理存储在表中的值,不能写入该值。使用PG_GETARG_xxx_COPY()宏可以保证结果是可写的。)第二个变体由PG_GETARG_xxx_SLICE()宏组成,该宏具有三个参数。第一个是函数参数的编号(如上所示)。第二个和第三个是要返回的段的偏移量和长度。偏移量从零开始计数,负长度要求返回值的其余部分。在存储类型为“外部”的情况下,这些宏可以更有效地访问大值的部分。(可以使用ALTER TABLE tablename ALTER column colname SET storage storagetype指定列的存储类型。存储类型是plain、external、extended、main中的一个)。

最后,版本1函数调用约定使返回集合结果、实现触发器函数、过程语言调用处理程序成为可能。有关更多详细信息,请参阅源代码发行版中的src/backend/utils/fmgr/README。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
35 3
|
8天前
|
SQL 开发框架 .NET
突破T-SQL限制:利用CLR集成扩展RDS SQL Server的功能边界
CLR集成为SQL Server提供了强大的扩展能力,突破了T-SQL的限制,极大地拓展了SQL 的应用场景,如:复杂字符串处理、高性能计算、图像处理、机器学习集成、自定义加密解密等,使开发人员能够利用 .NET Framework的丰富功能来处理复杂的数据库任务。
|
13天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
28 6
|
1月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
39 10
|
27天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
1月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
61 7
|
1月前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
30 4
|
1月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
1月前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
34 2
|
1月前
|
Java 编译器 C语言
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
22 3
下一篇
无影云桌面