写点什么

案例展示自定义 C 函数的实现过程

发布于: 2021 年 01 月 05 日

摘要:用户在使用数据库过程中,受限于内置函数的功能,部分业务不易实现时,可以使用自定义 C 函数实现特殊功能。本文通过两个示例展示自定义 C 函数的实现过程。


前言


用户在使用数据库过程中,常常受限于内置函数的功能,部分业务不易实现,或实现后性能较差,在这些场景出现时可以考虑使用 C 编写自定义函数来实现独立功能。


例如用户针对某些数据列需要使用 C 编写的特定算法进行计算,如果放在业务层所性能不能接受,就可以尝试有自定义 C 函数在实现功能的前提下保证实现效率。


粗略的来说,用户使用 C 编写的自定义函数会被被编译成动态库并且由数据库在需要的时候载入。在一个会话中第一次调用一个特定的用户定义函数时,数据库进程会把动态库文件载入到内存中以便该函数被调用。从这个角度来说,注册自定义函数时需要准备编译好的动态库文件和函数定义,以下会以实际操作举例。


数据类型


首先需要先明确,数据库中支持的数据类型在使用自定义 C 函数操作时,必须将数据库中的数据类型转换为数据库内核可以处理的相关类型,实际上数据库内核在处理内置函数输入时也是类似的操作:


例如比较常见的



常见的几种类型中,需要注意的是,对 text 类的函数,C 函数在实际处理时往往不是直接使用 gaussdb 内部的 text\* 类型进行处理的,而是使用 C 语言的标准类型 char\* 进行处理的。


这种情况下就可以先读取到入参的地址,再通过 gauss 提供的内置 C 函数(比如简单常用的 TextDatumGetCString)转换为 char\* 类型字符串进行操作。


应用实例


下面以两个简单的 HelloWorld 例子来说明自定义 C 函数创建的整体流程。


1. 最大公约数


最大公约数的计算比较简单,但内部必然会涉及循环,使用 SQL 实现只能通过类似 PL/pgSQL 自定义函数或存储过程的方法,如果要达到极限性能的话可以尝试使用自定义 C 函数实现。


如果以正常的 C/C++实现,我们可能会这样来编写代码


int gcd_c(int a, int b){    int c = a%b;    while(c) {        a = b;        b = c;        c = a%b;    }    return b;}
复制代码


如果转换成 gauss 可用的 C 函数需要进行改造,例如改造成如下文件,并命名未 gcd.cpp:


//postgres.h和fmgr.h为gauss中C函数固定宏和基本定义的头文件#include "postgres.h"#include "fmgr.h"
//PG_MODULE_MAGIC为固定调用宏,自定义c函数必须在文件开始位置包含PG_MODULE_MAGIC;
//下面两行也是固定调用,这两行可以指定出一个对外开放,并可在自定义C函数创建时被引用的函数//其中gcd为动态库对外可见接口,后面定义C函数时会用到extern "C" Datum gcd(PG_FUNCTION_ARGS);PG_FUNCTION_INFO_V1(gcd);
//实际处理函数int gcd_c(int a, int b){ int c = a%b; while(c) { a = b; b = c; c = a%b; } return b;}//主入口函数//PG_FUNCTION_ARGS是一个固定宏,实际是一个入参出参相关信息的结构体Datumgcd(PG_FUNCTION_ARGS){ //入参阶段 //检查 参数是否为空,如果为空,返回空,相当于对空做检查,防止创建函数时未指定strict属性,导致函数执行异常 if(PG_ARGISNULL(0) || PG_ARGISNULL(1)){ PG_RETURN_NULL(); }
//PG_GETARG_INT32(n)表示从PG_FUNCTION_ARGS结构体中抽取入参的第n个参数,并返回为int32类型 int32 arg1 = PG_GETARG_INT32(0); int32 arg2 = PG_GETARG_INT32(1);
//调用实际执行函数 int32 res = gcd_c(arg1, arg2);
//返回阶段 PG_RETURN_INT32(res); }
复制代码


编译动态库,编译时请保证 gcc/g++>=5.4


g++ -c -fpic -Wno-write-strings -fstack-protector-all gcd.cpp -I ${GAUSSHOME}/include/postgresql/server/cfunctiong++ -shared -fPIC -Wl,-z,now -o gcd.so gcd.o
复制代码


执行创建 C 函数命令:


create or replace function gcd_my(integer,integer)returns integeras 'xxxxx/gcd.so', 'gcd'language c strict not fenced immutable shippable;
复制代码


这个命令中


  • gcd_my 表示后面 sql 中调用该 C 函数时使用的名字

  • xxxxxx/gcd.so 表示的是当前环境上编译生成动态库放置的位置

  • language c 表示该自定义函数为 C 语言编写的函数

  • not fenced 表示该函数执行时使用的是非 fenced 模式(该模式后面小节会再次说明)

  • strict 表示输入不能为空,否则返回也为空(上部分 C 函数虽然以对空值做了处理,当前为了说明问题,此处也声明了 strict 属性)

  • 其余部分为 create function 的公共内容,具体可以参考手册中 create function 章节


执行函数


select gcd_my(12, 16);
复制代码



2. 将字符串中的第一个字母大写,其他小写


内置的字符串处理函数中有大小写转换函数,但没有这种类似只有第一个字符进行大小写转换的定制化函数,如此,我们就可以尝试使用自定义 C 函数进行实现。

如果以正常的 C/C++语言操作,我们可能会这样来写


#include <string.h>void upper_str(char* str){    bool has_first = false;    for(int i = 0; i< strlen(str); i++){        if((str[i]>='a' && str[i]<='z') || (str[i]>='a' && str[i]<='z')) {            if(has_first) {                str[i]=str[i]%0x20 + 'a';            } else {                str[i]=str[i]%0x20 + 'A';            }        }    }}
复制代码


如果转换成 gauss 可用的 C 函数可以改造成如下文件,并命名未 upper_str.cpp:


#include "postgres.h"#include "fmgr.h"//builtins.h中存在下面使用的TextDatumGetCString的定义#include "utils/builtins.h"#include <string.h>
PG_MODULE_MAGIC;
extern "C" Datum upper_str(PG_FUNCTION_ARGS);PG_FUNCTION_INFO_V1(upper_str);
//实际处理函数void upper_str_c(char* str){ bool has_first = false; for(int i = 0; i< strlen(str); i++){ if((str[i]>='A' && str[i]<='Z') || (str[i]>='a' && str[i]<='z')) { if(has_first) { str[i]=str[i]%0x20 + 'a' - 1; } else { str[i]=str[i]%0x20 + 'A' - 1; has_first=true; } } }}//主入口函数Datumupper_str(PG_FUNCTION_ARGS){ //入参阶段 if(PG_ARGISNULL(0)){ PG_RETURN_NULL(); } Datum source = PG_GETARG_DATUM(0); char *src = TextDatumGetCString(source); //实际调用函数 (void) upper_str_c(src); //返回阶段 //cstring_to_text可以将char*类型转换为gauss内置的text*类型 //PG_RETURN_TEXT_P宏可以返回text*类型的结构,被上层调用获取 PG_RETURN_TEXT_P(cstring_to_text(src)); }
复制代码


执行创建 C 函数命令:


create or replace function upper_str_my(text)returns textas 'xxxxxx/upper_str.so', 'upper_str'language c strict not fenced immutable shippable;
复制代码


执行函数


select upper_str_my('@hello World');
复制代码



注意事项


1. fenced/not fenced 模式


C 函数在注册时有一个选项时 fenced,这个模式是 gauss 提供的一种进程隔离机制。如果在 fenced 模式下实际执行的函数会放在一个单独启动的进程中执行,而 not fenced 模式则是在实际执行时和 gaussdb 同一进程。两种模式各有优劣,需要根据实际情况进行选择。比较建议的方式是,受限创建为 fenced 模式函数,调试无问题后,再重新注册为 not fenced 模式,以高效率模式运行。


* fenced 模式

优点:执行更安全,如果函数执行出现异常,不会影响到 CN/DN 进程,保证整个节点的稳定运行;

缺点:需要额外的进程开销,效率较低。

* not fenced 模式正好相反

优点:运行效率高,无额外开销;

缺点:如果自定义 C 代码编码有问题,容易造成 CN/DN 进程异常等严重问题。


2. C 函数编写时,需要注意注册时的参数类型和返回类型,系统会根据创建函数时指定的内容传给实际执行的函数,所以如果函数内部处理和创建时指定类型不一致容易出现不可预测的异常


3. C 函数编写时,需要注意对空值进行特殊处理,或者再创建函数时指定为 strict 属性的函数


4. C 函数实现时都是底层实现,应该严格控制不可靠 C 函数的创建,坚持慎重使用自定义 C 函数的原则


5. C 函数创建只能由具有 sysadmin 权限的用户进行创建,可以通过 grant 操作赋予其他用户执行权限


总结


自定义 C 函数的存在给用户提供了直接实现底层逻辑的机会,一个实现完善的自定义 C 函数,往往可以给业务带来极强的定制性和大幅度的性能提升。但定制性也是一把双刃剑,如果编写自定义 C 函数时做的不够完善存在逻辑问题,甚至内存溢出等严重问题,也极可能使系统崩溃。因此可以使用自定义 C 函数作为一个强有力的工具为实际业务增光添彩,但也需要谨慎的创建使用任意一个 C 函数避免误伤其他业务。


本文分享自华为云社区《初窥自定义 C 函数》,原文作者:sincatter 。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 01 月 05 日阅读数: 19
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
案例展示自定义C函数的实现过程