C-Language functions for PostgreSQL / C-функции для PostgreSQL

Автор: Иван Золотухин


Интро

Данная статья содержит материалы, посвященные написанию на C функций для PostgreSQL - оказывается, делать это довольно легко, ну а бонусов вы получаете существенно больше, чем если бы вы писали всю ту же логику на процедурных языках. Скомпиленные в shared object функции затем можно будет загрузить в PostgreSQL и использовать по своему усмотрению как SQL команды, например.

Простейшие функции можно посмотреть и в мануале (заходите сюда только после изучения этого документа). Да, еще обязательно посмотрите на туториал с OSCON-2004 под названием "Power PostgreSQL: Extending Database with C".

Но если вы хотите чего-то большего, то мануал быстро перестанет вас устраивать. У меня лично с ходу во всем разобраться не очень получилось - так что эта статья посвящена таким, как я :)

Важное предупреждение 1: функции написаны без использования best practices, это всего лишь работающие черновики (даже нет обработки нулевого result set-а, который может вернуть SPI_execute, например). Автор особо не парился над внешним видом своего кода, поэтому нельзя считать его готовым продакшн-вариантом.

Важное предупреждение 2: Статья имеет вид, далекий от финального (см. Важное предупреждение 1), следовательно статья будет меняться. Я только что получил лист замечаний от Федора Сигаева (за что ему большое спасибо!), который знает о сишных функциях для Постгреса всё (в отличие от меня), так что я попытаюсь учесть его комментарии. To be continued, одним словом.


Пишем Set Returning Function

Итак, попробуем написать простейшую Set Returning Function (функцию, возвращающую сет значений, result set) - у нас она будет брать на вход инт с длиной сета и на выходе будет возвращать сет значений от единицы до заданного числа (вот так просто и тупо). Такая тупость выбрана не случайно - в функции нет абсолютно никаких наворотов кроме каркаса для демонстрации multicall-работы SRF. Напомню, что в стандартном случае, SRF функция работает в режиме value-per-call, то есть она за каждый вызов возвращает только одно значение и вызывается столько раз, сколько рядов у нас в result set-е. Между вызовами информация хранится в специальной структуре - контексте функции.

#include "postgres.h" // main include file (include always)
#include "fmgr.h" // "Function Manager" for V1 style
#include "funcapi.h" // to return set of rows

/* Version 1 Calling Conventions - так нужно писать все функции теперь */
PG_FUNCTION_INFO_V1(iz_test);

Datum
iz_test(PG_FUNCTION_ARGS)
{
/* Тот самый контекст функции */
FuncCallContext *funcctx;
/* Тоже нужно для multicall persistence */
MemoryContext oldcontext;

/* заходим сюда только в первом вызове функции */
if (SRF_IS_FIRSTCALL()) {
/*
* инициализация структуры-контекста фунции для
* хранения информации между вызовами
*/
funcctx = SRF_FIRSTCALL_INIT();
/*
* переключаем контекст памяти на контекст, сохраняемый между вызовами
*/
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/*
* говорим, что функцию нужно дергать столько раз, сколько указано
* в ее первом аргументе
*/
funcctx->max_calls = PG_GETARG_INT32(0);

MemoryContextSwitchTo(oldcontext);
}
/* код, который исполняется при каждом вызове функции */
funcctx = SRF_PERCALL_SETUP(); // контекст функции освежили
if (funcctx->call_cntr < funcctx->max_calls) {
/*
* Это, собственно, возвращение каждого item-а
* Обратите внимание, что SRF_RETURN_NEXT в качестве аргументов
* принимает контекст функции для его обновления (хотя бы даже
* счетчик передвинуть) и собственно то, что нужно вернуть, только
* в виде Datum-а, который мы тут и делаем из инта
*/
SRF_RETURN_NEXT(funcctx, Int32GetDatum(funcctx->call_cntr));
} else {
// так нужно все заканчивать
SRF_RETURN_DONE(funcctx);
}
}

Дальше вы должны сами скомпилить это дело в .so (см. ниже про компиляцию с помощью Makefile), положить Постгресу в нужное место и просто создать эту функцию:

CREATE OR REPLACE FUNCTION
iz_test(integer) RETURNS setof int4 AS
'/usr/lib/pgsql/c-func_test.so', 'iz_test'
LANGUAGE C
STRICT;

Потом все будет выглядеть приблизительно вот так:

test_db=# select iz_test(10);
iz_test
---------
1
2
3
4
5
6
7
8
9
10
(10 rows)

Time: 0.300 ms

Простейшее использование Server Processing Interface

Теперь попробуем использовать Server Processing Interface (SPI) для того, чтобы мы могли выполнять SQL-запросы. Каркас multicall функции трогать не будем, просто добавим туда работу со SPI_* функциями. Известные по первому примеру места я уже не комментирую.

#include "postgres.h" // main include file (include always)
#include "fmgr.h" // "Function Manager" for V1 style
#include "executor/spi.h" // Server Processing Interface
#include "funcapi.h" // to return set of rows and cope with tuples
#include <string.h>
#include <stdio.h>
#include <stdlib.h> // мы будем использовать atoi()

PG_FUNCTION_INFO_V1(get_level1_c);

Datum
get_level1_c(PG_FUNCTION_ARGS)
{
int32 pid = PG_GETARG_INT32(0);

int spi_ret;
char sql[100]; // не будем особо париться, пишем чтобы работало
char *tupval;
FuncCallContext *funcctx;
MemoryContext oldcontext;
Datum result;

if (SRF_IS_FIRSTCALL()) {
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

/* Готовимся выполнять запрос */
SPI_connect(); // функция коннекта
// непосредственно сама строка запроса
snprintf(sql, sizeof(sql), "SELECT edge_pid2 FROM edge WHERE edge_pid1 = %d AND edge_pid2 <> %d", pid, pid);
/*
* выполняем запрос. 0 в качестве третьего аргумента означает,
* что нужно обработать все туплы
*/
spi_ret = SPI_execute(sql, true, 0);
/*
* наша функция будет вызвана столько раз, сколько туплов в
* нашем результате
*/
funcctx->max_calls = SPI_processed;

MemoryContextSwitchTo(oldcontext);
}

funcctx = SRF_PERCALL_SETUP();

if (funcctx->call_cntr < funcctx->max_calls) {
/*
* Получаем строковое значение из текущего тупла
* Обратите внимание, хотя мы выбирали в запросе всего одну колонку,
* ее индекс равен 1, а не 0! Я лично потратил пару часов, чтобы это
* понять, не повторяйте моих ошибок
*/
tupval = SPI_getvalue(SPI_tuptable->vals[funcctx->call_cntr], SPI_tuptable->tupdesc, 1);
// дальше тривиально - просто делаем инт из строки и в датум его
result = Int32GetDatum(atoi(tupval));
SRF_RETURN_NEXT(funcctx, result);
} else {
SPI_finish();
SRF_RETURN_DONE(funcctx);
}

}

Отладка (debug) кода

Отлаживать собственный код можно, пользуясь, например, макросом elog и выводя в виде NOTICE-ов содержание каких-либо переменных. Например:

elog(NOTICE, "My name is %s, I'm %d years old", "Vanya", 21);

По правильному замечанию Сергея Копосова, elog является устаревшим способом репорта об ошибках, правильнее использовать ereport, с помощью которого можно написать полностью эквивалентную конструкцию (подробнее читайте в документации):

ereport(NOTICE, (errmsg_internal("My name is %s, I'm %d years old", "Vanya", 21)));

Но если хочется делать все по-взрослому, то нужно использовать стандартный GNU debugger (gdb). Так как отлаживать мы будем процесс postmaster, нужно иметь привилегии пользователя, под которым он запущен. Итак, последовательность действий такова:

  1. Стартуем какого-либо клиента, например psql
  2. Из клиента выполняем SELECT pg_backend_pid(), узнавая тем самым pid процесса postmaster (это можно сделать и утилитой ps из командной строки)
  3. Загружаем из клиента нашу динамическую библиотеку: LOAD '/usr/lib/pgsql/c-func_test.so'
  4. В другой терминальной сессии под рутом, либо под хозяином postmaster-а (назовем его postgres, как это бывает обычно) стартуем дебаггер: gdb postgres server-process-id
  5. Ставим breakpoint в нашей функции: (gdb) break my-function
  6. Идем в назад в клиента и запускаем нашу функцию: SELECT my-function()
  7. Нажимаем continue в дебаггере: (gdb) c или читаем help, если мы в нем первый раз оказались: (gdb) help

Компиляция кода с помощью Makefile

Для начала вам подойдет простейший Makefile:

# Makefile for building C-functions shared objects for PostgreSQL
# Author: IZ
# Date: 2005/10/20

SERVER_INCLUDES += -I $(shell pg_config --includedir)
SERVER_INCLUDES += -I $(shell pg_config --includedir-server)

CFLAGS += -g $(SERVER_INCLUDES)


.SUFFIXES: .so

.c.so:
$(CC) $(CFLAGS) -fpic -c $<
$(CC) $(CFLAGS) -shared -o $@ $(basename $<).o
@echo Built!

clean:
-rm -f *.o *.so *~ 2>/dev/null
@echo Cleaned!

Затем просто запускаем make c-func_test.so и нам делается правильный со-шник в текущей директории. После этой процедуры уже можно делать LOAD в клиенте psql.

Важное примечание: уж не знаю по какой причине, но если я в качестве header-файлов беру те, что содержатся в pg_config --includedir-server, то есть из глобальных путей, при попытке загрузки so-шника в постгрес появляются сообщения вида undefined symbol: elog. Если при этом брать header-файлы из исходников постгреса, то есть указывать глобальные пути к ним, например, указывая в качестве ключа компилятору -I /home/iz/postgresql-8.0.2/src/include/, то определение elog-а найдется и все будет работать. Уж не знаю, почему при установке постгреса .h-файлы так неприятно обрезаются :(

 

Замечания

1. У вас не получится писать рекурсивные функции. Посмотрите на аргументы и возвращаемые значения, их типы и т.д. и догадайтесь сами, почему не получится.

2. Меня поразило отсутствие нормальной девелоперской документации к этой разработке. Я не считаю себе гуру в сишном программировании, скорее наоборот, но разве нельзя было как-нибудь по-нормальному все задокументировать?

Any feedback is welcome at iz at sai dot msu dot ru
Oct 2005

Вернуться к списку статей