八、返回数据集
C语言函数有两种返回数据集(多行数据)的方法:一种称为 ValuePerCall 方式,重复调用返回数据集的函数(每次调用传递相同的参数),每次调用返回一行,最后返回NULL表示没有更多的行。每次调用时,返回数据集的函数(SRF)必须保存足够多的状态以在整个调用中记住该做什么并且返回正确的下一条目。另一种称为 Materialize 方式,SRF填充并返回包含全部结果的tuplestore对象;一次调用获取全部结果,也无需保存内部调用状态。
在使用ValuePerCall模式时,需要注意查询可能不会执行完成;也就是说,由于LIMIT等选项,执行器可能会在获取完所有行之前停止调用返回值函数。这意味着在最后一个调用中执行清理操作是不安全的,因为这可能永远不会发生。对于需要访问外部资源(例如文件描述符)的函数,建议使用Materialize模式。
本节剩余部分介绍了一套常用助手宏(虽然不需要使用),这些宏用于ValuePerCall模式下的SRF(可视化数据查询)。有关Materialize模式的详细信息,请参见src/backend/utils/fmgr/README
。此外,PostgreSQL源代码中的contrib模块包含许多使用ValuePerCall和Materialize模式的SRF示例。
要使用此处描述的ValuePerCall支持宏,请包括funcapi.h。这些宏与一个结构FuncCallContext配合使用,该结构包含跨调用需要保存的状态信息。在调用SRF中,fcinfo->flinfo->fn_extra
用于在调用之间保持指向FuncCallContext的指针。宏会在第一次使用时自动填充该字段,并在后续使用时预期找到相同的指针。
typedef struct FuncCallContext { /* * Number of times we've been called before * * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and * incremented for you every time SRF_RETURN_NEXT() is called. */ uint64 call_cntr; /* * OPTIONAL maximum number of calls * * max_calls is here for convenience only and setting it is optional. * If not set, you must provide alternative means to know when the * function is done. */ uint64 max_calls; /* * OPTIONAL pointer to miscellaneous user-provided context information * * user_fctx is for use as a pointer to your own data to retain * arbitrary context information between calls of your function. */ void *user_fctx; /* * OPTIONAL pointer to struct containing attribute type input metadata * * attinmeta is for use when returning tuples (i.e., composite data types) * and is not used when returning base data types. It is only needed * if you intend to use BuildTupleFromCStrings() to create the return * tuple. */ AttInMetadata *attinmeta; /* * memory context used for structures that must live for multiple calls * * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory * context for any memory that is to be reused across multiple calls * of the SRF. */ MemoryContext multi_call_memory_ctx; /* * OPTIONAL pointer to struct containing tuple description * * tuple_desc is for use when returning tuples (i.e., composite data types) * and is only needed if you are going to build the tuples with * heap_form_tuple() rather than with BuildTupleFromCStrings(). Note that * the TupleDesc pointer stored here should usually have been run through * BlessTupleDesc() first. */ TupleDesc tuple_desc; } FuncCallContext;
使用此基础设施的SRF将使用的宏如下:
SRF_IS_FIRSTCALL() : 用于确定您的函数是首次被调用还是后续调用。在第一次调用(仅限于第一次)中,调用:
SRF_FIRSTCALL_INIT() 初始化FuncCallContext。 在每次函数调用中,包括第一次调用,请调用:
SRF_PERCALL_SETUP() 为使用FuncCallContext做好准备。
如果您的函数在当前调用中需要返回数据,则使用:
SRF_RETURN_NEXT(funcctx, result) 将其返回给调用者。(result 必须是 Datum 类型,可以是一个单个值或像上面描述的那样预处理的元组。)最后,当您的函数完成返回数据时,请使用:
SRF_RETURN_DONE(funcctx) 清理并结束 SRF。
当 SRF 被调用时,当前的内存上下文是一个短暂的上下文,将在每次调用之间被清除。这意味着您不需要为使用 palloc 分配的所有内容调用 pfree;无论如何它都会消失。但是,如果您想分配任何跨越调用的数据结构,则需要将其放在其他地方。multi_call_memory_ctx 引用的内存上下文是任何需要在 SRF 完成运行之前生存的数据的合适位置。 在大多数情况下,这意味着您应该在进行第一次调用设置时切换到 multi_call_memory_ctx。 使用 funcctx->user_fctx 存储指向任何跨调用数据结构的指针。(您在 multi_call_memory_ctx 中分配的数据将在查询结束时自动消失,因此无需手动释放这些数据。)
一个完整的伪代码示例如下:
Datum my_set_returning_function(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; Datum result; further declarations as needed if (SRF_IS_FIRSTCALL()) { MemoryContext oldcontext; funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* One-time setup code appears here: */ user code if returning composite build TupleDesc, and perhaps AttInMetadata endif returning composite user code MemoryContextSwitchTo(oldcontext); } /* Each-time setup code appears here: */ user code funcctx = SRF_PERCALL_SETUP(); user code /* this is just one way we might test whether we are done: */ if (funcctx->call_cntr < funcctx->max_calls) { /* Here we want to return another item: */ user code obtain result Datum SRF_RETURN_NEXT(funcctx, result); } else { /* Here we are done returning items, so just report that fact. */ /* (Resist the temptation to put cleanup code here.) */ SRF_RETURN_DONE(funcctx); } }
一个简单SRF返回复合类型的完整示例如下:
PG_FUNCTION_INFO_V1(retcomposite); Datum retcomposite(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; int call_cntr; int max_calls; TupleDesc tupdesc; AttInMetadata *attinmeta; /* stuff done only on the first call of the function */ if (SRF_IS_FIRSTCALL()) { MemoryContext oldcontext; /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); /* switch to memory context appropriate for multiple function calls */ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* total number of tuples to be returned */ funcctx->max_calls = PG_GETARG_INT32(0); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("function returning record called in context " "that cannot accept type record"))); /* * generate attribute metadata needed later to produce tuples from raw * C strings */ attinmeta = TupleDescGetAttInMetadata(tupdesc); funcctx->attinmeta = attinmeta; MemoryContextSwitchTo(oldcontext); } /* stuff done on every call of the function */ funcctx = SRF_PERCALL_SETUP(); call_cntr = funcctx->call_cntr; max_calls = funcctx->max_calls; attinmeta = funcctx->attinmeta; if (call_cntr < max_calls) /* do when there is more left to send */ { char **values; HeapTuple tuple; Datum result; /* * Prepare a values array for building the returned tuple. * This should be an array of C strings which will * be processed later by the type input functions. */ 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)); /* build a tuple */ tuple = BuildTupleFromCStrings(attinmeta, values); /* make the tuple into a datum */ result = HeapTupleGetDatum(tuple); /* clean up (this is not really necessary) */ pfree(values[0]); pfree(values[1]); pfree(values[2]); pfree(values); SRF_RETURN_NEXT(funcctx, result); } else /* do when there is no more left */ { SRF_RETURN_DONE(funcctx); } }
在SQL中声明此函数的一种方法是:
CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer); CREATE OR REPLACE FUNCTION retcomposite(integer, integer) RETURNS SETOF __retcomposite AS 'filename', 'retcomposite' LANGUAGE C IMMUTABLE STRICT;
另一种方法是使用OUT参数:
CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer, OUT f1 integer, OUT f2 integer, OUT f3 integer) RETURNS SETOF record AS 'filename', 'retcomposite' LANGUAGE C IMMUTABLE STRICT;
需要注意的是,这种方法下函数的输出类型在形式上是一个匿名record
类型。
九、多态参数和返回类型
C语言函数可以声明接受和返回第38.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
是零基的。get_call_result_type
也可作为get_fn_expr_rettype
的替代方案使用。还有一种 get_fn_expr_variadic
,它可以用于找出是否已将变长参数合并到数组中。这对于 VARIADIC "any"
函数非常有用,因为对于接受普通数组类型的变长函数,这种合并总是会发生的。
例如,假设我们要编写一个接受任何类型单个元素的函数,并返回该类型的单维数组:
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; bool isnull; 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"); /* get the provided element, being careful in case it's NULL */ isnull = PG_ARGISNULL(0); if (isnull) element = (Datum) 0; else element = PG_GETARG_DATUM(0); /* we have one dimension */ ndims = 1; /* and one element */ dims[0] = 1; /* and lower bound is 1 */ lbs[0] = 1; /* get required info about the element type */ get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); /* now build the array */ result = construct_md_array(&element, &isnull, 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 IMMUTABLE;
有一种仅限于 C 语言函数的多态类型变体:它们可以声明接受名为"any"
的类型的参数。(请注意,此类型名称必须用双引号括起来,因为它也是一个 SQL 保留字。)这类似于 anyelement
,除了它不会约束不同的 "any"
参数具有相同的类型,也无法帮助确定函数的结果类型。C 语言函数还可以将最后一个参数声明为VARIADIC "any"
。这将匹配一个或多个任何类型的实际参数(不一定相同)。这些参数将不会像普通的变长函数那样被收集到数组中;它们将单独传递给函数。使用 PG_NARGS()
宏和上述方法可以确定实际参数的数量及其类型,当使用此功能时。此外,这类函数的用户可能会在函数调用中使用 VARIADIC
关键字,期望该函数将处理数组元素作为单独的参数。如果需要,函数本身必须实现这种行为,并在使用 get_fn_expr_variadic
检测到实际参数标记为 VARIADIC
后执行。
十、共享内存和LWLock
插件可以在服务器启动时预留 LWLock 和共享内存分配。插件的共享库必须通过在 `shared_preload_libraries`
中指定它来提前加载。共享库应该在其 _PG_init
函数中注册一个 shmem_request_hook
。这个 shmem_request_hook
可以预留 LWLock 或共享内存。共享内存是通过你的 `shmem_request_hook`
调用:
void RequestAddinShmemSpace(int size)
LWLock 从你的shmem_request_hook
调用:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这将确保在名为 `tranche_name`
的下,可用一个大小为 `num_lwlocks`
的 LWLocks 数组。使用GetNamedLWLockTranche
获取指向此数组的指针。
可以在 PostgreSQL 源代码树中的 contrib/pg_stat_statements/pg_stat_statements.c
中找到一个 `shmem_request_hook`
示例。 为了避免可能的竞态条件,每个后台应在连接并初始化其共享内存分配时使用 LWLock AddinShmemInitLock
,如下所示:
static mystruct *ptr = NULL; if (!ptr) { bool found; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); ptr = ShmemInitStruct("my struct name", size, &found); if (!found) { initialize contents of shmem area; acquire any requested LWLocks using: ptr->locks = GetNamedLWLockTranche("my tranche name"); } LWLockRelease(AddinShmemInitLock); }
十一、在扩展中使用C++
尽管PostgreSQL后端是用C语言编写的,但只要遵循以下规则,也可以用C++编写扩展:
- 所有要被后端调用的函数需提供一个C语言接口,后端通过这些C函数调用C++函数。比如:可被后端访问的函数必须加上
extern C
标志。任何在后端与C++代码间通过指针传递的函数都必须这样声明。 - 使用合适的方法释放内存。比如:后端多使用
palloc()
分配内存,因此要使用pfree()
释放。此场景如果使用C++的delete
则会失败。 - 避免将异常传递到C代码 (在所有顶级
extern C
函数中使用catch-all块)。即使C++代码没有显示的抛出任何异常这也是必要的,因为诸如内存不足(OOM)之类的事件会持续抛出异常。任何异常都必须被捕获并传递适当的错误到C接口。如果必要,编译C++代码时使用-fno-exceptions
选项以完全消除异常,在此种场景下,你必须检查C++代码中的错误,比如使用new()
后检查返回结果是否为NULL。 - 如果从C++代码中调用后端函数,要确保C++调用栈中仅包含plain old data (POD)结构。这很必要,因为后端错误会生成一个长距离
longjmp()
,它无法正确展开带有非POD对象的C++调用栈。
总之,供后端调用的C++接口函数要声明成 extern C
,并且要避免异常、内存、调用栈泄漏。