支援的版本:目前 (17) / 16 / 15 / 14 / 13
開發版本:devel
不支援的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1

36.10. C 語言函數 #

使用者定義函數可以用 C 語言(或可以與 C 語言相容的語言,例如 C++)撰寫。這些函數會被編譯成動態可載入物件(也稱為共享函式庫),並由伺服器依需求載入。動態載入功能是區分 C 語言函數與 內部函數的原因 — 兩者的實際程式碼撰寫慣例基本上相同。(因此,標準內部函數函式庫是用戶定義 C 函數的豐富程式碼範例來源。)

目前,C 函數僅使用一種呼叫慣例(第 1 版)。透過為函數編寫 PG_FUNCTION_INFO_V1() 巨集呼叫來表示對該呼叫慣例的支援,如下所示。

36.10.1. 動態載入 #

在特定可載入物件檔案中的使用者定義函數在會話中第一次被呼叫時,動態載入器會將該物件檔案載入記憶體,以便可以呼叫該函數。因此,使用者定義 C 函數的 CREATE FUNCTION 必須為該函數指定兩項資訊:可載入物件檔案的名稱,以及要呼叫之該物件檔案中特定函數的 C 名稱(連結符號)。如果未明確指定 C 名稱,則假定它與 SQL 函數名稱相同。

以下演算法用於根據 CREATE FUNCTION 命令中給定的名稱來定位共享物件檔案

  1. 如果名稱是絕對路徑,則載入給定的檔案。

  2. 如果名稱以字串 $libdir 開頭,則該部分會替換為 PostgreSQL 套件函式庫目錄名稱,該名稱在建置時決定。

  3. 如果名稱不包含目錄部分,則會在組態變數 dynamic_library_path 指定的路徑中搜尋該檔案。

  4. 否則(檔案未在路徑中找到,或者它包含非絕對目錄部分),動態載入器將嘗試將該名稱視為給定的名稱,這很可能會失敗。(依賴目前的工作目錄是不可靠的。)

如果此序列不起作用,則將平台特定的共享函式庫檔案名稱副檔名(通常為 .so)附加到給定的名稱,並再次嘗試此序列。如果這也失敗,則載入將失敗。

建議將共享函式庫定位在相對於 $libdir 的位置或透過動態函式庫路徑。如果在不同的位置進行新的安裝,這可以簡化版本升級。$libdir 代表的實際目錄可以使用命令 pg_config --pkglibdir 找到。

PostgreSQL 伺服器執行的使用者 ID 必須能夠遍歷到您打算載入的檔案的路徑。使檔案或更高等級的目錄無法被 postgres 使用者讀取和/或執行是一個常見的錯誤。

無論如何,CREATE FUNCTION 命令中給定的檔案名稱會逐字記錄在系統目錄中,因此如果需要再次載入檔案,則會應用相同的程序。

注意

PostgreSQL 不會自動編譯 C 函數。物件檔案必須在 CREATE FUNCTION 命令中引用之前編譯。有關更多資訊,請參閱第 36.10.5 節

為了確保動態載入的物件檔案未載入到不相容的伺服器中,PostgreSQL 會檢查該檔案是否包含具有適當內容的 magic block。這使伺服器能夠檢測到明顯的不相容性,例如為不同主要版本的 PostgreSQL 編譯的程式碼。要包含一個 magic block,請在模組原始檔中的一個(且僅一個)中編寫此程式碼,並在包含標頭 fmgr.h 之後進行編寫

PG_MODULE_MAGIC;

首次使用後,動態載入的物件檔案會保留在記憶體中。在同一個會話中對該檔案中的函數進行的未來呼叫只會產生符號表查找的少量開銷。如果您需要強制重新載入物件檔案,例如在重新編譯後,請開始一個新的會話。

您可以選擇讓動態載入的檔案包含初始化函式。如果檔案包含一個名為 _PG_init 的函式,該函式將在檔案載入後立即被呼叫。該函式不接收任何參數,且應返回 void。目前沒有辦法卸載動態載入的檔案。

36.10.2. C 語言函式中的基本型別 #

為了了解如何編寫 C 語言函式,您需要了解 PostgreSQL 如何在內部表示基本資料型別,以及如何將它們傳遞給函式和從函式傳遞出來。在內部,PostgreSQL 將基本型別視為 記憶體區塊。您對型別定義的使用者自訂函式,反過來定義了 PostgreSQL 可以如何操作該型別。也就是說,PostgreSQL 只會儲存和檢索來自磁碟的資料,並使用您的使用者自訂函式來輸入、處理和輸出資料。

基本型別可以有以下三種內部格式

  • 傳值 (pass by value),固定長度

  • 傳參考 (pass by reference),固定長度

  • 傳參考 (pass by reference),可變長度

傳值型別的長度只能是 1、2 或 4 個位元組(如果您的機器上 sizeof(Datum) 是 8,則也可以是 8 個位元組)。您應該小心地定義您的型別,使其在所有架構上都具有相同的大小(以位元組為單位)。例如,long 型別很危險,因為它在某些機器上是 4 個位元組,而在其他機器上是 8 個位元組,而 int 型別在大多數 Unix 機器上是 4 個位元組。在 Unix 機器上,int4 型別的一個合理的實作可能是

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

(實際的 PostgreSQL C 程式碼將此型別稱為 int32,因為 C 語言中的一個慣例是 intXX 表示 XX 位元。因此,也請注意,C 型別 int8 的大小為 1 個位元組。SQL 型別 int8 在 C 語言中稱為 int64。另請參閱表 36.2。)

另一方面,任何大小的固定長度型別都可以透過傳參考方式傳遞。例如,以下是 PostgreSQL 型別的一個範例實作

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

只有指向這些型別的指標才能在 PostgreSQL 函式中傳入和傳出時使用。要返回這種型別的值,請使用 palloc 分配適量的記憶體,填寫分配的記憶體,然後返回指向它的指標。(此外,如果您只想返回與輸入參數之一相同資料型別的值,您可以跳過額外的 palloc,直接返回指向輸入值的指標。)

最後,所有可變長度型別也必須透過傳參考方式傳遞。所有可變長度型別都必須以一個不透明的長度欄位開始,該欄位恰好為 4 個位元組,將由 SET_VARSIZE 設定;永遠不要直接設定此欄位!要儲存在該型別中的所有資料必須位於緊接在該長度欄位之後的記憶體中。長度欄位包含結構的總長度,也就是說,它包含長度欄位本身的大小。

另一個重要點是避免在資料型別值中留下任何未初始化的位元;例如,請注意將結構中可能存在的任何對齊填充位元組歸零。如果沒有這樣做,規劃器可能會將邏輯上等效的資料型別常數視為不相等,從而導致效率低下(但並非不正確)的規劃。

警告

永遠不要修改傳參考輸入值的內容。如果您這樣做,您很可能會損壞磁碟上的資料,因為您收到的指標可能直接指向磁碟緩衝區。此規則的唯一例外情況在 Section 36.12 中進行了說明。

作為一個例子,我們可以將 text 型別定義如下

typedef struct {
    int32 length;
    char data[FLEXIBLE_ARRAY_MEMBER];
} text;

[FLEXIBLE_ARRAY_MEMBER] 標記表示資料部分的實際長度未由此宣告指定。

在操作可變長度型別時,我們必須小心分配正確的記憶體量並正確設定長度欄位。例如,如果我們想在 text 結構中儲存 40 個位元組,我們可以使用如下程式碼片段

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

VARHDRSZsizeof(int32) 相同,但使用巨集 VARHDRSZ 來引用可變長度型別的開銷大小被認為是一種良好的風格。此外,長度欄位必須使用 SET_VARSIZE 巨集設定,而不是簡單的賦值。

表 36.2 顯示了與 PostgreSQL 的許多內建 SQL 資料型別相對應的 C 型別。定義於 欄位給出了需要包含以取得型別定義的標頭檔。(實際定義可能位於列出的檔案所包含的不同檔案中。建議使用者堅持使用定義的介面。)請注意,您應該始終在任何伺服器程式碼的原始檔中首先包含 postgres.h,因為它宣告了許多您無論如何都需要的東西,並且因為首先包含其他標頭可能會導致移植性問題。

表 36.2. 內建 SQL 型別的等效 C 型別

SQL 型別 C 型別 定義於
boolean bool postgres.h (可能是編譯器內建)
box BOX* utils/geo_decls.h
bytea bytea* postgres.h
"char" char (編譯器內建)
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

現在我們已經介紹了基本型別的所有可能結構,我們可以展示一些實際函式的範例。

36.10.3. 版本 1 呼叫慣例 #

版本 1 呼叫慣例依賴巨集來抑制傳遞參數和結果的大部分複雜性。版本 1 函式的 C 宣告始終是

Datum funcname(PG_FUNCTION_ARGS)

此外,巨集呼叫

PG_FUNCTION_INFO_V1(funcname);

必須出現在同一個原始檔中。(按照慣例,它寫在函式本身之前。)internal-language 函式不需要此巨集呼叫,因為 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;

/* 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_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);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_PP(0);
    text  *arg2 = PG_GETARG_TEXT_PP(1);
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    int32 new_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 integer
     AS 'DIRECTORY/funcs', 'add_one'
     LANGUAGE C STRICT;

-- note overloading of SQL function name "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
     AS '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 text
     AS 'DIRECTORY/funcs', 'copytext'
     LANGUAGE C STRICT;

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'DIRECTORY/funcs', 'concat_text'
     LANGUAGE C STRICT;

此處,DIRECTORY 代表共享程式庫檔案的目錄(例如,包含本節範例程式碼的 PostgreSQL 教學目錄)。(更好的風格是在 AS 子句中使用 'funcs',並將 DIRECTORY 新增到搜尋路徑。在任何情況下,我們都可以省略共享程式庫的系統特定擴充功能,通常是 .so。)

請注意,我們已將函數指定為 strict,這表示如果任何輸入值為空值,系統應自動假定為空結果。這樣做,我們可以避免在函數程式碼中檢查空輸入。如果沒有這個,我們必須使用 PG_ARGISNULL() 顯式地檢查空值。

巨集 PG_ARGISNULL(n) 允許函數測試每個輸入是否為空值。(當然,只有在未宣告為 strict 的函數中才需要這樣做。)與 PG_GETARG_xxx() 巨集一樣,輸入引數從零開始計數。請注意,在確認引數不是空值之前,不應執行 PG_GETARG_xxx()。要回傳空結果,請執行 PG_RETURN_NULL();這適用於嚴格和非嚴格函數。

乍看之下,與使用純 C 呼叫慣例相比,版本 1 的程式碼慣例可能只是一種毫無意義的混淆。但是,它們確實允許我們處理可為 NULL 的引數/回傳值,以及 toasted(壓縮或異地儲存)的值。

版本 1 介面提供的其他選項是 PG_GETARG_xxx() 巨集的兩個變體。第一個是 PG_GETARG_xxx_COPY(),它保證回傳指定引數的副本,該副本可以安全地寫入。(普通的巨集有時會回傳指向實際儲存在表格中的值的指標,不得對其進行寫入。使用 PG_GETARG_xxx_COPY() 巨集保證可寫入的結果。)第二個變體包含 PG_GETARG_xxx_SLICE() 巨集,它接受三個引數。第一個是函數引數的編號(如上所述)。第二個和第三個是要回傳的區段的偏移量和長度。偏移量從零開始計數,負長度要求回傳該值的剩餘部分。在儲存類型為 external 的情況下,這些巨集提供更有效率地存取大型值的部分內容。(可以使用 ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype 來指定欄位的儲存類型。storagetypeplainexternalextendedmain 之一。)

最後,版本 1 函數呼叫慣例使得可以回傳集合結果 (第 36.10.8 節) 並實作觸發程序函數 (第 37 章) 和程序語言呼叫處理常式 (第 56 章)。有關更多詳細資訊,請參閱原始程式碼發行版中的 src/backend/utils/fmgr/README

36.10.4. 撰寫程式碼 #

在我們轉向更進階的主題之前,我們應該討論一些 PostgreSQL C 語言函數的程式碼撰寫規則。雖然可以將以 C 以外的語言編寫的函數載入到 PostgreSQL 中,但這通常很困難(如果可能的話),因為其他語言(如 C++、FORTRAN 或 Pascal)通常不遵循與 C 相同的呼叫慣例。也就是說,其他語言不會以相同的方式在函數之間傳遞引數和回傳值。因此,我們將假設您的 C 語言函數實際上是用 C 編寫的。

撰寫和建置 C 函數的基本規則如下

  • 使用 pg_config --includedir-server 找出 PostgreSQL 伺服器標頭檔案安裝在您的系統(或您的使用者將在其上執行的系統)上的位置。

  • 編譯和連結您的程式碼,以便可以將其動態載入到 PostgreSQL 中,始終需要特殊標誌。請參閱 第 36.10.5 節,詳細說明如何針對您的特定作業系統執行此操作。

  • 請記住為您的共享程式庫定義一個 magic block,如 第 36.10.1 節中所述。

  • 在配置記憶體時,請使用 PostgreSQL 函數 pallocpfree,而不是對應的 C 程式庫函數 mallocfreepalloc 配置的記憶體將在每個交易結束時自動釋放,從而防止記憶體洩漏。

  • 始終使用 memset 清零結構的位元組(或首先使用 palloc0 配置它們)。即使您分配給結構的每個欄位,也可能存在包含垃圾值的對齊填充(結構中的孔)。如果沒有這個,很難支援雜湊索引或雜湊聯結,因為您必須挑選出資料結構中唯一有意義的位元才能計算雜湊值。規劃器有時也依賴於透過位元相等性比較常數,因此如果邏輯上等效的值不是位元相等,您可能會得到不希望的規劃結果。

  • 大多數內部 PostgreSQL 類型都在 postgres.h 中宣告,而函數管理器介面(PG_FUNCTION_ARGS 等)在 fmgr.h 中,因此您至少需要包含這兩個檔案。為了方便攜帶,最好在任何其他系統或使用者標頭檔案之前首先包含 postgres.h。包含 postgres.h 也會為您包含 elog.hpalloc.h

  • 物件檔案中定義的符號名稱不得相互衝突,也不得與 PostgreSQL 伺服器可執行檔中定義的符號衝突。如果收到這樣的錯誤訊息,您將必須重新命名您的函數或變數。

36.10.5. 編譯和連結動態載入的函數 #

在您可以使用以 C 語言編寫的 PostgreSQL 擴充功能函數之前,必須以特殊的方式進行編譯和連結,才能產生可由伺服器動態載入的檔案。 準確地說,需要建立一個共享函式庫

除了本節包含的資訊外,您還應該閱讀作業系統的文件,特別是 C 編譯器 cc 和連結編輯器 ld 的手冊頁面。 此外,PostgreSQL 原始碼在 contrib 目錄中包含多個可用的範例。 但是,如果您依賴這些範例,您的模組將取決於 PostgreSQL 原始碼的可用性。

建立共享函式庫通常類似於連結可執行檔:首先,原始檔被編譯為目標檔,然後目標檔被連結在一起。 目標檔需要建立為位置無關程式碼 (PIC),這在概念上意味著當它們被可執行檔載入時,可以放置在記憶體的任意位置。(預定用於可執行檔的目標檔通常不會以這種方式編譯。)連結共享函式庫的命令包含特殊的標誌,以區分它與連結可執行檔(至少在理論上是如此 — 在某些系統上,實踐要醜陋得多)。

在下面的範例中,我們假設您的原始碼位於檔案 foo.c 中,我們將建立一個共享函式庫 foo.so。除非另有說明,否則中間目標檔將被呼叫為 foo.o。一個共享函式庫可以包含多個目標檔,但我們在這裡只使用一個。

FreeBSD

建立的編譯器標誌PIC-fPIC。建立共享函式庫的編譯器標誌是 -shared

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o

這適用於 FreeBSD 的 13.0 版,舊版本使用 gcc 編譯器。

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

建立的編譯器標誌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

提示

如果您覺得這太複雜,您應該考慮使用 GNU Libtool,它將平台差異隱藏在統一的介面之後。

然後,可以將產生的共享函式庫檔案載入到 PostgreSQL 中。當向 CREATE FUNCTION 命令指定檔案名稱時,必須給出共享函式庫檔案的名稱,而不是中間目標檔。請注意,系統的標準共享函式庫副檔名(通常是 .so.sl)可以從 CREATE FUNCTION 命令中省略,並且通常應該為了獲得最佳的可移植性而省略。

請參閱 第 36.10.1 節,了解伺服器期望在哪裡找到共享函式庫檔案。

36.10.6. 複合類型參數 #

複合類型沒有像 C 結構一樣的固定佈局。複合類型的實例可以包含空欄位。此外,屬於繼承層次結構一部分的複合類型可能與同一繼承層次結構的其他成員具有不同的欄位。因此,PostgreSQL 提供了一個函數介面,用於從 C 訪問複合類型的欄位。

假設我們要編寫一個函數來回答查詢

SELECT name, c_overpaid(emp, 1500) AS overpaid
    FROM emp
    WHERE name = 'Bill' OR name = 'Sam';

使用版本 1 的呼叫約定,我們可以將 c_overpaid 定義為

#include "postgres.h"
#include "executor/executor.h"  /* for GetAttributeByName() */

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    HeapTupleHeader  t = PG_GETARG_HEAPTUPLEHEADER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    Datum salary;

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

GetAttributeByNamePostgreSQL 系統函數,用於從指定的列中傳回屬性。它有三個參數:傳遞給函數的 HeapTupleHeader 類型的參數、所需屬性的名稱以及一個傳回參數,該參數指示屬性是否為空。 GetAttributeByName 傳回一個 Datum 值,您可以使用適當的 DatumGetXXX() 函數將其轉換為適當的資料類型。請注意,如果設定了空標誌,則傳回值毫無意義;在嘗試對結果執行任何操作之前,請務必檢查空標誌。

還有 GetAttributeByNum,它透過欄位號碼而不是名稱來選擇目標屬性。

以下命令在 SQL 中宣告函數 c_overpaid

CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
    AS 'DIRECTORY/funcs', 'c_overpaid'
    LANGUAGE C STRICT;

請注意,我們使用了 STRICT,因此我們不必檢查輸入參數是否為 NULL。

36.10.7. 傳回列(複合類型) #

要從 C 語言函數傳回列或複合類型值,您可以使用特殊的 API,該 API 提供巨集和函數來隱藏建構複合資料類型的大部分複雜性。要使用此 API,原始檔必須包含

#include "funcapi.h"

您可以透過兩種方式建構複合資料值(以下稱為「元組」):您可以從 Datum 值的陣列建構它,也可以從可以傳遞給元組欄位資料類型的輸入轉換函數的 C 字串陣列建構它。在任何一種情況下,您首先需要取得或建構元組結構的 TupleDesc 描述符。當使用 Datums 時,您可以將 TupleDesc 傳遞給 BlessTupleDesc,然後為每一列呼叫 heap_form_tuple。當使用 C 字串時,您可以將 TupleDesc 傳遞給 TupleDescGetAttInMetadata,然後為每一列呼叫 BuildTupleFromCStrings。在函數傳回一組元組的情況下,設定步驟可以在函數的第一次呼叫期間完成一次。

有幾個輔助函數可用於設定所需的 TupleDesc。在大多數傳回複合值的函數中,建議的執行方法是呼叫

TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
                                   Oid *resultTypeId,
                                   TupleDesc *resultTupleDesc)

傳遞給它與呼叫函數本身相同的 fcinfo 結構。(當然,這需要您使用 version-1 的呼叫慣例。) resultTypeId 可以指定為 NULL,或是作為接收函數回傳類型 OID 的區域變數位址。resultTupleDesc 應該是區域 TupleDesc 變數的位址。檢查結果是否為 TYPEFUNC_COMPOSITE;如果是,則 resultTupleDesc 已填入所需的 TupleDesc。(如果不是,您可以回報類似以下的錯誤訊息:「函數在無法接受 record 類型的環境中呼叫,但回傳 record。)

提示

get_call_result_type 可以解析多型函數結果的實際類型;因此它在回傳純量多型結果的函數中很有用,而不僅僅是在回傳複合類型的函數中。resultTypeId 輸出主要用於回傳多型純量的函數。

注意

get_call_result_type 有一個兄弟函數 get_expr_result_type,可用於解析由表達式樹表示的函數呼叫的預期輸出類型。當嘗試從函數外部確定結果類型時,可以使用它。還有 get_func_result_type,當只有函數的 OID 可用時可以使用。但是,這些函數無法處理宣告為回傳 record 的函數,並且 get_func_result_type 無法解析多型類型,因此您應該優先使用 get_call_result_type

以下是較舊且現已棄用的函數,用於取得 TupleDesc

TupleDesc RelationNameGetTupleDesc(const char *relname)

用於取得具名關係之列類型的 TupleDesc,以及

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

用於取得基於類型 OID 的 TupleDesc。這可用於取得基本或複合類型的 TupleDesc。但是,它不適用於回傳 record 的函數,並且無法解析多型類型。

一旦您有了 TupleDesc,請呼叫

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

如果您計劃使用 Datums,或

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

如果您計劃使用 C 字串。如果您正在編寫回傳集合的函數,您可以將這些函數的結果儲存在 FuncCallContext 結構中 — 分別使用 tuple_descattinmeta 欄位。

當使用 Datums 時,請使用

HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)

以 Datum 形式,根據使用者資料來建立 HeapTuple

當使用 C 字串時,請使用

HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)

以 C 字串形式,根據使用者資料來建立 HeapTuplevalues 是一個 C 字串陣列,每個字串對應於回傳列的一個屬性。每個 C 字串都應該是屬性資料類型之輸入函數所期望的形式。為了回傳其中一個屬性的 null 值,values 陣列中對應的指標應設定為 NULL。對於您回傳的每一列,都需要再次呼叫此函數。

一旦您建立了一個要從函數回傳的元組,它必須轉換為 Datum。使用

HeapTupleGetDatum(HeapTuple tuple)

HeapTuple 轉換為有效的 Datum。如果您打算只回傳單一列,則可以直接回傳此 Datum,或者可以在回傳集合的函數中將其用作目前的回傳值。

下一個章節提供了一個範例。

36.10.8. 回傳集合 #

C 語言函數有兩個選項可用於回傳集合(多列)。在一種方法中,稱為 ValuePerCall 模式,會重複呼叫回傳集合的函數(每次都傳遞相同的引數),並且它在每次呼叫時回傳一個新的列,直到它沒有更多列可以回傳,並透過回傳 NULL 來發出訊號。回傳集合的函數(SRF)因此必須跨呼叫儲存足夠的狀態,以記住它正在做什麼,並在每次呼叫時回傳正確的下一個項目。在另一種方法中,稱為 Materialize 模式,SRF 填入並回傳一個包含其整個結果的 tuplestore 物件;然後,整個結果只會進行一次呼叫,並且不需要呼叫間的狀態。

當使用 ValuePerCall 模式時,重要的是要記住查詢不保證會執行到完成;也就是說,由於諸如 LIMIT 之類的選項,執行器可能會在提取所有列之前停止呼叫回傳集合的函數。這表示在最後一次呼叫中執行清理活動是不安全的,因為可能永遠不會發生。建議對需要存取外部資源(例如檔案描述元)的函數使用 Materialize 模式。

本節的其餘部分記錄了一組輔助巨集,這些巨集通常用於(但不一定需要使用)使用 ValuePerCall 模式的 SRF。有關 Materialize 模式的更多詳細資訊,請參閱 src/backend/utils/fmgr/README。此外,PostgreSQL 來源發佈中的 contrib 模組包含許多使用 ValuePerCall 和 Materialize 模式的 SRF 範例。

要使用此處描述的 ValuePerCall 支援巨集,請包含 funcapi.h。這些巨集與一個 FuncCallContext 結構一起工作,該結構包含需要在呼叫之間儲存的狀態。在呼叫 SRF 內,fcinfo->flinfo->fn_extra 用於在呼叫之間儲存指向 FuncCallContext 的指標。這些巨集會在第一次使用時自動填寫該欄位,並期望在後續使用時在那裡找到相同的指標。

typedef struct FuncCallContext
{
    /*
     * Number of times we've been called before
     *
     * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and
     * incremented for you every time SRF_RETURN_NEXT() is called.
     */
    uint64 call_cntr;

    /*
     * OPTIONAL maximum number of calls
     *
     * max_calls is here for convenience only and setting it is optional.
     * If not set, you must provide alternative means to know when the
     * function is done.
     */
    uint64 max_calls;

    /*
     * OPTIONAL pointer to miscellaneous user-provided context information
     *
     * user_fctx is for use as a pointer to your own data to retain
     * arbitrary context information between calls of your function.
     */
    void *user_fctx;

    /*
     * OPTIONAL pointer to struct containing attribute type input metadata
     *
     * attinmeta is for use when returning tuples (i.e., composite data types)
     * and is not used when returning base data types. It is only needed
     * if you intend to use BuildTupleFromCStrings() to create the return
     * tuple.
     */
    AttInMetadata *attinmeta;

    /*
     * memory context used for structures that must live for multiple calls
     *
     * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used
     * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory
     * context for any memory that is to be reused across multiple calls
     * of the SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * OPTIONAL pointer to struct containing tuple description
     *
     * tuple_desc is for use when returning tuples (i.e., composite data types)
     * and is only needed if you are going to build the tuples with
     * heap_form_tuple() rather than with BuildTupleFromCStrings().  Note that
     * the TupleDesc pointer stored here should usually have been run through
     * BlessTupleDesc() first.
     */
    TupleDesc tuple_desc;

} FuncCallContext;

要由一個SRF使用此基礎架構的是:

SRF_IS_FIRSTCALL()

使用它來確定您的函數是第一次還是後續呼叫。在第一次呼叫(僅限)時,呼叫

SRF_FIRSTCALL_INIT()

以初始化 FuncCallContext。在每次函數呼叫(包括第一次)時,呼叫

SRF_PERCALL_SETUP()

以設定使用 FuncCallContext

如果您的函數在目前的呼叫中有資料要回傳,請使用

SRF_RETURN_NEXT(funcctx, result)

將其回傳給呼叫者。(result 必須是 Datum 類型,可以是單一值或如上所述準備的元組。)最後,當您的函數完成回傳資料時,請使用

SRF_RETURN_DONE(funcctx)

以清理並結束SRF.

SRF被呼叫時的記憶體內容是一個暫時性的內容,會在呼叫之間清除。這表示您不需要在您使用 palloc 分配的所有內容上呼叫 pfree;它無論如何都會消失。但是,如果您想要分配任何跨呼叫存在的資料結構,您需要將它們放在其他地方。由 multi_call_memory_ctx 引用的記憶體內容是一個適合存放任何需要在SRF完成運行之前存在的資料的位置。在大多數情況下,這表示您應該在執行第一次呼叫設定時切換到 multi_call_memory_ctx。使用 funcctx->user_fctx 來保存指向任何此類跨呼叫資料結構的指標。(您在 multi_call_memory_ctx 中分配的資料會在查詢結束時自動消失,因此無需手動釋放該資料。)

警告

雖然函數的實際參數在呼叫之間保持不變,但如果您在暫時的上下文中解除(detoast)參數值(這通常由 PG_GETARG_xxx 巨集以透明方式完成),則解除後的副本將在每個週期釋放。因此,如果您在 user_fctx 中保留對這些值的引用,則必須在解除後將它們複製到 multi_call_memory_ctx 中,或確保僅在該上下文中解除這些值。

一個完整的虛擬碼範例看起來如下所示

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    further declarations as needed

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* One-time setup code appears here: */
        user code
        if returning composite
            build TupleDesc, and perhaps AttInMetadata
        endif returning composite
        user code
        MemoryContextSwitchTo(oldcontext);
    }

    /* Each-time setup code appears here: */
    user code
    funcctx = SRF_PERCALL_SETUP();
    user code

    /* this is just one way we might test whether we are done: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Here we want to return another item: */
        user code
        obtain result Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Here we are done returning items, so just report that fact. */
        /* (Resist the temptation to put cleanup code here.) */
        SRF_RETURN_DONE(funcctx);
    }
}

一個簡單的完整範例SRF傳回複合類型看起來像

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    AttInMetadata       *attinmeta;

    /* stuff done only on the first call of the function */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* create a function context for cross-call persistence */
        funcctx = SRF_FIRSTCALL_INIT();

        /* switch to memory context appropriate for multiple function calls */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* total number of tuples to be returned */
        funcctx->max_calls = PG_GETARG_INT32(0);

        /* Build a tuple descriptor for our result type */
        if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("function returning record called in context "
                            "that cannot accept type record")));

        /*
         * generate attribute metadata needed later to produce tuples from raw
         * C strings
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* stuff done on every call of the function */
    funcctx = SRF_PERCALL_SETUP();

    call_cntr = funcctx->call_cntr;
    max_calls = funcctx->max_calls;
    attinmeta = funcctx->attinmeta;

    if (call_cntr < max_calls)    /* do when there is more left to send */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Prepare a values array for building the returned tuple.
         * This should be an array of C strings which will
         * be processed later by the type input functions.
         */
        values = (char **) palloc(3 * sizeof(char *));
        values[0] = (char *) palloc(16 * sizeof(char));
        values[1] = (char *) palloc(16 * sizeof(char));
        values[2] = (char *) palloc(16 * sizeof(char));

        snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1));
        snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1));
        snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1));

        /* build a tuple */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* make the tuple into a datum */
        result = HeapTupleGetDatum(tuple);

        /* clean up (this is not really necessary) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* do when there is no more left */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

在 SQL 中宣告此函數的一種方法是

CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);

CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
    RETURNS SETOF __retcomposite
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

另一種方法是使用 OUT 參數

CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
    OUT f1 integer, OUT f2 integer, OUT f3 integer)
    RETURNS SETOF record
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

請注意,在此方法中,函數的輸出類型在形式上是一個匿名的 record 類型。

36.10.9. 多型參數和傳回類型 #

C 語言函數可以宣告為接受和傳回在 第 36.2.5 節 中描述的多型類型。當函數的參數或傳回類型定義為多型類型時,函數作者無法預先知道它將被呼叫的資料類型,或需要傳回的資料類型。 fmgr.h 中提供了兩個常式,允許版本 1 的 C 函數發現其參數的實際資料類型以及預期傳回的類型。這些常式分別是 get_fn_expr_rettype(FmgrInfo *flinfo)get_fn_expr_argtype(FmgrInfo *flinfo, int argnum)。它們傳回結果或參數類型 OID,如果資訊不可用,則傳回 InvalidOid。結構 flinfo 通常以 fcinfo->flinfo 存取。參數 argnum 是以零為基底的。get_call_result_type 也可以用作 get_fn_expr_rettype 的替代方法。還有 get_fn_expr_variadic,可用於判斷可變參數是否已合併到陣列中。這對於 VARIADIC "any" 函數特別有用,因為對於採用普通陣列類型的可變函數,這種合併始終會發生。

例如,假設我們要編寫一個函數來接受任何類型的單個元素,並傳回該類型的一維陣列

PG_FUNCTION_INFO_V1(make_array);
Datum
make_array(PG_FUNCTION_ARGS)
{
    ArrayType  *result;
    Oid         element_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
    Datum       element;
    bool        isnull;
    int16       typlen;
    bool        typbyval;
    char        typalign;
    int         ndims;
    int         dims[MAXDIM];
    int         lbs[MAXDIM];

    if (!OidIsValid(element_type))
        elog(ERROR, "could not determine data type of input");

    /* get the provided element, being careful in case it's NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* we have one dimension */
    ndims = 1;
    /* and one element */
    dims[0] = 1;
    /* and lower bound is 1 */
    lbs[0] = 1;

    /* get required info about the element type */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* now build the array */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

以下命令在 SQL 中宣告函數 make_array

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'DIRECTORY/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

有一種僅適用於 C 語言函數的多型變體:它們可以宣告為採用類型為 "any" 的參數。(請注意,此類型名稱必須使用雙引號括起來,因為它也是一個 SQL 保留字。)它的作用類似於 anyelement,只不過它不會限制不同的 "any" 參數為同一類型,也不會幫助確定函數的結果類型。 C 語言函數也可以將其最後一個參數宣告為 VARIADIC "any"。這將匹配任何類型的一個或多個實際參數(不一定是同一類型)。這些參數不會像普通可變函數那樣被收集到陣列中;它們只會單獨傳遞給函數。在使用此功能時,必須使用 PG_NARGS() 巨集和上述方法來確定實際參數的數量及其類型。此外,此類函數的使用者可能希望在其函數呼叫中使用 VARIADIC 關鍵字,期望函數將陣列元素視為單獨的參數。如果需要,函數本身必須在使用 get_fn_expr_variadic 檢測到實際參數已標記為 VARIADIC 後,實現該行為。

36.10.10. 共享記憶體 #

36.10.10.1. 在啟動時請求共享記憶體 #

增益集可以在伺服器啟動時保留共享記憶體。為此,必須通過在 shared_preload_libraries 中指定增益集的共享函式庫來預先載入它。共享函式庫還應在其 _PG_init 函數中註冊一個 shmem_request_hook。此 shmem_request_hook 可以通過呼叫以下函數來保留共享記憶體

void RequestAddinShmemSpace(Size size)

每個後端都應通過呼叫以下函數來取得保留的共享記憶體的指標

void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)

如果此函數將 foundPtr 設定為 false,則呼叫者應繼續初始化保留的共享記憶體的內容。如果 foundPtr 設定為 true,則共享記憶體已經由另一個後端初始化,並且呼叫者無需進一步初始化。

為了避免競爭條件,每個後端在初始化其共享記憶體配置時,都應使用 LWLock AddinShmemInitLock,如下所示

static mystruct *ptr = NULL;
bool        found;

LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
    ... initialize contents of shared memory ...
    ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);

shmem_startup_hook 提供了一個方便的位置來放置初始化程式碼,但嚴格來說,並非必須將所有此類程式碼放置在此 hook 中。每個後端將在連線到共享記憶體後不久執行註冊的 shmem_startup_hook。請注意,增益集仍應在此 hook 中取得 AddinShmemInitLock,如上面的範例所示。

可以在 PostgreSQL 原始碼樹中的 contrib/pg_stat_statements/pg_stat_statements.c 中找到 shmem_request_hookshmem_startup_hook 的範例。

36.10.10.2. 在啟動後請求共享記憶體 #

還有另一種更靈活的保留共享記憶體的方法,可以在伺服器啟動後且在 shmem_request_hook 之外完成。為此,每個將使用共享記憶體的後端都應通過呼叫以下函數來取得其指標

void *GetNamedDSMSegment(const char *name, size_t size,
                         void (*init_callback) (void *ptr),
                         bool *found)

如果具有給定名稱的動態共享記憶體區段尚不存在,則此函數將分配它並使用提供的 init_callback 回呼函數對其進行初始化。如果該區段已經由另一個後端分配和初始化,則此函數僅將現有的動態共享記憶體區段連線到目前的後端。

與在伺服器啟動時保留的共享記憶體不同,在使用 GetNamedDSMSegment 保留共享記憶體時,無需取得 AddinShmemInitLock 或採取其他措施來避免競爭條件。此函數確保只有一個後端分配和初始化該區段,並且所有其他後端都收到指向完全分配和初始化的區段的指標。

可以在 PostgreSQL 原始碼樹中的 src/test/modules/test_dsm_registry/test_dsm_registry.c 中找到 GetNamedDSMSegment 的完整使用範例。

36.10.11. LWLocks #

36.10.11.1. 在啟動時請求 LWLocks #

附加元件可以在伺服器啟動時保留 LWLocks。與伺服器啟動時保留的共享記憶體一樣,附加元件的共享函式庫必須透過在 shared_preload_libraries 中指定它來預先載入,並且共享函式庫應在其 _PG_init 函式中註冊一個 shmem_request_hook。這個 shmem_request_hook 可以透過呼叫以下方式來保留 LWLocks:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

這確保了名為 tranche_name 的陣列包含 num_lwlocks 個 LWLocks。可以透過呼叫以下方式來取得指向此陣列的指標:

LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)

36.10.11.2. 啟動後請求 LWLocks #

還有另一種更靈活的方法來取得 LWLocks,可以在伺服器啟動後且在 shmem_request_hook 之外完成。為此,首先透過呼叫以下方式分配一個 tranche_id

int LWLockNewTrancheId(void)

接下來,初始化每個 LWLock,並將新的 tranche_id 作為引數傳遞:

void LWLockInitialize(LWLock *lock, int tranche_id)

與共享記憶體類似,每個後端都應確保只有一個程序分配新的 tranche_id 並初始化每個新的 LWLock。一種方法是僅在使用 AddinShmemInitLock 獨佔持有的情況下,在共享記憶體初始化程式碼中呼叫這些函式。如果使用 GetNamedDSMSegment,則在 init_callback 回呼函式中呼叫這些函式足以避免競爭條件。

最後,每個使用 tranche_id 的後端都應透過呼叫以下方式將其與 tranche_name 關聯起來:

void LWLockRegisterTranche(int tranche_id, const char *tranche_name)

可以在 PostgreSQL 原始碼樹的 contrib/pg_prewarm/autoprewarm.c 中找到 LWLockNewTrancheIdLWLockInitializeLWLockRegisterTranche 的完整使用範例。

36.10.12. 自訂等待事件 #

附加元件可以透過呼叫以下方式,在等待事件類型 Extension 下定義自訂等待事件:

uint32 WaitEventExtensionNew(const char *wait_event_name)

等待事件與面向使用者的自訂字串相關聯。可以在 PostgreSQL 原始碼樹的 src/test/modules/worker_spi 中找到一個範例。

可以在 pg_stat_activity 中檢視自訂等待事件

=# SELECT wait_event_type, wait_event FROM pg_stat_activity
     WHERE backend_type ~ 'worker_spi';
 wait_event_type |  wait_event
-----------------+---------------
 Extension       | WorkerSpiMain
(1 row)

36.10.13. 注入點 #

使用巨集宣告具有給定 name 的注入點

INJECTION_POINT(name);

在伺服器程式碼中的策略點上已經宣告了一些注入點。在新增注入點之後,需要編譯程式碼才能使該注入點在二進位檔案中可用。以 C 語言編寫的附加元件可以使用相同的巨集在其自身的程式碼中宣告注入點。

附加元件可以透過呼叫以下方式將回呼附加到已宣告的注入點:

extern void InjectionPointAttach(const char *name,
                                 const char *library,
                                 const char *function,
                                 const void *private_data,
                                 int private_data_size);

name 是注入點的名稱,在執行期間到達時,將執行從 library 載入的 functionprivate_data 是大小為 private_data_size 的資料私有區域,在執行時作為引數提供給回呼。

以下是 InjectionPointCallback 的回呼範例

static void
custom_injection_callback(const char *name, const void *private_data)
{
    uint32 wait_event_info = WaitEventInjectionPointNew(name);

    pgstat_report_wait_start(wait_event_info);
    elog(NOTICE, "%s: executed custom callback", name);
    pgstat_report_wait_end();
}

此回呼會以嚴重性 NOTICE 將訊息列印到伺服器錯誤日誌,但回呼可能會實作更複雜的邏輯。

或者,可以透過呼叫以下方式分離注入點

extern bool InjectionPointDetach(const char *name);

成功時,傳回 true,否則傳回 false

附加到注入點的回呼在所有後端都可用,包括在呼叫 InjectionPointAttach 後啟動的後端。它保持附加狀態,直到伺服器正在執行,或直到使用 InjectionPointDetach 分離注入點。

可以在 PostgreSQL 原始碼樹的 src/test/modules/injection_points 中找到一個範例。

啟用注入點需要使用 configure--enable-injection-points 或使用 Meson-Dinjection_points=true

36.10.14. 使用 C++ 進行擴充 #

雖然 PostgreSQL 後端是用 C 語言編寫的,但如果遵循以下準則,可以使用 C++ 編寫擴充:

  • 後端存取的所有函式都必須向後端提供 C 介面;然後,這些 C 函式可以呼叫 C++ 函式。例如,後端存取的函式需要 extern C 連結。對於在後端和 C++ 程式碼之間作為指標傳遞的任何函式,這也是必要的。

  • 使用適當的取消配置方法釋放記憶體。例如,大多數後端記憶體是使用 palloc() 分配的,因此請使用 pfree() 釋放它。在這種情況下使用 C++ delete 將會失敗。

  • 防止例外狀況傳播到 C 程式碼中(在所有 extern C 函式的頂層使用 catch-all 區塊)。即使 C++ 程式碼沒有明確拋出任何例外狀況,這也是必要的,因為像記憶體不足這樣的事件仍然可能拋出例外狀況。必須捕獲所有例外狀況,並將適當的錯誤傳遞回 C 介面。如果可能,請使用 -fno-exceptions 編譯 C++ 以完全消除例外狀況;在這種情況下,您必須檢查 C++ 程式碼中的失敗,例如,檢查 new() 傳回的 NULL。

  • 如果從 C++ 程式碼呼叫後端函式,請確保 C++ 呼叫堆疊僅包含普通的舊資料結構(POD)。這是必要的,因為後端錯誤會產生一個遙遠的 longjmp(),它無法正確地展開具有非 POD 物件的 C++ 呼叫堆疊。

總之,最好將 C++ 程式碼放置在與後端介接的 extern C 函式牆後,並避免例外狀況、記憶體和呼叫堆疊洩漏。

提交更正

如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清的地方,請使用此表單報告文件問題。