2025年9月25日: PostgreSQL 18 釋出!
支援版本: 當前 (18) / 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 會檢查該檔案是否包含具有適當內容的“魔術塊”。這允許伺服器檢測明顯的相容性問題,例如為不同主要版本的 PostgreSQL 編譯的程式碼。要包含魔術塊,請在模組的其中一個(且僅一個)原始檔中寫入以下內容,在包含標頭檔案 fmgr.h 之後

PG_MODULE_MAGIC;

PG_MODULE_MAGIC_EXT(parameters);

PG_MODULE_MAGIC_EXT 變體允許指定有關模組的附加資訊;目前,可以新增名稱和/或版本字串。(將來可能會允許更多欄位。)編寫如下所示

PG_MODULE_MAGIC_EXT(
    .name = "my_module_name",
    .version = "1.2.3"
);

隨後可以透過 pg_get_loaded_modules() 函式檢查名稱和版本。PostgreSQL 不限制版本字串的含義,但建議使用語義版本控制規則。

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

或者,動態載入的檔案可以包含一個初始化函式。如果檔案包含名為 _PG_init 的函式,則該函式將在載入檔案後立即呼叫。該函式不接收任何引數,並且應返回 void。目前無法解除安裝動態載入的檔案。

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

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

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

  • 按值傳遞,固定長度

  • 按引用傳遞,固定長度

  • 按引用傳遞,變長

按值傳遞的型別長度只能是 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 設定;切勿直接設定此欄位!儲存在該型別內的所有資料必須位於該長度欄位緊隨其後的記憶體中。長度欄位包含結構的總長度,也就是說,它包括長度欄位本身的大小。

另一個重要點是避免在資料型別值中留下任何未初始化的位;例如,注意將結構中可能存在的任何對齊填充位元組清零。否則,您的資料型別的邏輯等價常量可能會被規劃器視為不相等,從而導致低效(但不錯誤)的計劃。

警告

切勿修改傳引用輸入值的內容。如果這樣做,您很可能會損壞磁碟上的資料,因為您獲得的指標可能直接指向磁碟緩衝區。此規則的唯一例外在 第 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 名稱 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 語言函式,不需要此宏呼叫,因為 PostgreSQL 假定所有內部函式都使用版本 1 約定。但是,對於動態載入的函式,它卻是必需的。

在版本 1 函式中,每個實際引數都使用與引數資料型別對應的 PG_GETARG_xxx() 宏獲取。(在非嚴格函式中,需要先使用 PG_ARGISNULL() 檢查引數是否為 NULL;請參見下文。)結果使用返回型別的 PG_RETURN_xxx() 宏返回。PG_GETARG_xxx() 將要獲取的函式引數編號作為其引數,計數從 0 開始。PG_RETURN_xxx() 將要返回的實際值作為其引數。

要呼叫另一個版本 1 函式,可以使用 DirectFunctionCalln(func, arg1, ..., argn)。當您想呼叫標準內部庫中定義的函式時,這特別有用,使用與它們的 SQL 簽名類似的介面。

這些便利函式和類似函式可以在 fmgr.h 中找到。DirectFunctionCalln 系列將 C 函式名作為其第一個引數。還有 OidFunctionCalln 接受目標函式的 OID,以及其他一些變體。所有這些都期望函式的引數作為 Datum 提供,並且它們也返回 Datum。請注意,使用這些便利函式時,引數和結果都不能為 NULL。

例如,要從 C 呼叫 starts_with(text, text) 函式,您可以搜尋目錄並發現其 C 實現是 Datum text_starts_with(PG_FUNCTION_ARGS) 函式。通常,您會使用 DirectFunctionCall2(text_starts_with, ...) 來呼叫此類函式。但是,starts_with(text, text) 需要排序規則資訊,因此如果以這種方式呼叫,它將失敗並出現“無法確定用於字串比較的排序規則”錯誤。相反,您必須使用 DirectFunctionCall2Coll(text_starts_with, ...) 並提供所需的排序規則,通常只是從 PG_GET_COLLATION() 傳遞,如下例所示。

fmgr.h 還提供了方便 C 型別和 Datum 之間轉換的宏。例如,要將 Datum 轉換為 text*,可以使用 DatumGetTextPP(X)。雖然某些型別有像 TypeGetDatum(X) 這樣的宏用於反向轉換,但 text* 沒有;為此,使用通用宏 PointerGetDatum(X) 就足夠了。如果您的擴充套件定義了其他型別,通常也方便為您的型別定義類似的宏。

以下是使用版本 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);
}

/* A wrapper around starts_with(text, text) */

PG_FUNCTION_INFO_V1(t_starts_with);

Datum
t_starts_with(PG_FUNCTION_ARGS)
{
    text       *t1 = PG_GETARG_TEXT_PP(0);
    text       *t2 = PG_GETARG_TEXT_PP(1);
    Oid         collid = PG_GET_COLLATION();
    bool        result;

    result = DatumGetBool(DirectFunctionCall2Coll(text_starts_with,
                                                  collid,
                                                  PointerGetDatum(t1),
                                                  PointerGetDatum(t2)));
    PG_RETURN_BOOL(result);
}

假設以上程式碼已在檔案 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;

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

這裡,DIRECTORY 代表共享庫檔案的目錄(例如 PostgreSQL 教程目錄,其中包含本節中使用的示例程式碼)。(更好的風格是在 AS 子句中只使用 'funcs',在將 DIRECTORY 新增到搜尋路徑之後。無論如何,我們可以省略共享庫的系統特定副檔名,通常是 .so。)

請注意,我們已將函式指定為“嚴格”,這意味著如果任何輸入值為 null,系統應自動假定結果為 null。透過這樣做,我們避免了在函式程式碼中檢查 null 輸入。否則,我們必須使用 PG_ARGISNULL() 顯式檢查 null 值。

PG_ARGISNULL(n) 允許函式測試每個輸入是否為 null。(當然,這樣做只在未宣告為“嚴格”的函式中是必要的。)與 PG_GETARG_xxx() 宏一樣,輸入引數從零開始計數。請注意,在驗證引數不為 null 之前,應避免執行 PG_GETARG_xxx()。要返回 null 結果,請執行 PG_RETURN_NULL();這在嚴格和非嚴格函式中都有效。

乍一看,版本 1 編碼約定與使用普通的 C 呼叫約定相比,可能顯得毫無意義的晦澀。然而,它們確實允許我們處理可為空的引數/返回值以及“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.9 節)並實現觸發器函式(第 37 章)和過程語言呼叫處理程式(第 57 章)成為可能。有關更多詳細資訊,請參閱原始碼分發中的 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 節

  • 請記住為您的共享庫定義一個“魔術塊”,如 第 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. 伺服器 API 和 ABI 穩定性指南 #

本節包含關於 PostgreSQL 伺服器中 API 和 ABI 穩定性的擴充套件和其他伺服器外掛作者指南。

36.10.6.1. 概述 #

PostgreSQL 伺服器包含幾個明確劃分的伺服器外掛 API,例如函式管理器(fmgr,本章描述)、SPI第 45 章)以及專為擴充套件設計的各種鉤子。這些介面經過精心管理,以實現長期穩定性和相容性。然而,伺服器中的所有全域性函式和變數實際上構成了公開可用的 API,其中大部分在設計時並未考慮可擴充套件性和長期穩定性。

因此,雖然利用這些介面是有效的,但越偏離成熟的路徑,就越有可能在某個時候遇到 API 或 ABI 相容性問題。鼓勵擴充套件作者提供有關其要求的回饋,以便隨著時間的推移,隨著新使用模式的出現,某些介面可以被認為更穩定,或者可以新增新的、設計更好的介面。

36.10.6.2. API 相容性 #

API,即應用程式程式設計介面,是在編譯時使用的介面。

36.10.6.2.1. 主要版本 #

PostgreSQL 主要版本之間保證 API 相容性。因此,擴充套件程式碼可能需要修改原始碼才能與多個主要版本一起工作。這些通常可以透過預處理器條件(例如 #if PG_VERSION_NUM >= 160000)進行管理。使用超出明確劃分介面的複雜擴充套件通常需要為每個主要伺服器版本進行一些此類更改。

36.10.6.2.2. 次要版本 #

PostgreSQL 致力於避免在次要版本中出現伺服器 API 中斷。通常,與次要版本一起編譯和工作的擴充套件程式碼,也應該與同一主要版本的任何其他次要版本(過去或未來)一起編譯和工作。

當需要進行更改時,將謹慎管理,同時考慮擴充套件的要求。此類更改將在發行說明中進行溝通(附錄 E)。

36.10.6.3. ABI 相容性 #

ABI,或應用程式二進位制介面,是執行時使用的介面。

36.10.6.3.1. 主要版本 #

不同主要版本的伺服器故意具有不相容的 ABI。因此,使用伺服器 API 的擴充套件必須針對每個主要版本重新編譯。PG_MODULE_MAGIC 的包含(參見 第 36.10.1 節)確保為某個主要版本編譯的程式碼將被其他主要版本拒絕。

36.10.6.3.2. 次要版本 #

PostgreSQL 努力避免在次要版本中出現伺服器 ABI 中斷。通常,針對任何次要版本編譯的擴充套件應該與同一主要版本的任何其他次要版本(過去或未來)一起工作。

當需要進行更改時,PostgreSQL 將選擇侵入性最小的更改,例如將新欄位擠入填充空間或將其附加到結構末尾。除非擴充套件使用非常不尋常的程式碼模式,否則這些型別的更改不應影響擴充套件。

然而,在極少數情況下,即使是這種非侵入性更改也可能不切實際或不可能。在這種情況下,將仔細管理更改,同時考慮到擴充套件的要求。此類更改也將在發行說明中記錄(附錄 E)。

但請注意,伺服器的許多部分並未被設計或維護為可公開使用的 API(在大多數情況下,實際邊界也未明確定義)。如果出現緊急需求,對這些部分的更改將自然而然地較少考慮擴充套件程式碼,而不是對明確定義和廣泛使用的介面的更改。

此外,在沒有此類更改的自動化檢測的情況下,這不是一個保證,但歷史上此類重大更改極為罕見。

36.10.7. 複合型別引數 #

複合型別沒有像 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.8. 返回行(複合型別) #

要從 C 語言函式返回行或複合型別值,您可以使用一個特殊 API,該 API 提供了宏和函式來隱藏構建複合資料型別的大部分複雜性。要使用此 API,原始檔必須包含

#include "funcapi.h"

有兩種方法可以構建複合資料值(此後稱為“元組”):您可以從 Datum 值陣列構建它,或者從 C 字串陣列構建它,這些 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 結構。(這當然要求您使用版本 1 呼叫約定。)resultTypeId 可以指定為 NULL 或區域性變數的地址,以接收函式的返回型別 OID。resultTupleDesc 應該是一個區域性 TupleDesc 變數的地址。檢查結果是否為 TYPEFUNC_COMPOSITE;如果是,則 resultTupleDesc 已填充所需的 TupleDesc。(如果不是,您可以報告類似“函式在無法接受型別記錄的上下文中呼叫記錄”的錯誤。)

提示

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)

如果您打算使用 Datum,或者

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

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

使用 Datum 時,使用

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

以 Datum 形式給定使用者資料來構建 HeapTuple

當使用 C 字串時,使用

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

根據 C 字串形式的使用者資料構建 HeapTuplevalues 是一個 C 字串陣列,返回行的每個屬性一個。每個 C 字串都應採用屬性資料型別的輸入函式所期望的格式。為了返回其中一個屬性的空值,values 陣列中對應的指標應設定為 NULL。對於您返回的每一行,都需要再次呼叫此函式。

一旦您構建了一個要從函式返回的元組,它就必須轉換為 Datum。使用

HeapTupleGetDatum(HeapTuple tuple)

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

下一節將出現一個示例。

36.10.9. 返回集合 #

C 語言函式有兩種返回集合(多行)的選項。在一種稱為按呼叫返回值模式的方法中,集合返回函式被重複呼叫(每次傳遞相同的引數),並在每次呼叫時返回一個新行,直到它沒有更多行要返回並透過返回 NULL 來發出訊號。因此,集合返回函式(SRF)必須在呼叫之間儲存足夠的狀態,以記住它正在做什麼並在每次呼叫時返回正確的下一個專案。在另一種稱為具體化模式的方法中,SRF 填充並返回一個包含其整個結果的元組儲存物件;然後只發生一次對整個結果的呼叫,並且不需要呼叫間狀態。

在使用按呼叫返回值模式時,記住不能保證查詢會執行完成是很重要的;也就是說,由於 LIMIT 等選項,執行器可能會在所有行都被獲取之前停止對集合返回函式的呼叫。這意味著在最後一次呼叫中執行清理活動是不安全的,因為這可能永遠不會發生。對於需要訪問外部資源(例如檔案描述符)的函式,建議使用具體化模式。

本節的其餘部分文件了一組通常用於(但不是必須使用)使用 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 中分配的資料將在查詢結束時自動消失,因此也不需要手動釋放這些資料。)

警告

雖然函式實際引數在呼叫之間保持不變,但如果您在瞬態上下文中解包裝引數值(通常由 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.10. 多型引數和返回型別 #

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.11. 共享記憶體 #

36.10.11.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 為初始化程式碼提供了一個方便的位置,但並非所有此類程式碼都必須放置在此鉤子中。在 Windows(以及任何定義 EXEC_BACKEND 的地方),每個後端在其附加到共享記憶體後不久都會執行已註冊的 shmem_startup_hook,因此外掛仍應在此鉤子內獲取 AddinShmemInitLock,如上例所示。在其他平臺上,只有 postmaster 程序執行 shmem_startup_hook,每個後端都會自動繼承指向共享記憶體的指標。

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

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

還有另一種更靈活的保留共享記憶體的方法,可以在伺服器啟動後和 shmem_request_hook 之外完成。為此,每個將使用共享記憶體的後端都應透過呼叫獲取指向它的指標

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

如果具有給定名稱的動態共享記憶體段尚不存在,此函式將分配它並使用提供的 init_callback 回撥函式對其進行初始化。如果該段已被另一個後端分配和初始化,此函式只是將現有動態共享記憶體段附加到當前後端。

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

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

36.10.12. LWLocks #

36.10.12.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.12.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)

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

36.10.13. 自定義等待事件 #

外掛可以透過呼叫在等待事件型別 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.14. 注入點 #

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

INJECTION_POINT(name, arg);

伺服器程式碼中已在戰略位置聲明瞭一些注入點。在新增新的注入點後,程式碼需要編譯才能使該注入點在二進位制檔案中可用。C 語言編寫的外掛可以使用相同的宏在自己的程式碼中宣告注入點。注入點名稱應使用小寫字元,並用連字元分隔。arg 是在執行時傳遞給回撥的可選引數值。

執行注入點可能需要分配少量記憶體,這可能會失敗。如果您需要在不允許動態分配的關鍵部分中設定注入點,可以使用以下宏進行兩步操作

INJECTION_POINT_LOAD(name);
INJECTION_POINT_CACHED(name, arg);

在進入臨界區之前,呼叫 INJECTION_POINT_LOAD。它會檢查共享記憶體狀態,如果回撥處於活動狀態,則將其載入到後端私有記憶體中。在臨界區內部,使用 INJECTION_POINT_CACHED 執行回撥。

外掛可以透過呼叫將回調附加到已宣告的注入點

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,
                          void *arg)
{
    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 嚴重性列印到伺服器錯誤日誌中,但回撥可以實現更復雜的邏輯。

定義當達到注入點時要執行的操作的另一種方法是將測試程式碼新增到正常原始碼旁邊。如果操作例如依賴於載入模組無法訪問的區域性變數,這會很有用。然後可以使用 IS_INJECTION_POINT_ATTACHED 宏來檢查注入點是否已附加,例如

#ifdef USE_INJECTION_POINTS
if (IS_INJECTION_POINT_ATTACHED("before-foobar"))
{
    /* change a local variable if injection point is attached */
    local_var = 123;

    /* also execute the callback */
    INJECTION_POINT_CACHED("before-foobar", NULL);
}
#endif

請注意,附加到注入點的回撥不會由 IS_INJECTION_POINT_ATTACHED 宏執行。如果要執行回撥,您還必須像上面的示例一樣呼叫 INJECTION_POINT_CACHED

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

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.15. 自定義累積統計 #

用 C 語言編寫的外掛可以使用在 累積統計系統 中註冊的自定義累積統計型別。

首先,定義一個 PgStat_KindInfo,其中包含與註冊的自定義型別相關的所有資訊。例如

static const PgStat_KindInfo custom_stats = {
    .name = "custom_stats",
    .fixed_amount = false,
    .shared_size = sizeof(PgStatShared_Custom),
    .shared_data_off = offsetof(PgStatShared_Custom, stats),
    .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats),
    .pending_size = sizeof(PgStat_StatCustomEntry),
}

然後,每個需要使用此自定義型別的後端都需要使用 pgstat_register_kind 和一個唯一的 ID 來註冊它,該 ID 用於儲存與此統計型別相關的條目

extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
                                        const PgStat_KindInfo *kind_info);

在開發新擴充套件時,對 kind 使用 PGSTAT_KIND_EXPERIMENTAL。當您準備將擴充套件釋出給使用者時,請在 自定義累積統計 頁面上保留一個型別 ID。

PgStat_KindInfo 的 API 詳情可以在 src/include/utils/pgstat_internal.h 中找到。

註冊的統計型別與一個名稱和在共享記憶體中共享的唯一 ID 相關聯。每個使用自定義統計型別的後端都維護一個本地快取,用於儲存每個自定義 PgStat_KindInfo 的資訊。

將實現自定義累積統計型別的擴充套件模組放置在 shared_preload_libraries 中,以便在 PostgreSQL 啟動期間儘早載入它。

一個描述如何註冊和使用自定義統計資訊的示例可以在 src/test/modules/injection_points 中找到。

36.10.16. 使用 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(),該 longjmp() 無法正確展開包含非 POD 物件的 C++ 呼叫棧。

總而言之,最好將 C++ 程式碼置於 extern C 函式的屏障之後,這些函式與後端進行介面,並避免異常、記憶體和呼叫棧洩漏。

提交更正

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