9.5. C 語言函數

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

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

9.5.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 9.5.8 獲取更多資訊.

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

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

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

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

Table 9-1 列出了被裝載入PostgreSQL的 C 函數裡需要的當作參數的 C 類型。 "定義在" 列給出了等效的 C 類型定義的實際的頭文件 (實際的定義可能是在包含在列出的文件所包含的文件中. 我們建議使用者只使用這裡定義的接口.) 注意,你應該總是首先包括postgres.h, 因為它宣告了許多你需要的東西.

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

內建類型 C 類型 定義在
abstimeAbsoluteTimeutils/nabstime.h
boolboolinclude/c.h
box(BOX *)utils/geo_decls.h
bytea(bytea *)include/postgres.h
"char"charN/A
cidCIDinclude/postgres.h
datetime(DateTime *)include/c.h 或 include/postgres.h
int2int2 或 int16include/postgres.h
int2vector(int2vector *)include/postgres.h
integerinteger 或 int32include/postgres.h
float4(float4 *)include/c.h 或 include/postgres.h
double precision(double precision *)include/c.h 或 include/postgres.h
lseg(LSEG *)include/geo_decls.h
name(Name)include/postgres.h
oidoidinclude/postgres.h
oidvector(oidvector *)include/postgres.h
path(PATH *)utils/geo_decls.h
point(POINT *)utils/geo_decls.h
regprocregproc 或 REGPROCinclude/postgres.h
reltimeRelativeTimeutils/nabstime.h
text(text *)include/postgres.h
tidItemPointerstorage/itemptr.h
timespan(TimeSpan *)include/c.h 或 include/postgres.h
tintervalTimeIntervalutils/nabstime.h
xid(XID *)include/postgres.h

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 編譯器不會對陣列 下標金星範圍檢查.我們只需要分配足夠的空間,然後把陣列當做已經 宣告為合適長度的變數存取.(如果你不熟悉這個技巧,那麼你在開始 深入 PostgreSQL 伺服器編程之前可能需要 花點時間閱讀一些 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 表示附加尺寸是用於變長類型的 更好的風格.

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

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

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

下面是一些範例:

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

/* 傳遞數值 */
         
int
add_one(int arg)
{
    return arg + 1;
}

/* 傳遞參照,定長 */

double precision *
add_one_double precision(double precision *arg)
{
    double precision    *result = (double precision *) palloc(sizeof(double precision));

    *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 'PGROOT/tutorial/funcs' LANGUAGE C
     WITH (isStrict);

-- 注意:重載了名字為 add_one() 的 SQL 函數
CREATE FUNCTION add_one(float8) RETURNS float8
     AS 'PGROOT/tutorial/funcs',
        'add_one_float8'
     LANGUAGE C WITH (isStrict);

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'PGROOT/tutorial/funcs' LANGUAGE C
     WITH (isStrict);
                         
CREATE FUNCTION copytext(text) RETURNS text
     AS 'PGROOT/tutorial/funcs' LANGUAGE C
     WITH (isStrict);

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'PGROOT/tutorial/funcs' LANGUAGE C
     WITH (isStrict);

這裡的 PGROOT 代表 PostgreSQL 來源碼的全路徑. (更好的風格應該是在向搜尋路徑裡增加 PGROOT/tutorial 之後,在 AS 幾句裡只使用 'funcs', 不管怎樣,我們都可以省略和系統相關的共享庫擴展, 通常是 .so.sl.)

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

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

9.5.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()巨集傳回的.

下面是和上面一樣的函數,但是是用新風格編的:

#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_double precision(PG_FUNCTION_ARGS)
{
    /* 用於 FLOAT8 的巨集,隱藏其傳遞參照的本質 */
    double precision   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"(烤爐)(壓縮的或者超長的)值需要做的處理. 上面顯示的老風格的 copytextconcat_text函數在處理 toasted 的值的時候 實際上是錯的,因為它們在處理輸入時沒有調用 pg_detoast_datum(). (用於老風格動態裝載函數的句柄現在會處理這些細節, 不過與版本-1函數的所有可能性相比,它做的實在是不夠充分.)

版本-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指定.儲存類型是 plain,external,extended 或 main 之一.)

版本-1的函數調用風格也令我們可能傳回一"套" 結果並且實現觸發器函數和過程語言調用句柄.版本-1的代碼 也比版本-0的更容易移植,因為它沒有違反 ANSI C 對函數調用 協定的限制.更多的細節請參閱 源程式中的src/backend/utils/fmgr/README

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

復合類型不象 C 結構那樣有固定的布局. 復合類型的記錄可能包含空(null)域. 另外,一個屬於繼承層次一部分的復合類 型可能和同一繼承範疇的其他成員有不同的域/欄位. 因此, PostgreSQL 提供一個過程接口用於從 C 裡面存取復合類型.在 PostgreSQL 處理一個記錄集時, 每條記錄都將作為一個類型為 TUPLE(元組) 的不透明(opaque)的結構被傳遞給你的函數. 假設我們 為下面查詢寫一個函數

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

在上面的查詢裡,我們可以這樣定義c_overpaid

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

bool
c_overpaid(TupleTableSlot *t, /* the current row of EMP */
           int32 limit)
{
    bool isnull;
    int32 salary;

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

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

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() 巨集把它轉換成合適的資料類型.

下面的命令讓 PostgreSQL 知道c_overpaid函數:

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

9.5.6. 表函數 API

表函數 API 幫助我們建立使用者定義的 C 語言表函數(Section 9.7)。 表函數都是生成一個行集合的函數,這些行要麼是由基本(標量)資料類型, 要麼是由復合(多欄位)資料類型組成。API 分隔為兩個主要部件: 支援傳回復合資料類型的,以及支援傳回多行的(傳回集合的函數或者SRF (set returning function))。

表函數 API 巨集和函數來消除大多數制作復合資料類型和傳回多個結果的復雜性。 一個表函數必須遵循上面描述的版本-1的調用規則。另外,源文件必須包含:

#include "funcapi.h"

9.5.6.1. 傳回行(復合類型)

支援傳回復合資料類型(或者行)的表函數 API 是從 AttInMetadata 結構開始的。這個結構保存著需要從一個□的 C 字串裡建立一行的獨立欄位資訊的 陣列。它還保存一個指向 TupleDesc 的指針。這裡裝載的 資訊是源自 TupleDesc 的,它儲存在這裡是為了避免 每次調用表函數的額外的 CPU 開銷。如果是一個傳回集合的函數, 那麼在第一次調用函數的時候應該計算 AttInMetadata 結構一次, 然後保存起來為後面使用。

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 用的。 每次調用都可以(也應該)復用這個槽位。

After constructing an AttInMetadata structure, 在構造完一個 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 可以用於直接傳回, 或者是它可以用作在一個傳回集合的函數裡的目前傳回值。

範例在下面給出。

9.5.6.2. 傳回集合

一個傳回集合的函數(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;
    [user defined declarations]

    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)
    {
        /* 這裡我們想傳回另外一個條目:*/
         [user defined code]
         [obtain result Datum]
         SRF_RETURN_NEXT(funcctx, result);
     }
     else
     {
         /* 這裡我們完成傳回條目的工作了,只需要清理就OK了:*/
         [user defined code]
         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 字串陣列,稍後可以被合適的“in”函數處理。
          * 
          */
         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 int4, f2 int4, f3 int4);

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

參閱 contrib/tablefunc 獲取更多有關表函數的範例。

9.5.7. 書寫代碼

我們現在前往了書寫編程語言函數的更艱難的階段. 要注意:本手冊此章的內容不會讓你成為程式員. 在你嘗試用C 書寫用於 PostgreSQL 的函數之前,你必須對 C 有很深的了解(包括對指針的使用). 雖然可以用 C 以外的其他語 言如 FORTRANPascal 書寫用於PostgreSQL 的共享函數,但通常很麻煩(雖然是完全可能的),因為其他語言並不遵循和 C 一樣的 調用習慣. 其他語言與C的傳遞參數和傳回值的方式不一樣. 因此我們假設你的編程語言函數是用 C 寫的.

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

9.5.8. 編譯和連結動態連結的函數

在你能夠使用由 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 9.5.1 獲取有關伺服器 預期在哪裡找到共享庫的資訊.