33.7. C 語言函數

用戶定義的函數可以用 C 寫(或者是那些可以與 C 兼容的語言,比如 C++)。 這樣的函數是編譯進可動態裝載的對象的(也叫做共享庫)並且是由服務器根據需要裝載的。 動態裝載的特性是 "C 語言" 函數和"內部"函數之間相互區別的地方 --- 實際的編碼習慣在兩者之間實際上是一樣的。 (因此,標準的內部函數庫為寫用戶定義 C 函數提供了大量最好的樣例。)

目前對 C 函數有兩種調用傳統。新的"版本 1"的調用傳統是通過為該函數書寫一個 PG_FUNCTION_INFO_V1() 宏來標識的,象下面演示的那樣。缺少這個宏標識一個老風格的("版本 0")函數。 兩種風格裡在CREATE FUNCTION裡聲明的都是 C。 現在老風格的函數已經廢棄了,主要是因為移植性原因和缺乏功能, 不過出于兼容性原因,系統仍然支持它。

33.7.1. 動態裝載

當某個特定的可裝載對象文件裡的用戶定義的函數第一次被服務器會話調用時, 動態裝載器把函數的目標碼裝載入內存。 因此,用于用戶定義的 C 函數的 CREATE FUNCTION必須為函數聲明兩部分信息: 可裝載對象文件名字,和所聲明的在那個目標文件裡調用的函數的 C 名字(聯接符號)。 如果沒有明確聲明C名字,那麼就假設它與SQL函數名相同。

基于在 CREATE FUNCTION 命令中給出的名字, 下面的算法用于定位共享對象文件:

  1. 如果名字是一個絕對路徑名,則裝載給出的文件。

  2. 如果名字以字串 $libdir 開頭, 那麼該部分將被PostgreSQL庫目錄名代替, 該目錄是在制作的時候判定的。

  3. 如果名字不包含目錄部分,那麼在配置變量 dynamic_library_path 裡聲明的路徑裡查找。

  4. 否則(沒有在路徑裡找到該文件,或者它包含一個非絕對目錄部分), 那麼動態裝載器就會試圖拿這個名字來裝載,這樣幾乎可以肯定是要失敗的。(依靠當前工作目錄是不可靠的。)

如果這個順序不管用,那麼就給這個給出的名字附加上平台相關的共享庫文件名擴展(通常是 .so), 然後再重新按照上面的過程來一便。如果還是失敗,那麼裝載失敗。

注意: PostgreSQL 服務器運行時的用戶 ID 必須可以遍歷路徑到達你想裝載的文件。一個常見的錯誤就是把該文件或者一個高層目錄的權限設置為 postgres 用戶不可讀和/或不能執行。

在任何情況下,在 CREATE FUNCTION 命令裡給出的文件名是在系統表裡按照文本記錄的,因此, 如果需要再次裝載,那麼會再次運行這個過程。

注意: PostgreSQL 不會自動編譯一個函數; 在使用 CREATE FUNCTION 命令之前你必須編譯它。 參閱 Section 33.7.6 獲取更多信息。

注意: 在第一次使用之後,在內存中動態裝載了一個目標文件。 在同一次會話中的後繼的函數調用將只會產生很小的符號表查詢的過熱。 如果你需要強制對象文件的重載,比如你重新編譯了該文件,那麼可以使用 LOAD 命令或者開始一次新的會話。

我們建議使用與 $libdir 相對的目錄或者通過動態庫路徑定位共享庫。這樣,如果新版本安裝在一個不同的 位置,那麼就可以簡化版本升級。$libdir 表示的實際目錄位置可以用命令 pg_config --pkglibdir 找到。

PostgreSQL 版本 7.2 之前, 我們只能在 CREATE FUNCTION 中聲明目標文件的準確的絕對路徑。 目前這個方法已經過時了,因為這樣令函數定義毫無意義的不可移植。 最好是只聲明共享庫的名字,不帶路徑,也沒有擴展名。然後讓搜索機制提供那些信息。

33.7.2. 基本類型的 C 語言函數

要知道如何寫 C 語言的函數,你需要知道 PostgreSQL 在內部是如何表現基本數據類型的,以及它們是如何傳入函數以及傳出函數的。 PostgreSQL 內部把基本類型當作"一片內存"看待。 定義在某種類型上的用戶定義函數實際上定義了 PostgreSQL 對(該數據類型)可能的操作。 也就是說,PostgreSQL 只是從磁盤讀取和存儲該數據類型, 而使用你定義的函數來輸入,處理和輸出數據。基本類型可以有下面三種內部形態(格式)之一:

傳遞數值的類型的長度只能是1,2 或 4 字節。 (還有 8 字節,如果 sizeof(Datum) 在你的機器上是 8 的話。)。 你要仔細定義你的類型,確保它們在任何體系平台上都是相同尺寸(字節)。 例如,long 型是一個危險的類型因為在一些機器上它是 4 字節而在另外一些機器上是 8 字節, 而 int型在大多數 Unix 機器上都是4字節的。 在一個 Unix 機器上的 integer 合理的實現可能是:

/* 4-字節整數,傳值 */
typedef int integer;

另外,任何尺寸的定長類型都可以是傳遞引用型。例如,下面是一個 PostgreSQL 類型的實現:

/* 16-字節結構,傳遞引用 */
typedef struct
{
    double  x, y;
} Point;

只能使用指向這些類型的指針來在 PostgreSQL 函數裡傳入和傳出數據。 要返回這樣的類型的值,用 palloc() 分配正確數量的存儲器,填充這些存儲器,然後返回一個指向它的指針。 (另外,你可以通過返回指針的方法返回一個與輸入數據同類型的值。 但是,絕對不要 修改傳遞引用的輸入數值。)

最後,所有變長類型同樣也只能通過傳遞引用的方法來傳遞。 所有變長類型必須以一個正好 4 字節長的長度域開始, 並且所有存儲在該類型的數據必須放在緊接著長度域的存儲空間裡。 長度域是結構的全長,也就是說,包括長度域本身的長度。

比如,我們可以用下面方法定義一個 text 類型:

typedef struct {
    integer length;
    char data[1];
} text;

顯然,上面聲明的數據域的長度不足以存儲任何可能的字串。因為在 C中不可能聲明變長度的結構, 所以我們倚賴這樣的知識:C 編譯器不會對數組下標進行範圍檢查。 我們只需要分配足夠的空間,然後把數組當做已經聲明為合適長度的變量訪問。 (這是一個常用的技巧,你可以在許多 C 的教科書中讀到。)

當處理變長類型時,我們必須仔細分配正確的存儲器數量並正確設置長度域。 例如,如果我們想在一個 text 結構裡存儲 40 字節, 我們可能會使用象下面的代碼片段:

#include "postgres.h"
...
char buffer[40]; /* 我們的源數據 */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
destination->length = VARHDRSZ + 40;
memcpy(destination->data, buffer, 40);
...

VARHDRSZsizeof(int4) 一樣, 但是我們認為用宏 VARHDRSZ 表示附加尺寸是用于變長類型的更好的風格。

Table 33-1 列出了書寫一個使用了 PostgreSQL 內置類型的 C 函數裡需要的知道的哪個 SQL 類型對應哪個 C 類型。 "定義在" 列給出了需要包含以獲取該類型定義的頭文件(實際的定義可能是在包含在列出的文件所包含的文件中。 我們建議用戶只使用這裡定義的接口。) 注意,你應該總是首先包括 postgres.h, 因為它聲明了許多你需要的東西。

Table 33-1. 與內建的類型等效的 C 類型

內建類型 C 類型 定義在
abstimeAbsoluteTimeutils/nabstime.h
booleanboolpostgres.h (可能是編譯器內置)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(編譯器內置)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
smallint (int2)int2int16postgres.h
int2vectorint2vector*postgres.h
integer (int4)int4int32postgres.h
real (float4)float4*postgres.h
double precision (float8)float8*postgres.h
intervalInterval*utils/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocregprocpostgres.h
reltimeRelativeTimeutils/nabstime.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
timestampTimestamp*utils/timestamp.h
tintervalTimeIntervalutils/nabstime.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

既然我們已經討論了基本類型所有的可能結構, 我們便可以用實際的函數舉一些例子。

33.7.3. C 語言函數的版本-0 調用風格

我們先提供"老風格"的調用風格 --- 盡管這種做法現在已經不提倡了, 但它還是比較容易邁出第一步。在版本-0方法裡,C 函數的參數和結果只是用普通 C 風格聲明,但是要小心使用上面顯示的SQL數據類型的 C 表現形式。

下面是一些例子:

#include "postgres.h"
#include <string.h>

/* 傳遞數值 */

int
add_one(int arg)
{
    return arg + 1;
}

/* 傳遞引用,定長 */

float8 *
add_one_float8(float8 *arg)
{
    float8    *result = (float8 *) palloc(sizeof(float8));

    *result = *arg + 1.0;

    return result;
}

Point *
makepoint(Point *pointx, Point *pointy)
{
    Point     *new_point = (Point *) palloc(sizeof(Point));

    new_point->x = pointx->x;
    new_point->y = pointy->y;

    return new_point;
}

/* 傳遞引用,變長 */

text *
copytext(text *t)
{
    /*
     * VARSIZE 是結構以字節計的總長度
     */
    text *new_t = (text *) palloc(VARSIZE(t));
    VARATT_SIZEP(new_t) = VARSIZE(t);
    /*
     * VARDATA 是結構中一個指向數據區的指針
     */
    memcpy((void *) VARDATA(new_t), /* 目的 */
           (void *) VARDATA(t),     /* 源 */
           VARSIZE(t)-VARHDRSZ);    /* 多少字節 */
    return new_t;
}

text *
concat_text(text *arg1, text *arg2)
{
    int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    VARATT_SIZEP(new_text) = new_text_size;
    memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
    memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
           VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
    return new_text;
}

假設上面的代碼放在文件 funcs.c 並且編譯成了共享目標, 我們可以用下面的命令為 PostgreSQL 定義這些函數:

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

-- 注意:重載了名字為 add_one() 的 SQL 函數
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 教程目錄,那裡包含本節使用的例子的代碼。) (更好的風格應該是在加了 DIRECTORY 到搜索路徑之後, 在 AS 子句裡只使用 'funcs', 不管怎樣,我們都可以省略和系統相關的共享庫擴展,通常是 .so.sl。)

請注意我們把函數聲明為"strict"(嚴格),意思是說如果任何輸入值為 NULL, 那麼系統應該自動假設一個 NULL 的結果。這樣處理可以讓我們避免在函數代碼裡面檢查 NULL 輸入。 如果不這樣處理,我們就得明確檢查空值, 比如為每個傳遞引用的參數檢查空指針。(對于傳值類型的參數,我們甚至沒有辦法檢查!)

盡管這種老風格的調用風格用起來簡單,它確不太容易移植;在一些系統上, 我們用這種方法傳遞比 int 小的數據類型就會碰到困難。 而且,我們沒有很好的返回 NULL 結果的辦法, 也沒有除了把函數嚴格化以外的處理 NULL 參數的方法。下面要講的版本-1的方法則解決了這些問題。

33.7.4. C 語言函數的版本-1調用風格

版本-1 調用風格依賴宏來消除大多數傳遞參數和結果的復雜性。版本-1 風格函數的 C 定義總是下面這樣

Datum funcname(PG_FUNCTION_ARGS)

另外,下面的宏

PG_FUNCTION_INFO_V1(funcname);

也必須出現在同一個源文件裡(通常就可以寫在函數自身前面)。 對那些內部-語言函數而言,不需要調用這個宏, 因為PostgreSQL目前假設內部函數都是版本-1。 不過,對于動態鏈接的函數,它是必須的。

在版本-1函數裡, 每個實際參數都是用一個對應該參數的數據類型的 PG_GETARG_xxx()宏抓取的,結果是用返回類型的 PG_RETURN_xxx()宏返回的。 PG_GETARG_xxx() 接受要抓取的函數參數的編號作為其參數,編號是從 0 開始的。 PG_RETURN_xxx() 接受實際要返回的數值為自身的參數。

下面是和上面一樣的函數,但是是用版本-1風格編的:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"

/* 傳遞數值 */

PG_FUNCTION_INFO_V1(add_one);
         
Datum
add_one(PG_FUNCTION_ARGS)
{
    int32   arg = PG_GETARG_INT32(0);

    PG_RETURN_INT32(arg + 1);
}

/* 傳遞引用,定長 */

PG_FUNCTION_INFO_V1(add_one_double precision);

Datum
add_one_float8 precision(PG_FUNCTION_ARGS)
{
    /* 用于 FLOAT8 的宏,隱藏其傳遞引用的本質 */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* 這裡,我們沒有隱藏 Point 的傳遞引用的本質 */
    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);

Datum
copytext(PG_FUNCTION_ARGS)
{
    text     *t = PG_GETARG_TEXT_P(0);
    /*
     * VARSIZE 是結構以字節計的總長度
     */
    text     *new_t = (text *) palloc(VARSIZE(t));
    VARATT_SIZEP(new_t) = VARSIZE(t);
    /*
     * VARDATA 是結構中指向數據區的一個指針
     */
    memcpy((void *) VARDATA(new_t), /* 目的 */
           (void *) VARDATA(t),     /* 源 */
           VARSIZE(t)-VARHDRSZ);    /* 多少字節 */
    PG_RETURN_TEXT_P(new_t);
}

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_P(0);
    text  *arg2 = PG_GETARG_TEXT_P(1);
    int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    VARATT_SIZEP(new_text) = new_text_size;
    memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
    memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
           VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
    PG_RETURN_TEXT_P(new_text);
}

用到的 CREATE FUNCTION 命令和用于老風格的等效的命令一樣。

猛地一看,版本-1的編碼好象只是無目的地蒙人。但是它們的確給我們許多改進,因為宏可以隱藏許多不必要的細節。 一個例子在add_one_float8的編碼裡,這裡我們不再需要不停叮囑自己 float8 是傳遞引用類型。 另外一個例子是用于變長類型的宏 GETARG 隱藏了抓取"toasted"(烤爐)(壓縮的或者超長的)值需要做的處理。

版本-1的函數另一個巨大的改進是對 NULL 輸入和結果的處理。 宏 PG_ARGISNULL(n) 允許一個函數測試每個輸入是否為 NULL (當然,這件事只是對那些沒有聲明為 "strict" 的函數有必要)。 因為如果有PG_GETARG_xxx() 宏,輸入參數是從零開始計算的。請注意我們不應該執行 PG_GETARG_xxx(), 除非有人聲明了參數不是 NULL。 要返回一個 NULL 結果,執行一個 PG_RETURN_NULL(),這樣對嚴格的和不嚴格的函數都有效。

在新風格的接口中提供的其它的選項是 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的函數調用風格也令我們可能返回一"套"結果 (Section 33.7.9)並且實現觸發器函數(Chapter 35)和過程語言調用句柄(Chapter 47)。 版本-1 的代碼也比版本-0的更容易移植,因為它沒有違反 C 標準對函數調用協議的限制。更多的細節請參閱 源程序中的src/backend/utils/fmgr/README

33.7.5. 書寫代碼

在我們轉到更深的話題之前,我們要先討論一些 PostgreSQL C 語言函數的編碼規則。 雖然可以用 C 以外的其他語言如書寫用于PostgreSQL 的共享函數, 但通常很麻煩(雖然是完全可能的),因為其他像 C++,FORTRAN,或者 Pascal 這樣的語言並不遵循和 C 一樣的調用習慣。 也就是說,其他語言與C的傳遞參數和返回值的方式不一樣。 因此我們假設你的編程語言函數是用 C 寫的。

書寫和制作 C 函數的基本規則如下:

33.7.6. 編譯和鏈接動態鏈接的函數

在你能夠使用由 C 寫的 PostgreSQL 擴展函數之前,你必須 用一種特殊的方法編譯和鏈接它們,這樣才能生成可以被服務器 動態地裝載的文件.準確地說,我們需要創建一個 共享庫

如果需要超出本節所包含範圍的信息,那麼你應該閱讀你的操作系統的文檔, 特別是 C 編譯器,cc 和鏈接器, ld 的手冊頁. 另外,PostgreSQL 源代碼裡包含幾個 可以運行的例子,它們在 contrib 目錄裡. 不過,如果你依賴這些例子,那麼你就要把自己的模塊做得和 PostgreSQL 源代碼無關才行.

創建共享庫和鏈接可執行文件類似:首先把源代碼編譯成目標文件, 然後把目標文件鏈接起來.目標文件需要創建成 位置無關碼(position-independent code)PIC),概念上就是在可執行程序裝載它們的時候, 它們可以放在可執行程序的內存裡的任何地方, (用于可執行文件的目標文件通常不是用這個方式編譯的.) 鏈接動態庫的命令包含特殊標志,與鏈接可執行文件的命令是有區別的. --- 至少理論上如此.在一些系統裡的現實更惡心.

在下面的例子裡,我們假設你的源程序代碼在 foo.c 文件裡並且將創建成名字叫 foo.so的共享庫.中介的對象文件將叫做 foo.o,除非我們另外注明.一個共享庫可以 包含多個對象文件,不過我們在這裡只用一個.

BSD/OS

創建 PIC 的編譯器標志是 -fpic.創建共享庫的鏈接器標志是 -shared

gcc -fpic -c foo.c
ld -shared -o foo.so foo.o

上面方法適用于版本 4.0 的 BSD/OS

FreeBSD

創建 PIC 的編譯器標志是 -fpic.創建共享庫的鏈接器標志是 -shared

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

上面方法適用于版本 3.0 的 FreeBSD.

HP-UX

創建 PIC 的系統編譯器標志是 +z.如果使用 GCC 則是 -fpic. 創建共享庫的鏈接器標志是 -b.因此

cc +z -c foo.c

gcc -fpic -c foo.c

然後

ld -b -o foo.sl foo.o

HP-UX 使用 .sl 做共享庫擴展,和其它大部分系統不同.

IRIX

PIC 是缺省,不需要使用特殊的編譯器選項. 生成共享庫的鏈接器選項是 -shared.

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

Linux

創建 PIC 的編譯器標志是 -fpic.在一些平台上的一些環境下, 如果 -fpic 不能用那麼必須使用-fPIC. 參考 GCC 的手冊獲取更多信息. 創建共享庫的編譯器標志是 -shared.一個完整的例子看起來象:

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

MacOS X

這裡是一個例子。這裡假設開發工具已經安裝好了。

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

Tru64 UNIX

PIC 是缺省,因此編譯命令就是平常的那個. 帶特殊選項的 ld 用于鏈接:

cc -c foo.c
ld -shared -expect_unresolved '*' -o foo.so foo.o

用 GCC 代替系統編譯器時的過程是一樣的;不需要特殊的選項.

UnixWare

SCO 編譯器創建 PIC 的標志是-KPIC GCC-fpic. 鏈接共享庫時 SCO 編譯器用 -GGCC-shared

cc -K PIC -c foo.c
cc -G -o foo.so foo.o

or

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

技巧: 如果你覺得這些步驟實在太復雜,那麼你應該考慮使用 GNU Libtool,它把平台的差異隱藏在了一個統一的接口裡。

生成的共享庫文件然後就可以裝載到 PostgreSQL裡面去了.在給 CREATE FUNCTION 命令聲明文件名的時候,我們必須聲明 共享庫文件的名字而不是中間目標文件的名字.請注意你可以在 CREATE FUNCTION 命令上忽略 系統標準的共享庫擴展 (通常是.so.sl), 並且出于最佳的兼容性考慮也應該忽略.

回去看看 Section 33.7.1 獲取有關服務器 預期在哪裡找到共享庫的信息.

33.7.7. 復合類型的 C 語言函數

復合類型不象 C 結構那樣有固定的布局。 復合類型的實例可能包含空(null)域。 另外,一個屬于繼承層次一部分的復合類 型可能和同一繼承範疇的其他成員有不同的域/字段。 因此,PostgreSQL 提供一個過程接口用于從 C 裡面訪問復合類型。

假設我們為下面查詢寫一個函數

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

在上面的查詢裡,用版本 0 的調用接口,我們可以這樣定義c_overpaid

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

bool
c_overpaid(TupleTableSlot *t, /* EMP 的當前行*/
           int32 limit)
{
    bool isnull;
    int32 salary;

    salary = DatumGetInt32(GetAttributeByName(t, "salary", &isnull));
    if (isnull)
        return (false);
    return salary > limit;
}

在版本-1編碼,上面的東西會寫成下面這樣:

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

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    TupleTableSlot  *t = (TupleTableSlot *) PG_GETARG_POINTER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    int32 salary;

    salary = DatumGetInt32(GetAttributeByName(t, "salary", &isnull));
    if (isnull)
        PG_RETURN_BOOL(false);
    /* 另外,我們可能更希望將 PG_RETURN_NULL() 用在空薪水上 */

    PG_RETURN_BOOL(salary > limit);
}

GetAttributeByNamePostgreSQL 系統函數, 用來返回當前記錄的字段。它有三個參數:類型為 TupleTableSlot* 的傳入函數 的參數,你想要的字段名稱, 以及一個用以確定字段是否為空(null)的返回參數。 GetAttributeByName 函數返回一個Datum值, 你可以用對應的 DatumGetXXX() 宏把它轉換成合適的數據類型。

下面的命令在 SQL 裡聲明 c_overpaid 函數:

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

33.7.8. 從 C 函數裡返回行(復合類型)

要從一個 C 語言函數裡返回一個行或者一個復合類型的數值,我們可以使用一個特殊的 API, 它提供了許多宏和函數來消除大多數制作復合數據類型的復雜性。 要使用該 API,源代碼必須包含:

#include "funcapi.h"

支持返回復合數據類型(或者行)是從 AttInMetadata 結構開始的。這個結構保存著從一個裸的 C 字串裡創建一行所需要的各字段信息的數組。 這裡裝載的信息是源自 TupleDesc 的,它存儲在這裡是為了避免每次調用返回結果集的函數(見下節)的額外的計算開銷。 如果是一個返回結果集的函數, 那麼在第一次調用函數的時候應該計算 AttInMetadata 結構一次, 然後保存起來為後面使用。AttInMetadata 還保存一個指向原始的 TupleDesc 的指針。

typedef struct AttInMetadata
{
    /* 完整的 TupleDesc */
    TupleDesc       tupdesc;

    /* 輸入函數屬性 finfo 類型數組 */
    FmgrInfo       *attinfuncs;

    /* 屬性 typelem 類型數組 */
    Oid            *attelems;

    /* 屬性 typmod 類型數組 */
    int32        *atttypmods;
}     AttInMetadata;

為了幫助你填充這個結構,我們定義了一些函數和宏。用

TupleDesc RelationNameGetTupleDesc(const char *relname)

基于一個名字獲取 TupleDesc,或者

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

一個基于一個類型 OID 獲取TupleDesc。 它可以用于為一個基礎(標量)類型或者復合(關系)類型獲取一個 TupleDesc。然後

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

將返回一個指向 AttInMetadata 的指針,基于給出的 TupleDesc 做了初始化。AttInMetadata 可以用于和 C 字串連接獲取一個合適格式化的行(內部叫元組)。

要返回一個元組,你必須創建一個基于 TupleDesc 的元組槽。你可以用

TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc)

來初始化這個元組槽,或者通過其它方法(用戶提供的)獲取一個。 這個元組槽是用來創建一個函數返回的 Datum 用的。 每次調用都可以(也應該)復用這個槽位。

在構造完一個 AttInMetadata 結構以後, 我們可以用

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

制作一個 HeapTuple,以 C 字串的形式給出用戶數據。 "values" 是一個 C 字串的數組,返回行的每個字段對應其中一個。 每個 C 字串都應該是字段數據類型的輸入函數預期的形式。為了從其中一個字段中返回一個空值, values 數組中對應的指針應該設置為 NULL。這個函數將會需要為你返回的每個元組調用一次。

通過 TupleDescGetAttInMetadataBuildTupleFromCStrings 制作一個元組只有在你的函數計算作為字串返回的數值的時候才比較方便。 如果你的代碼需要把值當作一個 Datum 的集合進行計算, 你應該使用下層的 heap_formtuple 過程把 Datum 直接轉換成一個元組。 你仍然需要 TupleDesc 和一個 TupleTableSlot, 但是不需要 AttInMetadata 了。

一旦你制作了一個從你的函數中返回的元組,那麼該元組必須轉換成一個 Datum。使用

TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple)

從一個給出的元組和一個槽位中獲取一個 Datum。 如果你想只返回一行,那麼這個 Datum 可以用于直接返回, 或者是它可以用作在一個返回集合的函數裡的當前返回值。

例子在下面給出。

33.7.9. 從 C 語言函數裡返回集合

還有一個特殊的 API 用于提供從 C 語言函數中返回集合(多行)的支持。 一個返回集合的函數必須遵循版本-1的調用方式。同樣,源代碼必須包含 funcapi.h,就像上面說的那樣。

一個返回集合的函數(SRF)通常為它返回的每個項都調用一次。 因此 SRF 必須保存足夠的狀態用于記住它正在做的事情以及在每次調用的時候返回下一個項。 表函數 API 提供了 FuncCallContext 結構用于幫助控制這個過程。 fcinfo->flinfo->fn_extra 用于保存一個跨越多次調用的指向 FuncCallContext 的指針。

typedef struct
{
    /*
     * 我們前面已經被調用的次數
     *
     * 初始的時候,call_cntr 被 SRF_FIRSTCALL_INIT() 置為裡 0,並且
     * 每次你調用 SRF_RETURN_NEXT() 的時候都遞增
     */
    uint32 call_cntr;

    /*
     * 可選的最大調用數量
     *
     * 這裡的 max_calls 只是為了方便,設置它也是可選的
     * 如果沒有設置,你必須提供可選的方法來知道函數何時結束
     * 
     */
    uint32 max_calls;

    /*
     * 指向結果槽位的可選指針
     *
     * 槽位是在返回元組的時候使用的(也就是說,返回復合數據類型)
     * 如果返回基本類型(也就是說,標量),是不需要的
     */
    TupleTableSlot *slot;

    /*
     * 可選的指向用戶提供的雜項環境信息的指針
     *
     * user_fctx 用做一個指向你自己的結構的指針,包含任意提供給你的函數的調用間的環境信息
     * 
     */
    void *user_fctx;

    /*
     * 可選的指向包含屬性類型輸入元信息的結構數組的指針
     * 
     *
     * attinmeta 用于在返回元組的時候(也就是說返回復合數據類型)
     * 在只返回基本(也就是標量)數據類型的時候並不需要。
     * 只有在你準備用 BuildTupleFromCStrings() 創建返回元組的時候才需要它
     * 
     */
    AttInMetadata *attinmeta;

    /*
     * 用于必須在多次調用間存活的結構的內存環境
     *
     * multi_call_memory_ctx 是由 SRF_FIRSTCALL_INIT() 為你設置的,並且由
     * SRF_RETURN_DONE() 用于清理。它是用于存放任何需要跨越多次調用 SRF 之間重復使用的內存
     * 
     * 
     */
    MemoryContext multi_call_memory_ctx;
} FuncCallContext;

一個 SRF 使用自動操作 FuncCallContext 結構(我們可以通過 fn_extra 找到它)的若幹個函數和宏。用

SRF_IS_FIRSTCALL()

來判斷你的函數是第一次調用還是後繼的調用。(只有)在第一次調用的時候,用

SRF_FIRSTCALL_INIT()

初始化 FuncCallContext。在每次函數調用時(包括第一次),使用

SRF_PERCALL_SETUP()

為使用 FuncCallContext 做恰當的設置以及清理任何前面的回合裡面剩下的已返回的數據。

如果你的函數有數據要返回,使用

SRF_RETURN_NEXT(funcctx, result)

返回給調用者。(result 必須是個 Datum, 要麼是單個值,要麼是象前面介紹的那樣準備的元組。)最後,如果你的函數結束了數據返回,使用

SRF_RETURN_DONE(funcctx)

清理並結束SRF

SRF 被調用的時候的內存環境是一個臨時的環境, 在調用之間將會被清理掉。這意味著你不需要 pfree 所有你 palloc 的東西;它會自動消失的。不過,如果你想分配任何跨越調用存在的數據結構, 那你就需要把它們放在其它什麼地方。被 multi_call_memory_ctx 引用的環境適合用于保存那些需要直到 SRF 結束前都存活的數據。 在大多數情況下,這意味著你在做第一次調用的設置的時候應該切換到 multi_call_memory_ctx

一個完整的偽代碼例子看起來像下面這樣:

Datum
my_Set_Returning_Function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    MemoryContext     oldcontext;
    還有更多的聲明

    if (SRF_IS_FIRSTCALL())
    {
        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* 這裡放出現一次的設置代碼:*/
        用戶定義代碼
        if 返回復合
            制作 TupleDesc,以及可能還有 AttInMetadata
            獲取槽位
            funcctx->slot = slot;
        endif 返回復合
        用戶定義代碼
        MemoryContextSwitchTo(oldcontext);
    }

    /* 每次都執行的設置代碼在這裡出現:*/
    用戶定義代碼
    funcctx = SRF_PERCALL_SETUP();
    用戶定義代碼

    /* 這裡只是用來測試我們是否完成的一個方法:*/
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* 這裡我們想返回另外一個條目:*/
         用戶代碼
         獲取結果 Datum
         SRF_RETURN_NEXT(funcctx, result);
     }
     else
     {
         /* 這裡我們完成返回條目的工作了,只需要清理就OK了:*/
         用戶代碼
         SRF_RETURN_DONE(funcctx);
     }
 }
 

一個返回復合類型的完整 SRF 例子看起來象這樣:

 PG_FUNCTION_INFO_V1(testpassbyval);

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

      /* 只是再第一次調用函數的時候幹的事情 */
      if (SRF_IS_FIRSTCALL())
      {
         MemoryContext oldcontext;

         /* 創建一個函數環境,用于在調用間保持住 */
         funcctx = SRF_FIRSTCALL_INIT();

         /* 切換到適合多次函數調用的內存環境 */
         oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

         /* 要返回的元組總數 */
         funcctx->max_calls = PG_GETARG_UINT32(0);

         /*
          * 為 __testpassbyval 元組制作一個元組描述
          */
         tupdesc = RelationNameGetTupleDesc("__testpassbyval");

         /* 用這個 tupdesc 為一個元組分配槽位 */
         slot = TupleDescGetSlot(tupdesc);

         /* 將槽位賦予給函數環境 */
         funcctx->slot = slot;

         /*
          * 生成稍後從裸 C 字串生成元組的屬性元數據
          * 
          */
         attinmeta = TupleDescGetAttInMetadata(tupdesc);
         funcctx->attinmeta = attinmeta;

         MemoryContextSwitchTo(oldcontext);
     }

     /* 每次函數調用都要做的事情 */
     funcctx = SRF_PERCALL_SETUP();

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

     if (call_cntr < max_calls)    /* 在還有需要發送的東西時繼續處理 */
     {
         char       **values;
         HeapTuple    tuple;
         Datum        result;

         /*
          * 準備一個數值數組用于在我們的槽位中存儲。
          * 它應該是一個 C 字串數組,稍後可以被合適的類型輸入函數處理。
          * 
          */
         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));

         /* 制作一個元組 */
         tuple = BuildTupleFromCStrings(attinmeta, values);

         /* 把元組做成 datum */
         result = TupleGetDatum(slot, tuple);

         /* 清理(這些實際上並非必要) */
         pfree(values[0]);
         pfree(values[1]);
         pfree(values[2]);
         pfree(values);

          SRF_RETURN_NEXT(funcctx, result);
     }
     else    /* 在沒有數據殘留的時候幹的事情 */
     {
          SRF_RETURN_DONE(funcctx);
     }
 }
 

下面是用于支持的 SQL 代碼

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

 CREATE OR REPLACE FUNCTION testpassbyval(integer, integer) RETURNS setof __testpassbyval
   AS 'filename','testpassbyval' LANGUAGE 'c' IMMUTABLE STRICT;
 

參閱源碼發布包裡的 contrib/tablefunc 獲取更多有關返回集合的函數的例子。

33.7.10. 多態參數和返回類型

C 語言函數可以聲明為接受和返回多態的類型 anyelementanyarray。 參閱 Section 33.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 是以 0 為基的。

比如,假設我們想寫一個函數接受任意類型的一個元素,並且返回該類型的一個一維數組:

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

    /* 獲取提供的元素 */
    element = PG_GETARG_DATUM(0);

    /* 我們的維數是 1 */
    ndims = 1;
    /* 有一個元素 */
    dims[0] = 1;
    /* 數組下界是 1*/
    lbs[0] = 1;

    /* 獲取有關元素類型需要的信息 */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* 然後制作數組 */
    result = construct_md_array(&element, 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 STRICT;

請注意使用 STRICT;這一點非常重要,因為代碼沒有認真測試空輸入。