Back-End

3 nov, 2014

Misturando códigos PHP e C++ com SWIG

Publicidade

A linguagem PHP ganhou popularidade por conta dos aplicativos web, já que tem interfaces para bancos de dados e serviços web. Frameworks inteiros, como o Zend são escritos em PHP. Embora toda essa popularidade e as muitas interfaces sejam maravilhosas, pode acontecer de você ter lógicas de negócios escritas em C ou C++ que gostaria de acessar do código web PHP. Uma opção seria reescrever o código em PHP e fazer novos testes, mas dessa forma você teria que manter duas versões do mesmo programa. Em vez disso, você pode pegar o código C++ que quer usar e deixar com que o SWIG (Simplified Wrapper and Interface Generator) faça o trabalho pesado ao criar vinculações de linguagem que permitam acesso ao código do PHP.

O SWIG é uma ferramenta de desenvolvimento de código livre que faz a interface das linguagens C e C++ com diversas linguagens de script, incluindo PHP, Perl, Python, Tcl e Ruby. Dependendo de como você usa o SWIG, não será necessário manter nenhum código vinculado para fazer as coisas acontecerem. Por exemplo, o SWIG pode usar um arquivo de cabeçalho C++ para uma classe e gerar o código vinculado necessário para que o PHP crie objetos daquela classe e chame os métodos naqueles objetos. Se quiser adicionar um novo método ou classe, simplesmente adicione-os ao projeto C++ e reconstrua, e eles estarão automaticamente disponíveis no PHP.

Embora os artigos sobre a vinculação de linguagens normalmente discutam o acesso de uma linguagem a partir de outra, você perceberá que precisa seguir os dois caminhos. Os projetos modernos em C++, por exemplo, permitem que objetos mandem “sinais” quando eventos interessantes ocorrem, têm funções que são registradas para serem usadas como chamadas durante o processamento. Para tirar vantagem total desse tipo de código C++ do PHP, você precisa ter a possibilidade de incluir o código C++ dentro do código PHP. Tudo isso precisa acontecer no código C++, que sequer percebe que está fazendo chamadas dentro de uma implementação PHP.

O SWIG é uma ferramenta útil para essa finalidade porque pode gerar vínculos de código para muitas linguagens. Além de PHP, Perl, Python e Ruby, é possível usar SWIG com LISP, Ocaml e outras linguagens. Portanto, informações de partes amplas deste artigo podem ser aplicadas para gerar vínculos para outras linguagens.

Começando de forma simples

Para seguir adiante, será necessário instalar os pacotes de desenvolvimento para PHP. Por exemplo, em Fedora, é o php-devel.

O primeiro exemplo é uma classe C++ bastante simples para exemplificar como o SWIG opera. Depois, vamos observar o que será necessário para um trabalho mais sério de vínculo de linguagens, como a exposição das bibliotecas padrão de template (STL) std::vector, std::map e correlatas; smart pointers; alguns recursos de geração de linguagem de interface, renomear métodos e funções para a linguagem-alvo etc. Finalmente, também vamos observar como ligar sinais C++ e mecanismos slot para chamar funções PHP a partir do C++.

Cada exemplo usa o conhecimento adquirido nas etapas prévias e posso omitir alguns detalhes cobertos em exemplos anteriores por conta de espaço. Para manter as coisas simples, evitarei ferramentas automáticas e simplesmente mostrarei os comandos crus que foram utilizados para compilar e gerar código em cada passo.

Embora o uso do SWIG requeira algum conhecimento adicional, o processo é o mesmo para trabalhar com classes maiores ou então com muitas classes ao mesmo tempo. Com o SWIG, é possível trabalhar com uma classe única ou centenas de classes de forma igualmente rápida.

Expondo uma classe C++ para PHP

A declaração da primeira classe, simple1, é exibida no cabeçalho simple1.hpp mostrada na listagem 1. Para limpar a interface um pouco, usei a classe std::string para aceitar uma string com o nome do objeto, que está armazenado em m_pubB. O método hidden não pode ser chamado pelo PHP porque está protegido. O método shown simplesmente retorna duas vezes o valor dado a ele como o primeiro parâmetro. A implementação é exibida na listagem 2. Tudo isso é código C++ comum. Dado o cabeçalho e o arquivo de implementação para simple1.hh e simple1.cpp, os comandos

g++ ‑shared ‑fpic simple1.cpp U    
    ‑o libsimple1.so 
sudo install ‑m 755 libsimple1.so U
   /usr/local/lib

gerarão uma biblioteca compartilhada chamada libsimple1.so. Perceba que os comandos forçam o uso de código, independentemente de posição (PIC), porque está gerando uma biblioteca compartilhada.

Listagem 1: Classe simple1

01 #include <string>
02
03 class simple1
04 {
05 protected:
06 int m_prot1;
07 int hidden( int a );
08 public:
09
10 sim ple1(const std::string& name );
11
12 int m_pubA;
13 std::string m_pubB;
14 int shown(

Listagem 2: Simple1

01 #include "simple1.hh"
02
03 int
04 simple1::hidden( int a )
05 {
06 return a;
07 }
08
09 simple1::simple1( const std::string& name )
10 :
11 m_pubB( name )
12 {
13 }
14
15 int
16 simple1::shown( int a )
17 {
18 return a+a;
19 }

O PIC tem a vantagem de ser carregado em diferentes localizações na memória, o que é útil para bibliotecas compartilhadas, porque permite ao vinculador dinâmico a movimentação deles quando múltiplas bibliotecas quiserem, do contrário, o mesmo endereço. Nesse ponto, presumo que você já tem a biblioteca libsimple1.so instalada no diretório /usr/local/lib e seu sistema está configurado para encontrar esse caminho. Outra forma é usar o LD_LI‐ BRARY_PATH e -L se você quiser usar a biblioteca a partir da localização atual.

O arquivo de cabeçalho e a biblioteca compartilhada são quase tudo o que você precisa para usar a classe simple1 a partir de um programa C++, embora seja possível incluir o cabeçalho e vinculá-lo ao objeto compartilhado (libsimple1.so) e usar simple1 de muitas aplicações C++. Até agora, as coisas estão ligadas a desenvolvimento C++.

Agora, é hora de usar SWIG para gerar os vínculos de linguagem de forma que seja possível criar objetos simple1 e chamar métodos deles do PHP. O SWIG usa um arquivo de definição de interface que descreve classes, funções etc., que devem ser expostas à linguagem para a qual queremos gerar vínculos. A princípio, isso pode soar como qualquer outro pesadelo de manutenção. Você não precisa manter o arquivo de definição de interface à medida que muda o código C++? A definição de interface SWIG para simple é:

%module simple1module
%include stl.i
%include "simple1.hh"

%{
 
     #include "simple1.hh"

%}

As linhas não têm declaração de classe ou nada que remeta à duplicação da definição do simple 1 (listagem 1). A primeira linha declara o nome de módulo para esse vínculo. Uma das restrições aqui é que o nome do módulo não pode ser usado por uma classe na qual já estejamos trabalhando, então você não pode ter um módulo nomeado como simple1, porque este já é usado para uma classe. Para que as coisas fiquem melhores e mais claras, uso o nome simple1module. O único outro lugar no qual esse nome aparece é no código PHP, quando ele inclui o módulo include(), então é bom usar um nome bem descritivo.

O resto do arquivo parece se repetir sozinho, incluindo uma referência à simple1.hh duas vezes. A razão para essa duplicação é que a diretiva %include diz ao SWIG para examinar outras definições de interface (foo.i) ou os arquivos de cabeçalho C++ para os quais você gostaria da vinculação. O SWIG lerá qualquer arquivo de cabeçalho listado com %include, além de analisá-lo e gerar o código para empacotar para a linguagem que você escolheu. O código do empacotador será escrito como simple1_wrap.cpp por padrão.

Esse empacotador gerado provavelmente precisará de acesso a alguns dos arquivos de cabeçalho para compilar. Por exemplo, você não pode chamar simple1::shown() sem ter a declaração da classe no escopo. Isto é, em programação C++ normal, caso do simple1.wrap.cpp, primeiro será necessário usar #include “simple1.hh” antes de chamar shown(). O SWIG copiará o bloco de código definido por %{ … %} para dentro do arquivo de código do empacotador gerado (simpe1_wrapp.cpp), de forma que a compilação seja correta. Nesse exemplo, precisaremos do simple1.hh no escopo, de forma que o #include esteja dentro das chaves, para ser compilado com fcc, porque a definição de classe simple1 está no escopo. O SWIG não liga para o código dentro das chaves – você pode colocar qualquer coisa que queira no arquivo simple1_wrapp.cpp dentro dos blocos com chave.

Em resumo, a linha %include “simple1.hh” diz ao SWIG para gerar código de forma que o PHP possa chamar métodos da classe simple1. A linha também permite que o código gerado (simple1_wrap.cpp) seja compilado com fcc porque a definição de classe simple1 está no escopo. O SWIG não percebe o que está escrito dentro das chaves, você pode colocar o que quiser dentro do arquivo simple1_wrap.cpp dentro do bloco.

A linha %include stl.i inclui um arquivo de typemap para a distribuição SWIG. Mais à frente, iremos analisar os typemaps, mas, por enquanto, é suficiente dizer que stl.i diz ao SWIG como fazer a classe std::string e outras classes STL disponíveis no script PHP.

Agora que você fez a parte difícil e criou simple.i, os dois comandos

swig ‐c++ ‐php5 simple1.i
g++ ‐shared ‐fpic `php‐config ‐‐includes`
         ‐Dzend_error_noreturn=zend_error
          simple1_wrap.cpp ‐L`pwd`
         ‐lsimple1 ‐o simple1module.so

geram o vínculo da linguagem PHP e a compilam junto da biblioteca compartilhada. A primeira linha gera um simple1.wrap.cpp e o simple1module.php. O primeiro arquivo é o código C++, que permite acesso ao simple1 do PHP. O segundo arquivo é um script PHP, para o qual você usa o include() em um arquivo PHP para carregar a vinculação da linguagem e estabelecer acesso. A segunda linha usa GCC para compilar o arquivo fonte simple1_wrapp.cpp – gerado em uma biblioteca compartilhada – e vinculá-lo á biblioteca simple1. Dessa forma, o módulo simple1module contém tanto o código de vinculação SWIG, quanto a implementação do simple1.

A biblioteca compartilhada simple1module.so é uma extensão PHP e deve ser instalada no diretório de extensão PHP:

php‐config ‐‐extension‐dir
sudo install ‐m 755 simple1module.so
     `php‐config ‐‐extension‐dir`

O primeiro comando diz onde o PHP está procurando extensões, e o segundo instala o simple1module.so de forma que o PHP possa encontrá-lo automaticamente.

A única coisa que falta ser feita é pegar a classe simple1 C++ para um spin do PHP. O script simple1_test.php da listagem 3 cria um novo objeto simple1, chama um método e lê uma variável de instância do objeto C++.

Listagem 3: Código PHP usando a classe simple1

01 <?php
02
03 include_once("simple1module.php");
04 print "started...\n";
05
06 $foo = new simple1( "hello" );
07
08 print "foo.pubB : " . $foo‑>m_pubB . "\n";
09 print "foo.shown() : " . $foo‑>shown(181) . "\n";
10
11 ?>

Note que não faz diferença para o código PHP se a classe simple 1 é escrita em C++ ou PHP. O código PHP não se importa com o fato de que o construtor está esperando uma string C++ (std::string) em vez de uma string PHP.

Você pode ver o código em ação na listagem 3, usando o comando:

$ php ‐d enable_dl=On simple1_test.php started...
foo.pubB : hello
foo.shown() : 362

Uma vez que o simple1_test.php inclui o simple1module.php, o arquivo de extensão simple1module.so será carregado, permitindo que objetos simple1 sejam criados. Dependendo da sua configuração, talvez seja preciso habilitar carregamento dinâmico de bibliotecas com o parâmetro -d, como exibido. A biblioteca compartilhada simple1module.so está vinculada à biblioteca C++ compartilhada libsimple1.so para trazê-la na implementação da classe simple1.

Embora um esforço inicial seja requerido para expor uma simples classe PHP, os únicos novos arquivos que você realmente criou manualmente foram o arquivo SWIG simple1.i e o script PHP. O arquivo simple1.i é apenas uma inclusão do header simple1.hh e algumas inclusões SWIG. No final, você conseguiu criar um objeto simple1, acessar seus membros de dados e chamar um método bastante natural para PHP. A vantagem de usar o SWIG dessa forma é que, se você modificar a classe simple1, o SWIG cuida de atualizar toda a extensão PHP para você.

C++ e PHP: contêineres padrão

Agora que você sabe como o SWIG funciona e como usá-lo do PHP, pode usar esse conhecimento para expor código C++ mais complicado ao PHP, incluindo código que aceita ou retorna std::vector, std::list, std::map, ou outras opções; códigos que usam smart pointers para gestão de recursos e códigos que têm classes que emitem sinais, permitindo respostas sem final específico a eventos.

A classe simple2, mostrada na listagem 4, se constrói sobre a classe simple 1 e adiciona três novos métodos, mostrados no final da definição de classe. Perceba que o getMapRange leva a referência a um std::map como seu argumento em vez de fazer uma cópia. Nenhuma mudança à definição de classe C++ foi feita para tornar a vida mais fácil, ao criar vinculações de linguagens ou no uso do SWIG. A definição de três novos métodos é padrão e cria somente dados de teste, retornando-o.

Listagem 4: Usando containers padrão

01 #include <string>
02 #include <vector>
03 #include <map>
04
05 class simple2
06 {
07 ...
08 std::pair< std::string, std::string > getPair();
09 std::vector< std::string > getVector();
10 std: :vector< std::string > getMapRange(
const std::map< int, std::string >& a );
11 };

O arquivo de interface SWIG, simple2.i, é exibido na listagem 5. Para descobrir como expor tipos mais complicados, como templates STL para PHP, o SWIG usa algo chamado typemap. Por exemplo, algumas linguagens têm tipos de classe integer em vez de usar somente 32/64/N bits de armazenamento, como o C++. Um typemap pode distinguir como o SWIG converte de tipos C/C++, como int ou std::string, para tipos que a linguagem cliente pode usar, PHP nesse caso.

Listagem 5: Arquivo de interface SWIG, simple2.i

01 %module simple2module
02 %include stl.i
03 %include "simple2.hh"
04
05 %{
06 #include "simple2.hh"
07 %}
08
09 namespace std {
10 specialize_std_map_on_key( int,CHECK,CONVERT_FROM,
CONVERT_TO)
11 }
12 %template(IntVector) std::vector< int >;
13 %template(StringVector) std::vector< std::string >;
14 %template(StringPair) std::pair< std::string,
std::string >;
15 %template(IntStringMap) std::map< int, std::string >;

Outro truque que um typemap pode executar é criar novos métodos na interface cliente. Você deve estar se perguntando como o SWIG pode injetar novos métodos em classes C++ existentes, o que normalmente não é permitido. O SWIG pode adicionar métodos a classes porque está gerando um empacotador que a linguagem cliente chama. Então, pode criar um novo método com o código para desempenhar alguma operação ou pode renomear métodos na classe, mas tudo isso acontece somente no empacotador.

Você pode querer renomear um método para um formato mais natural para a linguagem cliente. Por exemplo, um programador PHP pode esperar encontrar um método is_empty() para testar se a coleção possui itens ou não, em vez de usar o método empty() que o std::vector oferece. Criar novos métodos também pode fazer com que a interface do empacotador fique mais atrativa para programadores. Por exemplo, um programador pode procurar um método pop de uma coleção em vez da combinação de método back();/pop_ back(), que um programador C++ usaria.

As últimas linhas do arquivo simple2.i dizem ao SWIG quais instâncias de templates devem ser empacotadas. Normalmente, no C++, você não lista explicitamente quais types são usados para instanciar uma classe de template – o compilador faz isso para você automaticamente, porque templates estão sendo usados. O problema nisso com o SWIG é que ele olha somente para o cabeçalho que contém a definição da classe do template. O SWIG não tem uma forma de saber sozinho da definição para quais instâncias da classe template criar empacotadores, então você precisa dizer ao SWIG que quer instâncias int e std::string do std::vector e quais instâncias std::map e std::pair faz.

A linha specialize_std_map_on_key (linha 10, listagem 5) é a adição mais complicada para simple2.i. Olhando o arquivo PHP std_map.i com SWIG, você perceberá uma coleção de macros especializadas. Normalmente, o mapa typemap presume que a chave e o valor usados pelo empacotador são apontadores para objetos C++. Isso funciona bem para coisas como std::string porque o código do empacotador gerado passará em torno dos apontadores para std:: string ao retornar um std::string para o PHP, ou ao aceitar uma como parte da chamada da API.

O problema é que int é passado pelo valor entre C++ e PHP em vez de atravessar um nível de apontador, de forma que você precisa chamar specialize_std_map_on_ key para que o SWIG saiba que, se ele empacotar um stc::map com int como chave, deve esperar que a chave int seja passada por valor, em vez de ser passada por meio de um pointer.

Embora o SWIG cuide de boa parte das questões, agora você precisa lidar com um pouco mais de complexidade. Por exemplo, você instanciou alguns templates explicitamente e especificou um typemap porque um tipo primitivo é passado por valor, em vez de através de um apontador.

O script PHP simple2_test.php exibido na listagem 6 leva a nova extensão para um spin. Como pode ver no blockB, você pode criar uma std::vector< std:: string > a partir do PHP, além de populá-la e enumerá-la diretamente. Perceba que o código PHP usa esses nomes de métodos porque o typemap stc_vector.i renomeou o método empty() para is_empty() e criou um único método pop() de forma que não tenhamos que chamar back() e pop_back() do PHP. No blockE, um std::map<> é criado e passado para o código C++ para atualizar um std::vector<> como resultado.

Listagem 6: Usando o simple2 do PHP

01 <?php
02
03 include_once("simple2module.php");
04
05 $foo = new simple2( "hello" );
06
07 // blockA
08 $a = new IntVector();
09 $a‑>push(5);
10 $a‑>push(15);
11 $a‑>push(35);
12 while( ! $a‑>is_empty() )
13 {
14 print "pop: " . $a‑>pop() . "\n";
15 }
16
17 // blockB
18 $b = new StringVector();
19 echo var_dump($b);
20 $b‑>push("hi manual");
21 $b‑>push("hello");
22 print "b.sz:" . $b‑>size() . "\n";
23 while( ! $b‑>is_empty() )
24 {
25 print "pop: " . $b‑>pop() . "\n";
26 }
27
28 // blockE
29 print "------ calling
simple2::getMapRange()
---------------------------\n";
30 $m = new IntStringMap();
31 $m->set( 7, "value" );
32 $m->set( 13, "else" );
33 $v = $foo->getMapRange( $m );
34 echo var_dump($v);
35 print "getVec.ret.sz:" . $v->size()
. "\n";
36 while( ! $v->is_empty() )
37 {
38 print "pop: " . $v->pop() . "\n";
39 }
40 ?>

Smart pointers: estendendo e renomeando em tempo real

Um grande problema com os exemplos relativos ao simple1 e simple2 é que objetos da classe não são acessados por meio de smart pointers (apontadores inteligentes). Para alguns exemplos simples, isso não importa muito, mas para grandes projetos C++, objetivos provavelmente serão acessados com o uso de smart pointers, como a classe shared_ptr<> do C++ Technical Report 1.

A declaração simple3.hh na listagem 7 é baseada em simple2.hh, mas adiciona provisões de smart pointers. A implementação createSimple cria uma instância de objeto e a retorna como shared_ptr<>.

Listagem 7: Declaração de classe simple3

01 #include <tr1/memory>
02 class simple3;
03 typedef std::tr1::shared_ptr< simple3 > h_simple;
04
05 class simple3
06 { ...
07 public:
08 ~simple3();
09 static h_simple createSimple( const std::string& name );

A interface SWIG é exibida na listagem 8. Em uma primeira impressão, parece que o template tr1::shared_ptr está sendo declarado aqui. Mas como a declaração aparece fora do bloco %{…%}, você está conversando com o SWIG por meio da declaração tr1::shared_ ptr. Ao declarar o método operator‐>(), o SWIG ficará ciente de que um T* pode ser obtido de um shared_ptr, que é, no final das contas, a principal coisa que você quer com um smart pointer. Você não precisa realmente que o SWIG empacote as declarações reais e completas do smart pointer. Em vez disso, você só precisa do suficiente para chegar ao T* que ele contém. A distinção de mostrar somente parte da declaração de classe para o SWIG pode ser útil quando a declaração C++ real é complexa, mas você não precisa de exposição total. Você não entrará em risco aqui, porque a std::tr1::shared_ptr<> real tem um método operator->() compatível com a implementação, então quando compilar o simple3_wrapp.cpp gerado pelo SWIG, o método será encontrado.

Listagem 8: Arquivo de interface SWIG simple3

01 %{
02 #include "simple3.hh"
03 %}
04
05 %include <tr1/memory>
06 namespace std {
07 namespace tr1 {
08 template < class T >
09 class shared_ptr
10 {
11 public:
12 shared_ptr( const shared_ptr<T>& );
13 T* operator->();
14 };
15 };
16 };
17
18 %template(h_simple) std::tr1::shared_ptr< simple3 >;
19 …

O uso de smart pointer é bem natural do PHP:

$foo = simple3::createSimple( "hello" );
print "foo.pubB : " . U
           $foo‐>m_pubB . "\n";

A principal diferença é usar o método estático create-simple para obter o apontador do objeto. Para compilar simple3, você pode precisar adicionar um ‐I /usr/include/c++/4.4.4 à invocação do SWIG.

Modificações

Já mostramos como o std::vector<> foi empacotado pelo std_vector.i para renomear o método empty() e incluir o novo método pop() dentro da classe std::vector, pelo menos até onde o PHP pode ver. A parte relevante do php/std_vecotr.i da distribuição SWIG está na listagem 9. A palavra-chave %rename leva o novo nome como um argumento, e o nome de um método existente na classe para o resto da linha.

Listagem 9: Estendendo o std::vector com SWIG

01 namespace std {
02
03 template<class T> class vector {
04 public:
05 ...
06 bool empty() const;
07 %rename(is_empty) empty;
08 ...
09 %extend {
10 T pop() throw (std::out_of_range) {
11 if (self->size() == 0)
12 throw std::out_of_range(" pop from empty vector");
13 T x = self->back();
14 self->pop_back();
15 return x;
16

Primeiro, você deve declarar o verdadeiro método empty() para dizer ao SWIG a assinatura apropriada; então, deverá renomeá-lo. Perceba que você pode usar o %rename com métodos de classes que foram previamente incluídas com %included. O SWIG então gera o código – por exemplo, simple3_wrapp.cpp, que depende que a classe std::vector tenha um verdadeiro método empty(). A palavra-chave extend inclui novos métodos que você gostaria de classificar para ter em uma vinculação de linguagem. Você deve estar se perguntando como o C++ pode, magicamente, ter métodos adicionais. Na verdade, o C++ não muda. O método pop() é o único disponível na classe PHP ou, mais exatamente, só existe no código C++ gerado para a vinculação de linguagem – no simple3_wrap.cpp, por exemplo.

Como alternativa, você pode usar a palavra-chave %ignore para dizer ao SWIG para não empacotar uma classe como método para a vinculação de linguagem. Essa abordagem pode ser trabalhosa e não faz muito sentido para a linguagem chamar um método.