3. 扩展开发入门

PostgreSQL 的扩展机制允许开发者把自定义函数、类型、操作符、索引访问方法等能力以“可安装模块”的形式挂载到数据库实例中。与直接修改内核源码相比,扩展开发更安全、更易分发,也更容易随版本升级而维护。本节从一个最小可用的 C 语言扩展出发,完整走通“写源码 → 编译成共享库 → 编写控制文件 → 安装到数据库 → 调用验证”的闭环,并重点解释 PG_MODULE_MAGIC、函数声明、PG_FUNCTION_INFO_V1 这三个几乎每个扩展都会出现的要素。

一、扩展的最小构成

一个可被 CREATE EXTENSION 安装的 PostgreSQL 扩展,通常至少包含三类文件:

  • C 源码文件:实现具体逻辑,例如 add_one.c,编译后生成共享库 add_one.so(Windows 下为 add_one.dll)。
  • 控制文件:add_one.control,描述扩展的默认版本、模块路径、注释等元信息,是 CREATE EXTENSION 的入口。
  • SQL 脚本文件:add_one--1.0.sql,声明扩展对外暴露的 SQL 对象,例如 CREATE FUNCTION。

这三者分工明确:C 代码负责“算”,控制文件负责“被识别”,SQL 脚本负责“被注册”。缺少任意一个,扩展都无法通过标准方式安装。

二、PG_MODULE_MAGIC:让服务器认识你的模块

PostgreSQL 在加载一个共享库时,会先检查库中是否存在一个名为 Pg_magic_func 的符号,并校验其返回的魔数结构是否与当前服务器 ABI 兼容。这个检查由宏 PG_MODULE_MAGIC 自动完成。它的作用是防止把为不同大版本、不同编译选项构建的扩展错误地加载进当前实例,从而避免内存布局不一致导致的崩溃。

因此,在每个扩展的 C 源文件中,必须且只能出现一次 PG_MODULE_MAGIC;,通常放在 #include "postgres.h" 之后、其他函数定义之前。它不产生运行时逻辑,但它是模块被服务器“接纳”的通行证。

三、函数声明与 PG_FUNCTION_INFO_V1

PostgreSQL 调用 C 函数时,并不直接使用普通的 C 调用约定,而是通过 Datum 作为统一的值载体,并借助 FunctionCallInfo 传递参数、空值标记和上下文。为了让服务器知道某个 C 函数应当以“版本 1”的调用约定被调用,需要在源码中使用 PG_FUNCTION_INFO_V1(funcname); 宏。它会为该函数生成一个 pg_finfo_funcname 符号,服务器在解析 CREATE FUNCTION 时据此确认调用接口。

与之配套的是函数定义形式:返回类型和参数类型都写成 Datum,并通过 PG_GETARG_*、PG_RETURN_* 系列宏完成取值与返回。下面是一个最小示例,实现整数加一:

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

PG_MODULE_MAGIC;

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_ARGS 展开为 FunctionCallInfo fcinfo,PG_GETARG_INT32(0) 取出第 0 个参数并转换为 int32,PG_RETURN_INT32 把结果包装成 Datum 返回。注意:PG_FUNCTION_INFO_V1 必须与函数定义在同一编译单元中,且函数名大小写要与 SQL 中声明的一致(PostgreSQL 默认会把未加引号的标识符折叠为小写)。

四、控制文件与 SQL 脚本

控制文件 add_one.control 内容非常简短:

comment = 'add one to an integer'
default_version = '1.0'
module_pathname = '$libdir/add_one'
relocatable = true

其中 module_pathname 指向共享库在安装目录中的位置,$libdir 由 PostgreSQL 在安装时替换为实际的库目录。relocatable = true 表示扩展对象可以随 ALTER EXTENSION ... SET SCHEMA 迁移。

SQL 脚本 add_one--1.0.sql 负责把 C 函数注册为数据库函数:

CREATE FUNCTION add_one(integer)
RETURNS integer
AS 'MODULE_PATHNAME', 'add_one'
LANGUAGE C STRICT IMMUTABLE;

这里的 'MODULE_PATHNAME' 会被控制文件中的 module_pathname 替换,第二个字符串 'add_one' 是共享库中导出的 C 符号名。STRICT 表示任一参数为 NULL 时函数不执行、直接返回 NULL;IMMUTABLE 表示相同输入永远得到相同输出,便于优化器做常量折叠。

五、编译与安装流程

最省事的方式是使用 PostgreSQL 提供的 PGXS 构建框架。在扩展目录下放置一个 Makefile:

MODULES = add_one
EXTENSION = add_one
DATA = add_one--1.0.sql

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

随后执行:

make
sudo make install

make 会调用与当前 pg_config 匹配的编译器和头文件,生成 add_one.so;make install 会把共享库复制到 $libdir,把控制文件和 SQL 脚本复制到 share/extension。安装完成后,在数据库中执行:

CREATE EXTENSION add_one;
SELECT add_one(41);

若返回 42,说明整条链路已经打通。若报错 could not open extension control file,通常是控制文件未安装到 share/extension;若报错 undefined symbol,则多半是 SQL 脚本中的 C 符号名与源码不一致,或共享库未正确安装。

PostgreSQL C 扩展:从源码到可调用函数 C 源码 add_one.c #include "postgres.h" #include "fmgr.h" PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(add_one); Datum add_one(PG_FUNCTION_ARGS) { PG_RETURN_INT32(...); } PGXS 编译 make make install → add_one.so 安装目标目录 $libdir/add_one.so share/extension/add_one.control share/extension/add_one--1.0.sql 控制文件 + SQL 脚本 + 共享库 三者缺一不可 数据库内注册与调用 CREATE EXTENSION add_one; 读取 control 文件 执行 SQL 脚本 CREATE FUNCTION AS 'MODULE_PATHNAME' 'add_one' LANGUAGE C 绑定 C 符号 SELECT add_one(41) → 42 验证扩展可用 STRICT / IMMUTABLE 生效

六、常见问题与判断依据

第一次写扩展时,最容易踩的坑集中在符号与路径上。可以用下面的对照快速定位:

现象可能原因处理方向
could not open extension control file控制文件未安装到 share/extension检查 make install 是否成功、PG_CONFIG 是否指向目标实例
undefined symbol: add_oneSQL 脚本中的 C 符号名与源码不一致核对 PG_FUNCTION_INFO_V1 与 AS 后第二个字符串
incompatible library version共享库与服务器 ABI 不匹配确认 PG_MODULE_MAGIC 存在,且用同一 pg_config 编译
函数返回 NULL 而非预期值声明了 STRICT 但传入了 NULL确认调用参数非空,或去掉 STRICT 并在函数内处理空值
实践建议:先用 add_one 这类纯计算函数跑通全流程,再逐步引入 PG_GETARG_TEXT_PP、PG_RETURN_TEXT_P 等处理可变长类型,最后再尝试返回集合、触发器函数或自定义类型。每增加一种参数类型,都要同步检查 SQL 声明与 C 宏是否匹配。

掌握这条最小链路后,后续无论是写性能监控函数、自定义聚合,还是实现索引访问方法,都只是在同一套骨架里替换函数体与 SQL 声明。真正需要反复确认的,始终是模块魔数、调用约定宏、符号名与安装路径这四件事。