Iniciando em perlguts, perlapi, perlcall e perlembed

Daniel Ruoso
Publicado em 18/07/2007

Iniciando em perlguts, perlapi, perlcall e perlembed

Em algum momento você vai precisar passar por isso, então por que não dar uma olhada enquanto você ainda não precisa?

Tracei esse caminho quando quis fazer uma brincadeira de tentar construir uma estrutura de dados em C que eu pudesse importar do Perl da forma o mais fácil possível. A idéia é simples, dentro de um programa feito primeiramente em C, construir uma estrutura de dados flexível de uma forma muito simples e conseguir importar essa estrutura de dentro do Perl também da forma mais simples possível.

É claro que XML, YAML ou qualquer outro formato transitório passou pela minha cabeça. Mas gerar esses arquivos em C não é tão simples, e eu queria reduzir ao máximo a complexidade do parse quando fosse lido. Dessa forma, pensei que seria interessante guardar já no C em um formato que o Perl conseguisse ler nativamente. Foi então que cheguei no módulo Storable.

O módulo Storable é uma forma bastante estável de serialização do Perl e faz parte da distribuição padrão, então isso tornaria o parse algo simples como:

 use Storable qw(thaw);
 use Data::Dumper;
 local $/ = undef;
 my $b = ;
 print Dumper(thaw($b))."\n";

Nesse teste, eu espero, em STDIN, o conteúdo de uma referência serializado usando o método Storable::freeze, cujo oposto é Storable::thaw. Esse não é o modo mais comum de utilizar o Storable, mas elimina a obrigatoriedade de lidar com um arquivo e você tem um conjunto de bytes que você pode usar para o que quiser, como por exemplo, enviar por um socket. Nesse teste, vamos executar o programa em C que irá jogar o conteúdo do freeze para STDOUT que será então lido pelo script Perl acima.

Começando pelo perlguts

A primeira coisa é entender como o perl representa cada um dos tipos de variáveis internamente. Por dentro, tudo acaba por ser um struct em C que guarda as informações dentro dele. E o que é elegante nisso é que você, quando lida com Perl em C, pode manipular essas estruturas exatamente da mesma maneira que o próprio perl manipula. Por isso o seu executável em C (no final desse artigo), vai estar linkado com o libperl, mas veja bem, o próprio interpretador perl também está linkado com ela então bem, o interpretador na verdade é o libperl (e é por isso que é tão fácil embarcar Perl em outras aplicações, como o PostgreSQL) mas isso já seria perlembed, e ainda estamos no perlguts.

Então, se os tipos de variáveis no perl são representadas por structs, existe uma relação entre os tipos de variáveis em Perl e em C. Então aí vai...

Perl

C

 $test

 SV* test

 @test

 AV* test

 %test

 HV* test

E como você sabe, uma referência também é um scalar, então

Perl

C

 $a = \@b

 SV* a = newRV((SV*)b)

Mas como em C as coisas não são tão simples, as operações normais não são tão simples, então...

Perl

C

 $a = 1

 SV* a = newSViv(1)

 $b = 1.1

 SV* b = newSVnv(1.1)

 $c = "1.1"

 SV* c = newSVpv("1.1",3)

Por outro lado você ganha um bônus, o perl faz as numificações e stringificações por você, e você nem precisa saber o formato original, não por acaso, exatamente como o perl funciona.

Perl

C

 $d = $a + $b + $c

 double d = SvNV(a) + SvNV(b) + SvNV(c)

 $d = $a . $b . $c

 char* d; sprintf(d,"%s%s%s", SvPV_nolen(a), SvPV_nolen(b), SvPV_nolen(c))

Foi nessa hora que eu pensei que é mais fácil programar em C em Perl do que em C em C.

Mas então vamos lá, agora falta a gente montar uma estrutura de dados um pouco mais complicada...

        AV* data = newAV();
        int i;
        for (i = 0; i < 3; i++) {
                HV* hash = newHV();
                U32 ret = 0;
                hv_store(hash,"ASDFGH",6,newSViv(i),ret);
                av_push(data,newRV_noinc((SV*)hash));
        }

Mas se você ainda lembra que o código é em C, você deve estar pensando que está faltando algum free, e que provavelmente tem algum memory leak. E, sim, do jeito que está aí as variáveis não seriam liberadas. Como você provavelmente sabe, o perl libera a memória utilizando um garbage collector baseado em contagem de referências, ou seja, cada vez que você cria uma referência para uma variável, o refcount é incrementado e cada vez que uma dessas referências é destruída o refcount é decrementado. Quando o refcount chega a zero, a variável é destruída. Mas o que acontece é que as operações newSV, newAV e newHV criam variáveis com o refcount em 1, então é preciso decrementar o reference count para que elas sejam limpadas, ou criar um escopo perl e dizer que a variável é apenas daquele escopo.

Para fazer isso vamos utilizar algumas macros que, no fim das contas, representam apenas abrir e fechar um escopo.

O que em Perl seria "{", aqui passa a ser...

 dSP;
 ENTER;
 SAVETMPS;

e o que seria "}", aqui passa a ser...

 FREETMPS;
 LEAVE;

E é então que chegamos ao conceito de mortalidade de variáveis. O que acontece é que você precisa dizer ao perl que uma variável deverá ser destruída ao sair do escopo. O que significa basicamente "agendar" um decremento de referência para o fim desse escopo. O que basicamente é algo como dizer que a variável é "my". Mas não é a mesma coisa. Não é a mesma coisa simplesmente por que é você que está criando cada variável, então é você que tem que dizer que aquela variável é "mortal".

Como o processo de tornar mortal é apenas um agendamento do decremento da referência, se você chamar mais vezes do que o necessário você vai receber warnings de tentativa de fazer free de um scalar inválido, que é um "double free" no sentido do perl.

No entanto, é preciso tomar cuidado, por que a operação newRV pode ser usada incrementando ou não o refcount. Você pode usar newRV_noinc ou newRV_inc. E a outra coisa que é preciso ter cuidado é que um hash quando for destruído vai diminuir o refcount dos seus elementos, assim como um array, quando for destruído também vai diminuir o refcount dos seus elementos.

Dessa forma aquele código fica assim:

Perl

C

 my @a = ();
 for my $i (0..2) {
       my %h = ();
       $h{ASDFGH} = $i;
       push @a, \%h;
 }

        dSP;
        ENTER;
        SAVETMPS;
        AV* data = (AV*)sv_2mortal((SV*)newAV());
        int i;
        for (i = 0; i < 3; i++) {
                HV* hash = (HV*)sv_2mortal((SV*)newHV());
                U32 ret = 0;
                hv_store(hash,"ASDFGH",6,newSViv(i),ret);
                av_push(data,newRV_inc((SV*)hash));
        }
        FREETMPS;
        LEAVE;

O código escrito acima foi mais ou menos uma tradução, mas algumas coisas poderiam ser evitadas. Por exemplo: dentro do "for", primeiro faz um sv_2mortal para dizer que aquela referencia deve ser decrementada posteriormente, e depois é utilizado o newRV_inc quando vai adicionar o hash no array. Se pararmos para pensar dois segundos, podemos evitar isso simplesmente utilizando o newRV_noinc. O que acontece é que o HV* hash já vai ter o refcount em 1, e quando o array for destruído o seu refcount será imediatamente 0 e ele também será destruído. Uma forma interessante de ver como isso acontece é utilizar o valgrind para ver se você deixou algum memory leak. Dessa forma, terminamos essa primeira parte com a construção da estrutura de dados Perl em C (já brincando com o refcount).

        dSP;
        ENTER;
        SAVETMPS;
        AV* data = (AV*)sv_2mortal((SV*)newAV());
        int i;
        for (i = 0; i < 3; i++) {
                HV* hash = (HV*)newHV();
                U32 ret = 0;
                hv_store(hash,"ASDFGH",6,newSViv(i),ret);
                av_push(data,newRV_noinc((SV*)hash));
        }
        FREETMPS;
        LEAVE;

Agora um passeio rápido pelo perlapi

Bem, como vamos chamar um método do Storable, precisamos, é claro, fazer "use Storable". Seria completamente possível utilizar o que vamos falar mais na frente sobre o perlcall e fazer um eval de "use Storable". Mas como queremos brincar um pouco mais, vamos até o perlapi e descobrimos o módulo load_module. Esse método é consideravelmente simples. Recebe o nome do módulo, opcionalmente a versão e ainda mais opcionalmente ainda os parâmetros do use.

então...

Perl

C

 require Storable;

 SV* module = newSVpv("Storable",8);
 load_module(PERL_LOADMOD_NOIMPORT,module,NULL);

Neste momento a flag PERL_LOADMOD_NOIMPORT é importante, por que nós não estaremos no contexto de uma aplicação normal Perl, então o import falharia. Mas não vamos entrar muito nesses detalhes. O importante dessa parte é perceber que as funcionalidades do interpretador Perl estão completamente acessíveis do C.

O nosso objetivo real, perlcall

O que queriamos mesmo era passar um valor para uma função Perl, então é aí que chegamos ao perlcall, que explica como chamar códigos Perl de dentro do C, e mais importante que isso, como passar informações do C para o Perl. Então vamos lá... Para fins de delimitação de escopo, vamos considerar que vamos ter uma função wrapper em C para a função freeze do Storable. dessa forma, a assinatura é a seguinte:

 int storable_freeze(char** output, SV* data);

Que tem mais ou menos o mesmo sentido da função Storable::freeze, recebe um scalar (no caso, SV* data) e retorna um conjunto de bytes com os dados serializados (nesse caso, vai definir output para um ponteiro alocado do tamanho do valor retornado pela função. Considerando isso, o nosso código inicial ganha mais 4 linhas (dentro do escopo principal) para chamar esse método e escrever a serialização para STDOUT chegando ao seguinte:

 void test_storable() {
         dSP;
         ENTER;
         SAVETMPS;
         AV* data = (AV*)sv_2mortal((SV*)newAV());
         int i;
         for (i = 0; i < 3; i++) {
                 HV* hash = (HV*)newHV();
                 U32 ret = 0;
                 hv_store(hash,"ASDFGH",6,newSViv(i),ret);
                 hv_store(hash,"ASDFGHIJ",8,newSViv(i),ret);
                 av_push(data,newRV_noinc((SV*)hash));
         }
         char* serialized;
         int len = storable_freeze(&serialized,(SV*)data);
         write(1,serialized,len);
         free(serialized);
         FREETMPS;
         LEAVE;
 }

Então agora vamos trabalhar na função storable_freeze. Em primeiro lugar, definimos o início e o fim de um escopo:

 int storable_freeze(char** output, SV* data) {
          dSP;
         ENTER;
         SAVETMPS;
         STRLEN length;

         LEAVE;
         FREETMPS;
         return length;
 }

Agora falta chamar o "Storable::freeze" e obter o resultado. Mas para chamar o método precisamos passar os parâmetros...

Como você provavelmente sabe, o Perl lida com parâmetros e retornos como listas. O método recebe uma lista de argumentos (acessível por @_) e retorna outra lista. Então você precisa submeter essa lista e capturá-la de volta. Para isso, existe uma série de macros que ajudam a fazer tudo certinho. Para começar, você precisa dizer que vai manipular essa lista...

 PUSHMARK(SP);
 // aqui voce pode manipular a lista
 PUTBACK;

E, de uma forma bastante intuitiva, você faz push dos argumentos. No nosso caso...

 XPUSHs(newRV_inc((SV*)data));

E, na volta, você pode querer pegar os retornos... para isso, chama a macro POPs (o s é de scalar, quando você ler a documentação vai ver que você pode já obter numificado ou stringificado.

 SPAGAIN;
 SV* serialized  = POPs;
 PUTBACK;

Dessa forma, falta apenas fazer o chamado ao método "Storable::freeze" e retornar a string. Para isso vamos chamar o call_pv com a flag G_SCALAR para dizer que o contexto da chamada é scalar. Depois vamos converter em char* e retornar. Mas como temos um problema de escopo, vamos fazer um malloc e um memcpy. Segue então o código completo para o storable_freeze.

 int storable_freeze(char** output, SV* data) {
         dSP;
         ENTER;
         SAVETMPS;
         STRLEN len;
         // preparar os parâmetros
         PUSHMARK(SP);
         XPUSHs(newRV_inc((SV*)data));
         PUTBACK;
         // chamar o método
         call_pv("Storable::freeze",G_SCALAR);
         // Obter o retorno
         SPAGAIN;
         SV* serialized = POPs;
         PUTBACK;
         // colocar no char*
         char *ret = SvPV(serialized,len);
         *output = malloc(len);
         memcpy(*output,ret,len);
         // encerrar o escopo
         FREETMPS;
         LEAVE;
         return len;
 }

E finalmente, o perlembed

E agora, a parte final, é a de conseguir rodar código Perl de dentro do C. Para isso é necessário inicializar o interpretador Perl dentro do seu programa, é necessário compilar o seu programa com um conjunto de opções específico, que é aquilo que foi utilizado para compilar a versão do Perl que você está usando.

Em primeiro lugar, vamos aos includes. Para fazer as coisas que fizemos aqui até agora você vai precisar de dois includes do Perl,

 #include 
 #include 

Mas já que estamos falando de includes, uma vez que usamos o memcpy aqui, string.h também é necessário.

 #include 

Sempre que utilizamos o perl embarcado em um programa em C, precisamos utilizar a variável estática do interpretador...

 static PerlInterpreter *my_perl;

A parte de inicializar e finalizar o interpretador se resume em grande parte a copiar+colar. Com uma atenção especial para a inicialização do XS, uma vez que queremos utilizar o Storable que também usa o XS. Então... aí vai o código final.

 #define PERL_CODE /*
 # To test this run
 # cc -o test.o test.c `perl -MExtUtils::Embed -e ccopts -e ldopts` && ./test.o | perl test.c
 use Storable qw(thaw);
 use Data::Dumper;
 local $/ = undef;
 my $b = ;
 print Dumper(thaw($b))."\n";
 __END__
                   */
 #include 
 #include 
 #include 
 /***    The Perl interpreter    ***/
 static PerlInterpreter *my_perl;

 int storable_freeze(char** output, SV* data) {
         dSP;
         ENTER;
         SAVETMPS;
         STRLEN len;
         PUSHMARK(SP);
         XPUSHs(newRV_inc((SV*)data));
         PUTBACK;
         call_pv("Storable::freeze",G_SCALAR);
         SPAGAIN;
         SV* serialized = POPs;
         PUTBACK;
         char *ret = SvPV(serialized,len);
         *output = malloc(len);
         memcpy(*output,ret,len);
         FREETMPS;
         LEAVE;
         return len;
 }

 void test_storable() {
         dSP;
         ENTER;
         SAVETMPS;
         AV* data = (AV*)sv_2mortal((SV*)newAV());
         int i;
         for (i = 0; i < 3; i++) {
                 HV* hash = (HV*)newHV();
                 U32 ret = 0;
                 hv_store(hash,"ASDFGH",6,newSViv(i),ret);
                 hv_store(hash,"ASDFGHIJ",8,newSViv(i),ret);
                 av_push(data,newRV_noinc((SV*)hash));
         }
         char* serialized;
         int len = storable_freeze(&serialized,(SV*)data);
         write(1,serialized,len);
         free(serialized);
         FREETMPS;
         LEAVE;
 }




 EXTERN_C void xs_init (pTHX);
 EXTERN_C void boot_DynaLoader (pTHX_ CV* cv);
 EXTERN_C void xs_init(pTHX) {
    char *file = __FILE__;
    dXSUB_SYS;
    newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);
 }

 int main(int argc, char** argv, char** env) {
         PERL_SYS_INIT3(&argc,&argv,&env);
         my_perl = perl_alloc();
         perl_construct(my_perl);
         PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
         char *embedding[] = { "", "-e", "0" };
         perl_parse(my_perl, xs_init, 3, embedding, (char **)NULL);

         SV* module = newSVpv("Storable",8);
         load_module(PERL_LOADMOD_NOIMPORT,module,NULL);

         test_storable();

         perl_destruct(my_perl);
         perl_free(my_perl);
         PERL_SYS_TERM();
         return 0;
 }

Então para testar, você pode baixar o arquivo test.c e rodar

 cc -o test.o test.c `perl -MExtUtils::Embed -e ccopts -e ldopts` && ./test.o | perl test.c

AUTHOR

Daniel Ruoso

blog comments powered by Disqus