Encape GObjects em Python

Não é preciso ser um guru de C para encapar módulos para Python

Aprender a encapar módulos C GTK+ para usar em Python permitirá que você use um GObject com código C sempre que quiser, quer você seja ou não proficiente em C.

Ross Burton, Software Engineer, OneEighty Software Ltd

Ross Burton é graduado em Ciência da Computação e, durante o dia, cria códigos em Java e para sistemas embarcados. À noite, para fugir desse horror, ele prefere Python, C e GTK+. O email de Ross é ross@burtonini.com.



07/Dez/2012 (Primeira publicação 07/Dez/2012)

Python é uma linguagem maravilhosa para criar interfaces gráficas. Graças à velocidade em que é possível escrever o código e à ausência de um ciclo de compilação demorado, as interfaces podem estar em operação em minutos e poderão ser usadas pouco depois disso. Associado à capacidade do Python de vincular facilmente a bibliotecas nativas, surge um ambiente excelente.

gnome-python é o pacote que encapa GNOME e suas bibliotecas associadas para Python. Isso permite criar aplicativos em Python que têm a mesma aparência dos aplicativos GNOME principais, mas em uma fração do tempo que levaria para escrever em C.

No entanto, não usar C tem uma desvantagem. A maior parte do GNOME é escrita em C, portanto, para que widgets sejam usados em Python, é necessário encapá-los. Essa é uma tarefa rápida para pessoas que sabem como funciona o encapamento, mas não é automática, e a menos que os widgets sejam parte das bibliotecas principais do GNOME, ou ao menos muito útil, ele não será encapado. Os desenvolvedores de C precisam escrever códigos mais complicados, mas eles recebem os novos brinquedos primeiro!

Mas não precisa ser assim. Embora o processo de encapar widgets seja, tradicionalmente, uma arte conhecida apenas por algumas pessoas, ela não é tão difícil. Se você puder encapar novos widgets ao encontrá-los, é possível usá-los em programas Python imediatamente.

Este artigo descreve como encapar um GObject codificado em C (a classe base definitiva de todos os widgets GTK+ e de muitos objetos relacionados) para que possa ser usado em código Python. gnome-python versão 1.99.x deve estar instalado na sua máquina (se não estiver, consulte Recursos para obter um link). Se você estiver usando pacotes, certifique-se de que o pacote de desenvolvimento esteja instalado. Também é necessário que Python 2.2 e seus cabeçalhos estejam instalados. Supõe-se que você tenha conhecimento de Make, Python, GTK+ 2 e um pouco de C.

Para demonstrar o processo, eu irei encapar EggTrayIcon, um widget GTK+ que abstrai um ícone na área de notificação. Esta biblioteca está no CVS do GNOME, no módulo libegg. Ao final deste artigo, teremos um módulo Python nativo chamado trayicon, que contém um objeto TrayIcon.

Para começar, coloque eggtrayicon.c e eggtrayicon.h (os links estão no final deste artigo na seção Recursos) em um novo diretório. Essa fonte foi configurada para um ambiente automake (mas não estará nesse ambiente), por isso, remova #include <config.h> dos arquivos ou crie um arquivo vazio chamado config.h e, em seguida, crie um makefile vazio. Nós preencheremos esse arquivo ao longo do artigo.

Criando a definição de interface

A primeira etapa para encapar esse objeto é criar trayicon.defs, um arquivo que especifica a API do objeto. Os arquivos de definição são escritos em uma linguagem semelhante a Scheme e, embora sejam fáceis de produzir para pequenas interfaces, podem ser cansativos de escrever para grandes interfaces, ou para iniciantes.

gnome-python inclui uma ferramenta chamada h2def. Essa ferramenta analisa um arquivo de cabeçalho e gera um arquivo de definição bruto. Observe que, como ela não analisa de fato o código C, mas apenas usa expressões regulares, ela espera GObjects com formatação tradicional e pode não analisar corretamente código C com formatação estranha.

Para gerar o arquivo de definições inicial, chamamos h2def desta forma:

python /usr/share/pygtk/2.0/codegen/h2def.py eggtrayicon.h > trayicon.defs

Observe que será necessário alterar o caminho para o diretório onde h2def.py está, caso você não o tenha instalado em /usr.

Se olharmos para o arquivo de definições gerado agora, ele deve fazer algum sentido. Há uma definição para a classe EggTrayIcon, os construtores, os métodos send_message e cancel_message. Esse arquivo não tem nenhum erro óbvio, e não queremos remover nenhum dos métodos ou campos, portanto, não é necessário editá-lo. Observe que esse arquivo não é específico de Python e pode ser usado por outras ligações entre linguagens também.


Gerando o wrapper

Agora que temos a definição da interface, podemos gerar a maior parte do wrapper Python. Isso envolve a geração de um arquivo de substituição. Arquivos de substituição informam ao gerador de código quais cabeçalhos devem ser incluídos, qual será o nome dos módulos etc.

Um arquivo de substituição é separado em várias seções, usando %% (no estilo de lex/yacc). Essas seções definem os cabeçalhos a serem incluídos, o nome do módulo, os módulos Python a serem incluídos, as funções a serem ignoradas e, por fim, as funções de encapamento manual. Este é o arquivo de substituição inicial do nosso módulo trayicon.

Listagem 1. trayicon.override
%%
headers
#include <Python.h>               
#include "pygobject.h"
#include "eggtrayicon.h"
%%
modulename trayicon                     
%%
import gtk.Plug as PyGtkPlug_Type       
%%
ignore-glob
  *_get_type                            
%%

Vamos examinar esse código novamente com maior atenção:

  1. headers
    #include <Python.h>
    #include "pygobject.h"
    #include "eggtrayicon.h"
    São os arquivos de cabeçalho a serem incluídos durante o desenvolvimento do wrapper. Python.h e pygobject.h precisam ser incluídos sempre, e, como estamos encapando eggtrayicon.h, precisamos incluí-lo também.
  2. modulename trayicon
    A especificação modulename declara o módulo no qual o wrapper estará.
  3. import gtk.Plug as PyGtkPlug_Type
    São as importações Python para o wrapper. Observe a convenção de nomenclatura. É necessário segui-la para que o módulo seja compilado. Geralmente basta importar a superclasse para o objeto. Por exemplo, se o objeto herdou diretamente do GObject, usaríamos:
    import gobject.GObject as PyGObject_Type
  4. ignore-glob
    *_get_type
    É um padrão glob (uma expressão regular estilo shell) de nomes das funções a serem ignoradas. Python lida com o código de tipo para nós, portanto, ignoramos as funções *_get_type. Do contrário, elas seriam encapadas.

Agora que desenvolvemos nosso arquivo de substituição, podemos usá-lo para gerar o wrapper. As ligações gnome-python oferecem uma ferramenta mágica para gerar o wrapper, que podemos usar trivialmente. Inclua o seguinte no makefile:

Listagem 2. Makefile inicial
DEFS='pkg-config --variable=defsdir pygtk-2.0'         

trayicon.c: trayicon.defs trayicon.override            
    pygtk-codegen-2.0 --prefix trayicon \              
    --register $(DEFS)/gdk-types.defs \                
    --register $(DEFS)/gtk-types.defs \
    --override trayicon.override \                     
    trayicon.defs > $@

Novamente, em mais detalhes:

  1. DEFS='pkg-config --variable=defsdir pygtk-2.0'
    DEFS é o caminho que contém os arquivos de definição da ligação Python GTK+.
  2. trayicon.c: trayicon.defs trayicon.override
    O código C gerado depende do arquivo de definição e do arquivo de substituição.
  3. pygtk-codegen-2.0 --prefix trayicon \
    Aqui o gerador de código gnome-python é chamado. O argumento prefix é usado como um prefixo para os nomes de variáveis dentro do código gerado. Pode ser o que você quiser, mas o uso do nome do módulo mantém os nomes de símbolos consistentes.
  4. --register $(DEFS)/gdk-types.defs \
    --register $(DEFS)/gtk-types.defs \
    Nosso módulo usa tipos de GLib e GTK+, portanto, também precisamos dizer ao gerador de código para carregá-los.
  5. --override trayicon.override \
    Esse argumento passa o arquivo de substituição que criamos para o gerador de código.
  6. trayicon.defs > $@
    Aqui a opção final para o gerador de código é o próprio arquivo de definições. O gerador de código usa a saída padrão como saída, portanto, redirecionamos isso para o destino, trayicon.c.

Se executarmos agora make trayicon.c e olharmos o arquivo gerado, vemos código C encapando cada função em EggTrayIcon. Não se preocupe com o aviso No ArgType for GdkScreen* ― é normal.

Como se pode ver, o código de encapamento pode ser complicado, portanto, somos gratos ao gerador de código por cada linha que escreve para nós. Posteriormente, aprenderemos como encapar manualmente métodos individuais quando queremos fazer um ajuste fino no encapamento, sem precisar criar todo o wrapper nós mesmos.


Criando o módulo

Agora que a maior parte do wrapper foi criada, precisamos de uma maneira de iniciá-lo. Isso envolve a criação de trayiconmodule.c, que pode ser considerada a função main() dos módulos Python. Esse arquivo contém código padrão (semelhante ao arquivo de substituição), que nós modificamos levemente. Este é o trayiconmodule.c que usaremos:

Listagem 3: Código do módulo TrayIcon
#include <pygobject.h>
 
void trayicon_register_classes (PyObject *d); 
extern PyMethodDef trayicon_functions[];
 
DL_EXPORT(void)
inittrayicon(void)
{
    PyObject *m, *d;
 
    init_pygobject ();
 
    m = Py_InitModule ("trayicon", trayicon_functions);
    d = PyModule_GetDict (m);
 
    trayicon_register_classes (d);
 
    if (PyErr_Occurred ()) {
        Py_FatalError ("can't initialise module trayicon");
    }
}

É necessário explicar alguns detalhes aqui, pois há várias fontes para a palavra trayicon. O nome da função inittrayicon e o nome com o qual o módulo é inicializado são o nome real do módulo Python e, portanto, o nome do objeto compartilhado final. O array trayicon_functions e a função trayicon_register_classes têm seus nomes baseados no argumento --prefix do gerador de código. Como observado, é melhor mantê-los iguais para que o código desse arquivo não fique confuso demais.

Apesar da possível confusão com as fontes de nomes, esse código C é bem simples. Ele inicializa o GObject e o módulo trayicon e registra as classes no Python.

Agora que temos todas as peças, podemos gerar um objeto compartilhado. Adicione isso ao makefile:

Listagem 4. Adições ao makefile
CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.    
LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'                                   
 
trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o                             
    $(CC) $(LDFLAGS) -shared $^ -o $@

Novamente, vamos examinar linha por linha:

  1. CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.
    Essa linha define os sinalizadores de compilação C. Usamos pkg-config para obter os caminhos de inclusão de GTK+ e PyGTK.
  2. LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'
    Essa linha define os sinalizadores do vinculador. Novamente usando pkg-config para obter os caminhos de biblioteca corretos.
  3. trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o
    O objeto compartilhado é construído a partir do código gerado, o código de módulo que acabamos de escrever, e a implementação de EggTrayIcon. As regras implícitas desenvolvem os arquivos .o a partir dos arquivos .c que criamos.
  4. $(CC) $(LDFLAGS) -shared $^ -o $@
    Aqui desenvolvemos a biblioteca compartilhada final.

Agora a execução de make trayicon.so deve gerar o código C a partir das definições, compilar os três arquivos C e, por fim, vincular tudo. Muito bem, desenvolvemos nosso primeiro módulo Python. Se houve falha na compilação e vinculação, verifique novamente os estágios e certifique-se de que não há avisos anteriores que causam erros mais tarde.

Agora que temos trayicon.so, podemos tentar usá-lo em um programa Python. Um bom ponto de partida é carregá-lo e listar seus membros. Execute python em um shell para acessar o interpretador interativo e insira os comandos abaixo.

Listagem 5. Teste interativo do TrayIcon
$ python
Python 2.2.2 (#1, Jan 18 2003, 10:18:59)
[GCC 3.2.2 20030109 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pygtk
>>> pygtk.require("2.0")
>>> import trayicon
>>> dir (trayicon)
['TrayIcon', '__doc__', '__file__', '__name__']

O resultado de dir deve ter sido para você o mesmo que foi aqui. Agora estamos prontos para um exemplo maior!

Listagem 6. Exemplo Hello
#! /usr/bin/python
import pygtk
pygtk.require("2.0")
import gtk
import trayicon                               
t = trayicon.TrayIcon("MyFirstTrayIcon")      
t.add(gtk.Label("Hello"))                     
t.show_all()
gtk.main()

Analisando linha por linha:

  1. #! /usr/bin/python
    import pygtk
    pygtk.require("2.0")
    import gtk
    import trayicon
    Aqui, nós primeiro exigimos e importamos as ligações GTK+ e, em seguida, importamos o novo módulo.
  2. t = trayicon.TrayIcon("MyFirstTrayIcon")
    Agora criamos uma instância de trayicon.TrayIcon. Observe que o construtor toma um argumento em cadeia, o nome do ícone.
  3. t.add(gtk.Label("Hello"))
    Elementos TrayIcon são contêineres de GTK+, portanto, é possível incluir qualquer coisa neles. Aqui eu incluo um widget de rótulo.
  4. t.show_all()
    gtk.main()
    Aqui eu configuro os widgets para que sejam visíveis e inicio o loop de eventos principal do GTK+.

Agora, se você ainda não o fez, inclua o applet Notification Area no Painel do GNOME (clique com o botão direito no painel e, em seguida, em "Add to Panel" -> Utility -> Notification Area). Quando o programa de teste for executado, deve aparecer "Hello" na bandeja. Legal, né?

Figura 1. Exemplo Hello
Exemplo Hello

O que mais a área de notificação permite fazer? Bem, os programas podem dizer à área de notificação que exiba uma mensagem. A maneira como essa mensagem é exibida de fato depende da implementação. No momento, a área de notificação do GNOME exibe uma dica de ferramenta. É possível exibir uma mensagem por meio da função send_message(). Uma rápida olhada na API mostra que ela espera um tempo limite e a mensagem, portanto, isto deve funcionar:

...
t = trayicon.TrayIcon("test")
...
t.send_message(1000, "My First Message")

Mas não funciona. O protótipo de C é send_message(int timeout, char* message, int length), portanto, a API Python também espera um ponteiro de caractere e um tamanho. Isto funciona:

...
t = trayicon.TrayIcon("test")
...
message = "My First Message"
t.send_message(1000, message, len(message))

Mas isso é um pouco feio. Isso é Python: a programação deve ser simples. Se continuarmos nesse caminho, teremos no final C sem ponto e vírgula. Felizmente, podemos encapar métodos individualmente usando o gerador de código gnome-python.


Ajuste fino da interface

O que temos até o momento é a função send_message(int timeout, char *message, int length). Seria melhor se a API Python para EggTrayIcon permitisse chamar send_message(timeout, message). Felizmente, isso não é muito difícil.

Para isso, é necessário editar trayicon.override novamente. É aqui que o nome do arquivo faz sentido: esse arquivo contém principalmente funções de wrapper substituídas manualmente. Explicar como elas funcionam é muito mais difícil que simplesmente mostrar e analisar um exemplo, portanto, aqui está uma função encapada manualmente, send_message code.

Listagem 7. Substituição manual
override egg_tray_icon_send_message kwargs 
static PyObject*
_wrap_egg_tray_icon_send_message(PyGObject *self,
                                 PyObject *args, PyObject *kwargs) 
{
    static char *kwlist[] = {"timeout", "message", NULL}; 
    int timeout, len, ret;
    char *message;

    if (!PyArg_ParseTupleAndKeywords(args, kwargs,    
                                     "is#:TrayIcon.send_message", kwlist,
                                     &timeout, &message, &len))
        return NULL;
    ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
                                     timeout, message, len);
    return PyInt_FromLong(ret); 
}

Para ficar claro, vamos examinar linha por linha novamente:

  1. override egg_tray_icon_send_message kwargs
    Essa linha informa ao gerador de código que nós forneceremos uma definição manual de egg_tray_icon_send_message e que ele não deve gerar uma.
  2. static PyObject*
    _wrap_egg_tray_icon_send_message(PyGObject *self,
    PyObject *args, PyObject *kwargs)
    É o protótipo da ponte de Python para C. É composta de um ponteiro para o GObject no qual o método está sendo chamado, um array de argumentos e um array de argumentos de palavras-chaves. O valor de retorno é sempre um PyObject*, como todos os valores em Python são objetos (mesmo números inteiros).
  3. {
    static char *kwlist[] = {"timeout", "message", NULL};
    int timeout, len, ret;
    char *message;
    Esse array define os nomes para o argumento de palavra-chave que essa função entende. Permitir o uso de um argumento de palavra-chave não é essencial, mas pode deixar códigos com muitos argumentos mais claros e não exige muito trabalho adicional.
  4. if (!PyArg_ParseTupleAndKeywords(args, kwargs,
    "is#:TrayIcon.send_message", kwlist,
    &timeout, &message, &len))
    return NULL;
    Essa chamada de função complicada faz a análise do argumento. Nós damos a lista de argumentos de palavra-chave que entendemos e todos os argumentos que foram dados, e ela define os valores apontados para os argumentos finais. A cadeia de caractere de aparência críptica declarou os tipos de variáveis esperados e será explicada posteriormente.
  5. ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
    timeout, message, len);
    return PyInt_FromLong(ret);
    }
    Aqui nós efetivamente chamamos egg_tray_icon_send_message e convertemos o int retornado em um PyObject.

Isso parece um pouco assustador à primeira vista, mas foi inicialmente copiado do código gerado em trayicon.c. Na maioria das situações, isso é perfeitamente possível se tudo que você quiser é fazer ajuste fino dos parâmetros esperados. Basta copiar e colar a função relevante do C gerado, incluir a linha de substituição mágica e editar o código até que ele faça o que você quer.

A mudança mais importante é modificar os parâmetros esperados. A cadeia de caractere de aspecto obscuro na função PyArg_ParseTupleAndKeywords define os argumentos esperados. Originalmente era isi:TrayIcon.send_message; isso significa que os parâmetros são um int, um char* (s para cadeia de caractere) e um int; e, que se houver uma exceção, a função é chamada TrayIcon.send_message. Não queremos ter que especificar o tamanho da cadeia de caractere em código Python, portanto, alteramos o isi para is#. Usar s# em vez de s significa que PyArg_ParseTupleAndKeywords irá calcular automaticamente o tamanho da cadeia de caractere e configurar outra variável para nós. Exatamente o que queríamos.

Para usar o novo wrapper, basta desenvolver novamente o objeto compartilhado e alterar a chamada send_message no programa de teste para:

t.send_message(1000, message)

Se tudo ocorreu segundo o plano, esse exemplo modificado deve ter o mesmo comportamento, porém com código mais claro.


Final de jogo

Nós encapamos um pequeno, porém útil, GObject C para que pudesse ser usado em Python e até customizamos o wrapper segundo nossas necessidades. As técnicas aqui podem ser aplicadas muitas vezes a diferentes objetos, permitindo usar qualquer GObject encontrado em Python.


Download

DescriçãoNomeTamanho
Sample codel-wrap.zip7KB

Recursos

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux
ArticleID=848563
ArticleTitle=Encape GObjects em Python
publish-date=12072012