36.10. Funções na linguagem C #

36.10.1. Carregamento dinâmico
36.10.2. Tipos de dados base em funções na linguagem C
36.10.3. Convenções de chamada versão 1
36.10.4. Escrita de código
36.10.5. Compilação e ligação de funções carregadas dinamicamente
36.10.6. Orientações sobre a estabilidade da API e da ABI do servidor
36.10.7. Argumentos de tipo de dados composto
36.10.8. Retorno de linhas (tipos compostos)
36.10.9. Retorno de conjuntos
36.10.10. Argumentos polimórficos e tipos de dados de retorno
36.10.11. Memória compartilhada
36.10.12. LWLocks
36.10.13. Eventos de espera personalizados
36.10.14. Pontos de injeção
36.10.15. Estatísticas cumulativas personalizadas
36.10.16. Uso do C++ para extensibilidade

As funções definidas pelo usuário podem ser escritas em C (ou em uma linguagem compatível com C, como C++). Estas funções são compiladas em objetos dinamicamente carregáveis (também chamados de bibliotecas compartilhadas), carregados pelo servidor sob demanda. O recurso de carregamento dinâmico é o que distingue as funções na linguagem C das funções internas — as convenções de codificação usadas são essencialmente as mesmas para as duas. (Portanto, a biblioteca de funções internas padrão é uma rica fonte de exemplos de codificação para funções C definidas pelo usuário.)

No momento, é usada apenas uma convenção de chamada para funções escritas em C (versão 1). O suporte para esta convenção de chamada é indicado escrevendo a chamada da macro PG_FUNCTION_INFO_V1() na função C, conforme mostrado abaixo.

36.10.1. Carregamento dinâmico #

Na primeira vez que a função definida pelo usuário, em um determinado arquivo objeto carregável, é chamada em uma sessão, o carregador dinâmico carrega este arquivo objeto na memória para que a função possa ser chamada. O comando CREATE FUNCTION para uma função C definida pelo usuário deve, portanto, especificar duas informações para a função: o nome do arquivo objeto carregável, e o nome C (símbolo de ligação) da função específica a ser chamada neste arquivo objeto. Se o nome C não for especificado explicitamente, presume-se que seja idêntico ao nome da função SQL.

O seguinte algoritmo é usado para localizar o arquivo objeto compartilhado com base no nome fornecido no comando CREATE FUNCTION:

  1. Se o nome for um caminho absoluto, o arquivo indicado será carregado.

  2. Se o nome começar com a cadeia de caracteres $libdir, esta parte será substituída pelo nome do diretório da biblioteca de pacotes do PostgreSQL, determinado em tempo de construção.

  3. Se o nome não contiver a parte do diretório, o arquivo é procurado no caminho especificado pela variável de configuração dynamic_library_path.

  4. Caso contrário (o arquivo não foi encontrado no caminho, ou a parte do diretório não é absoluta), o carregador dinâmico tentará usar o nome fornecido, o que provavelmente vai falhar. (Não é confiável depender do diretório de trabalho atual.)

Se esta sequência não funcionar, a extensão de nome de arquivo de biblioteca compartilhada específica da plataforma (geralmente .so) será anexada ao nome fornecido, e esta sequência será tentada novamente. Se isto também falhar, a carga falhará.

Recomenda-se localizar as bibliotecas compartilhadas em relação a $libdir, ou através do caminho de biblioteca dinâmica. Isto simplifica as atualizações de versão, se a nova instalação estiver em um local diferente. O diretório real representado por $libdir pode ser encontrado com o comando pg_config --pkglibdir.

O ID de usuário que o servidor PostgreSQL executa deve poder percorrer o caminho para o arquivo que se pretende carregar. Tornar o arquivo, ou um diretório de nível superior, não legível e/ou não executável pelo usuário postgres, é um erro comum.

Em qualquer um dos casos, o nome do arquivo fornecido no comando CREATE FUNCTION é registrado literalmente nos catálogos do sistema, portanto, se o arquivo precisar ser carregado novamente, o mesmo procedimento será aplicado.

Nota

O PostgreSQL não compila uma função C automaticamente. O arquivo-fonte deve ser compilado antes de ser referenciado em um comando CREATE FUNCTION. Veja Compilação e ligação de funções carregadas dinamicamente para obter informações adicionais.

Para garantir que o arquivo objeto carregado dinamicamente não seja carregado em um servidor incompatível, o PostgreSQL verifica se o arquivo contém o bloco mágico com o conteúdo apropriado. O bloco mágico permite que o servidor detecte incompatibilidades óbvias, como código compilado para uma versão principal do PostgreSQL diferente. Para incluir o bloco mágico, escreva-o em um (e apenas um) dos arquivos-fonte do módulo, após ter incluído o cabeçalho fmgr.h:

PG_MODULE_MAGIC;

ou

PG_MODULE_MAGIC_EXT(parameters);

A variante PG_MODULE_MAGIC_EXT permite a especificação de informações adicionais sobre o módulo; No momento, é possível adicionar um nome e/ou uma cadeia de caracteres de versão. (Mais campos poderão ser permitidos no futuro.) Deve ser escrito algo como isto:

PG_MODULE_MAGIC_EXT(
    .name = "nome_do_meu_módulo",
    .version = "1.2.3"
);

Em seguida, o nome e a versão podem ser examinados através da função pg_get_loaded_modules(). O significado da cadeia de caracteres de versão não é restrito pelo PostgreSQL, mas se recomenda o uso de regras semânticas de versão.

Após ser usado pela primeira vez, o arquivo objeto carregado dinamicamente permanece retido na memória. Chamadas futuras na mesma sessão para a(s) função(ões) neste arquivo incorrerão apenas na pequena sobrecarga de uma procura na tabela de símbolos. Se for necessário forçar o recarregamento de um arquivo objeto, por exemplo, após recompilá-lo, deve ser iniciada uma nova sessão.

Opcionalmente, um arquivo carregado dinamicamente pode conter uma função de inicialização. Se o arquivo incluir uma função chamada _PG_init, esta função será chamada imediatamente após o carregamento do arquivo. Esta função não recebe nenhum parâmetros e deve retornar void. No momento não existe forma de descarregar um arquivo carregado dinamicamente.

36.10.2. Tipos de dados base em funções na linguagem C #

Para saber como escrever funções na linguagem C, é necessário saber como o PostgreSQL representa internamente os tipos de dados base, e como eles podem ser passados de e para as funções. Internamente, o PostgreSQL considera um tipo de dados base como um blob de memória. Por sua vez, as funções definidas pelo usuário definidas sobre um tipo de dados, definem a maneira como o PostgreSQL pode operar neste tipo de dados. Ou seja, o PostgreSQL apenas armazena e recupera os dados do disco, e usa as funções definidas pelo usuário para inserir, processar e enviar os dados.

Os tipos de dados base podem ter um dos três formatos internos:

  • passado por valor, comprimento fixo

  • passado por referência, comprimento fixo

  • passado por referência, comprimento variável

Os tipos de dados passados por valor podem ter apenas 1, 2 ou 4 bytes de comprimento (também 8 bytes, se sizeof(Datum) for 8 na máquina). Deve-se ter o cuidado de definir os tipos de dados de forma que tenham o mesmo tamanho (em bytes) em todas as arquiteturas. Por exemplo, o tipo de dados long é perigoso, porque tem 4 bytes em algumas máquinas e 8 bytes em outras, enquanto o tipo de dados int tem 4 bytes na maioria das máquinas Unix. Uma implementação razoável do tipo de dados int4 em máquinas Unix pode ser:

/* inteiro de 4 bytes, passado por valor */
typedef int int4;

(O código real C do PostgreSQL chama este tipo de dados de int32, porque há uma convenção na linguagem C que intXX significa XX bits Da mesma forma, note também que o tipo de dados C int8 tem o tamanho de 1 byte. O tipo de dados SQL int8 é chamado de int64 na linguagem C. Veja também Tabela 36.2.) [115]

Por outro lado, os tipos de dados de comprimento fixo de qualquer tamanho podem ser passados por referência. Por exemplo, aqui está um exemplo de implementação de um tipo de dados do PostgreSQL:

/* estrutura de 16 bytes, passada por referência */
typedef struct
{
    double  x, y;
} Point;

Somente podem ser usados ponteiros para estes tipos de dados ao passá-los dentro e fora das funções do PostgreSQL. Para retornar um valor desse tipo de dados, deve ser alocada a quantidade certa de memória com palloc, preenchida a memória alocada, e retornado um ponteiro para ela. (Além disso, se for desejado retornar apenas o mesmo valor de um dos argumentos de entrada que é do mesmo tipo de dados, pode ser saltado o palloc extra, e apenas retornado um ponteiro para o valor de entrada.)

Finalmente, todos os tipos de dados de comprimento variável também devem ser passados por referência, e devem começar com um campo de tamanho opaco de exatamente 4 bytes, que será definido por SET_VARSIZE; nunca defina este campo diretamente! Todos os dados a serem armazenados dentro desse tipo de dados devem estar localizados na memória imediatamente após este campo de comprimento. O campo de comprimento contém o comprimento total da estrutura, ou seja, inclui o tamanho do próprio campo de comprimento.

Outro ponto importante é evitar deixar bits não inicializados dentro dos valores de tipo de dados; por exemplo, cuide para zerar quaisquer bytes de preenchimento de alinhamento que possam estar presentes em struct. Sem isto, constantes logicamente equivalentes de seu tipo de dados podem ser vistas como desiguais pelo planejador, levando a planos ineficientes (embora não incorretos).

Atenção

Nunca modifique o conteúdo de um valor de entrada passado por referência. Se isto for feito, provavelmente os dados no disco serão corrompidos, porque o ponteiro recebido pode apontar diretamente para um buffer de disco. A única exceção a esta regra é explicada em Agregações definidas pelo usuário.

Como exemplo, o tipo de dados text pode ser definido da seguinte forma:

typedef struct {
    int32 length;
    char data[FLEXIBLE_ARRAY_MEMBER];
} text;

A notação [FLEXIBLE_ARRAY_MEMBER] significa que o comprimento real da parte de dados não é especificado por esta declaração.

Ao manusear tipos de dados de comprimento variável, deve-se cuidar para alocar a quantidade correta de memória, e definir o campo de comprimento corretamente. Por exemplo, se quisermos armazenar 40 bytes em uma estrutura text, podemos usar um fragmento de código como este:

#include "postgres.h"
...
char buffer[40]; /* dados armazenados */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ é o mesmo que sizeof(int32), mas é considerado um bom estilo usar a macro VARHDRSZ para se referir ao tamanho da sobrecarga para um tipo de dados de comprimento variável. Além disso, o campo de comprimento deve ser definido usando a macro SET_VARSIZE, e não por simples atribuição.

A Tabela 36.2 mostra os tipos de dados C correspondentes a muitos dos tipos de dados SQL nativos do PostgreSQL. A coluna Definido em indica o arquivo de cabeçalho que precisa ser incluído para obter a definição do tipo de dados. (A definição real pode estar em um arquivo diferente incluído no arquivo listado. Recomenda-se que os usuários sigam a interface definida.) Note que se deve sempre incluir postgres.h primeiro em qualquer arquivo-fonte do código do servidor, porque este arquivo declara uma série de coisas sempre necessárias, e porque incluir outros cabeçalhos primeiro pode causar problemas de portabilidade.

Tabela 36.2. Tipos C equivalentes para tipos de dados SQL integrados

Tipo SQL Tipo C Definido em
booleanboolpostgres.h (talvez integrado ao compilador)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(nativo do compilador)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
float4 (real)float4postgres.h
float8 (double precision)float8postgres.h
int2 (smallint)int16postgres.h
int4 (integer)int32postgres.h
int8 (bigint)int64postgres.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
numericNumericutils/numeric.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocRegProcedurepostgres.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
timestampTimestampdatatype/timestamp.h
timestamp with time zoneTimestampTzdatatype/timestamp.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

Agora que foram examinadas todas as estruturas possíveis para os tipos de dados base, podem ser mostrados alguns exemplos de funções

36.10.3. Convenções de chamada versão 1 #

A convenção de chamada versão 1 conta com macros para suprimir grande parte da complexidade de passar argumentos e resultados. A declaração C de uma função versão 1 é sempre:

Datum funcname(PG_FUNCTION_ARGS)

Além disso, a chamada de macro

PG_FUNCTION_INFO_V1(funcname);

deve aparecer no mesmo arquivo-fonte. (Por convenção, é escrito logo antes da própria função.) Esta chamada de macro não é necessária para funções na linguagem internal, porque o PostgreSQL assume que todas as funções internas usam a convenção versão 1. Entretanto, é necessário para funções carregadas dinamicamente.

Em uma função versão 1, cada argumento real é obtido usando a macro PG_GETARG_xxx() correspondente ao tipo de dados do argumento. (Em funções não estritas, deve haver uma verificação prévia sobre a nulidade do argumento, usando PG_ARGISNULL(); veja abaixo.) O resultado é retornado usando a macro PG_RETURN_xxx() para o tipo de dados retornado. PG_GETARG_xxx() recebe como argumento o número do argumento da função a ser buscado, onde a contagem começa em 0. PG_RETURN_xxx() recebe como argumento o valor real a ser retornado.

Para chamar outra função da versão 1, pode-se usar DirectFunctionCalln(func, arg1, ..., argn). Isto é particularmente útil quando se deseja chamar funções definidas na biblioteca interna padrão, usando uma interface semelhante à sua assinatura SQL.

Estas funções de conveniência e outras semelhantes podem ser encontradas no arquivo fmgr.h. A família de funções DirectFunctionCalln esperam que o primeiro argumento seja o nome de uma função escrita em C. Existe também a função OidFunctionCalln que recebe o OID da função alvo, além de algumas outras variantes. Todas estas opções esperam que os argumentos da função sejam fornecidos como Datum, e da mesma forma elas retornam Datum. Observe que nem os argumentos nem o resultado podem ser NULL ao usar estas funções auxiliares.

Por exemplo, para chamar a função starts_with(text, text) a partir do C, pode-se pesquisar no catálogo e descobrir que sua implementação em C é a função. Datum text_starts_with(PG_FUNCTION_ARGS) Normalmente se usaria DirectFunctionCall2(text_starts_with, ...) para chamar esta função. Entretanto, starts_with(text, text) requer informação de ordenação, portanto irá falhará com a mensagem não foi possível determinar qual ordenação usar para a comparação de cadeias de caracteres. se for chamada dessa maneira. Em vez disso, deve-se usar DirectFunctionCall2Coll(text_starts_with, ...) e fornecer a ordenação desejada, que normalmente é simplesmente repassada de PG_GET_COLLATION(), como mostrado no exemplo abaixo.

O arquivo fmgr.h também fornece macros que facilitam conversões entre tipos de dados C e Datum. Por exemplo, para converter Datum em text*, pode-se usar DatumGetTextPP(X). Embora alguns tipos de dados tenham macros com nomes como TypeGetDatum(X) para a conversão inversa, text* não tem; é suficiente usar a macro genérica PointerGetDatum(X) para isto. Se a extensão definir tipos de dados adicionais, geralmente é conveniente definir macros semelhantes também para estes tipos de dados.

A seguir estão alguns exemplos usando a convenção de chamada versão 1:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include "varatt.h"

PG_MODULE_MAGIC;

/* por valor */

PG_FUNCTION_INFO_V1(add_one);

Datum
add_one(PG_FUNCTION_ARGS)
{
    int32   arg = PG_GETARG_INT32(0);

    PG_RETURN_INT32(arg + 1);
}

/* por referência, comprimento fixo */

PG_FUNCTION_INFO_V1(add_one_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* As macros para FLOAT8 ocultam sua natureza de passagem por referência. */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* Aqui, a natureza de passagem por referência de Point não é ocultada. */
    Point     *pointx = PG_GETARG_POINT_P(0);
    Point     *pointy = PG_GETARG_POINT_P(1);
    Point     *new_point = (Point *) palloc(sizeof(Point));

    new_point->x = pointx->x;
    new_point->y = pointy->y;

    PG_RETURN_POINT_P(new_point);
}

/* por referência, comprimento variável */

PG_FUNCTION_INFO_V1(copytext);

Datum
copytext(PG_FUNCTION_ARGS)
{
    text     *t = PG_GETARG_TEXT_PP(0);

    /*
     * VARSIZE_ANY_EXHDR é o tamanho da estrutura em bytes, menos
     * VARHDRSZ ou VARHDRSZ_SHORT de seu cabeçalho.
     * Construir a cópia com comprimento integral do cabeçalhp.
     */
    text     *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
    SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);

    /*
     * VARDATA é um ponteiro para a região de dados da nova estrutura.
     * A fonte pode ser um dado curto, portanto seus dados devem
     * ser recuperados através de VARDATA_ANY.
     */
    memcpy(VARDATA(new_t),          /* destino */
           VARDATA_ANY(t),          /* origem */
           VARSIZE_ANY_EXHDR(t));   /* quantidade de bytes */
    PG_RETURN_TEXT_P(new_t);
}

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_PP(0);
    text  *arg2 = PG_GETARG_TEXT_PP(1);
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    SET_VARSIZE(new_text, new_text_size);
    memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
    memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
    PG_RETURN_TEXT_P(new_text);
}

/* Um invólucro em torno de starts_with(text, text) */

PG_FUNCTION_INFO_V1(t_starts_with);

Datum
t_starts_with(PG_FUNCTION_ARGS)
{
    text       *t1 = PG_GETARG_TEXT_PP(0);
    text       *t2 = PG_GETARG_TEXT_PP(1);
    Oid         collid = PG_GET_COLLATION();
    bool        result;

    result = DatumGetBool(DirectFunctionCall2Coll(text_starts_with,
                                                  collid,
                                                  PointerGetDatum(t1),
                                                  PointerGetDatum(t2)));
    PG_RETURN_BOOL(result);
}

Supondo que o código acima tenha sido editado no arquivo funcs.c [116], e compilado em um objeto compartilhado, as funções para o PostgreSQL podem ser definidas com comandos como estes:

CREATE FUNCTION add_one(integer) RETURNS integer
     AS 'DIRECTORY/funcs', 'add_one'
     LANGUAGE C STRICT;

-- note a sobrecarga do nome da função SQL "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
     AS 'DIRECTORY/funcs', 'add_one_float8'
     LANGUAGE C STRICT;

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'DIRECTORY/funcs', 'makepoint'
     LANGUAGE C STRICT;

CREATE FUNCTION copytext(text) RETURNS text
     AS 'DIRECTORY/funcs', 'copytext'
     LANGUAGE C STRICT;

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'DIRECTORY/funcs', 'concat_text'
     LANGUAGE C STRICT;

CREATE FUNCTION t_starts_with(text, text) RETURNS boolean
     AS 'DIRECTORY/funcs', 't_starts_with'
     LANGUAGE C STRICT;

Nos comandos acima, DIRECTORY representa o diretório dos arquivos de biblioteca compartilhada (por exemplo, o diretório src/inlude da distribuição do código-fonte do PostgreSQL). (Um estilo melhor seria usar apenas 'funcs' na cláusula AS, após adicionar DIRECTORY ao caminho de procura. Em qualquer um dos casos, pode ser omitida a extensão específica do sistema para biblioteca compartilhada, geralmente .so.)

Note que as funções foram especificadas como STRICT, significando que o sistema deve assumir automaticamente um resultado nulo se qualquer valor de entrada for nulo. Ao fazer isto, se evita ter que verificar entradas nulas no código da função. Sem isto, teríamos que verificar valores nulos explicitamente, usando PG_ARGISNULL().

A macro PG_ARGISNULL(n) permite a função testar se cada entrada é nula. (É claro que só há necessidade de fazer isto em funções não declaradas como STRICT.) Assim como nas macros PG_GETARG_xxx(), os argumentos de entrada são contados começando em zero. Note que se deve parar de executar PG_GETARG_xxx() até ser verificado que o argumento não é nulo. Para retornar um resultado nulo, execute PG_RETURN_NULL(); isto funciona em funções estritas e não estritas.

À primeira vista, as convenções de codificação versão 1 podem parecer apenas obscurantismo sem sentido, em comparação com o uso de convenções de chamada C simples. No entanto, estas convenções permitem lidar com valores de argumentos e valores retornados que podem ser nulos, e valores toast (compactados ou fora de linha).

Outras opções fornecidas pela interface versão 1 são duas variantes das macros PG_GETARG_xxx(). A primeira delas, PG_GETARG_xxx_COPY(), garante retornar uma cópia do argumento especificado que pode ser escrita com segurança. (Às vezes, as macros normais retornam um ponteiro para um valor que está fisicamente armazenado em uma tabela, onde não deve ser escrito. O uso das macros PG_GETARG_xxx_COPY() garante um resultado que pode ser escrito.) A segunda variante consiste nas macros PG_GETARG_xxx_SLICE() que recebem três argumentos. O primeiro é o número do argumento da função (como acima). O segundo e o terceiro argumentos são o deslocamento e o comprimento do segmento a ser retornado. Os deslocamentos são contados a partir de zero, e um comprimento negativo solicita que o restante do valor seja retornado. Estas macros fornecem acesso mais eficiente a partes de grandes valores no caso de terem o tipo de armazenamento external. (O tipo de armazenamento de uma coluna pode ser especificado usando ALTER TABLE nome_da_tabela ALTER COLUMN nome_da_coluna SET STORAGE tipo_de_armazenamento, onde tipo_de_armazenamento é um entre plain, external, extended, ou main.)

Finalmente, as convenções de chamada de função versão 1 permitem retornar resultados de conjunto (Retorno de conjuntos), implementar funções de gatilho (Gatilhos), e tratadores de chamadas em linguagem procedural (Escrita de tratador de linguagem procedural). Para obter mais detalhes veja o arquivo src/backend/utils/fmgr/README na distribuição do código-fonte.

36.10.4. Escrita de código #

Antes de seguirmos para tópicos mais avançados, devemos discutir algumas regras de codificação para as funções do PostgreSQL escritas na linguagem C. Embora seja possível carregar funções escritas em linguagens diferentes de C no PostgreSQL, isto é geralmente difícil (quando não impossível) porque outras linguagens, como C++, FORTRAN ou Pascal, geralmente não seguem a mesma convenção de chamada do C. Ou seja, outras linguagens não passam argumentos e retornam valores entre funções da mesma forma. Por este motivo, é assumido que suas funções em linguagem C são realmente escritas em C.

As regras básicas para escrever e construir funções C são as seguintes:

  • Use pg_config --includedir-server para descobrir onde os arquivos de cabeçalho do servidor PostgreSQL estão instalados em seu sistema (ou no sistema onde seus usuários estarão executando).

  • Compilar e vincular o código para poder ser carregado dinamicamente no PostgreSQL sempre requer sinalizadores especiais. Veja Compilação e ligação de funções carregadas dinamicamente para obter uma explicação detalhada sobre como fazer isto no seu sistema operacional específico.

  • Lembre-se de definir o bloco mágico para a biblioteca compartilhada, conforme descrito em Carregamento dinâmico.

  • Ao alocar memória, use as funções do PostgreSQL palloc e pfree em vez das funções correspondentes da biblioteca C malloc e free. A memória alocada por palloc será liberada automaticamente ao final de cada transação, evitando vazamento de memória.

  • Sempre devem ser zerados os bytes das estruturas usando memset (ou alocado com palloc0 antes de tudo). Mesmo atribuindo valor a cada campo da estrutura, pode haver preenchimento de alinhamento (buracos na estrutura) contendo valores inúteis. Sem isto, fica difícil oferecer suporte a índices de hash, ou junções de hash, porque devem ser selecionados apenas os bits significativos da estrutura de dados para calcular o hash. Às vezes, o planejador também depende da comparação de constantes por meio da igualdade bit a bit, portanto, podem ser obtidos resultados de planejamento indesejáveis se valores logicamente equivalentes não forem iguais bit a bit.

  • A maioria dos tipos de dados internos do PostgreSQL são declarados no arquivo postgres.h, enquanto as interfaces do gerenciador de função (PG_FUNCTION_ARGS, etc.) estão no arquivo fmgr.h, portanto é necessário incluir pelo menos estes dois arquivos. Por motivos de portabilidade, é melhor incluir o arquivo postgres.h primeiro, antes de qualquer outro arquivo de cabeçalho do sistema ou do usuário. Incluir postgres.h também inclui elog.h e palloc.h.

  • Os nomes dos símbolos definidos nos arquivos objeto não devem entrar em conflito entre si, ou com os símbolos definidos no executável do servidor PostgreSQL. Será necessário renomear as funções ou variáveis caso se receba mensagens de erro neste sentido.

36.10.5. Compilação e ligação de funções carregadas dinamicamente #

Antes que se possa usar as funções de extensão do PostgreSQL escritas em C, estas funções devem ser compiladas e vinculadas de uma maneira especial para produzir um arquivo que possa ser carregado dinamicamente pelo servidor. Falando precisamente, há necessidade de se criar uma biblioteca compartilhada.

Para obter informações além do que está contido nesta seção, deve ser lida a documentação do sistema operacional, em particular as páginas de manual do compilador C (cc), do vinculador (ld), e do vinculador/carregador dinâmico (ld.so). Além disso, o código-fonte do PostgreSQL contém vários exemplos funcionais no diretório contrib. Entretanto, se ficar dependente desses exemplos, tornará os módulos dependentes da disponibilidade do código-fonte do PostgreSQL.

A criação de bibliotecas compartilhadas é geralmente análoga à vinculação de executáveis: primeiro os arquivos contendo o código-fonte são compilados em arquivos objeto e, em seguida, os arquivos objeto são vinculados. Os arquivos objeto precisam ser criados como código independente de posição (position-independent code, PIC), significando conceitualmente que podem ser colocados em um local arbitrário na memória quando são carregados pelo executável. (Arquivos objeto destinados a executáveis geralmente não são compilados dessa maneira.) O comando para vincular uma biblioteca compartilhada contém sinalizadores especiais para distingui-lo da vinculação de um executável (pelo menos em teoria — em alguns sistemas a prática é muito mais feia).

Nos exemplos a seguir, é assumido que o código-fonte está no arquivo foo.c, e será criada a biblioteca compartilhada foo.so. O arquivo objeto intermediário será chamado foo.o, a menos que esteja indicado de outra forma. Uma biblioteca compartilhada pode conter mais de um arquivo objeto, mas será usado apenas um aqui.

FreeBSD

O sinalizador do compilador para criar PIC é -fPIC. Para criar bibliotecas compartilhadas, o sinalizador do compilador é -shared.

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o

Isto é aplicável a partir da versão 13.0 do FreeBSD, as versões mais antigas usavam o compilador gcc.

Linux

O sinalizador do compilador para criar PIC é -fPIC. O sinalizador do compilador para criar uma biblioteca compartilhada é -shared. Um exemplo completo se parece com:

cc -fPIC -c foo.c
cc -shared -o foo.so foo.o

macOS

A seguir está um exemplo que assume estarem instaladas as ferramentas do desenvolvedor.

cc -c foo.c
cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o

NetBSD

O sinalizador do compilador para criar PIC é -fPIC. Para sistemas ELF, é usado o compilador com o sinalizador -shared para vincular bibliotecas compartilhadas. Para os sistemas antigos, não-ELF, é usado ld -Bshareable.

gcc -fPIC -c foo.c
gcc -shared -o foo.so foo.o

OpenBSD

O sinalizador do compilador para criar PIC é -fPIC. É usado ld -Bshareable para vincular bibliotecas compartilhadas.

gcc -fPIC -c foo.c
ld -Bshareable -o foo.so foo.o

Solaris

O sinalizador do compilador para criar PIC é -KPIC com o compilador da SUN, e -fPIC com o GCC. Para vincular bibliotecas compartilhadas, a opção do compilador é -G nos dois compiladores, ou como alternativa -shared no GCC.

cc -KPIC -c foo.c
cc -G -o foo.so foo.o

ou

gcc -fPIC -c foo.c
gcc -G -o foo.so foo.o

Dica

Se achar isto muito complicado, considere usar a GNU Libtool, que esconde as diferenças de plataforma por trás de uma interface uniforme.

O arquivo de biblioteca compartilhada resultante pode ser então carregado no PostgreSQL. Ao especificar o nome do arquivo para o comando CREATE FUNCTION, deve-se fornecer o nome do arquivo da biblioteca compartilhada, e não o arquivo objeto intermediário. Note que a extensão de biblioteca compartilhada padrão do sistema (geralmente .so ou .sl) pode ser omitida do comando CREATE FUNCTION, e normalmente deve ser omitido para uma melhor portabilidade.

Veja em Carregamento dinâmico onde o servidor espera encontrar os arquivos de biblioteca compartilhada.

36.10.6. Orientações sobre a estabilidade da API e da ABI do servidor #

Esta seção contém orientações para autores de extensões e outros plugins de servidor sobre a estabilidade da API e da ABI no servidor PostgreSQL.

36.10.6.1. Geral #

O servidor PostgreSQL contém diversas APIs bem definidas para plugins de servidor, como o gerenciador de funções (fmgr, descrito neste capítulo), SPI (Interface de programação servidor), e vários ganchos projetados especificamente para extensões. Estas interfaces são cuidadosamente gerenciadas para garantir estabilidade e compatibilidade a longo prazo. Entretanto, todo o conjunto de funções e variáveis ​​globais no servidor constitui efetivamente a API publicamente utilizável, e a maior parte dela não foi projetada com a extensibilidade e a estabilidade a longo prazo em mente.

Portanto, embora aproveitar estas interfaces seja válido, quanto mais alguém se afasta do caminho já trilhado, maior a probabilidade de encontrar problemas de compatibilidade de API ou de ABI em algum momento. Os autores de extensões são incentivados a fornecer informações sobre seus requisitos, para que, com o tempo, à medida que novos padrões de uso surgirem, certas interfaces possam ser consideradas mais estáveis ​​ou novas interfaces, com melhor projeto, possam ser adicionadas.

36.10.6.2. Compatibilidade da API #

A API (Application Programming Interface), ou Interface de Programação de Aplicações, é a interface usada em tempo de compilação.

36.10.6.2.1. Versões principais #

Não há nenhuma garantia de compatibilidade de API entre as versões principais do PostgreSQL. Portanto, o código da extensão pode exigir alterações no código-fonte para funcionar com várias versões principais. Estes problemas geralmente podem ser gerenciados com condições de pré-processador, como #if PG_VERSION_NUM >= 160000. Extensões sofisticadas que utilizam interfaces além das bem definidas geralmente exigem algumas dessas alterações para cada versão principal do servidor.

36.10.6.2.2. Versões secundárias #

O PostgreSQL se esforça para evitar quebras na API do servidor em versões secundárias. Em geral, o código de extensão que compila e funciona com uma versão secundária também deve compilar e funcionar com qualquer outra versão secundária da mesma versão principal, passada ou futura.

Quando uma alteração for necessária, ela será cuidadosamente gerenciada, levando em consideração os requisitos das extensões. Estas alterações serão comunicadas nas notas de lançamento (Notas da versão).

36.10.6.3. Compatibilidade da ABI #

A ABI (Application Binary Interface), ou Interface Binária de Aplicação, é a interface usada em tempo de execução.

36.10.6.3.1. Versões principais #

Servidores de diferentes versões principais possuem ABIs intencionalmente incompatíveis. Portanto, as extensões que utilizam APIs de servidor devem ser recompiladas para cada versão principal. A inclusão do PG_MODULE_MAGIC (veja Carregamento dinâmico) garante que o código compilado para uma versão principal seja rejeitado por outras versões principais.

36.10.6.3.2. Versões secundárias #

O PostgreSQL se esforça para evitar quebras de ABI do servidor em versões secundárias. Em geral, uma extensão compilada para qualquer versão secundária deve funcionar com qualquer outra versão secundária da mesma versão principal, passada ou futura.

Quando uma alteração for necessária, o PostgreSQL escolherá a alteração menos invasiva possível, por exemplo, inserindo um novo campo no espaço de preenchimento ou anexando-o ao final de uma estrutura. Este tipo de alteração não deve afetar as extensões, a menos que usem padrões de código muito incomuns.

Porém, em casos raros, mesmo estas modificações minimamente invasivas podem se mostrar impraticáveis ​​ou impossíveis. Neste caso, a modificação será cuidadosamente gerenciada, levando em consideração as necessidades das extensões. Estas alterações também serão documentadas nas notas de lançamento (Notas da versão).

Observe, no entanto, que muitas partes do servidor não são projetadas ou mantidas como APIs para acesso público (e que, na maioria dos casos, o limite real também não está bem definido). Caso surjam necessidades urgentes, as alterações nessas partes serão naturalmente feitas com menos consideração pelo código da extensão do que as alterações em interfaces bem definidas e amplamente utilizadas.

Além disso, na ausência de detecção automatizada dessas alterações, isto não é uma garantia, mas historicamente estas alterações que causam incompatibilidade têm sido extremamente raras.

36.10.7. Argumentos de tipo de dados composto #

Os tipos de dados compostos não têm uma disposição fixa como as estruturas C. As instâncias de um tipo de dados composto podem conter campos nulos. Além disso, os tipos de dados compostos que fazem parte de uma hierarquia de herança podem ter campos diferentes dos outros membros da mesma hierarquia de herança. Portanto, o PostgreSQL fornece uma interface de função para acessar os campos de tipos de dados compostos na linguagem C.

Suponha que se deseja escrever uma função para responder à consulta:

SELECT name, c_overpaid(emp, 1500) AS overpaid
    FROM emp
    WHERE name = 'Bill' OR name = 'Sam';

Usando as convenções de chamada versão 1, pode-se definir c_overpaid como:

#include "postgres.h"
#include "executor/executor.h"  /* para GetAttributeByName() */

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    HeapTupleHeader  t = PG_GETARG_HEAPTUPLEHEADER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    Datum salary;

    salary = GetAttributeByName(t, "salary", &isnull);
    if (isnull)
        PG_RETURN_BOOL(false);
    /* Como alternativa, pode-se preferir usar PG_RETURN_NULL() para salários nulos. */

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByName é a função do sistema PostgreSQL que retorna os atributos da linha especificada. Possui três argumentos: um argumento do tipo HeapTupleHeader passado para a função, o nome do atributo desejado, e um parâmetro de retorno informando se o atributo é nulo. GetAttributeByName retorna um valor Datum que pode ser convertido para o tipo de dados apropriado usando a função DatumGetXXX() apropriada. Note que o valor retornado não tem sentido se o sinalizador de nulo estiver definido; sempre deve ser verificado o sinalizador de nulo antes de tentar fazer qualquer coisa com o resultado.

Também existe a função GetAttributeByNum, que seleciona o atributo de destino pelo número da coluna, em vez do nome.

O comando a seguir declara a função c_overpaid no SQL:

CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
    AS 'DIRECTORY/funcs', 'c_overpaid'
    LANGUAGE C STRICT;

Repare que foi usado STRICT para não ser necessário verificar se os argumentos de entrada são nulos.

36.10.8. Retorno de linhas (tipos compostos) #

Para retornar uma linha, ou um valor de tipo de dados composto, em uma função na linguagem C, pode ser usada uma API especial que fornece macros e funções para ocultar grande parte da complexidade da criação de tipos de dados compostos. Para usar esta API, o arquivo-fonte deve incluir:

#include "funcapi.h"

Existem duas maneiras de construir um valor de dados composto (doravante chamado de tupla): pode ser construído a partir de uma matriz de valores Datum, ou de uma matriz de cadeia de caracteres C, que podem ser passadas para as funções de conversão de entrada dos tipos de dados da coluna da tupla. Nos dois casos, é necessário primeiro obter ou construir um descritor TupleDesc para a estrutura da tupla. Ao trabalhar com Datums, se passa o TupleDesc para o BlessTupleDesc, e depois se chama heap_form_tuple para cada uma das linhas. Ao trabalhar com cadeia de caracteres C, se passa o TupleDesc para o TupleDescGetAttInMetadata, e depois se chama BuildTupleFromCStrings para cada uma das linhas. No caso da função retornar um conjunto de tuplas, todas as etapas de configuração podem ser feitas uma vez durante a primeira chamada da função.

Estão disponíveis várias funções auxiliares para configurar o TupleDesc necessário. A maneira recomendada de fazer isto, na maioria das funções que retornam valores compostos, é chamar

TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
                                   Oid *resultTypeId,
                                   TupleDesc *resultTupleDesc)

passando a mesma estrutura fcinfo passada para a própria função chamadora. (Isto obviamente requer que sejam usadas as convenções de chamada versão 1.) resultTypeId pode ser especificado como NULL ou como o endereço de uma variável local para receber o tipo de resultado da função OID. resultTupleDesc deve ser o endereço de uma variável local TupleDesc. Verifique se o resultado é TYPEFUNC_COMPOSITE; se for, então resultTupleDesc foi preenchido com o TupleDesc necessário. (Se não for, pode ser relatado um erro nas linhas de função retornando registro chamada em contexto que não pode aceitar o tipo de dados registro.)

Dica

get_call_result_type pode resolver o tipo de dados real do resultado de uma função polimórfica; portanto, é útil em funções que retornam resultados polimórficos escalares, e não apenas em funções que retornam tipos de dados compostos. A saída de resultTypeId é útil principalmente para funções que retornam escalares polimórficos.

Nota

A função get_call_result_type tem a irmã gêmea get_expr_result_type, que pode ser usada para resolver o tipo de dados de saída esperado para uma chamada de função representada por uma árvore de expressão. Isto pode ser usado ao tentar determinar o tipo de resultado de fora da própria função. Também existe a função get_func_result_type, que pode ser usada quando apenas o OID da função está disponível. No entanto, estas funções não conseguem lidar com funções declaradas retornando record, e get_func_result_type não pode resolver tipos de dados polimórficos, então deve ser usado preferencialmente get_call_result_type.

As funções mais antigas e em obsolescência (deprecated) para obter TupleDescs são

TupleDesc RelationNameGetTupleDesc(const char *relname)

para obter o TupleDesc para o tipo de dados de linha de uma relação com nome, e

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

para obter o TupleDesc baseado em um tipo de dados OID. Isto pode ser usado para obter um TupleDesc para um tipo de dados base ou composto. No entanto, não funciona para uma função retornando record, e não pode resolver tipos de dados polimórficos.

Uma vez obtido o TupleDesc, chame

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

se planeja trabalhar com Datums, ou

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

se planeja trabalhar com cadeias de caracteres C. Se estiver escrevendo uma função retornando conjunto, então poderá salvar os resultados dessas funções na estrutura FuncCallContext — use o campo tuple_desc ou attinmeta, respectivamente.

Ao trabalhar com Datums, use

HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)

para construir uma HeapTuple a partir dos dados na forma de Datum.

Ao trabalhar com cadeia de caracteres C, use

HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)

para construir uma HeapTuple a partir dos dados na forma de cadeias de caracteres C. values é uma matriz de cadeias de caracteres C, uma para cada atributo da linha retornada. Cada cadeia de caracteres C deve estar no formato esperado pela função de entrada do tipo de dados do atributo. Para retornar o valor nulo para um dos atributos, o ponteiro correspondente na matriz valores deve ser definido como NULL. Esta função precisa ser chamada novamente para cada linha retornada.

Após construir a tupla de retorno da função, a tupla deve ser convertida em Datum. Use

HeapTupleGetDatum(HeapTuple tuple)

para converter HeapTuple em um Datum válido. Este Datum pode ser retornado diretamente se for pretendido retornar apenas uma única linha, ou pode ser usado como o valor retornado atual em uma função retornando conjunto.

É mostrado um exemplo na próxima seção.

36.10.9. Retorno de conjuntos #

As funções na linguagem C têm duas opções para retornar conjuntos (múltiplas linhas). Em um método, chamado modo ValuePerCall, a função retornando de conjunto é chamada repetidamente (passando os mesmos argumentos todas às vezes), e retorna uma nova linha a cada chamada, até que não haja mais linhas para retornar, sinalizando isto retornando NULL. A função retornando conjunto (SRF) deve, portanto, salvar condição de estado suficiente nas chamadas para lembrar o que estava fazendo, e retornar o próximo item correto em cada chamada. No outro método, chamado modo Materialize, uma SRF preenche e retorna um objeto tuplestore contendo todo o resultado; então ocorre apenas uma chamada para todo o resultado, não havendo necessidade de salvar o estado entre chamadas.

Ao usar o modo ValuePerCall, é importante lembrar não haver garantia de que a consulta será executada até completar; ou seja, devido a opções como LIMIT, o executor pode parar de fazer chamadas para a função retornando conjunto antes que todas as linhas tenham sido buscadas. Isto significa não ser seguro deixar para realizar as atividades de limpeza na última chamada, porque isto pode nunca acontecer. É recomendável usar o modo Materialize para funções que precisam de acesso a recursos externos, como descritores de arquivo.

O restante dessa seção documenta o conjunto de macros auxiliares comumente usadas (embora não havendo necessidade de serem usadas) para as SRFs usando o modo ValuePerCall. Detalhes adicionais sobre o modo Materialize podem ser encontrados no arquivo src/backend/utils/fmgr/README. Além disso, os módulos contrib na distribuição do código-fonte do PostgreSQL, contêm muitos exemplos de SRFs usando os modos ValuePerCall e Materialize.

Para usar as macros de suporte ValuePerCall descritas aqui, deve ser incluído o arquivo funcapi.h. Estas macros trabalham com uma estrutura FuncCallContext, que contém o estado que precisa ser salvo nas chamadas. Dentro do SRF que efetua a chamada, é usado fcinfo->flinfo->fn_extra para manter um ponteiro para FuncCallContext entre as chamadas. As macros preenchem automaticamente este campo no primeiro uso, e esperam encontrar o mesmo ponteiro lá nos usos subsequentes.

typedef struct FuncCallContext
{
    /*
     * Número de chamadas anteriores
     *
     * call_cntr é inicializado como 0 por SRF_FIRSTCALL_INIT(),
     * e incrementado toda vez que SRF_RETURN_NEXT() é chamado.
     */
    uint64 call_cntr;

    /*
     * OPCIONAL: número máximo de chamadas
     *
     * max_calls está aqui apenas por conveniência, e configurá-lo é
     * opcional. Se não for definido, devem ser fornecidos meios
     * alternativos para saber quando a função está concluída.
     */
    uint64 max_calls;

    /*
     * OPCIONAL: ponteiro para diversas informações de contexto fornecidas
     * pelo usuário.
     *
     * user_fctx deve ser usado como ponteiro para os seus próprios
     * dados, para reter informações de contexto arbitrárias entre
     * as chamadas de sua função.
     */
    void *user_fctx;

    /*
     * OPCIONAL: ponteiro para estrutura contendo metadados de entrada
     * de tipo de dados de atributo
     *
     * attinmeta deve ser usado ao retornar tuplas (ou seja, tipos de
     * dados compostos), não sendo usado para retornar tipos de dados base.
     * Só é necessário se for pretendido usar BuildTupleFromCStrings()
     * para criar a tupla de retorno.
     */
    AttInMetadata *attinmeta;

    /*
     * Contexto de memória usado para estruturas que devem sobreviver
     * entre várias chamadas
     *
     * multi_call_memory_ctx é definido por SRF_FIRSTCALL_INIT(),
     * e usado por SRF_RETURN_DONE() para limpeza.
     * É o contexto de memória mais apropriado para qualquer memória
     * a ser reutilizada em várias chamadas da SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * OPCIONAL: ponteiro para a struct contendo a descrição da tupla
     *
     * tuple_desc deve ser usado ao retornar tuplas (ou seja, tipos de
     * dados compostos), só sendo necessário se as tuplas forem construídas
     * com heap_form_tuple(), em vez de BuildTupleFromCStrings().
     * Note que o ponteiro TupleDesc armazenado aqui geralmente deve
     * ter sido executado primeiro por meio de BlessTupleDesc().
     */
    TupleDesc tuple_desc;

} FuncCallContext;

As macros a serem utilizadas por uma SRF utilizando esta infraestrutura são:

SRF_IS_FIRSTCALL()

Deve ser usada para determinar se a função está sendo chamada pela primeira vez, ou depois. Na primeira chamada (somente), deve ser chamada

SRF_FIRSTCALL_INIT()

para inicializar FuncCallContext. Em toda chamada de função, incluindo a primeira, deve ser chamada

SRF_PERCALL_SETUP()

para configurar para usar FuncCallContext.

Se a função tiver dados para retornar na chamada atual, deve ser usado

SRF_RETURN_NEXT(funcctx, result)

para retorná-los para quem efetuou a chamada. (result deve ser do tipo de dados Datum, seja um único valor ou uma tupla preparada conforme descrito acima.) Por fim, quando a função terminar de retornar dados, deve ser usado

SRF_RETURN_DONE(funcctx)

para limpar e terminar a SRF.

O contexto de memória corrente quando a SRF é chamada é um contexto transitório, que será limpo entre as chamadas. Isto significa não haver necessidade de chamar pfree para liberar tudo que foi alocado usando palloc; vai ser liberado de qualquer maneira. Entretanto, se for desejado alocar estruturas de dados que persistam entre chamadas, elas devem ser colocadas em outro lugar. O contexto de memória referenciado por multi_call_memory_ctx é um local adequado para qualquer dado que precise sobreviver até que a SRF termine sua execução. Geralmente, isto significa que se deve alternar para multi_call_memory_ctx ao fazer a configuração da primeira chamada. Deve ser usado funcctx->user_fctx para manter um ponteiro para qualquer estrutura de dados de chamada cruzada. (Os dados alocados em multi_call_memory_ctx desaparecem automaticamente quando a consulta termina, então não é necessário liberar estes dados manualmente, também.)

Atenção

Quando os argumentos reais da função permanecem inalterados entre as chamadas, se o valor dos argumentos forem lidos no contexto transitório (o que é normalmente feito de forma transparente pela macro PG_GETARG_xxx), então as cópias serão liberadas a cada ciclo. Da mesma maneira, se forem mantidas referências a estes valores em user_fctx, eles deverão ser copiados para multi_call_memory_ctx após lidos, ou se garantir que os valores são lidos apenas neste contexto.

Um exemplo de pseudocódigo completo se parece com o seguinte:

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    declarações adicionais conforme necessário

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* O código de configuração de uma única vez aparece aqui: */
        código do usuário
        if retorna tipo de dados composto
            construir TupleDesc, e talvez AttInMetadata
        endif retorna tipo de dados composto
        código do usuário
        MemoryContextSwitchTo(oldcontext);
    }

    /* O código de configuração de cada vez aparece aqui: */
    código do usuário
    funcctx = SRF_PERCALL_SETUP();
    código do usuário

    /* Esta é apenas uma das maneiras para testar se terminamos: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Aqui queremos retornar outro item: */
        código do usuário
        obter o Datum do resultado
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Aqui concluímos o retorno dos itens, então basta informar esse fato. */
        /* (Resista à tentação de inserir código de limpeza aqui.) */
        SRF_RETURN_DONE(funcctx);
    }
}

Um exemplo completo de uma SRF simples retornando tipo de dados composto se parece com:

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    AttInMetadata       *attinmeta;

    /* coisas feitas apenas na primeira chamada da função */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* criar um contexto de função para persistência entre chamadas */
        funcctx = SRF_FIRSTCALL_INIT();

        /* alternar para o contexto de memória apropriado */
        /* para múltiplas chamadas da função */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* número total de tuplas a serem retornadas */
        funcctx->max_calls = PG_GETARG_INT32(0);

        /* construir um descritor de tupla para o nosso tipo de resultado. */
        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")));

        /*
         * gerar metadados de atributos necessários posteriormente para
         * produzir tuplas a partir de cadeias de caracteres C puras
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* tarefas realizadas em toda chamada da função */
    funcctx = SRF_PERCALL_SETUP();

    call_cntr = funcctx->call_cntr;
    max_calls = funcctx->max_calls;
    attinmeta = funcctx->attinmeta;

    /* fazer quando ainda houver mais para retornar */
    if (call_cntr < max_calls)
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Preparar uma matriz de valores para construir a tupla retornada.
         * Deverá ser uma matriz de cadeias de caracteres em C que
         * será  processada posteriormente pelas funções de entrada
         * de tipo de dados.
         */
        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));

        /* construir a tupla */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* transformar a tupla em datum */
        result = HeapTupleGetDatum(tuple);

        /* Limpeza (isso não é realmente necessário) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* fazer quando não houver mais nada a ser feito */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

Uma maneira de declarar esta função no 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;

Uma maneira diferente é usar parâmetros 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;

Note que neste método o tipo de dados de saída da função é formalmente um tipo de dados record anônimo.

36.10.10. Argumentos polimórficos e tipos de dados de retorno #

As funções na linguagem C podem ser declaradas aceitando e retornando os tipos de dados polimórficos descritos em Tipos de dados polimórficos. Quando os argumentos ou tipos de dados retornados de uma função são definidos como tipos polimórficos, o autor da função não pode saber com antecedência com qual tipo de dados ela será chamada, ou precisará retornar. Existem duas rotinas fornecidas em fmgr.h para permitir que uma função C, usando as convenções versão 1, descubra os tipos de dados reais de seus argumentos, e o tipo de dados de retorno esperado. Estas rotinas se chamam get_fn_expr_rettype(FmgrInfo *flinfo) e get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Elas retornam o resultado, ou o OID do tipo de dados do argumento, ou InvalidOid se a informação não estiver disponível. A estrutura flinfo é normalmente acessada como fcinfo->flinfo. O parâmetro argnum tem como base zero. A função get_call_result_type também pode ser usada como alternativa para get_fn_expr_rettype. Também existe a função get_fn_expr_variadic, que pode ser usada para descobrir se os argumentos variádicos foram unidos em uma matriz. Isto é útil principalmente para as funções VARIADIC "any", uma vez que esta união sempre ocorre para funções variádicas que usam tipos de dados matrizes comuns.

Por exemplo, supondo que queiramos escrever uma função para aceitar um único elemento de qualquer tipo de dados e retornar uma matriz unidimensional desse tipo de dados:

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");

    /* obter o elemento fornecido, tomando cuidado caso ele seja NULL. */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* temos uma dimensão */
    ndims = 1;
    /* e um elemento */
    dims[0] = 1;
    /* e o limite inferior é 1 */
    lbs[0] = 1;

    /* obter as informações necessárias sobre o tipo de dados do elemento. */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* agora construir a matriz */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

O seguinte comando declara a função make_array no SQL:

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'DIRECTORY/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

Existe uma variante de polimorfismo que só está disponível para funções na linguagem C: elas podem ser declaradas aceitando parâmetros do tipo de dados "any". (Note que este nome de tipo de dados deve estar entre aspas, porque também é uma palavra reservada do SQL.) Funciona como anyelement, exceto por não restringir os diferentes argumentos "any" a serem todos do mesmo tipo de dados, nem ajuda a determinar o tipo de dados do resultado da função. Uma função na linguagem C também pode declarar seu parâmetro final como VARIADIC "any". Isto corresponderá a um ou mais argumentos reais de qualquer tipo de dados (não necessariamente do mesmo tipo de dados). Estes argumentos não serão reunidos em uma matriz, como acontece com funções variádicas normais; eles apenas serão passados para a função em separado. A macro PG_NARGS(), e os métodos descritos acima, devem ser usados para determinar o número de argumentos reais e seus tipos de dados ao usar este recurso. Além disso, os usuários dessa função podem querer usar a palavra-chave VARIADIC em sua chamada de função, com a expectativa de que a função trate os elementos da matriz como argumentos separados. A própria função deve implementar este comportamento, se desejado, após usar get_fn_expr_variadic para detectar qual argumento real foi marcado com VARIADIC.

36.10.11. Memória compartilhada #

36.10.11.1. Solicitação de memória compartilhada na inicialização #

Os módulos (add-ins) podem reservar memória compartilhada na inicialização do servidor. Para fazer isto, a biblioteca compartilhada do módulo deve ser pré-carregada, especificando-a em shared_preload_libraries. A biblioteca compartilhada também deve registrar um shmem_request_hook em sua função _PG_init. Este shmem_request_hook pode reservar memória compartilhada chamando:

void RequestAddinShmemSpace(Size size)

Cada processo servidor em segundo plano (backend) deve obter um ponteiro para a memória compartilhada reservada chamando:

void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)

Se esta função definir foundPtr como false, quem chama deverá proceder à inicialização do conteúdo da memória compartilhada reservada. Se foundPtr for definido como true, a memória compartilhada já foi inicializada por outro processo servidor em segundo plano e quem chama não irá precisar inicializá-la. further.

Para evitar condições de disputa, cada processo servidor em segundo plano deve usar o LWLock AddinShmemInitLock ao inicializar sua alocação de memória compartilhada, como mostrado aqui:

static mystruct *ptr = NULL;
bool        found;

LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
    ... Inicializar o conteúdo da memória compartilhada. ...
    ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);

shmem_startup_hook oferece um local conveniente para o código de inicialização, mas não é estritamente necessário que todo este código seja colocado neste gancho. No Windows (e em qualquer outro lugar onde EXEC_BACKEND esteja definido), cada processo servidor em segundo plano executa o shmem_startup_hook registrado logo após se conectar à memória compartilhada, para que os módulos ainda possam adquirir AddinShmemInitLock dentro deste gancho, como mostrado no exemplo acima. Nas outras plataformas, apenas o processo postmaster executa o shmem_startup_hook, e cada processo servidor em segundo plano herda automaticamente os ponteiros para a memória compartilhada.

Pode ser encontrado um exemplo de shmem_request_hook e shmem_startup_hook em contrib/pg_stat_statements/pg_stat_statements.c na distribuição do código-fonte do PostgreSQL.

36.10.11.2. Solicitação de memória após a inicialização #

Existe outro método, mais flexível, para reservar memória compartilhada, que pode ser feito após a inicialização do servidor e fora do shmem_request_hook. Para fazer isto, cada processo servidor em segundo plano que utilizará a memória compartilhada deve obter um ponteiro para ela chamando o seguinte comando:

void *GetNamedDSMSegment(const char *name, size_t size,
                         void (*init_callback) (void *ptr),
                         bool *found)

Se ainda não existir um segmento de memória compartilhada dinâmica com o nome fornecido, esta função o alocará e o inicializará com a função de retorno de chamada init_callback fornecida. Se o segmento já tiver sido alocado e inicializado por outro processo servidor em segundo plano, esta função simplesmente irá anexar o segmento de memória compartilhada dinâmica existente ao processo servidor em segundo plano corrente.

Ao contrário da memória compartilhada reservada na inicialização do servidor, não é necessário adquirir AddinShmemInitLock ou tomar qualquer outra ação para evitar condições de disputa ao reservar memória compartilhada com GetNamedDSMSegment. Esta função garante que apenas um processo servidor em segundo plano aloque e inicialize o segmento e que todos os outros processos servidores em segundo plano recebam um ponteiro para o segmento totalmente alocado e inicializado.

Um exemplo completo de uso de GetNamedDSMSegment pode ser encontrado no arquivo src/test/modules/test_dsm_registry/test_dsm_registry.c na distribuição do código-fonte do PostgreSQL.

36.10.12. LWLocks #

36.10.12.1. Solicitação de LWLocks na inicialização #

Os módulos podem reservar LWLocks na inicialização do servidor. Assim como a memória compartilhada reservada na inicialização do servidor, a biblioteca compartilhada do módulo deve ser pré-carregada, especificando-a em shared_preload_libraries, e a biblioteca compartilhada deve registrar um shmem_request_hook em sua função _PG_init. Este shmem_request_hook pode reservar LWLocks chamando:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

Isto garante que uma matriz de num_lwlocks LWLocks esteja disponível sob o nome tranche_name. Um ponteiro para esta matriz pode ser obtido chamando:

LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)

36.10.12.2. Solicitação de LWLocks após a inicialização #

Existe outro método, mais flexível, para obter LWLocks que pode ser feito após a inicialização do servidor e fora de um shmem_request_hook. Para isto, primeiro deve ser alocado um tranche_id chamando:

int LWLockNewTrancheId(void)

Em seguida, inicializar cada LWLock, passando o novo tranche_id como argumento:

void LWLockInitialize(LWLock *lock, int tranche_id)

Semelhante à memória compartilhada, cada processo servidor em segundo plano deve garantir que apenas um processo aloque um novo tranche_id e inicialize cada novo LWLock. Uma maneira de fazer isto é chamar estas funções apenas no seu código de inicialização de memória compartilhada com a função AddinShmemInitLock sendo mantida exclusivamente. Se for usado GetNamedDSMSegment, chamar estas funções na função de retorno de chamada init_callback é suficiente para evitar condições de disputa.

Por fim, cada processo servidor em segundo plano que utiliza o tranche_id deve associá-lo a um tranche_name chamando:

void LWLockRegisterTranche(int tranche_id, const char *tranche_name)

Um exemplo completo de uso de LWLockNewTrancheId, LWLockInitialize e LWLockRegisterTranche pode ser encontrado no arquivo contrib/pg_prewarm/autoprewarm.c na distribuição do código-fonte do PostgreSQL.

36.10.13. Eventos de espera personalizados #

Os módulos podem definir eventos de espera personalizados no tipo de evento de espera Extension chamando:

uint32 WaitEventExtensionNew(const char *wait_event_name)

O evento de espera está associado a uma cadeia de caracteres personalizada visível para o usuário. Pode ser encontrado um exemplo no diretório src/test/modules/worker_spi na distribuição do código-fonte do PostgreSQL.

Podem ser vistos eventos de espera personalizados em pg_stat_activity:

=# SELECT wait_event_type, wait_event FROM pg_stat_activity
     WHERE backend_type ~ 'worker_spi';
 wait_event_type |  wait_event
-----------------+---------------
 Extension       | WorkerSpiMain
(1 linha)

36.10.14. Pontos de injeção #

Um ponto de injeção com um determinado name é declarado usando a macro:

INJECTION_POINT(name, arg);

Já existem alguns pontos de injeção declarados em locais estratégicos dentro do código do servidor. Após adicionar um novo ponto de injeção, o código precisa ser compilado para que este ponto de injeção esteja disponível em binário. Módulos escritos na linguagem C podem declarar pontos de injeção em seu próprio código usando a mesma macro. Os nomes dos pontos de injeção devem usar letras minúsculas, com os termos separados por hifens. arg é um valor de argumento opcional fornecido ao retorno de chamada em tempo de execução.

A execução de um ponto de injeção pode exigir a alocação de uma pequena quantidade de memória, o que pode falhar. Se houver necessidade de um ponto de injeção em uma seção crítica onde alocações dinâmicas não são permitidas, pode-se usar uma abordagem em duas etapas com as seguintes macros:

INJECTION_POINT_LOAD(name);
INJECTION_POINT_CACHED(name, arg);

Antes de entrar na seção crítica, deve-se chamar INJECTION_POINT_LOAD. Isto verifica o estado da memória compartilhada e carrega o retorno de chamada na memória privada do processo servidor em segundo plano se estiver ativo. Dentro da seção crítica, deve-se usar INJECTION_POINT_CACHED para executar o retorno de chamada.

Os módulos podem associar funções de retorno de chamada a um ponto de injeção já declarado, chamando:

extern void InjectionPointAttach(const char *name,
                                 const char *library,
                                 const char *function,
                                 const void *private_data,
                                 int private_data_size);

name é o nome do ponto de injeção, que, quando alcançado durante a execução, executará a function carregada da library. private_data é uma área privada de dados de tamanho private_data_size fornecida como argumento para a função de retorno de chamada quando executada.

Aqui está um exemplo de retorno de chamada para InjectionPointCallback:

static void
custom_injection_callback(const char *name,
                          const void *private_data,
                          void *arg)
{
    uint32 wait_event_info = WaitEventInjectionPointNew(name);

    pgstat_report_wait_start(wait_event_info);
    elog(NOTICE, "%s: executed custom callback", name);
    pgstat_report_wait_end();
}

Esta função de retorno escreve uma mensagem no registro de erros do servidor com a severidade NOTICE, mas as funções de retorno podem implementar uma lógica mais complexa.

Uma forma alternativa de definir a ação a ser tomada quando um ponto de injeção for atingido é adicionar o código de teste junto com o código-fonte normal. Isto pode ser útil se a ação, por exemplo, depender de variáveis ​​locais que não são acessíveis aos módulos carregados. A macro IS_INJECTION_POINT_ATTACHED pode então ser usada para verificar se um ponto de injeção está conectado como, por exemplo:

#ifdef USE_INJECTION_POINTS
if (IS_INJECTION_POINT_ATTACHED("before-foobar"))
{
    /* change a local variable if injection point is attached */
    local_var = 123;

    /* also execute the callback */
    INJECTION_POINT_CACHED("before-foobar", NULL);
}
#endif

Note que a função de retorno de chamada associada ao ponto de injeção não será executada pela macro IS_INJECTION_POINT_ATTACHED . Se for desejado executar a chamada de retorno, também deverá ser chamado INJECTION_POINT_CACHED, como no exemplo acima.

Opcionalmente, é possível desanexar um ponto de injeção chamando:

extern bool InjectionPointDetach(const char *name);

No caso de sucesso é retornado true, e false caso contrário.

Uma chamada de retorno associada a um ponto de injeção está disponível em todos os processos servidores em segundo plano, incluindo os processos servidores em segundo plano iniciados após a chamada de InjectionPointAttach. Permanecerá conectado enquanto o servidor estiver em execução, ou até que o ponto de injeção seja desconectado usando InjectionPointDetach.

Pode ser encontrado um exemplo no diretório src/test/modules/injection_points na distribuição do código-fonte do PostgreSQL.

A ativação dos pontos de injeção requer --enable-injection-points com configure, ou -Dinjection_points=true com Meson.

36.10.15. Estatísticas cumulativas personalizadas #

É possível que módulos escritos na linguagem C utilizem tipos personalizados de estatísticas cumulativas registradas no >Configuração de coleta de estatísticas.

Primeiro, deve ser definido um PgStat_KindInfo que inclua todas as informações relacionadas ao tipo personalizado registrado. Por exemplo:

static const PgStat_KindInfo custom_stats = {
    .name = "custom_stats",
    .fixed_amount = false,
    .shared_size = sizeof(PgStatShared_Custom),
    .shared_data_off = offsetof(PgStatShared_Custom, stats),
    .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats),
    .pending_size = sizeof(PgStat_StatCustomEntry),
}

Então, cada processo servidor em segundo plano que precisar usar este tipo personalizado deverá registrá-lo com pgstat_register_kind e um ID exclusivo usado para armazenar as entradas relacionadas a este tipo de estatística:

extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
                                        const PgStat_KindInfo *kind_info);

Ao desenvolver uma nova extensão, deve ser usado PGSTAT_KIND_EXPERIMENTAL para kind. Quando o módulo estiver pronto para ser liberado para os usuários, deve ser reservado um ID de tipo na página Custom Cumulative Statistics.

Os detalhes da API para PgStat_KindInfo podem ser encontrados no arquivo src/include/utils/pgstat_internal.h da distribuição do código-fonte do PostgreSQL.

O tipo de estatística registrada está associado a um nome e a um ID exclusivo compartilhado em todo o servidor na memória compartilhada. Cada processo servidor em segundo plano que utiliza um tipo personalizado de estatísticas mantém um cache local armazenando as informações de cada PgStat_KindInfo personalizado.

O módulo de extensão que implementa o tipo de estatísticas cumulativas personalizadas deve ser colocado em shared_preload_libraries para que seja carregado na inicialização do PostgreSQL.

Um exemplo que descreve como registrar e usar estatísticas personalizadas pode ser encontrado no diretório src/test/modules/injection_points da distribuição do código-fonte do PostgreSQL.

36.10.16. Uso do C++ para extensibilidade #

Embora o servidor do PostgreSQL seja escrito em C, é possível escrever extensões em C++ se estas diretrizes forem seguidas:

  • Todas as funções acessadas pelo servidor devem apresentar uma interface C para o servidor; estas funções C podem então chamar funções C++. Por exemplo, a vinculação extern C é necessária para funções acessadas pelo servidor. Isto também é necessário para quaisquer funções passadas como ponteiros entre o servidor e o código C++.

  • Libere a memória usando o método de liberação apropriado. Por exemplo, a maioria da memória do servidor é alocada usando a função palloc(), então deve ser usada a função pfree() para liberá-la. Nesses casos, usar a função delete do C++ vai falhar.

  • Evite a propagação das exceções para o código C (use um bloco pega-tudo (catch-all) no nível superior de todas as funções extern C). Isto é necessário mesmo se o código C++ não lançar nenhuma exceção explicitamente, porque eventos como f alta de memória ainda podem lançar exceções. Toda exceção deve ser capturada, e os erros apropriados devem ser passados de volta para a interface C. Se possível, compile o código C++ com a opção -fno-exceptions para eliminar inteiramente as exceções; nesses casos, deve ser verificado se há falhas no código C++, por exemplo, verificando se há NULL retornado por new().

  • Se forem chamadas funções do servidor a partir do código C++, deve-se certificar de que a pilha de chamadas C++ contenha apenas estruturas de dados antigas simples (POD). Isto é necessário, porque os erros do servidor geram um salto via longjmp() que não desfaz corretamente a pilha de chamadas C++ com objetos não-POD.

Em resumo, é melhor colocar o código C++ atrás de uma parede de funções extern C fazendo a interface com o servidor, e evitar vazamento de exceção, memória e pilha de chamadas.



[115] stdint.h é um arquivo de cabeçalho da biblioteca padrão C, introduzido na seção 7.18 da biblioteca padrão C99, visando permitir que os programadores escrevam código mais portável, fornecendo um conjunto de typedefs especificando tipos de dados inteiros de largura exata, juntamente com os valores mínimo e máximo permitidos definidos para cada tipo de dados, usando macros. C Programming/stdint.h (N. T.)

[116] Presente no diretório do Tutorial (src/tutorial) da distribuição do código-fonte do PostgreSQL, que contém o código para os exemplos usados nesta seção (N. T.)