31.9. C 語言函數

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

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

31.9.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 31.9.6 獲取更多訊息。

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

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

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

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

Table 31-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

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

31.9.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的方法則解決了這些問題。

31.9.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 31.9.10)並且實現觸發器函數(Chapter 32)和過程語言調用句柄(Chapter 45)。 版本-1 的代碼也比版本-0的更容易移植,因為它沒有違反 C 標準對函數調用協議的限制。更多的細節請參閱 源程序中的src/backend/utils/fmgr/README

31.9.5. 書寫代碼

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

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

31.9.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 31.9.1 獲取有關伺服器 預期在哪裡找到共享庫的訊息。

31.9.7. 擴展的製作架構

如果您在考慮發佈您的PostgreSQL擴展模塊,那麼給他們設置一個可移植的製作系統可能會相當困難。 因此PostgreSQL安裝提供了一個用於擴展的製作架構,叫做 PGXS, 這樣,簡單的擴展模塊可以在一個已經安裝了的伺服器上製作了。 請注意這個架構並不是企圖用於實現一個統一的、可以用於製作所有與PostgreSQL相關的軟件的架構; 它只是用於自動化那些簡單的伺服器擴展模塊的製作。對於更複雜的包,您還是需要書寫自己的製作系統。

要在您的擴展中使用該架構,您必須寫一個簡單的 makefile。 在該makefile裡,您需要設置一些變量並且最後包括全局的 PGXS makefile。 下面是一個製作一個包含一個共享庫,一個 SQL 腳本,和一個文件文本文件的叫做 isbn_issn 的例子:

MODULES = isbn_issn
DATA_built = isbn_issn.sql
DOCS = README.isbn_issn

PGXS := $(shell pg_config --pgxs)
include $(PGXS)

最後兩行應該總是一樣的。在文件的前面,您賦予變量或者增加客戶化的 make 規則。

可以設置下列變量:

MODULES

一個需要從同個根的原始碼上製作的共享對象的列資料表(不要在這個列資料表裡包含後綴)

DATA

安裝到 prefix/share/contrib 的隨機文件

DATA_built

需要先製作的,安裝到 prefix/share/contrib 裡面的隨機文件。

DOCS

安裝到 prefix/doc/contrib 裡面的隨機文件

SCRIPTS

安裝到 prefix/bin 裡面的腳本文件(非二進制)

SCRIPTS_built

安裝到 prefix/bin 裡面的,需要先製作的腳本文件(非二進制)。

REGRESS

回歸測試案例的列資料表(沒有後綴)

或者最多聲明下面兩個之一:

PROGRAM

一個需要製作的二進制文件(在 OBJS 裡面列出目標文件)

MODULE_big

一個需要製作的共享對像(在 OBJS 裡列出目標文件)

還可以設置下列變量:

EXTRA_CLEAN

make clean 裡刪除的額外的文件

PG_CPPFLAGS

將增加到 CPPFLAGS

PG_LIBS

將增加到 PROGRAM 鏈接行裡

SHLIB_LINK

將增加到 MODULE_big 連接行裡

把這個 makefile 以 Makefile 的名字放在保存您的擴展的目錄裡。 然後您就可以執行 make 來編譯,然後用 make install 安裝您的模塊。 這個擴展是為 pg_config 命令在您的路徑裡找到的第一個 PostgreSQL 安裝編譯和安裝的。

31.9.8. 復合類型的 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(HeapTupleHeader 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)
{
    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);
    /* 另外,我們可能更希望將 PG_RETURN_NULL() 用在空薪水上 */

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByNamePostgreSQL 系統函數, 用來返回目前記錄的字串。它有三個參數:類型為 HeapTupleHeader 的傳入函數的參數,您想要的字串名稱, 以及一個用以確定字串是否為空(null)的返回參數。 GetAttributeByName 函數返回一個Datum值, 您可以用對應的 DatumGetXXX() 宏把它轉換成合適的資料類型。 請注意,如果設置了空標誌,那麼返回值是無意義的, 在準備對結果做任何處理之前,總是要先檢查空標誌。

還有一個 GetAttributeByNum,它用字串編號而不是字串名選取目標字串。

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

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

請注意我們使用了 STRICT,這樣我們就不需要檢查輸入參數是否有 NULL。

31.9.9. 從 C 函數里返回行(復合類型)

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

#include "funcapi.h"

製作一個復合類型資料值(也就是一個"資料")有兩種方法: 您可以從一個 Datum 值數組裡製作,也可以從一個可以傳遞給該資料的字串類型的輸入轉換函數的 C 字串數組裡製作。 不管是哪種方式,您首先都需要為資料結構獲取或者製作一個 TupleDesc 描述符。 在使用 Datum 的時候,您給 BlessTupleDesc 傳遞這個 TupleDesc 然後為每行調用 heap_formtuple。 在使用 C 字串的時候,您給 TupleDescGetAttInMetadata 傳遞 TupleDesc, 然後為每行調用 BuildTupleFromCStrings。 如果是一個函數返回一個資料集合的場合,所有設置步驟都可以在第一次調用該函數的時候一次性完成。

有幾個簡便函數可以幫助您設置初始的 TupleDesc。 如果您想使用命名的復合類型,您可以從系統資料表裡獲取訊息。用

TupleDesc RelationNameGetTupleDesc(const char *relname)

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

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

一個基於一個類型 OID 獲取TupleDesc。 它可以用於為一個基礎(標量)類型或者復合(關係)類型獲取一個 TupleDesc。 在書寫一個返回 record 的函數的時候,那麼預期的 TupleDesc 必須由調用者傳遞進來。

一旦您有了一個 TupleDesc,如果您想使用 Datum,那麼調用

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

如果您想用 C 字串,那麼調用

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

如果您在寫一個返回集合的函數,那麼您可以您這些函數的結果保存在 FuncCallContext 結構裡 — 分別使用 tuple_desc 或者 attinmeta 字串。

在使用 Datum 的時候,使用

HeapTuple heap_formtuple(TupleDesc tupdesc, Datum *values, char *nulls)

製作一個 HeapTuple,它把資料以 Datum 的形式交給用戶。

在使用 C 字串的時候,用

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

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

一旦您製作了一個從您的函數中返回的資料,那麼該資料必須轉換成一個 Datum。使用

HeapTupleGetDatum(HeapTuple tuple)

把一個 HeapTuple 轉換為一個有效的 Datum。 如果您想只返回一行,那麼這個 Datum 可以用於直接返回, 或者是它可以用作在一個返回集合的函數里的目前返回值。

例子在下面給出。

31.9.10. 從 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;

    /*
     * 指向結果槽位的可選指針
     *
     * 這個資料類型已經過時,只用於向下兼容。也就是那些使用
     * 廢棄的 TupleDescGetSlot() 的用戶定義 SRF
     */
    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;

    /*
     * 可選的指針,指向包含資料描述的結構
     *
     * tuple_desc 用於返回資料(也就是說復合資料類型)
     * 並且只是在您想使用 heap_formtuple() 而不是  BuildTupleFromCStrings() 製作資料的
     * 時候需要。請注意這裡儲存的 TupleDesc 指針通常應該先用heap_formtuple()
     * BlessTupleDesc() 處理。
     */
    TupleDesc tuple_desc;

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

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

         MemoryContextSwitchTo(oldcontext);
     }

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

     call_cntr = funcctx->call_cntr;
     max_calls = funcctx->max_calls;
     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 = HeapTupleGetDatum(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 獲取更多有關返回集合的函數的例子。

31.9.11. 多態參數和返回類型

C 語言函數可以聲明為接受和返回多態的類型 anyelementanyarray。 參閱 Section 31.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;這一點非常重要,因為代碼沒有認真測試空輸入。