案例展示自定義C函式的實現過程

華為雲開發者社群發表於2021-01-05
摘要:使用者在使用資料庫過程中,受限於內建函式的功能,部分業務不易實現時,可以使用自定義C函式實現特殊功能。本文通過兩個示例展示自定義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是一個固定巨集,實際是一個入參出參相關資訊的結構體
Datum
gcd(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/cfunction
g++ -shared -fPIC -Wl,-z,now -o gcd.so gcd.o

執行建立C函式命令:

create or replace function gcd_my(integer,integer)
returns integer
as '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);

案例展示自定義C函式的實現過程

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;
            }
        }
    }
}
//主入口函式
Datum
upper_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 text
as 'xxxxxx/upper_str.so', 'upper_str'
language c strict not fenced immutable shippable;

執行函式

select upper_str_my('@hello World');

案例展示自定義C函式的實現過程

注意事項

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 。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章