Python Bindings: C API & SWIG
© 2009 Rudá Moura
Binding? O que é isso e para que serve?
Um binding é literalmente a ligação ou ponte entre dois sistemas. No caso de bindings para Python, chama-se de extensão a ligação entre bibliotecas desenvolvidas em C ou C++ para o uso direto no interpretador Python.
Para fazer uma extensão, é necessário “embrulhar” (wrap) a API da biblioteca através de métodos que são expostos por uma API definida em C do Python.
Por que eu devo fazer uma extensão em Python? O que ganho com isso? Talvez o motivo seja...
O propósito desse artigo é apresentar um wrapper em cima da biblioteca readline ou libedit nos sistemas BSD, por ser de fácil implementação e ter boa disponibilidade nos sistemas Unix; só é bom lembrar que o interpretador Python já provê a mesma funcionalidade com o modulo readline.
Python C API
Para trabalhar com a API de extensões deve-se incluir o cabeçalho Python.h, este arquivo inclui “de brinde” os cabeçalhos stdio.h, string.h, errno.h,
limits.h e stdlib.h.
Na Python C API, todos os nomes visíveis de símbolos começam com o prefix Py e os nomes internos têm como prefixo _Py. A API disponibiliza um tipo opaco PyObject (na verdade um ponteiro para esse tipo) que é análogo aos objetos em Python.
Os objetos PyObject vivem sempre na memória heap, a exceção à regra são os objetos PyTypeObject declarados como static.
Todo o PyObject possuem um tipo associado e são os elementos utilizados como argumentos e retorno de valores para o interpretador. São definidas macros especiais para testar os tipos, por exemplo PyList_Check(array) retorna verdadeiro ou falso se a variável array for do tipo lista.
Como os objetos vivem na heap, é necessário fazer o controle manual de referências, através das macros Py_INCREF e Py_DECREF, essa é uma tarefa propensa a erros de vazamento de memória.
Vamos parar um pouco com a teoria e ir para prática. No arquivo de inclusão readline.h podemos ver a declaração das funções:
char *readline(const char *); int add_history(const char *);
readline() espera a entrada de dados do usuário, até que seja finalizada por um ENTER (que não é armazenado), o resultado é uma string pré-alocada com os dados digitados. O parâmetro da função é um prompt que é impresso para o usuário.add_history acrescenta o parâmetro no histórico do usuário. Útil para permitir voltar o histórico, como faz a linha de comando do shell Bash.Vejamos como fazer o wrapper de readline()
#include <Python.h>
#include <readline/readline.h>
PyObject *
fn_readline(PyObject *self, PyObject *args)
{
char *prompt;
char *line;
PyObject *o;
if (!PyArg_ParseTuple(args, "s", &prompt))
return NULL;
line = readline(prompt);
o = Py_BuildValue("s", line);
free(line);
return o;
}
PyObject* funcao(PyObject *self, PyObject *args) é a forma geral de qualquer função ou método (por isso o self) emcapsulado pela API em C;PyArg_ParseTuple “desempacota” um objeto Python para uso em C. Neste caso, o argumento da função é uma tupla, mas o parâmetro que queremos é uma string;NULL sinaliza ao interpretador uma condição de erro;Py_BuildValue é utilizado para construir (“embrulhar”) um novo objeto Python (um PyObject *) a partir de elementos em C. neste caso, uma string em Python a partir de uma string em C;readline() na variável line deve ser desalocada para evitar vazamento de memória.PyObject.Continuaremos para a função add_history():
PyObject *
fn_add_history(PyObject *self, PyObject *args)
{
char *line;
int status;
PyObject *o;
if (!PyArg_ParseTuple(args, "s", &line))
NULL;
status = add_history(line);
o = Py_BuildValue("i", status);
return o;
}
Basicamente igual a função anterior, a única diferença é o retorno de um valor inteiro em vez de uma string. Esse valor não tem muita importância, poderia-se retornar um None, mas o None possui uma peculiaridade, vejamos...
// Versão alternativa, retorna None
PyObject *
fn_add_history(PyObject *self, PyObject *args)
{
char *line;
if (!PyArg_ParseTuple(args, "s", &line))
NULL;
add_history(line);
Py_INCREF(Py_None);
return Py_None;
}
O idioma para retorna um objeto None exige o controle referências. Não se deve confundir o retorno de NULL, que repassa uma condição de erro com o retorno de None, que é um valor perfeitamente normal.
O que falta para finalizar o binding é a declaração de quais são as funções que o módulo disponibiliza e sua iniciação, vejamos agora.
static PyMethodDef
methods[] = {
{"readline", fn_readline, METH_VARARGS, NULL},
{"add_history", fn_add_history, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
initreadline(void)
{
(void) Py_InitModule("readline", methods);
}
Já temos tudo o que é necessário para o binding, está na hora de gerar o módulo para uso no Python, esta linha de compilação funciona no Mac OS X, para o Linux troque -dynamiclib por -PIC.
$ gcc -dynamiclib -I/usr/include/python2.5 -lpython -lreadline mod_readline.c -o readline.so
Testando...
>>> import readline
>>> dir(readline)
['__doc__', '__file__', '__name__', 'add_history', 'readline']
>>> line = readline.readline("prompt$ ")
prompt$ Sisters of Mercy
>>> print line
Sisters of Mercy
>>> readline.add_history("Bird On The Wire")
0
>>> readline.add_history("So Long, Marianne")
0
>>> readline.readline("] ")
] seta para cima navega no histórico
Quais são as vantagens de se usar a API em C?
Quais são as desvantagens?
SWIG
O SWIG (Simplified Wrapper and Interface Generator) é um sistema que permite gerar bindings de bibliotecas em C ou C++ para diversas linguagens de programação, em especial olharemos os requisitos de uso do SWIG para a linguagem Python.
O SWIG é um projeto maduro, surgiu em 1995 e praticamente automatiza o processo de criação de módulos de extensão em Python.
O processo de geração dos bindings começa com a definição de um arquivo contendo as inclusões das bibliotecas que se deseja fazer o binding, além de outra seção que permite filtrar quais elementos da biblioteca serão expostos para os bindings.
O SWIG trabalha diretamente com os arquivos #include, portanto um bom conhecimento de C faz-se necessário. Em baixo nível, o SWIG gera código em C de cola para a API em C do Python.
O resultado do processamento do utilitário swig é um arquivo em C que deve ser compilado e linkado diretamente com a biblioteca que se deseja fazer binding e o arquivo em Python que expõe a API como um módulo em Python.
Arquivo de interface readline.i do binding para o SWIG. Este arquivo possui três partes lógicas:
%module que define qual será o nome do módulo para Python;%{ ... %} é reservado para arquivos de inclusão;Embora o arquivo readline.h contenha várias funções e variáveis, nós só estamos interessados apenas em expor as funções readline() e add_history().
// readline.i
%module readline
%{
#include <readline/readline.h>
%}
char *readline(const char *);
int add_history(const char *);
O processo completo para a geração do binding é então:
$ swig -python readline.i $ gcc -c readline_wrap.c -I/usr/include/python2.5 $ gcc -dynamiclib readline_wrap.o -lpython -lreadline -o _readline.so
No caso do exemplo de readline, são gerados o arquivo em C readline_wrap.c e o módulo em Python que se chama readline.py.
Exemplo de uso:
>>> import readline
>>> readline.add_history("quit")
0
>>> line = readline.readline("] ")
] quit tecla para cima
>>> print line
quit
>>> print type(line)
<type 'str'>
A maneira mais fácil de usar o SWIG é incluir literalmente o conteúdo do arquivo ou dos arquivos de inclusão da biblioteca que se deseja fazer o binding no arquivo de interface, pode ser um tiro certo fazer isso. Algo como:
%module readline
%{
#include <readline/readline.h>
#include <readline/history.h>
%}
%include /usr/include/readline/readline.h
%include /usr/include/readline/history.h
Infelizmente o processo falha miseravelmente, mas não é objetivo desse artigo explicar o porque.
Quais são as vantagens do SWIG?
Quais são as desvantagens do SWIG?
.i, caso contrário é melhor usar um binding direto com a API em C.