PostgreSQL C 扩展开发实战

从零开发 PostgreSQL C 扩展:control 文件与版本化 SQL 脚本、PG_MODULE_MAGIC 与 _PG_init、PG_FUNCTION_INFO_V1 与 Datum 调用约定、PG_GETARG 与 PG_RETURN 宏、text 与 bytea 与数组类型处理、palloc 与内存上下文、ereport 与 PG_TRY 错误处理、SPI 执行 SQL、自定义类型与操作符类、PGXS 构建、REGRESS 回归测试与版本兼容。

PostgreSQL 的可扩展性不是口号——pg_stat_statements、pg_trgm、PostGIS、pgvector 全都是扩展。扩展能在不修改内核的前提下,向 SQL 层注册函数、类型、操作符、索引访问方法乃至 FDW。当某个计算用 SQL 写太慢、或需要访问内核内部结构时,C 扩展就是最终答案。

核心认知:扩展是「共享库 + 元数据脚本」的组合。共享库导出 C 符号,SQL 脚本把符号注册成数据库对象,二者通过 PG_FUNCTION_INFO_V1 的符号命名约定绑定。


一、扩展骨架

1.1 三个组成部分

一个扩展由三部分组成:mylib.control(扩展元数据)、mylib--1.0.sql(安装脚本,定义 SQL 可见对象)、mylib.so(编译产物,PGXS 自动生成)。升级时再加 mylib--1.0--1.1.sql,ALTER EXTENSION mylib UPDATE 会按序执行。安装路径由 pg_config 给出:--sharedir(control 与 sql 脚本)、--pkglibdir(.so)、--pgxs(PGXS 入口)。

1.2 control 文件

# mylib.control
comment = '字符串相似度计算扩展'
default_version = '1.0'
module_pathname = '$libdir/mylib'
relocatable = true
superuser = false

module_pathname 是 SQL 脚本里 AS 'MODULE_PATHNAME' 的替换值;relocatable 决定能否 ALTER EXTENSION ... SET SCHEMA;superuser = false 表示普通用户也能安装(库须在可信路径)。

1.3 版本化 SQL 脚本

-- mylib--1.0.sql
\echo Use "CREATE EXTENSION mylib" to load this file. \quit
CREATE FUNCTION mylib.similarity(text, text)
RETURNS float8 AS 'MODULE_PATHNAME', 'mylib_similarity'
LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;

AS 的第二个参数是 C 里注册的函数名。STRICT 表示任一参数为 NULL 就不进 C 层;IMMUTABLE 与 PARALLEL SAFE 让规划器能做常量折叠与并行执行。

1.4 PG_MODULE_MAGIC 与 _PG_init

#include "postgres.h"
#include "fmgr.h"

PG_MODULE_MAGIC;   /* 必须,校验编译期与运行期 ABI 一致 */

void _PG_init(void);   /* 模块加载时调用一次 */

void
_PG_init(void)
{
    DefineCustomIntVariable("mylib.threshold", "相似度阈值", NULL,
                            &mylib_threshold, 80, 0, 100, PGC_USERSET, 0,
                            NULL, NULL, NULL);
    MarkGUCPrefixReserved("mylib");
}

缺 PG_MODULE_MAGIC 会直接报 incompatible library。_PG_init 是注册 GUC、初始化全局缓存的入口。


二、C 函数基础

2.1 PG_FUNCTION_INFO_V1 与调用约定

PG_FUNCTION_INFO_V1(mylib_similarity);

Datum
mylib_similarity(PG_FUNCTION_ARGS)
{
    /* 函数体 */
}

该宏生成一个 pg_finfo_mylib_similarity 符号,PostgreSQL 通过它找到函数并确认调用约定版本。没有这一行,函数无法被调用。C 函数只有两种签名:Datum f(PG_FUNCTION_ARGS) 或 Datum f(FunctionCallInfo fcinfo);参数与返回值都通过 Datum(一个能容纳指针或整数的机器字)传递。PG_FUNCTION_ARGS 展开为 FunctionCallInfo fcinfo,取值用 fcinfo->args[n].value、判空用 fcinfo->args[n].isnull、个数用 fcinfo->nargs。

2.2 PG_GETARG 与 PG_RETURN 宏

参数用 PG_GETARG_<类型>(n) 取出,返回值用 PG_RETURN_<类型>(v),例如 int32 a = PG_GETARG_INT32(0); ... PG_RETURN_INT32(a + b);。

PG_GETARG_INT32(n)    PG_RETURN_INT32(v)     PG_GETARG_FLOAT8(n)  PG_RETURN_FLOAT8(v)
PG_GETARG_INT64(n)    PG_RETURN_INT64(v)     PG_GETARG_BOOL(n)    PG_RETURN_BOOL(v)
PG_GETARG_TEXT_PP(n)  PG_RETURN_TEXT_P(v)    PG_ARGISNULL(n)      PG_RETURN_NULL()

_PP 后缀表示「解压后的 varlena 指针」,它可能返回临时副本,只应读取不应修改。


三、类型处理

3.1 text 与 varchar

Datum
string_length(PG_FUNCTION_ARGS)
{
    text *t = PG_GETARG_TEXT_PP(0);
    char *data = VARDATA_ANY(t);          /* 数据指针 */
    int   len  = VARSIZE_ANY_EXHDR(t);    /* 不含头部的长度 */
    PG_RETURN_INT32(len);
}

构造返回的 text 用 cstring_to_text:PG_RETURN_TEXT_P(cstring_to_text(psprintf("hi %s", name)));

3.2 bytea

Datum
xor_bytes(PG_FUNCTION_ARGS)
{
    bytea *a = PG_GETARG_BYTEA_PP(0), *b = PG_GETARG_BYTEA_PP(1);
    int n = Min(VARSIZE_ANY_EXHDR(a), VARSIZE_ANY_EXHDR(b));
    bytea *out = (bytea *) palloc(VARHDRSZ + n);
    SET_VARSIZE(out, VARHDRSZ + n);

    unsigned char *pa = (unsigned char *) VARDATA_ANY(a);
    unsigned char *pb = (unsigned char *) VARDATA_ANY(b);
    unsigned char *po = (unsigned char *) VARDATA(out);
    for (int i = 0; i < n; i++) po[i] = pa[i] ^ pb[i];
    PG_RETURN_BYTEA_P(out);
}

VARHDRSZ 是 varlena 头部长度(4 字节),分配与 SET_VARSIZE 都必须算上它。

3.3 array

#include "utils/array.h"
#include "utils/lsyscache.h"

Datum
array_sum_int(PG_FUNCTION_ARGS)
{
    ArrayType *arr = PG_GETARG_ARRAYTYPE_P(0);
    int16 typlen; bool typbyval; char typalign;
    Datum *elems; bool *nulls; int nelems;
    int64 sum = 0;

    get_typlenbyvalalign(INT4OID, &typlen, &typbyval, &typalign);
    deconstruct_array(arr, INT4OID, typlen, typbyval, typalign,
                      &elems, &nulls, &nelems);
    for (int i = 0; i < nelems; i++)
        if (!nulls[i]) sum += DatumGetInt32(elems[i]);
    PG_RETURN_INT64(sum);
}

deconstruct_array 把数组拆成 Datum 指针加 null 标志数组,是处理数组的标准入口。VARIADIC text[] 参数在 C 层同样是普通数组;多态类型需用 get_fn_expr_argtype(fcinfo->flinfo, 0) 取实际类型 OID 再分派。


四、内存管理

4.1 palloc 与 pfree

char *buf  = palloc(1024);         /* 当前内存上下文中分配 */
char *zero = palloc0(1024);        /* 分配并清零 */
char *big  = repalloc(buf, 4096);  /* 扩容 */
pfree(buf);                        /* 显式释放 */

不要用 malloc/free:palloc 的内存绑定内存上下文,函数返回时自动回收,内存不足时抛 ERROR 而非返回 NULL。混用会导致泄漏或崩溃。

4.2 内存上下文

MemoryContext oldcxt = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
MyStruct *s = palloc(sizeof(MyStruct));   /* 生存期与本次调用一致 */
MemoryContextSwitchTo(oldcxt);

跨调用存活的数据要放进长生命周期上下文,例如 AllocSetContextCreate(TopMemoryContext, "mylib cache", ALLOCSET_DEFAULT_SIZES) 创建的静态上下文。


五、错误处理

5.1 ereport 与 elog

if (len < 0)
    ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                    errmsg("长度不能为负:%d", len),
                    errhint("请传入非负整数")));
elog(DEBUG1, "processing %d rows", n);
elog(WARNING, "threshold %d out of range, clamped", th);

ereport 是结构化错误报告,ERROR 级别终止当前事务,由 PostgreSQL 的长跳转实现,C 层无需手动清理 palloc 内存——上下文会被自动重置。

5.2 PG_TRY 与 PG_CATCH

只在需要清理外部资源(文件描述符、锁)时使用:

MemoryContext oldcxt = CurrentMemoryContext;
PG_TRY();
{
    risky_operation();               /* 可能抛错 */
}
PG_CATCH();
{
    MemoryContextSwitchTo(oldcxt);   /* 必须先切回安全上下文 */
    ErrorData *edata = CopyErrorData();
    FlushErrorState();
    close(fd);                       /* 清理外部资源 */
    ereport(ERROR, (errcode(edata->sqlerrcode), errmsg("%s", edata->message)));
}
PG_END_TRY();

PG_CATCH 里必须先切回安全上下文,否则后续 palloc 会分配在即将被销毁的错误上下文中。


六、SPI 执行 SQL

SPI(Server Programming Interface)让 C 函数执行 SQL。

#include "executor/spi.h"

Datum
count_rows(PG_FUNCTION_ARGS)
{
    char *tbl = text_to_cstring(PG_GETARG_TEXT_PP(0));
    int ret, nrows; int64 result;

    if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed");
    ret = SPI_execute(psprintf("SELECT count(*) FROM %s", quote_identifier(tbl)),
                      true /* read_only */, 0);
    if (ret != SPI_OK_SELECT)
        elog(ERROR, "query failed: %s", SPI_result_code_string(ret));

    nrows  = SPI_processed;
    result = (nrows > 0) ? DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0],
                                 SPI_tuptable->tupdesc, 1, NULL)) : 0;
    SPI_finish();
    PG_RETURN_INT64(result);
}

要点:SPI_connect 与 SPI_finish 必须配对;从 SPI_tuptable 取出的 Datum 在 SPI_finish 后失效,需深拷贝;标识符用 quote_identifier,值用 SPI_execute_with_args 参数化;read_only = false 时才能执行 INSERT 或 UPDATE。


七、自定义类型与操作符类

自定义类型需定义输入输出函数,再用 CREATE TYPE 注册:

CREATE TYPE mylib.counter;
CREATE FUNCTION mylib.counter_in(cstring) RETURNS mylib.counter
    AS 'MODULE_PATHNAME', 'counter_in' LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION mylib.counter_out(mylib.counter) RETURNS cstring
    AS 'MODULE_PATHNAME', 'counter_out' LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE mylib.counter (
    INPUT = mylib.counter_in, OUTPUT = mylib.counter_out,
    INTERNALLENGTH = 8, PASSEDBYVALUE, ALIGNMENT = double
);

要让类型支持 B-tree 索引,需实现比较函数并定义操作符类。FUNCTION 1 是 B-tree 的必需支撑函数,返回 int 表示大小关系:

CREATE OPERATOR CLASS mylib.counter_ops
    DEFAULT FOR TYPE mylib.counter USING btree AS
    OPERATOR 1 <, OPERATOR 3 =, OPERATOR 5 >,
    FUNCTION 1 mylib.counter_cmp(mylib.counter, mylib.counter);

八、构建与测试

8.1 PGXS Makefile

MODULE_big = mylib
OBJS = src/mylib.o src/counter.o
EXTENSION = mylib
DATA = mylib--1.0.sql
REGRESS = basic counter

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

8.2 回归测试

PGXS 的 REGRESS 目标执行 sql/<name>.sql 并把输出与 expected/<name>.out 比对,用 make installcheck 触发(需要运行中的实例)。

-- sql/basic.sql
CREATE EXTENSION mylib;
SELECT mylib.similarity('hello', 'hallo');
DROP EXTENSION mylib;

8.3 版本兼容

#if PG_VERSION_NUM >= 150000
    value = MakeExpandedObjectReadOnly(...);   /* PG15 及以上 */
#else
    value = ...;
#endif

PG_VERSION_NUM 如 160000 表示 16.0;跨大版本时优先用稳定的 PG_GETARG_* 宏,避免直接依赖内部结构体字段。


常见问题(FAQ)

扩展加载报 incompatible library 怎么排查

九成是 PG_MODULE_MAGIC 缺失,或 .so 由不同版本的 pg_config 编译。先用 pg_config --version 确认编译头文件版本与运行实例一致,再检查源码是否包含 PG_MODULE_MAGIC。用 nm -D mylib.so | grep Pg_magic_func 可确认该符号是否存在。

为什么不能用 malloc 分配返回给 SQL 的内存

返回值在函数返回后仍被上层使用,而 malloc 的内存不在内存上下文中,PostgreSQL 无法回收,会造成泄漏。更严重的是 ereport(ERROR) 的长跳转会绕过 free,导致永久泄漏。一律用 palloc 系列。

STRICT 与 IMMUTABLE 该不该加

STRICT 建议加,它让 NULL 参数直接返回 NULL,省去 C 层判空。IMMUTABLE 只在函数对相同输入永远返回相同结果、且不读数据库时才能加;误加会让规划器错误地常量折叠。读表的函数应标 STABLE 或 VOLATILE。

如何调试 C 扩展

用 gdb 附加到后端进程:先 SELECT pg_backend_pid() 拿到 PID,再 gdb -p <pid>,设断点后从客户端触发函数。编译时加 -g -O0(make PG_CFLAGS="-g -O0")。也可用 elog(DEBUG1, ...) 配合 client_min_messages = debug1 输出中间状态。


相关阅读

延伸阅读


完整示例(一键复制)

# ========== Makefile ==========
MODULE_big = mylib
OBJS = mylib.o
EXTENSION = mylib
DATA = mylib--1.0.sql
REGRESS = basic

PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
# ========== mylib.control ==========
comment = '字符串相似度计算扩展'
default_version = '1.0'
module_pathname = '$libdir/mylib'
relocatable = true
superuser = false
/* ========== mylib.c ========== */
#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(mylib_similarity);

Datum
mylib_similarity(PG_FUNCTION_ARGS)
{
    text *ta = PG_GETARG_TEXT_PP(0), *tb = PG_GETARG_TEXT_PP(1);
    const char *a = VARDATA_ANY(ta), *b = VARDATA_ANY(tb);
    int la = VARSIZE_ANY_EXHDR(ta), lb = VARSIZE_ANY_EXHDR(tb);
    int n = Min(la, lb), i = 0, mx = Max(la, lb);

    while (i < n && a[i] == b[i]) i++;
    if (mx == 0) PG_RETURN_FLOAT8(1.0);
    PG_RETURN_FLOAT8((double) i / (double) mx);
}
-- ========== 注册并测试 ==========
CREATE EXTENSION mylib;
SELECT mylib.similarity('hello', 'hallo');
# ========== 构建 ==========
make && sudo make install && make installcheck

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询