콘텐츠로 이동

(KO) PostgreSQL 함수 매니저(fmgr) — V1 호출 규약, 조회 파이프라인, 확장 ABI

목차

사용자 정의 함수(UDF, user-defined function)를 지원하는 관계형 엔진은 호출 시점에 두 질문에 답해야 한다. 코드는 어디 있는가인수와 결과를 어떻게 주고받는가다. 이 답이 엔진의 **함수 호출 인터페이스(FCI, function-call interface)**를 형성한다. FCI는 쿼리 실행기와 내장 함수, SQL 표현식, C 확장, PL/pgSQL 프로시저 사이의 계약이다.

FCI 설계에는 두 가지 핵심 긴장이 있다.

  1. 조회 비용 대 호출 비용. 함수 OID를 호출 가능한 주소로 해석하려면 카탈로그 접근이 필요하다. 수백만 행에 같은 함수를 적용하는 쿼리에서 행마다 조회 비용을 지불하면 성능이 무너진다. 표준적 해법은 경로를 두 단계로 나누는 것이다. 한 번만 실행하는 조회 단계에서 캐시 가능한 디스크립터를 만들고, 호출 단계에서는 그 디스크립터만 사용해 카탈로그를 다시 건드리지 않는다. 이 분리는 쿼리 계층의 prepared statement와 plan cache가 동기를 공유한다.

  2. 타입 안전성 대 균일성. C 직접 호출은 인수 타입을 정밀하게 표현하지만, 호출자를 특정 시그니처 하나에 묶는다. 새 타입마다 새 진입점이 필요해진다. 엔진들은 대신 모든 인수와 반환값을 단일한 넓은 타입으로 통일한다. PostgreSQL은 Datum을 사용한다. 64비트 플랫폼에서 스칼라 값을 직접 담거나 힙 할당 값의 포인터를 담기에 충분한 uintptr_t다. 타입별 변환 매크로 (PG_GETARG_INT32, PG_RETURN_FLOAT8)가 경계에서 타입 안전성을 복원한다.

프로덕션 SQL 엔진의 실질적 요구에서 세 가지 추가 기능이 나온다.

  • strict-null 단락(short-circuit). strict로 표시된 함수는 인수 중 하나라도 NULL이면 호출하지 않는다. 결과는 묵시적으로 NULL이다. 이를 매니저가 강제하면 모든 함수 본체에서 방어적 코드가 사라진다.
  • 집합 반환 함수(SRF, set-returning function). SQL은 함수가 행의 집합을 반환하도록 허용한다. SQL Server의 테이블값 함수, Oracle의 파이프라인 함수에 해당한다. 호출 규약은 값-per-호출(이터레이터) 방식과 일괄 구체화(materialize) 방식을 지원해야 한다.
  • 확장 ABI 안정성. 한 마이너 버전에서 컴파일된 동적 모듈은 비호환 메이저 버전에서 로드를 거부해야 한다. 로더가 사용자 함수를 호출하기 전에 잘 알려진 심볼을 확인하는 magic-struct 패턴이 표준 해법이다.

이 세 가지에 대한 PostgreSQL의 답은 fmgr.h, fmgr.c, 그리고 해당 서브시스템의 1차 설계 문서인 src/backend/utils/fmgr/README에 담겨 있다.

아래 패턴들은 PostgreSQL, Oracle, DB2, SQL Server, MySQL 전반에서 반복된다. 다음 절의 PostgreSQL 구체적 선택들은 이 공유 공간 안에서 조정한 다이얼 값이다.

두 단계 조회 + 호출 디스크립터

섹션 제목: “두 단계 조회 + 호출 디스크립터”

거의 모든 엔진은 함수 해석(코드 주소 찾기, 카탈로그 메타데이터 읽기)과 함수 호출(인수 전달, 결과 수집)을 분리한다. 해석된 메타데이터는 실행기가 쿼리(또는 플랜) 수명 동안 캐시하는 per-function 디스크립터 구조체에 저장된다. 디스크립터에는 코드 포인터, 기대 인수 수, strictness 플래그, 그리고 언어 핸들러가 파싱된 함수 상태를 캐시하는 per-call 스크래치 슬롯(fn_extra)이 담긴다.

타입마다 별도 C 진입점(add_int32, add_float8 …) 대신 엔진들은 임의의 값을 담기에 충분한 단일 스칼라 타입을 사용한다. Oracle은 타입 코드가 담긴 dvoid *를, SQL Server는 자체 변형 타입 구조체를, MySQL은 Item 계층을 쓴다. PostgreSQL은 Datum을 사용한다. 모든 인수는 NullableDatum[] 배열로 도착하고, 모든 결과는 Datum + isnull 플래그로 반환된다.

함수의 call handler는 그 자체가 pg_language에 등록된 C 함수다. 매니저가 PL/pgSQL 함수를 해석할 때 PL/pgSQL 바이트코드를 직접 호출하지 않는다. PL/pgSQL call handler를 호출하고, handler가 바이트코드를 해석한다. handler는 매니저가 C 함수에 전달했을 동일한 FunctionCallInfo를 받는다. 따라서 매니저 관점에서 조회 이후 모든 언어는 동일하게 보인다.

세션 수준 공유 라이브러리 캐시

섹션 제목: “세션 수준 공유 라이브러리 캐시”

함수 호출마다 .so/.dll을 로드하는 비용은 너무 크다. 엔진들은 파일 경로를 키로 하는 세션 수준 열린 라이브러리 핸들 캐시를 유지한다. 어떤 라이브러리의 함수를 처음 호출할 때 파일을 dlopen하고 주소를 해석한다. 이후 호출은 캐시된 핸들을 재사용한다. PostgreSQL은 두 번째 계층을 추가한다. (fn_oid, xmin, ctid)를 키로 해석된 C 함수 포인터를 저장하는 해시 테이블이다. 반복 호출에서 dlsym조차 건너뛸 수 있는 구조다.

동적 로드 확장을 지원하는 엔진은 다른 ABI로 컴파일된 모듈의 로드를 차단해야 한다. 로더는 사용자 함수를 호출하기 전에 로드된 라이브러리에서 잘 알려진 심볼(PostgreSQL에서는 Pg_magic_func)을 찾는다. 그 심볼은 ABI에 중요한 상수들의 구조체를 반환한다. 로더는 이를 자신의 컴파일 타임 값과 비교한다. 불일치 시 유용한 오류 메시지와 함께 로드를 중단한다.

개념 / 패턴PostgreSQL 이름
함수 조회 디스크립터FmgrInfo (fmgr.h:56)
per-call 인수/결과 컨테이너FunctionCallInfoBaseData / FunctionCallInfo (fmgr.h:85)
균일 값 타입Datum (postgres.h)
디스크립터 내 코드 포인터FmgrInfo.fn_addrPGFunction typedef
언어 디스패처(call handler)PL 함수의 경우 fn_addr = 언어의 lanplcallfoid
세션 수준 라이브러리 캐시fmgr.cCFuncHash 해시 테이블
내장 함수 테이블fmgr_builtins[] + fmgr_builtin_oid_index[] (fmgrtab.h)
ABI 가드Pg_magic_struct / PG_MODULE_MAGIC 매크로 (fmgr.h)
strict-null 단락FmgrInfo.fn_strict; FunctionCallInvoke 전 호출자가 검사
집합 반환 함수 지원fcinfo->resultinfoReturnSetInfo 노드
per-call 핸들러 스크래치 슬롯FmgrInfo.fn_extra
security-definer / proconfig 래퍼fmgr_security_definer 인터포저 함수

PostgreSQL의 함수 매니저, 보편적으로 fmgr로 불리는 이 컴포넌트는 실행기가 모든 SQL 호출 가능 단위를 부를 때 통과하는 단일 관문이다. 인트리 src/backend/utils/fmgr/README가 설계 권위자다. 이 절은 REL_18_STABLE 소스(커밋 273fe94)를 기준으로 그 내용을 정리한다.

구조는 세 계층으로 나뉜다.

  1. 디스크립터 계층FmgrInfofmgr_info를 통한 채워 넣기.
  2. 호출 계층FunctionCallInfoBaseData, FunctionCallInvoke, 그리고 DirectFunctionCall / FunctionCallNcoll / OidFunctionCallNcoll 패밀리.
  3. 확장 ABI 계층PG_FUNCTION_INFO_V1, PG_MODULE_MAGIC, 그리고 dfmgr.c 라이브러리 로더.

FmgrInfo는 쿼리(또는 플랜)당 한 번씩 함수 OID를 해석한 결과다.

// FmgrInfo — src/include/fmgr.h
typedef struct FmgrInfo
{
PGFunction fn_addr; /* pointer to function or handler to be called */
Oid fn_oid; /* OID of function (NOT of handler, if any) */
short fn_nargs; /* number of input args (0..FUNC_MAX_ARGS) */
bool fn_strict; /* function is "strict" (NULL in => NULL out) */
bool fn_retset; /* function returns a set */
unsigned char fn_stats; /* collect stats if track_functions > this */
void *fn_extra; /* extra space for use by handler */
MemoryContext fn_mcxt; /* memory context to store fn_extra in */
fmNodePtr fn_expr; /* expression parse tree for call, or NULL */
} FmgrInfo;

fn_addr는 호출 계층이 실행 시점에 사용하는 유일한 필드다. 내장 함수라면 C 함수 주소가 직접 들어간다. C 확장이면 dlopen으로 해석한 심볼 주소다. PL/pgSQL, PL/Python 등 절차적 언어(PL)의 경우에는 해당 언어의 call handler 주소가 들어간다. handler는 fn_oid를 사용해 실제 함수 본체를 찾는다. fn_extra는 handler의 per-call 캐시 슬롯이다. PL/pgSQL handler는 첫 번째 호출 이후 컴파일된 함수 파스 트리를 여기에 저장해 이후 호출에서 재파싱을 건너뛴다.

fmgr_info(공개 진입점)는 fmgr_info_cxt_security에 위임한다. 실제 분기 로직은 여기에 있다.

// fmgr_info_cxt_security — src/backend/utils/fmgr/fmgr.c
static void
fmgr_info_cxt_security(Oid functionId, FmgrInfo *finfo, MemoryContext mcxt,
bool ignore_security)
{
const FmgrBuiltin *fbp;
/* ... */
if ((fbp = fmgr_isbuiltin(functionId)) != NULL)
{
/* Fast path: built-in, skip pg_proc lookup */
finfo->fn_nargs = fbp->nargs;
finfo->fn_strict = fbp->strict;
finfo->fn_addr = fbp->func;
finfo->fn_oid = functionId;
return;
}
/* Otherwise look up pg_proc via syscache */
procedureTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(functionId));
/* ... */
if (!ignore_security &&
(procedureStruct->prosecdef || ... || FmgrHookIsNeeded(functionId)))
{
finfo->fn_addr = fmgr_security_definer; /* wrap in security layer */
return;
}
switch (procedureStruct->prolang)
{
case INTERNALlanguageId: /* alias for a built-in */
fbp = fmgr_lookupByName(prosrc);
finfo->fn_addr = fbp->func;
break;
case ClanguageId:
fmgr_info_C_lang(functionId, finfo, procedureTuple);
break;
case SQLlanguageId:
finfo->fn_addr = fmgr_sql;
break;
default:
fmgr_info_other_lang(functionId, finfo, procedureTuple);
break;
}
finfo->fn_oid = functionId;
ReleaseSysCache(procedureTuple);
}

분기는 네 개의 리프로 끝난다. 각각 prolang 값에 대응한다.

prolangfn_addr에 설정되는 값비고
내장 함수(빠른 경로)fbp->func (syscache 불필요)fmgr_isbuiltinfmgr_builtin_oid_index[]를 O(1) 배열 조회
INTERNALlanguageIdfmgr_lookupByName을 통한 fbp->func내장 함수 사용자 별칭; 느린 경로
ClanguageIddlopen 해석 심볼 (CFuncHash 경유)fmgr_info_C_lang이 dlopen + CFuncHash 처리
SQLlanguageIdfmgr_sql (SQL 함수 평가기)SQL 함수 본체를 인라인 해석
그 외언어의 lanplcallfoidfmgr_info_other_langpg_language 조회

내장 함수 빠른 경로. fmgr_isbuiltin은 syscache 조회를 완전히 피한다.

// fmgr_isbuiltin — src/backend/utils/fmgr/fmgr.c
static const FmgrBuiltin *
fmgr_isbuiltin(Oid id)
{
uint16 index;
if (id > fmgr_last_builtin_oid)
return NULL;
index = fmgr_builtin_oid_index[id];
if (index == InvalidOidBuiltinMapping)
return NULL;
return &fmgr_builtins[index];
}

fmgr_builtins[]fmgr_builtin_oid_index[]는 빌드 시스템이 pg_proc.dat에서 fmgrtab.c로 코드 생성한 배열이다. 인덱스 배열은 OID를 슬롯으로 O(1) 단순 배열 참조로 매핑한다. 내장 함수 경로는 락 없이 몇 개의 명령어로 끝난다.

C 확장 캐시. fmgr_info_C_langpg_proc에서 prosrc(심볼 이름)와 probin(라이브러리 파일 경로)을 읽고, load_external_function을 호출해 .so를 열며(dfmgr.cdlopen 핸들을 캐시), fetch_finfo_recordPG_FUNCTION_INFO_V1 디스크립터를 검증한 뒤 해석된 주소를 (fn_oid, xmin, ctid) 키로 CFuncHash에 저장한다. 캐시된 xmin/ctid를 현재 pg_proc 튜플과 비교해 오래된 항목을 교체하는 구조다.

security-definer 인터포저. prosecdef가 true이거나, proconfig가 설정되어 있거나, 플러그인 훅(fmgr_hook)이 활성화된 경우 fn_addr는 실제 함수 대신 fmgr_security_definer로 설정된다. 호출 시점에 fmgr_security_definer는 사용자 ID를 전환하고 GUC 오버라이드를 적용한 뒤, fn_extra에 캐시된 내부 FmgrInfo로 실제 함수를 호출한다. 이 구조 덕분에 호출자는 security-definer 여부를 인식하지 않아도 된다.

계층 2 — FunctionCallInfo 호출 계층

섹션 제목: “계층 2 — FunctionCallInfo 호출 계층”

FunctionCallInfoBaseData(FunctionCallInfo로 typedef)는 모든 함수에 전달되는 per-call 컨테이너다.

// FunctionCallInfoBaseData — src/include/fmgr.h
typedef struct FunctionCallInfoBaseData
{
FmgrInfo *flinfo; /* ptr to lookup info used for this call */
fmNodePtr context; /* pass info about context of call */
fmNodePtr resultinfo; /* pass or return extra info about result */
Oid fncollation; /* collation for function to use */
bool isnull; /* function must set true if result is NULL */
short nargs; /* # arguments actually passed */
NullableDatum args[]; /* flexible array of (Datum value, bool isnull) */
} FunctionCallInfoBaseData;

context는 호출 컨텍스트 정보를 담는 Node *다. 트리거 함수에는 TriggerData, 집계/윈도우 함수에는 AggState / WindowAggState, 저장 프로시저에는 CallContext, 소프트 오류 호출자에는 ErrorSaveContext가 온다. IsA(context, X) 패턴으로 함수가 자신의 호출 컨텍스트를 감지한다. resultinfo는 집합 반환 함수에 ReturnSetInfo를 전달한다.

LOCAL_FCINFO로 스택 할당. FunctionCallInfoBaseData는 인수를 위한 가변 길이 배열 멤버를 가지므로, 호출자는 LOCAL_FCINFO 매크로로 적절한 크기의 구조체를 스택에 할당한다.

// LOCAL_FCINFO — src/include/fmgr.h
#define LOCAL_FCINFO(name, nargs) \
union { \
FunctionCallInfoBaseData fcinfo; \
char fcinfo_data[SizeForFunctionCallInfo(nargs)]; \
} name##data; \
FunctionCallInfo name = &name##data.fcinfo

union이 정렬을 보장한다. SizeForFunctionCallInfo(nargs)offsetof(args) + sizeof(NullableDatum) * nargs를 계산한다. 힙 할당에는 palloc(SizeForFunctionCallInfo(nargs))를 사용한다.

호출 매크로. 실제 호출은 단일 간접 함수 호출 하나다.

// FunctionCallInvoke — src/include/fmgr.h
#define FunctionCallInvoke(fcinfo) ((* (fcinfo)->flinfo->fn_addr) (fcinfo))

세 가지 호출 지점 패턴. 호출자는 가용한 컨텍스트에 따라 세 패밀리 중 하나를 선택한다.

// DirectFunctionCall1Coll — src/backend/utils/fmgr/fmgr.c
// For calling a known PGFunction pointer directly; no FmgrInfo needed.
Datum
DirectFunctionCall1Coll(PGFunction func, Oid collation, Datum arg1)
{
LOCAL_FCINFO(fcinfo, 1);
InitFunctionCallInfoData(*fcinfo, NULL, 1, collation, NULL, NULL);
fcinfo->args[0].value = arg1;
fcinfo->args[0].isnull = false;
result = (*func) (fcinfo);
if (fcinfo->isnull)
elog(ERROR, "function %p returned NULL", (void *) func);
return result;
}

세 패밀리와 그 용도는 다음과 같다.

패밀리사용 시점비고
DirectFunctionCallNcollPGFunction*를 직접 보유; NULL 인수/결과 불허FmgrInfo 없음; non-NULL 결과를 단언
FunctionCallNcollFmgrInfo * 보유 (이전 fmgr_info에서)pgstat_init_function_usage 추적; NULL 허용
OidFunctionCallNcollOID만 있을 때fmgr_info + FunctionCallNcoll 인라인 호출

C 함수 작성 규약. 모든 fmgr 호출 가능 C 함수는 Datum func(PG_FUNCTION_ARGS) 시그니처를 가진다. PG_FUNCTION_ARGSFunctionCallInfo fcinfo로 확장된다. 인수는 타입별 매크로로 꺼내고 결과도 같은 방식으로 반환한다.

// Example fmgr-callable function using V1 convention
Datum
int4add(PG_FUNCTION_ARGS)
{
int32 arg1 = PG_GETARG_INT32(0);
int32 arg2 = PG_GETARG_INT32(1);
PG_RETURN_INT32(arg1 + arg2);
}

PG_GETARG_INT32(n)DatumGetInt32(fcinfo->args[n].value)로 확장된다. PG_RETURN_INT32(x)return Int32GetDatum(x)로 확장된다. text 같은 varlena 타입에서는 PG_GETARG_TEXT_PP(n)이 TOAST 해제 후 값을 반환하고, PG_RETURN_TEXT_P(x)가 포인터를 Datum으로 반환한다. non-strict 함수는 PG_ARGISNULL(n)으로 NULL 여부를 먼저 확인한다. strict 함수는 NULL 인수를 받지 않는다. 호출자의 null 검사가 먼저 발동하기 때문이다.

PG_FUNCTION_INFO_V1. 모든 C 확장 함수는 PG_FUNCTION_INFO_V1 매크로로 호출 규약을 선언해야 한다.

// PG_FUNCTION_INFO_V1 — src/include/fmgr.h
#define PG_FUNCTION_INFO_V1(funcname) \
extern PGDLLEXPORT Datum funcname(PG_FUNCTION_ARGS); \
extern PGDLLEXPORT const Pg_finfo_record * CppConcat(pg_finfo_,funcname)(void); \
const Pg_finfo_record * \
CppConcat(pg_finfo_,funcname) (void) \
{ \
static const Pg_finfo_record my_finfo = { 1 }; \
return &my_finfo; \
} \
extern int no_such_variable

이 매크로는 api_version = 1을 반환하는 동반 pg_finfo_<funcname> 함수를 생성한다. fetch_finfo_record는 이름으로(psprintf("pg_finfo_%s", funcname)) 이 동반 심볼을 찾아 버전을 검증한다. 심볼을 찾지 못하면 “SQL-callable functions need an accompanying PG_FUNCTION_INFO_V1(funcname).” 힌트와 함께 오류를 발생시킨다. 버전-0(“구식”) 규약은 제거되었고, V1이 유일하게 지원되는 규약이다.

PG_MODULE_MAGIC. per-function 버전 검사 외에, 공유 라이브러리 전체의 ABI 검사도 통과해야 한다. PG_MODULE_MAGIC 매크로(또는 신형 PG_MODULE_MAGIC_EXT(...))는 잘 알려진 심볼 Pg_magic_func 아래에 Pg_magic_struct를 방출한다.

// Pg_magic_struct and Pg_abi_values — src/include/fmgr.h
typedef struct
{
int version; /* PostgreSQL major version */
int funcmaxargs; /* FUNC_MAX_ARGS */
int indexmaxkeys; /* INDEX_MAX_KEYS */
int namedatalen; /* NAMEDATALEN */
int float8byval; /* FLOAT8PASSBYVAL */
char abi_extra[32]; /* see pg_config_manual.h */
} Pg_abi_values;

dfmgr.cload_external_function은 새로 로드된 라이브러리에서 Pg_magic_func()를 호출하고 반환된 구조체를 서버 자체 값과 필드별로 비교한다. 불일치 시 incompatible_module_error()로 불일치 필드를 명시한 오류를 발생시킨다. 다른 NAMEDATALEN로 컴파일된 라이브러리가 구조체 레이아웃 불일치로 크래시를 일으키는 것을 막는 장치다.

PostgreSQL의 일반 오류 경로(ereport(ERROR, ...))는 longjmp로 언와인드하며 전체 서브트랜잭션 정리를 요구한다. 잘못된 형식의 입력을 거부하기만 하면 되는 데이터타입 입력 함수에는 비용이 너무 크다. PG14부터 함수는 소프트 오류로 보고할 수 있다.

// errsave / ereturn pattern — src/backend/utils/fmgr/README
// Instead of: ereport(ERROR, (errcode(...), errmsg(...)));
// Write: errsave(fcinfo->context, (errcode(...), errmsg(...)));
// Or combine: ereturn(fcinfo->context, (Datum) 0, (errcode(...), errmsg(...)));

fcinfo->contextErrorSaveContext 노드이면 errsave는 오류 정보를 해당 노드에 저장하고 정상 반환한다. 함수는 더미 Datum을 반환한다. context가 NULL이거나 다른 노드 타입이면 errsaveereport(ERROR)와 동일하게 동작한다. 소프트 오류를 받으려는 호출자는 ErrorSaveContext를 할당해 fcinfo->context로 전달하고, 호출 후 escontext.error_occurred를 확인한다. README는 소프트 오류를 복구 가능한 상황(잘못된 입력 구문, 범위 초과 값)에만 제한한다. 내부 오류와 OOM은 여전히 하드 경로를 써야 한다.

pg_proc에서 proretset = true로 표시된 함수는 fcinfo->resultinfoReturnSetInfo 노드를 받는다. 두 가지 반환 모드가 지원된다.

값-per-호출 모드(이터레이터): 함수가 반복 호출된다. 각 행에서 ReturnSetInfo.isDoneExprMultipleResult로 설정하고, 완료 시 ExprEndResult로 설정한다. 실행기가 루프로 호출한다. 이 모드의 함수는 마지막 호출에서 리소스를 정리하면 안 된다. 실행기가 LIMIT 등으로 조기 종료할 수 있기 때문이다.

funcapi.h 매크로가 프로토콜을 캡슐화한다. 첫 번째 호출 감지는 “fn_extra가 아직 NULL인가?”로 단순화되고, 호출 간 상태는 fn_extra에 매달린 FuncCallContext에 저장된다.

// SRF value-per-call macros — src/include/funcapi.h
#define SRF_IS_FIRSTCALL() (fcinfo->flinfo->fn_extra == NULL)
#define SRF_FIRSTCALL_INIT() init_MultiFuncCall(fcinfo)
#define SRF_PERCALL_SETUP() per_MultiFuncCall(fcinfo)
#define SRF_RETURN_NEXT(_funcctx, _result) \
do { \
ReturnSetInfo *rsi; \
(_funcctx)->call_cntr++; \
rsi = (ReturnSetInfo *) fcinfo->resultinfo; \
rsi->isDone = ExprMultipleResult; \
PG_RETURN_DATUM(_result); \
} while (0)
#define SRF_RETURN_DONE(_funcctx) \
do { \
ReturnSetInfo *rsi; \
end_MultiFuncCall(fcinfo, _funcctx); \
rsi = (ReturnSetInfo *) fcinfo->resultinfo; \
rsi->isDone = ExprEndResult; \
PG_RETURN_NULL(); \
} while (0)

init_MultiFuncCall은 정확히 한 번 실행된다. 함수가 집합을 받아들이는 컨텍스트에서 호출됐는지 단언(IsA(fcinfo->resultinfo, ReturnSetInfo))하고, fn_mcxt의 자식으로 “SRF multi-call context”라는 전용 AllocSetContextCreate 컨텍스트를 생성하며, FuncCallContext를 0으로 초기화해 fn_extra에 저장한다. 또한 RegisterExprContextCallback으로 shutdown_MultiFuncCall 콜백을 등록해 실행기가 스캔을 조기 종료(LIMIT)하더라도 호출 간 컨텍스트가 해제되도록 한다.

// init_MultiFuncCall — src/backend/utils/fmgr/funcapi.c
FuncCallContext *
init_MultiFuncCall(PG_FUNCTION_ARGS)
{
FuncCallContext *retval;
if (fcinfo->resultinfo == NULL || !IsA(fcinfo->resultinfo, ReturnSetInfo))
ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("set-valued function called in context that cannot accept a set")));
if (fcinfo->flinfo->fn_extra == NULL)
{
ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
MemoryContext multi_call_ctx;
multi_call_ctx = AllocSetContextCreate(fcinfo->flinfo->fn_mcxt,
"SRF multi-call context",
ALLOCSET_SMALL_SIZES);
retval = (FuncCallContext *)
MemoryContextAllocZero(multi_call_ctx, sizeof(FuncCallContext));
retval->multi_call_memory_ctx = multi_call_ctx;
fcinfo->flinfo->fn_extra = retval;
RegisterExprContextCallback(rsi->econtext, shutdown_MultiFuncCall,
PointerGetDatum(fcinfo->flinfo));
}
else
elog(ERROR, "init_MultiFuncCall cannot be called more than once");
return retval;
}

per_MultiFuncCallfn_extraFuncCallContext *로 캐스팅하는 한 줄짜리다. call_cntr(SRF_RETURN_NEXT가 증가)과 선택적 max_calls가 루프 카운터 역할을 하고, user_fctx는 SRF가 필요한 커서 상태를 저장하는 per-SRF 스크래치 포인터다. shutdown 콜백이 해제를 책임지므로, README의 경고가 성립한다. 값-per-호출 SRF는 SRF_RETURN_DONE이 실행된다고 가정하면 안 된다. 파일 디스크립터나 열린 커서 같은 비메모리 리소스는 이 모드에서 안전하지 않으며, 그런 경우에는 구체화 모드를 사용해야 한다.

구체화 모드(materialize mode): 함수가 econtext->ecxt_per_query_memoryTuplestore를 생성하고 한 번의 호출에서 모두 채운 뒤, 포인터와 TupleDescReturnSetInfo에 저장하고 returnMode = SFRM_Materialize로 설정한다. funcapi.cInitMaterializedSRF 헬퍼가 보일러플레이트를 캡슐화한다.

// InitMaterializedSRF — src/backend/utils/fmgr/funcapi.c
void
InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags)
{
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
/* sanity checks on rsinfo ... */
per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
old_context = MemoryContextSwitchTo(per_query_ctx);
tupstore = tuplestore_begin_heap(random_access, false, work_mem);
MemoryContextSwitchTo(old_context);
rsinfo->returnMode = SFRM_Materialize;
rsinfo->setResult = tupstore;
rsinfo->setDesc = stored_tupdesc;
}

funcapi.c는 per-call 상태가 필요한 값-per-호출 SRF를 위해 init_MultiFuncCall / per_MultiFuncCall / end_MultiFuncCall도 제공한다. 상태는 fn_mcxt에 할당된 FuncCallContext에 저장된다.

호출 경로 + SRF 값-per-호출 프로토콜

섹션 제목: “호출 경로 + SRF 값-per-호출 프로토콜”

아래 다이어그램은 pg_proc OID에서 시작해 두 단계 조회/호출 분리를 거쳐 반환된 Datum에 이르는 단일 값의 전체 흐름을 추적한다. 이어서 집합 반환 함수가 fn_extra 첫 번째 호출 래치(latch)로 행마다 같은 FunctionCallInvoke 엣지를 재사용하는 방식을 보여준다.

flowchart TD
  OID["pg_proc OID<br/>(functionId)"]
  FI["fmgr_info / fmgr_info_cxt_security<br/>(lookup phase, once per query)"]
  FBP["fmgr_isbuiltin<br/>builtin? fn_addr = fbp->func"]
  CL["fmgr_info_C_lang<br/>CFuncHash / fetch_finfo_record"]
  OL["fmgr_info_other_lang / fmgr_sql<br/>fn_addr = call handler"]
  FINFO["FmgrInfo populated<br/>fn_addr, fn_nargs, fn_strict, fn_extra"]
  LF["LOCAL_FCINFO(fcinfo, nargs)<br/>+ InitFunctionCallInfoData<br/>+ fill args[].value / .isnull"]
  STRICT{"fn_strict &&<br/>any arg isnull?"}
  NULLOUT["result = NULL<br/>(skip the call)"]
  INV["FunctionCallInvoke(fcinfo)<br/>(* fn_addr)(fcinfo)"]
  BODY["function body<br/>Datum f(PG_FUNCTION_ARGS)<br/>PG_GETARG_* / PG_RETURN_*"]
  RET["result Datum + fcinfo->isnull"]

  OID --> FI
  FI -->|"builtin fast path"| FBP
  FI -->|"C language"| CL
  FI -->|"SQL / PL"| OL
  FBP --> FINFO
  CL --> FINFO
  OL --> FINFO
  FINFO --> LF
  LF --> STRICT
  STRICT -->|"yes"| NULLOUT
  STRICT -->|"no"| INV
  INV --> BODY
  BODY --> RET

  RET -->|"fn_retset SRF only"| FIRST{"SRF_IS_FIRSTCALL?<br/>fn_extra == NULL"}
  FIRST -->|"yes"| INIT["SRF_FIRSTCALL_INIT<br/>init_MultiFuncCall<br/>alloc FuncCallContext in fn_extra"]
  FIRST -->|"no"| PER["SRF_PERCALL_SETUP<br/>per_MultiFuncCall"]
  INIT --> PER
  PER --> MORE{"call_cntr <<br/>max_calls?"}
  MORE -->|"yes"| NEXT["SRF_RETURN_NEXT<br/>isDone = ExprMultipleResult<br/>executor re-invokes via fn_addr"]
  MORE -->|"no"| DONE["SRF_RETURN_DONE<br/>end_MultiFuncCall<br/>isDone = ExprEndResult"]
  NEXT -->|"loop"| INV

그림 2 — fmgr 호출 경로와 SRF 값-per-호출 프로토콜. 위쪽 절반은 per-value 경로다. fmgr_infoFmgrInfo를 한 번 채우고, 이후 각 호출 스택은 LOCAL_FCINFOfcinfo를 할당하고, strict-null 단락을 적용하며, FunctionCallInvoke로 디스패치한다. 아래쪽 절반(fn_retset 분기)은 값-per-호출 루프를 보여준다. fn_extra == NULL 래치가 첫 번째 호출( FuncCallContext 할당)과 이후 호출을 구분하며, SRF_RETURN_NEXTSRF_RETURN_DONEExprEndResult를 설정할 때까지 같은 FunctionCallInvoke 엣지를 재진입한다.

flowchart TD
  EX["실행기<br/>(ExecMakeTableFunctionResult,<br/>ExprEvalStep 등)"]
  FI["fmgr_info<br/>(조회 단계)"]
  FCI["FunctionCallInvoke<br/>(호출 단계)"]
  BI["fmgr_isbuiltin<br/>O(1) OID 인덱스"]
  CH["CFuncHash<br/>(C 확장 캐시)"]
  DL["dfmgr.c<br/>load_external_function<br/>+ fetch_finfo_record"]
  SD["fmgr_security_definer<br/>(인터포저)"]
  PL["fmgr_info_other_lang<br/>→ lanplcallfoid"]
  SQL["fmgr_sql<br/>(SQL 함수 평가기)"]
  FN["fn_addr<br/>(PGFunction*)"]

  EX -->|"fmgr_info(oid, &flinfo)"| FI
  FI -->|"내장 함수 OID"| BI
  FI -->|"C 언어"| CH
  CH -->|"캐시 미스"| DL
  FI -->|"보안/훅"| SD
  FI -->|"SQL 언어"| SQL
  FI -->|"PL 언어"| PL
  BI --> FN
  CH --> FN
  SD --> FN
  SQL --> FN
  PL --> FN
  FN -->|"FunctionCallInvoke(fcinfo)"| FCI
  EX -->|"LOCAL_FCINFO + 인수"| FCI

그림 1 — fmgr 컴포넌트 흐름. 실행기는 두 단계를 별도로 구동한다. 조회 단계(왼쪽)는 fn_addr가 설정된 FmgrInfo를 생성하고, 호출 단계(오른쪽)는 fn_addrFunctionCallInfo를 전달한다. CFuncHash는 반복 호출 시 dfmgr.c 라이브러리 로드를 건너뛴다. fmgr_security_definerprosecdef, proconfig, 또는 플러그인 훅이 활성화된 경우 fn_addr로 삽입된다.

  • fmgr_info — 공개 진입점. CurrentMemoryContextignore_security = falsefmgr_info_cxt_security에 위임한다.
  • fmgr_info_cxt — 동일하지만 부가 데이터를 위한 명시적 MemoryContext를 받는다.
  • fmgr_info_cxt_security — 분기 핵심. 내장 함수 빠른 경로 검사, syscache 조회, prolang 스위치.
  • fmgr_isbuiltinfmgr_builtin_oid_index에서 O(1) 배열 조회. FmgrBuiltin * 또는 NULL을 반환한다.
  • fmgr_lookupByNamefmgr_builtins[]를 이름으로 선형 탐색. INTERNALlanguageId 별칭에만 사용된다.
  • fmgr_info_C_lang — C 확장 경로. CFuncHash에서 lookup_C_func, 그 다음 load_external_function + fetch_finfo_record, 그 다음 record_C_func.
  • fetch_finfo_recordpg_finfo_<name> 심볼 찾기, 호출, api_version == 1 검증.
  • lookup_C_func / record_C_funcCFuncHash get/set. xmin + ctid로 오래된 항목 감지.
  • fmgr_info_other_lang — PL handler 경로. ignore_security = truefmgr_info_cxt_security를 재귀 호출해 lanplcallfoid를 해석한다.
  • fmgr_security_definer — 인터포저. 실제 FmgrInfo + userid + GUC 목록을 fn_extra에 캐시하고 호출 후 복원한다.
  • fmgr_info_copyFmgrInfo 얕은 복사. fn_extra를 0으로 만들어 handler 상태가 별칭되지 않도록 한다.
  • fmgr_symbol — 유틸리티. OID로 C 심볼을 식별하는 (mod, fn) 문자열을 반환. pg_get_function_sqlbody와 JIT에서 사용된다.
  • fmgr_internal_function — 역방향 조회. fmgr_builtins[]에서 이름으로 OID를 찾는다.
  • FunctionCallInvoke — 매크로. fn_addr를 통한 단일 간접 호출.
  • InitFunctionCallInfoData — 매크로. args[] 제외, FunctionCallInfoBaseData의 모든 스칼라 필드를 채운다.
  • LOCAL_FCINFO — 매크로. 올바른 크기의 FunctionCallInfoBaseData를 스택에 할당.
  • DirectFunctionCallNColl (1–9) — 알려진 PGFunction 포인터 직접 호출. FmgrInfo 없음. NULL 인수/결과 불허.
  • CallerFInfoFunctionCall1/2DirectFunctionCall과 유사하지만 FmgrInfo *를 받는다. 호출자가 보유한 fn_extra를 지속시키고 싶을 때 유용하다.
  • FunctionCallNColl (0–9) — FmgrInfo *를 통한 호출. pgstat_init_function_usage 추적.
  • OidFunctionCallNColl (0–9) — OID로 호출. 인라인으로 fmgr_info + FunctionCallNcoll 수행.
  • PG_FUNCTION_INFO_V1 — 매크로. pg_finfo_<name> 심볼 생성.
  • PG_MODULE_MAGIC / PG_MODULE_MAGIC_EXT — 매크로. Pg_magic_struct가 담긴 Pg_magic_func 심볼 생성.
  • load_external_function (dfmgr.c) — dlopen + dlsym과 라이브러리 캐시. ABI 검사를 위해 Pg_magic_func 호출.
  • fetch_finfo_recordpg_finfo_<name> 찾기 + 검증.
  • InitMaterializedSRF — 구체화 모드 SRF를 위해 ReturnSetInfoTuplestore + TupleDesc를 설정한다.
  • init_MultiFuncCall / per_MultiFuncCall / end_MultiFuncCallFuncCallContext를 통한 값-per-호출 SRF 상태 관리.
  • get_call_result_type — 호출 표현식에서 다형 함수의 실제 반환 타입을 해석한다. SRF가 TupleDesc를 구성할 때 사용한다.

위치 힌트 (2026-06-05 기준, 커밋 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”
심볼파일라인
FmgrInfosrc/include/fmgr.h56
FunctionCallInfoBaseDatasrc/include/fmgr.h85
PGFunction typedefsrc/include/fmgr.h40
LOCAL_FCINFOsrc/include/fmgr.h110
InitFunctionCallInfoDatasrc/include/fmgr.h150
FunctionCallInvokesrc/include/fmgr.h172
PG_FUNCTION_ARGSsrc/include/fmgr.h193
PG_ARGISNULLsrc/include/fmgr.h209
PG_RETURN_NULLsrc/include/fmgr.h345
PG_FUNCTION_INFO_V1src/include/fmgr.h415
Pg_finfo_recordsrc/include/fmgr.h394
Pg_abi_valuessrc/include/fmgr.h466
Pg_magic_structsrc/include/fmgr.h478
FmgrBuiltinsrc/include/utils/fmgrtab.h25
fmgr_isbuiltinsrc/backend/utils/fmgr/fmgr.c76
fmgr_lookupByNamesrc/backend/utils/fmgr/fmgr.c101
fmgr_infosrc/backend/utils/fmgr/fmgr.c127
fmgr_info_cxtsrc/backend/utils/fmgr/fmgr.c137
fmgr_info_cxt_securitysrc/backend/utils/fmgr/fmgr.c147
fmgr_symbolsrc/backend/utils/fmgr/fmgr.c281
fmgr_info_C_langsrc/backend/utils/fmgr/fmgr.c349
fmgr_info_other_langsrc/backend/utils/fmgr/fmgr.c418
fetch_finfo_recordsrc/backend/utils/fmgr/fmgr.c455
lookup_C_funcsrc/backend/utils/fmgr/fmgr.c515
record_C_funcsrc/backend/utils/fmgr/fmgr.c539
fmgr_info_copysrc/backend/utils/fmgr/fmgr.c580
fmgr_internal_functionsrc/backend/utils/fmgr/fmgr.c595
fmgr_security_definersrc/backend/utils/fmgr/fmgr.c632
DirectFunctionCall1Collsrc/backend/utils/fmgr/fmgr.c792
FunctionCall1Collsrc/backend/utils/fmgr/fmgr.c1129
OidFunctionCall1Collsrc/backend/utils/fmgr/fmgr.c1411
InitMaterializedSRFsrc/backend/utils/fmgr/funcapi.c76
init_MultiFuncCallsrc/backend/utils/fmgr/funcapi.c133
per_MultiFuncCallsrc/backend/utils/fmgr/funcapi.c208
end_MultiFuncCallsrc/backend/utils/fmgr/funcapi.c220
get_call_result_typesrc/backend/utils/fmgr/funcapi.c276
FuncCallContextsrc/include/funcapi.h57
SRF_IS_FIRSTCALLsrc/include/funcapi.h305
SRF_FIRSTCALL_INITsrc/include/funcapi.h307
SRF_PERCALL_SETUPsrc/include/funcapi.h309
SRF_RETURN_NEXTsrc/include/funcapi.h311
SRF_RETURN_DONEsrc/include/funcapi.h329
  • fmgr_isbuiltin은 이진 탐색이 아닌 O(1) 배열 조회다. fmgr.c:76–93에서 확인. 코드 생성된 uint16 배열 fmgr_builtin_oid_index[id]를 읽어 &fmgr_builtins[index]를 반환한다. 반복이 없다. “fast lookup only possible if original oid still assigned” 주석이 재할당된 OID(사용자 CREATE FUNCTION ... INTERNAL)는 fmgr_lookupByName 선형 탐색으로 내려감을 명시한다.

  • CFuncHash는 OID만이 아닌 xmin + ctid로 오래된 항목을 감지한다. lookup_C_func (fmgr.c:515–533)에서 확인. fn_xmin == HeapTupleHeaderGetRawXmin(t_data)ItemPointerEquals(&fn_tid, &t_self) 두 조건이 모두 참일 때만 캐시 항목을 수락한다. 세션 재시작 없이 pg_proc 튜플 업데이트(ALTER FUNCTION)를 감지하는 구조다.

  • V0 호출 규약은 완전히 제거되었다. README가 “the V0 interface has been removed”라고 명시한다. 확인: fmgr_info_C_langapi_version 스위치 (fmgr.c:399–410)에 case 1만 있다. 그 외 값은 모두 elog(ERROR, "unrecognized function API version: %d")로 떨어진다. case 0 분기가 없다.

  • fmgr_security_definer는 외부 fcinfo에 투명하다. fmgr.c:738–777에서 확인. fcinfo->flinfo를 저장하고, 캐시된 내부 FmgrInfo로 교체한 뒤, FunctionCallInvoke로 호출하고, 성공 경로와 PG_CATCH 경로 모두에서 fcinfo->flinfo를 복원한다. 호출자의 인수가 담긴 외부 fcinfo는 변경 없이 전달되며, flinfo만 실행 시간 동안 교체된다.

  • PG_MODULE_MAGIC_EXT는 PG18에서 추가된 것이다. 인수 없이 쓰는 PG_MODULE_MAGIC_EXT()는 기존 PG_MODULE_MAGIC과 동일하다. fmgr.h:478–495에서 확인. Pg_magic_structnameversion 필드가 있으나 기존 매크로를 쓰면 NULL로 유지된다. 이 필드들은 ABI 검사에서 비교되지 않는다. 정보 제공용이다.

  • errsave/ereturn을 통한 소프트 오류는 호출자가 fcinfo->contextErrorSaveContext *를 전달해야만 활성화된다. README §“Handling Soft Errors”에서 확인. context가 NULL이거나 ErrorSaveContext가 아니면 errsaveereport(ERROR)를 호출한다. 옵트인하지 않는 호출자에게는 동작 변화가 없다.

  1. fmgr_hook / needs_fmgr_hook 플러그인 API. fmgr.c:39–40에 두 전역 함수 포인터(fmgr_hook, needs_fmgr_hook)가 선언되어 있지만 인트리 호출자가 없다. 확장 전용 포인트다. 이 훅을 설정하는 플러그인이 해야 할 일의 문서는 fmgr.c의 헤더 주석에만 있다. 조사 경로: src/include/fmgr.h에서 fmgr_hook_type을 검색하고 호출 지점을 추적. 이 훅을 사용하는 서드파티 확장(예: pg_hint_plan) 사례를 참고.

  2. CallerFInfoFunctionCall1/2 사용 사례. 호출자가 제공한 FmgrInfo *를 알려진 PGFunction에 전달하는 이 함수들의 주석에는 “DirectFunctionCall 함수와 유사하지만 FmgrInfo가 제공된다”고만 나온다. 인트리 사용자는 array_maputils/adt/ 몇 곳뿐이다. 일반적인 호출 지점에서 DirectFunctionCall 대비 성능 이점이 있는지는 문서화되어 있지 않으며, README도 다루지 않는다.

  3. fn_expr 설정의 일관성. FmgrInfo.fn_expr은 “호출에 대한 표현식 파스 트리, 또는 NULL”로 설명되며 “함수가 아닌 인수에 관한 정보”라고 명시된다. 실행기의 다양한 플랜 노드 타입(스캔 노드 대 프로젝션 대 집계 전이)에 걸쳐 이 필드가 얼마나 일관되게 설정되는지는 여기서 검증되지 않았다. 조사 경로: executor/에서 fmgr_info_set_expr 탐색.

PostgreSQL 너머 — 비교 설계와 연구 프론티어

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”
  • Oracle의 call-spec과 PL/SQL 함수 매니저. Oracle의 CALL_SPEC 메커니즘과 PL/SQL 엔진 → 네이티브 컴파일 파이프라인은 동일한 조회 비용/타입 안전성 긴장을 다룬다. Oracle의 DETERMINISTICRESULT_CACHE 함수 속성은 PostgreSQL의 strict + provolatile 플래그에 대응한다. 나란히 비교하면 PostgreSQL 플래너가 캐시할 수 있는 것과 없는 것이 명확해진다.

  • SQL Server의 CLR 통합과 함수 ABI. SQL Server의 CLR 통합은 ABI 검증에 .NET 메타데이터를 사용한다. PostgreSQL의 PG_MODULE_MAGIC보다 풍부한 버전이다. 관리/비관리 경계는 PostgreSQL의 fmgr_security_definer 패턴과 흥미로운 비교점이 된다.

  • MySQL의 UDF ABI (xxx_init / xxx / xxx_deinit). MySQL의 세 함수 UDF API (init, main, deinit)는 PostgreSQL의 fn_extra per-call 캐시 슬롯에 직접 대응한다. 둘 다 “한 번 파싱, 여러 번 실행” 문제를 해결한다. MySQL의 ABI에는 module-magic 유사물이 없어 버전 불일치 시 오류 대신 크래시가 발생한다.

  • DuckDB의 함수 등록 API. DuckDB의 ScalarFunction / TableFunction C++ API는 동일한 두 단계 패턴(메타데이터와 함께 등록 → DataChunk로 호출)을 더 높은 추상화 수준으로 노출한다. 스칼라 Datum 대신 벡터화된 청크를 사용한다는 점이 차이다. PostgreSQL의 SRF 구체화 모드와 비교하면 per-tuple 대 배치 벡터화 트레이드오프를 드러낼 수 있다. PostgreSQL 자체 벡터화 연구 방향과도 연관된다.

  • 확장 ABI 설계의 “INIT 함수” 접근법. PostgreSQL의 _PG_init 훅 (fmgr.h:434PGDLLEXPORT 선언)은 라이브러리 로드당 한 번 호출되며, background worker, 훅, 커스텀 락 매니저를 등록하는 표준 위치다. Linux의 module_init / module_exit과 유사한 패턴이다. DuckDB의 LoadInternal이나 SQLite의 sqlite3_auto_extension과 비교하면 postgres-extensions.md 문서에 유용한 내용이 된다.

  • src/backend/utils/fmgr/README — V1 FCI의 1차 설계 문서. FmgrInfo, FunctionCallInfo, 호출 컨텍스트, TOAST 처리, SRF 모드, 소프트 오류, 함수 핸들러 노트에 대한 권위 있는 설명.
  • Database System Concepts (Silberschatz, Korth & Sudarshan, 7판) — §9.5 “Accessing SQL from a Programming Language”와 §27 “PostgreSQL”.
  • src/backend/utils/fmgr/fmgr.c — 조회 파이프라인, 호출 헬퍼, CFuncHash, fmgr_security_definer.
  • src/backend/utils/fmgr/funcapi.c — SRF 지원 (InitMaterializedSRF, MultiFuncCall, get_call_result_type).
  • src/backend/utils/fmgr/dfmgr.c — 동적 라이브러리 로더 (dlopen 캐시, load_external_function, Pg_magic_func ABI 검사).
  • src/include/fmgr.h — fmgr 사용자를 위한 모든 공개 타입, 매크로, 함수 선언.
  • src/include/utils/fmgrtab.hFmgrBuiltin, fmgr_builtins[], fmgr_builtin_oid_index[].
  • src/include/funcapi.hReturnSetInfo, FuncCallContext, TypeFuncClass, SRF 헬퍼 선언.