Entries from March 2011 ↓

Python bindings para libepub usando Shiboken

Tenho um Kindle, gosto muito dele, mas infelizmente não posso dizer o mesmo sobre o formato usado para os livros. Não que eu tenha feito um estudo detalhado do formato mobi e chegado a conclusão que ele é tecnicamente inferior. Não. O problema é que ePub é tão mais popular, o que significa mais livros, e mais ferramentas com as quais brincar. Calibre é ótimo para converter formatos de eBooks, mas eu prefiro a simplicidade de baixar um livro, copiar pro dispositivo e já sair lendo.

Quando meu Kindle não está à mão, meu leitor substituto é um N900 com MeBook, um dos meus aplicativos preferidos – ele procura por livros (no formato ePub) no Feedbooks, baixa pro dispositivo e os mostra numa lista bem bacana, com capas e tudo. Examinando o código fonte do MeBook aprendi que ele usa a libepub para ler eBooks ePub, então pensei que seria interessante fazer bindings Python para libepub. E aqui estamos.

Ah, e mais uma coisa antes de começarmos, encontrei um projeto Python no github chamado libepub. Não verifiquei ainda, mas o menciono aqui pelo bem da informação.

Shiboken

Shiboken é a ferramenta de renome mundial usada para gerar o mundialmente famoso PySide (que ocore ter alcançado a versão 1.0 semana passada – viva!) Ela é uma das melhores ferramentas disponíveis para gerar bindings Python para bibliotecas C++. Mas isso sou eu falando – eu nela. Contudo, libepub é escrita em C, e mesmo que Shiboken possa gerar bindings para um punhado de funções globais e juntá-las num módulo Python, fica uma porcaria. De forma que terei de fazer algumas preparações para que a libepub apareça bela em Python.

Pré-requisitos: arquivos de desenvolvimento (i.e. os cabeçalhos) para ApiExtractor, GeneratorRunner, Shiboken, e libepub. (Para usuários de Debian/Ubuntu isto significa: libapiextractor-dev, libgenrunner-dev, libshiboken-dev, and libepub-dev.) E o compilador C++ mais o CMake, é claro.

Lembrete: ApiExtractor, GeneratorRunner e Shiboken são feitos com Qt (somente módulos principais, nenhuma delas tem ui), mas os bindings gerados não dependem da Qt de forma alguma. Exceto, obviamente, se a biblioteca para qual os bindings serão gerados já a use.

libepub

Primeiro, vamos dar uma olhada na libepub. Ela tem três estruturas usadas como ponteiros opacos:

// Contem informacao sobre o arquivo ePub.
struct epub;
// Objeto iterador para o indice.
struct titerator;
// Objeto iterador para o conteudo do livro.
struct eiterator;

Se você quiser ler o conteúdo de um arquivo ePub, chame a função que irá criar uma estrutura epub, itere por seu índice com um titerator, e depois pelos conteúdos mesmos usando um eiterator.

A maioria das funções seguem aquele formato “C orientado a objetos” visto em outras bibliotecas, com o primeiro argumento sendo um ponteiro para a estrutura que representa o “this” ou “self” em linguagens OO. Exemplos:

void
epub_dump(struct epub* epub);

unsigned char**
epub_get_metadata(struct epub* epub,
                  enum epub_metadata type,
                  int* size);

int
epub_get_data(struct epub* epub,
              const char* name,
              char** data);

CMake

Usarei CMake como o sistema de compilação dos bindings pois me sinto confortável com ele – é o mesmo usado no PySide e no conjunto de ferramentas de geração do Shiboken. Não darei muita atenção à esta parte do processo, apenas veja os arquivos CMakeLists.txt; explicação mais detalhada sobre o processo de compilar um binding pode ser encontrada em PySide Binding Generation Tutorial. Para informação realmente básica, veja PySide CMake Primer.

C++ Wrapper feito à mão para uma biblioteca em C

Para gerar bindings Python para uma biblioteca C++, o sujeito precisa escrever uma descrição XML chamada type system, que declarará o quê precisa ser exposto para Python, e o quê sofrerá modificação: funções globais, classes, namespaces, enums. Por exemplo, se em C++ temos a classe Rectangle, eu a declararia no type system dessa forma:

<value-type name='Rectangle' />

Todos os métodos pertencentes à Rectangle serão expostos em Python automaticamente. O mesmo não vale para as funções em C que representam os métodos para a estrutura epub. Infelizmente não há modo de dizer para o gerador Shiboken que quero a estrutura epub e suas funções representadas como uma classe com métodos, sendo assim precisamos escrever um fino envoltório C++ ao redor das estruturas C. Isto seria um bocado de trabalho para uma grande biblioteca C, mas mesmo com uma minúscula sinto o desconforto de precisar recorrer a este tipo de gambiarra. O type system deveria ser expressivo o bastante para gerar bindings para estruturas e funções em C como se elas fossem objetos e métodos apropriados. Vou marcar isto para futuras melhorias comunitárias/fora-do-trabalho no Shiboken.

Vejamos um pedaço de código do arquivoepub_cpp_wrapper.h:

class EPub {
public:
    ...

    ~EPub() { epub_close(m_epub); }

    static inline EPub*
    open(const char* filename, int debug = 0) {
        struct epub* book = epub_open(filename, debug);
        if (book)
            return new EPub(book);
        return 0;
    }

    ...
    inline void dump() { epub_dump(m_epub); }

    inline int
    get_data(const char* name, char** data) {
        return epub_get_data(m_epub, name, data);
    }

    ...

private:
    explicit EPub(struct epub* ptr) : m_epub(ptr) {}
    EPub(const EPub&amp; other) {}
    EPub&amp; operator=(const EPub&amp; other) {}
    struct epub* m_epub;
};

Note que todos os métodos são marcados como inline para fazer este envoltório o mais fino possível. (GCC, estou olhando pra você!)

Epub::open

A função epub_open se torna o método estático EPub::open que retornará um novo objeto EPub para o arquivo ePub dado pelo parâmetro filename, ou um null pointer se o arquivo for inválido ou não existir.
O construtor para esta classe foi feito privado de forma que o único modo de criar objetos EPub é via EPub::open, que nunca criará um objeto EPub inválido.

~Epub

Em C o responsável por liberar a estrutura epub é epub_close, mas não criarei um método EPub::close(), pois o equivalente C++ para ele é o destrutor da classe.

Um binding Python gerado para o que temos até agora seria mais ou menos assim:

import epub
book = epub.EPub.open('sample.epub')
title = book.get_metadata(epub.EPUB_TITLE)

Claro que não expliquei como isto seria gerado, que o módulo se chamaria epub, o que é epub.EPUB_TITLE, e como Python saberia o que fazer com unsigned char**, mas tenhamos paciência.

Movendo enums por aí

EPUB_TITLE é um valor vindo do enum epub_metadata, se ambos forem exportados para Python conforme estão, eles terão esta aparência:

import epub
epub.epub_metadata
epub.EPUB_TITLE

Assim fica bem feio e fuleiro. Já que epub_metadata é um enum relacionado ao objeto epub (como nos informa o prefixo epub_), será natural que ele seja movido para dentro da class EPub. No meu mundo de fantasia, a tag do type system que descreve um enum C++ para Python teria a opção de movê-lo para dentro de outro objeto, e também de ser renomeado. Algo desse tipo:

<enum-type name='epub_metadata'
           rename='metatada'
           move-into='EPub'
           remove-enum-value-prefix='EPUB_' />

E aqui temos outro item para a lista de “por fazer” do Shiboken. Enquanto esta funcionalidade não é implementada, terei de fazê-lo manualmente.

class EPub {
public:
    enum metadata {
        ID = int(EPUB_ID),
        TITLE = int(EPUB_TITLE),
        CREATOR = int(EPUB_CREATOR),
        CONTRIB = int(EPUB_CONTRIB),
        SUBJECT = int(EPUB_SUBJECT),
        PUBLISHER = int(EPUB_PUBLISHER),
        DESCRIPTION = int(EPUB_DESCRIPTION),
        DATE = int(EPUB_DATE),
        TYPE = int(EPUB_TYPE),
        FORMAT = int(EPUB_FORMAT),
        SOURCE = int(EPUB_SOURCE),
        LANG = int(EPUB_LANG),
        RELATION = int(EPUB_RELATION),
        COVERAGE = int(EPUB_COVERAGE),
        RIGHTS = int(EPUB_RIGHTS),
        META = int(EPUB_META)
    };
    ...
    inline unsigned char**
    get_metadata(metadata type, int* size) {
        return epub_get_metadata(m_epub,
                                 epub_metadata(type),
                                 size);
    }
    ...
};

Os valores do enum epub_metadata foram convertidos para int para impedir o ApiExtractor de emitir toda sorte de warnings dizendo que ele não sabe quem são esses caras.

De qualquer modo, é terrível… ter esse gerador incrementado, e ter de escrever tudo isso… nãããão!

TIterator e EIterator

As estruturas C titerator e eiterator serão envolvidas pelas class C++ TIterator e EIterator, respectivamente, e como a classe EPub seus construtores serão privados.

class TIterator {
public:
    enum type {
        NAVMAP = int(TITERATOR_NAVMAP),
        GUIDE = int(TITERATOR_GUIDE),
        PAGES = int(TITERATOR_PAGES)
    };
    ~TIterator() { epub_free_titerator(m_iter); }
    inline bool isValid() {
        return epub_tit_curr_valid(m_iter);
    }
    ...
private:
    friend class EPub;
    explicit TIterator(struct titerator* iter)
        : m_iter(iter) {}
    struct titerator* m_iter;
};

Novas instâncias de TIterator e EIterator são criadas por métodos de EPub, por isso ela deve ser feita amiga (friend) das classes iteradoras.

class EPub {
public:
    ...
    inline EIterator*
    get_iterator(EIterator::type type, int opt = 0) {
        struct eiterator* it = epub_get_iterator(m_epub, eiterator_type(type), opt);
        if (it)
            return new EIterator(it);
        return 0;
    }
    inline TIterator*
    get_titerator(TIterator::type type, int opt = 0) {
        struct titerator* it = epub_get_titerator(m_epub, titerator_type(type), opt);
        if (it)
            return new TIterator(it);
        return 0;
    }
    ...
};

python-epub

Agora é hora de dizer ao gerador o quê entra e o quê será modificado nos bindings.

O cabeçalho global

O cabeçalho global é um arquivo que inclui todos os outros cabeçalhos da biblioteca que será analizada. No cabeçalho global o desenvolvedor de bindings pode também adicionar algumas cláusulas, como um #define que irá causar alguma condição de mudança nos cabeçalhos da biblioteca alvo, que irá afetar o binding gerado.

O arquivo epub_global.h que uso aqui é muito simples:

#ifndef EPUB_GLOBAL_H
#define EPUB_GLOBAL_H

#include &lt;epub.h&gt;
#include &lt;epub_shared.h&gt;
#include &lt;epub_version.h&gt;

#include "epub_cpp_wrappers.h"

// ApiExtractor reclamara' se
// encontrar apenas pre-definicoes.
struct titerator {};
struct eiterator {};

#endif

Como o comentário nos diz, ApiExtractor não gosta quando encontra pré-declarações sem definições. Escolhi adicionar estas definições falsas para struct titerator e struct eiterator. (Por alguma razão o gerador nada diz sobre struct epub.) Outra opção seria não usar estas definições falsas, mas adicionar uma linha ao arquivo type system dizendo ao gerador para ignorar avisos relativos à estas estruturas.

A descrição do Type System

Aqui segue um pedaço de código do arquivo typesystem_epub.xml.

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <rejection enum-name='epub_metadata' />
    ...
    <object-type name='EPub'>
        <enum-type name='metadata' />
        ...
    </object-type>
</typesystem>

O enum epub_metadata não é exportado para Python (ao menos não diretamente) e para evitar que o gerador emita seus alertas, ele precisa ser explicitamente rejeitado; seu substituto C++ é adicionado depois dentro do object-type EPub. O mesmo acontece com os outros enums.

Na notação do type system object-type se refere aos objetos que são passados somente como ponteiros (como EPub, cujos construtor e operador de cópia são privados). Se, de outro modo, o objeto pode ser passado como valor, ele deve ser declarado como value-type, conforme nosso exemplo de Rectangle mencionado anteriormente.

O Protocolo de Iterador de Python

Em Python, se um objeto suporta o Protocolo de Iterador, poderei usar em comandos for, desta forma:

from epub import EPub, TIterator
book = EPub.open(self.epub_file)
for toc_it in book.get_titerator(TIterator.NAVMAP):
    if not toc_it.isValid():
        continue
    print 'link : ' + toc_it.link()
    print 'label: ' + toc_it.label()

Seguir o Protocolo de Iterador de Python consiste tão somente de um objeto implementar os métodos __iter__() e __next__().

Nota: o nome correto para o método é next() e não __next__(). Este é apenas um bug menor no gerador, que acabei de encontrar.

O XML para adicionar as funcionalidades do protocolo de iterador à classe TIterator será extenso, de forma que o partirei em duas partes, a primeira lidando com templates de código do type system.

Os templates do Type System

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <template name='iterator.__iter__'>
        Py_INCREF(%PYSELF);
        %PYARG_0 = %PYSELF;
    </template>
    <template name='iterator.__next__'>
        if (%CPPSELF.next()) {
            <insert-template name='iterator.__iter__' />
        } else {
            PyErr_SetNone(PyExc_StopIteration);
        }
    </template>
    ...

Os métodos de iterador para TIterator e EIterator terão exatamente a mesma implementação, então é esperto usar os templates do type system, e ter o código, e seus eventuais bugs, num único lugar.

O iterator.__iter__ método precisa apenas returnar o próprio objeto com seu contador de referências incrementado em um. iterator.__next__ chama o next() do objeto C++ subjacente (e também retorna a si próprio, e incrementa o refcounter), e sobe uma exceção Python StopIteration quando chega ao fim.

As variáveis do type system %PYSELF, %PYARG_0 e %CPPSELF são substituídas por valores dependentes do contexto onde são usados (e.g. as classes TIterator ou EIteration). Verifique a documentation para saber seus significados.

Adicionando os métodos de iteração

    ...
    <object-type name='TIterator'>
        ...
        <modify-function signature='next()' remove='all' />
        <add-function signature='__iter__' return-type='PyObject*'>
            <inject-code class='target' position='beginning'>
                <insert-template name='iterator.__iter__' />
            </inject-code>
        </add-function>
        <add-function signature='__next__' return-type='PyObject*'>
            <inject-code class='target' position='beginning'>
                <insert-template name='iterator.__next__' />
            </inject-code>
        </add-function>
    </object-type>
     ...
</typesystem>

Primeiro o next() original do C++ é removido, então aqueles para o protocolo de iterador do Python são adicionados, usando a tag <insert-template/> para inserir o código personalizado definido anteriormente. A exatas mesmas linhas serão adicionadas à classe EIterator.

Só mais um pouco de gambiarras

Apenas um obstáculo permanece no caminho de ter iteradores Python apropriados. Quando o comando for de Python for usado para iterar sobre um objeto iterável, na primeira rodada ele chama o método __iter__ do objeto, e imediatamente após ele chama next, e continua chamando next para cada nova iteração.

O problema aqui é que o iterador C subjacente retorna um objeto levando conteúdo apropriado quando __iter__ é chamado, então a forma como a iteração do for Python funciona fará o primeiro item válido ser pulado. Uma gambiarra para este caso é usar uma flag no wrapper C++ que verificará se o iterador acabou de ser criado, de forma que ele não pulará para o iterador seguinte quando next for chamada pela primeira vez.

class TIterator {
public:
    ...
    inline bool next() {
        if (m_isFirst) {
            m_isFirst = false;
            return true;
        }
        return epub_tit_next(m_iter);
    }
private:
    friend class EPub;
    explicit TIterator(struct titerator* iter)
        : m_iter(iter), m_isFirst(true) {}
    struct titerator* m_iter;
    bool m_isFirst;
};

Pude ter esta liberdade porque TIterator é uma classe completamente sob meu (eu, o desenvolvedor do binding) controle. Se struct titerator fosse uma classe C++ desde o começo, esta solução não seria a melhor. Talvez se a libshiboken (a biblioteca de apoio usada por todos os bindings gerados pelo Shiboken) fornecesse uma classe iteradora base para lidar com essa diferença particular entre iteradores Python e C++. Ou talvez as classes geradas, quando identificadas como iteráveis pela presença dos métodos do protocolo de iteração adicionados pelo desenvolvedor do binding, poderiam ter tal funcionalidade. A última opção me parece a melhor, e esse é mais um item pra minha lista de melhorias futuras.

Conversões Personalizadas

Retornando valores unicode

Os métodos EIterator::curr() e EIterator::curr_url() returnam valores do tipo char*, que não tem um conversor (const char* é que tem), por isso escrevi um código específico para converter isto para unicode do Python.

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <template name='return_char_pointer'>
        char* %0 = %CPPSELF.%FUNCTION_NAME();
        if (%0) {
            %PYARG_0 = PyUnicode_DecodeUTF8(%0, strlen(%0), "strict");
        } else {
            Py_INCREF(Py_None);
            %PYARG_0 = Py_None;
        }
    </template>
    ...
    <object-type name='EIterator'>
        ...
        <modify-function signature='curr()'>
            <inject-code class='target' position='beginning'>
                <insert-template name='return_char_pointer' />
            </inject-code>
        </modify-function>
        <modify-function signature='curr_url()'>
            <inject-code class='target' position='beginning'>
                <insert-template name='return_char_pointer' />
            </inject-code>
        </modify-function>
        ...
    </object-type>
    ...
</typesystem>

Modificando a assinatura de um método

As assinaturas de alguns métodos C++ não podem ser automaticamente convertidas para código Python que faça sentido, então adicionei mais um pouco de código feito sob medida para tratar das situações caso a caso. Por exemplo, o método

unsigned char**
EPub::get_metadata(metadata type, int* size);

O argumento int* size recebe um ponteiro para um int, que conterá o tamanho da lista de strings unicode retornada como unsigned char**. Em Python ele meramente retornará uma lista de objetos unicode, e a chamada à get_metadata não precisa do argumento size.

Creio que a descrição do type system será o bastante para ver como o sistema de modificação funciona.

<object-type name='EPub'>
  <enum-type name='metadata' />
  <modify-function signature='get_metadata(EPub::metadata,int*)'>
    <modify-argument index='2'>
      <remove-argument />
    </modify-argument>
    <modify-argument index='return'>
      <replace-type modified-type='PyTuple' />
    </modify-argument>
    <inject-code class='target'>
      unsigned char** data = 0;
      int size;
      data = %CPPSELF.%FUNCTION_NAME(%1, &size);
      if (data) {
        %PYARG_0 = PyTuple_New(size);
        PyObject* uni = 0;
        for (int i = 0; i < size; ++i) {
         uni = PyUnicode_DecodeUTF8((const char*)data[i],
                                    strlen((const char*)data[i]),
                                    "strict");
         PyTuple_SetItem(%PYARG_0, i, uni);
       }
       for (int i = 0; i < size; ++i)
         free(data[i]);
       free(data);
     } else {
       Py_INCREF(Py_None);
       %PYARG_0 = Py_None;
     }
   </inject-code>
  </modify-function>
  ...
</object-type>

Baixando, compilando, etc.

Chega de explicações, agora vamos experimentar o código. Lembre-se que precisará também dos arquivos de desenvolvimento que mencionei muito tempo atrás

Clone a versão mais recente do repositório git:

git clone git://github.com/setanta/python-epub.git

ou baixe o tarball: python-epub.tar.bz2

No diretório do código fonte crie um diretório build e…

cd python-epub
mkdir build
cd build
cmake ..
make
ctest

O último comando merece uma conversa à respeito.

Testar, Testar, Testar

Quando trabalhando com desenvolvimento de bindings há uma miríade de coisas que podem dar errado, e muitas delas dão errado em completo silêncio. Tendo isto em mente, eu digo-lhe que ter testes de unidade torna a vida do desenvolvedor de bindings suportável.

Veja o diretório de testes para exemplos de como usar o bingind python-epub.

Para resultados detalhados dos testes, rode ctest com a opção -V (verboso).

Exemplo de UI (senão seria tedioso)

Mas a própria UI é bem tediosa, embora feita com o espetacular PySide, ela nem sequer mostra imagens. Por outro lado, com um pouco de código agora posso ver o conteúdo de um ebook ePub.

De dentro do diretório python-epub/build chame o visualizador de ePub dessa forma:

python ../simple-ui/bookviewer.py ../tests/beyond-the-wall-of-sleep.epub

Ele sempre espera um parâmetro com o caminho para um arquivo ePub, neste caso “Beyond the Wall of Sleep” de Lovecraft.

Screenshots:

Simplest ePub viewer (made with PySide)

Python bindings for libepub using Shiboken

I have a Kindle, I like it very much, but unfortunately I can’t say the same about the format it used for books. Not that I have made a detailed study of the mobi format and came to the conclusion that it is technically inferior. No. The problem is that ePub is so much more popular, which mean more books, and more tools to play with. Calibre is awesome to convert between eBook formats, but I prefer the simplicity of downloading a book, copying it to the device and reading right away.

When my Kindle is not at hand, my replacement reader is a N900 with MeBook, one of my favorite apps – it looks for books (in ePub format) on Feedbooks, downloads to device and show them in a nice list with covers and all. Looking at MeBook source code I learned that it uses libepub to read the ePub eBooks, so I thought it would be interesting to make a Python binding to libepub. And here we are.

Ah, and one more thing before we start, I found a related Python project on github called libepub. Hadn’t checked it yet, but I mention it here for information’s sake.

Shiboken

Shiboken is the world famous tool used to generate the world famous PySide bindings (that happen to have reached 1.0 last week – yay!) It is one of the best tools around to generate Python bindings for C++ libraries. But that’s me talking – I work on it. But libepub is written in C, and even though Shiboken can generate bindings for a bunch of global functions and put them together in a Python module, that’s crappy. So I’ll have to make some preparations to make libepub appear beautiful in Python.

Pre-requisites: development files (i.e. the headers) for ApiExtractor, GeneratorRunner, Shiboken, and libepub. (For Debian/Ubuntu users this means: libapiextractor-dev, libgenrunner-dev, libshiboken-dev, and libepub-dev.) And the C++ compiler plus CMake, of course.

Remainder: ApiExtractor, GeneratorRunner and Shiboken are made with Qt (only the core libs, no ui for them), but the bindings generated do not depend on Qt at all. Except, obviously, if the library being wrapped already does.

libepub

First let’s have an overview of libepub. It has three structures used as opaque pointers:

// Contains information about the ePub file.
struct epub;
// Iterator object for the Table of Contents.
struct titerator;
// Iterator object for the book contents.
struct eiterator;

If you want to read the contents of an ePub file, call the function that will create an epub structure, iterate through its table of contents with a titerator, and then the contents themselves with an eiterator.

Most functions follow that “object oriented C” format seen in other libraries, with the first argument being a pointer to the structure that represents the “this” or “self” in OO languages. Examples:

void
epub_dump(struct epub* epub);

unsigned char**
epub_get_metadata(struct epub* epub,
                  enum epub_metadata type,
                  int* size);

int
epub_get_data(struct epub* epub,
              const char* name,
              char** data);

CMake

I’ll use CMake as the build system for the bindings because I feel comfortable with it – it is the same used in PySide and all the Shiboken generator tool chain. I’ll not give too much attention to this part of the process, just check the CMakeLists.txt files; more detailed explanation on the process of building a binding can be found in the PySide Binding Generation Tutorial. For really really basic information check PySide CMake Primer.

Handmade C++ Wrapper for a C library

To generate Python bindings for a C++ library one must write a XML description called type system, which will declare what must be exposed on Python land, and if any of this needs modification: global functions, classes, namespaces, enums. For example, if in C++ I have the class Rectangle, I would declare it in the type system this way:

<value-type name='Rectangle' />

All the methods belonging to Rectangle would be exposed to Python automatically. The same is not true for the C functions that representing the methods for the epub struct. Unfortunately there’s no way to tell the Shiboken generator that I want to have the epub structure and functions represented as a class with methods, so we have to make a thin C++ wrapper around the C structures. That would be a lot of work for a huge C library, but even with a tiny one I feel uncomfortable having to resort for this kind of hackery. The type system should be expressive enough to bind C structs and functions as if they were proper objects with methods. I’ll mark this for future community/out-of-work improvements on Shiboken.

Let’s see a snippet from epub_cpp_wrapper.h:

class EPub {
public:
    ...

    ~EPub() { epub_close(m_epub); }

    static inline EPub*
    open(const char* filename, int debug = 0) {
        struct epub* book = epub_open(filename, debug);
        if (book)
            return new EPub(book);
        return 0;
    }

    ...
    inline void dump() { epub_dump(m_epub); }

    inline int
    get_data(const char* name, char** data) {
        return epub_get_data(m_epub, name, data);
    }

    ...

private:
    explicit EPub(struct epub* ptr) : m_epub(ptr) {}
    EPub(const EPub&amp; other) {}
    EPub&amp; operator=(const EPub&amp; other) {}
    struct epub* m_epub;
};

Notice that all methods were marked as inline to make this wrapper as thinner as possible. (GCC, I’m looking at you!)

Epub::open

The epub_open function becomes the static method EPub::open that will return a new EPub object for the ePub file given by filename parameter, or a null pointer if the file is invalid or doesn’t exist.
The constructor for this class was made private so the only way to create EPub objects is via EPub::open, that’ll never create an invalid EPub.

~Epub

In C the responsible for freeing the epub structure is epub_close, but I’ll not make it EPub::close() because the C++ equivalent for it is the class’ destructor.

A generated Python binding for what we have until now would look roughly like this:

import epub
book = epub.EPub.open('sample.epub')
title = book.get_metadata(epub.EPUB_TITLE)

Of course I didn’t explained how this would be generated, that the module would be called epub, what is epub.EPUB_TITLE, and how Python would know what to do with unsigned char**, but bear with me.

Moving enums around

EPUB_TITLE is a value from the epub_metadata enum, if exposed to Python as they are, they’ll look like this:

import epub
epub.epub_metadata
epub.EPUB_TITLE

Which is pretty ugly and lame. Since epub_metadata is an enum related to the epub object (as the epub_ prefix tells us), it would be natural that it was moved inside the EPub class. In my fantasy world, the type system tag that describes a C++ enum to Python would have the option to move it inside another object, and also to be renamed. Something along these lines:

<enum-type name='epub_metadata'
           rename='metatada'
           move-into='EPub'
           remove-enum-value-prefix='EPUB_' />

And here I have another thing for a Shiboken TODO list. While this feature is not implemented, I’ll have to do it manually.

class EPub {
public:
    enum metadata {
        ID = int(EPUB_ID),
        TITLE = int(EPUB_TITLE),
        CREATOR = int(EPUB_CREATOR),
        CONTRIB = int(EPUB_CONTRIB),
        SUBJECT = int(EPUB_SUBJECT),
        PUBLISHER = int(EPUB_PUBLISHER),
        DESCRIPTION = int(EPUB_DESCRIPTION),
        DATE = int(EPUB_DATE),
        TYPE = int(EPUB_TYPE),
        FORMAT = int(EPUB_FORMAT),
        SOURCE = int(EPUB_SOURCE),
        LANG = int(EPUB_LANG),
        RELATION = int(EPUB_RELATION),
        COVERAGE = int(EPUB_COVERAGE),
        RIGHTS = int(EPUB_RIGHTS),
        META = int(EPUB_META)
    };
    ...
    inline unsigned char**
    get_metadata(metadata type, int* size) {
        return epub_get_metadata(m_epub,
                                 epub_metadata(type),
                                 size);
    }
    ...
};

The epub_metadata enum values were cast to int to prevent the ApiExtractor to emitting a bunch of warnings saying that it cannot tell who these guys are.

Anyways, it’s so awful… to have a fancy generator, and having to write all this… noooo!

TIterator and EIterator

The C structures titerator and eiterator will be wrapped by the C++ classes TIterator and EIterator, respectively, and like the EPub class their constructors are private.

class TIterator {
public:
    enum type {
        NAVMAP = int(TITERATOR_NAVMAP),
        GUIDE = int(TITERATOR_GUIDE),
        PAGES = int(TITERATOR_PAGES)
    };
    ~TIterator() { epub_free_titerator(m_iter); }
    inline bool isValid() {
        return epub_tit_curr_valid(m_iter);
    }
    ...
private:
    friend class EPub;
    explicit TIterator(struct titerator* iter)
        : m_iter(iter) {}
    struct titerator* m_iter;
};

New instances of TIterator and EIterator are created by EPub methods, because of that it must be a friend of the iterator classes.

class EPub {
public:
    ...
    inline EIterator*
    get_iterator(EIterator::type type, int opt = 0) {
        struct eiterator* it = epub_get_iterator(m_epub, eiterator_type(type), opt);
        if (it)
            return new EIterator(it);
        return 0;
    }
    inline TIterator*
    get_titerator(TIterator::type type, int opt = 0) {
        struct titerator* it = epub_get_titerator(m_epub, titerator_type(type), opt);
        if (it)
            return new TIterator(it);
        return 0;
    }
    ...
};

python-epub

Now it’s time to say to the generator which goes in and which must change in the bindings.

The global header

The global header is a file that includes all other headers of the library that will be analyzed. In the global header the binding developer may also add some tweaks, like a #define that will trigger some condition in the target library headers, that will affect the generated binding.

The epub_global.h that I use here is very simple:

#ifndef EPUB_GLOBAL_H
#define EPUB_GLOBAL_H

#include &lt;epub.h&gt;
#include &lt;epub_shared.h&gt;
#include &lt;epub_version.h&gt;

#include "epub_cpp_wrappers.h"

// ApiExtractor complains if it finds only pre-definitions.
struct titerator {};
struct eiterator {};

#endif

As the commentary tells us, ApiExtractor doesn’t like when it finds forward declarations without definitions. I choose to add these two bogus definitions for struct titerator and struct eiterator. (For some reason the generator said nothing about struct epub.) Other option would be not to add those bogus definitions, but add a line to the type system file telling the generator to ignore warnings relative to those structures.

The Type System description

Here follows a snippet from typesystem_epub.xml.

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <rejection enum-name='epub_metadata' />
    ...
    <object-type name='EPub'>
        <enum-type name='metadata' />
        ...
    </object-type>
</typesystem>

The epub_metadata enum is not exported to Python (at least not as it is) and to avoid the generator emitting his warnings, it must be explicitly rejected; its C++ substitute is added afterwards inside the EPub object type. The same happens to the other enums.

In the type system notation object-type refers to objects that are passed around solely as pointers (like EPub, whose copy constructor and operator are private). If, otherwise, the object can be passed as value, it should be declared as an value-type, as our Rectangle example mentioned before.

Python’s Iterator Protocol

In Python, if an object supports the Iterator Protocol I can use it on for statements, like this:

from epub import EPub, TIterator
book = EPub.open(self.epub_file)
for toc_it in book.get_titerator(TIterator.NAVMAP):
    if not toc_it.isValid():
        continue
    print 'link : ' + toc_it.link()
    print 'label: ' + toc_it.label()

Following the Python Iterator Protocol consists solely of an object implementing the methods __iter__() and __next__().

Note: the correct name for the method is next() and not __next__(). That's a minor bug in the generator that I just found.

The XML to add iterator protocol features into TIterator class will be lengthy, so I'll split it into two parts, the first dealing with type system code templates.

Type System templates

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <template name='iterator.__iter__'>
        Py_INCREF(%PYSELF);
        %PYARG_0 = %PYSELF;
    </template>
    <template name='iterator.__next__'>
        if (%CPPSELF.next()) {
            <insert-template name='iterator.__iter__' />
        } else {
            PyErr_SetNone(PyExc_StopIteration);
        }
    </template>
    ...

The iterator methods for TIterator and EIterator have exactly the same implementation, so it’ll be smart to use type system templates and have the code, and its eventual bugs, in a single place.

iterator.__iter__ method just need to return the object itself with its reference counter incremented by one. iterator.__next__ calls the underlying C++ object’s next() (it also returns itself, and increments the refcounter), and raises a Python StopIteration exception when it reaches the end.

The type system variables %PYSELF, %PYARG_0 and %CPPSELF are replaced by values dependent on the context where they are used (e.g. TIterator or EIteration classes). Check the documentation for their meaning·

Adding the iterator methods

    ...
    <object-type name='TIterator'>
        ...
        <modify-function signature='next()' remove='all' />
        <add-function signature='__iter__' return-type='PyObject*'>
            <inject-code class='target' position='beginning'>
                <insert-template name='iterator.__iter__' />
            </inject-code>
        </add-function>
        <add-function signature='__next__' return-type='PyObject*'>
            <inject-code class='target' position='beginning'>
                <insert-template name='iterator.__next__' />
            </inject-code>
        </add-function>
    </object-type>
     ...
</typesystem>

First the original C++ next() is removed, then the ones for the Python iterator protocol are added, using <insert-template/> tag to insert the previously defined custom code. Exactly the same lines will be added to EIterator class.

Just one more bit of hackery

Only one obstacle remains on the way of having proper Python iterators. When Python’s for statement is used to iterate through an iterable object, in the first round it calls the object’s __iter__ method, and immediately after it calls next, and keeps calling next for each new iteration.

The problem here is that our underlying C iterator returns an object loaded with proper content when __iter__ is called, then the way that Python’s for iteration works will cause the first item to be bypassed. A workaround for this case is to use a flag on the C++ wrapper that checks if the iterator has just been created, so that it will not move forward when next is called on it for the first time.

class TIterator {
public:
    ...
    inline bool next() {
        if (m_isFirst) {
            m_isFirst = false;
            return true;
        }
        return epub_tit_next(m_iter);
    }
private:
    friend class EPub;
    explicit TIterator(struct titerator* iter)
        : m_iter(iter), m_isFirst(true) {}
    struct titerator* m_iter;
    bool m_isFirst;
};

I had this freedom because TIterator is a class completely under my (me, the binding developer) control. If struct titerator were a C++ class from the beginning that approach would not be the best. Perhaps libshiboken (the supporting library used by all Shiboken generated bindings) should provide a base iterator class to handle this particular difference between Python and C++ iterators. Or perhaps the generated class, when identified as an iterable by the presence of iterator protocol methods added by the binding developer, should have such provisions. The latter options seems best, and that’s one more item for my list of future improvements.

Custom Conversions

Returning unicode values

The EIterator::curr() methods EIterator::curr_url() returns values of char* type, which doesn’t have a converter (const char* does have), so I’ve written a custom piece of code to convert it to Python’s unicode.

<?xml version='1.0'?>
<typesystem package='epub'>
    ...
    <template name='return_char_pointer'>
        char* %0 = %CPPSELF.%FUNCTION_NAME();
        if (%0) {
            %PYARG_0 = PyUnicode_DecodeUTF8(%0, strlen(%0), "strict");
        } else {
            Py_INCREF(Py_None);
            %PYARG_0 = Py_None;
        }
    </template>
    ...
    <object-type name='EIterator'>
        ...
        <modify-function signature='curr()'>
            <inject-code class='target' position='beginning'>
                <insert-template name='return_char_pointer' />
            </inject-code>
        </modify-function>
        <modify-function signature='curr_url()'>
            <inject-code class='target' position='beginning'>
                <insert-template name='return_char_pointer' />
            </inject-code>
        </modify-function>
        ...
    </object-type>
    ...
</typesystem>

Modifying a method’s signature

Some C++ method signatures couldn’t automatically be converted to meaningful Python code, so I added more custom code to handle the situation on a case by case basis. For example, the method

unsigned char**
EPub::get_metadata(metadata type, int* size);

The int* size argument receives a pointer to an int, which will contain the size of the list of unicode strings returned as unsigned char**. In Python it would merely return a list of unicode objects, and the call to get_metadata doesn’t need the size argument.

I believe the type system description is enough to see how the modification system works.

<object-type name='EPub'>
  <enum-type name='metadata' />
  <modify-function signature='get_metadata(EPub::metadata,int*)'>
    <modify-argument index='2'>
      <remove-argument />
    </modify-argument>
    <modify-argument index='return'>
      <replace-type modified-type='PyTuple' />
    </modify-argument>
    <inject-code class='target'>
      unsigned char** data = 0;
      int size;
      data = %CPPSELF.%FUNCTION_NAME(%1, &size);
      if (data) {
        %PYARG_0 = PyTuple_New(size);
        PyObject* uni = 0;
        for (int i = 0; i < size; ++i) {
         uni = PyUnicode_DecodeUTF8((const char*)data[i],
                                    strlen((const char*)data[i]),
                                    "strict");
         PyTuple_SetItem(%PYARG_0, i, uni);
       }
       for (int i = 0; i < size; ++i)
         free(data[i]);
       free(data);
     } else {
       Py_INCREF(Py_None);
       %PYARG_0 = Py_None;
     }
   </inject-code>
  </modify-function>
  ...
</object-type>

Downloading, building, etc.

Enough with the explanations, now let’s try the code. Remember that you’ll need also development files that I mentioned long time ago.

Clone the latest version from the git repository:

git clone git://github.com/setanta/python-epub.git

or download the tarball: python-epub.tar.bz2

Inside the source code directory create a build directory and …

cd python-epub
mkdir build
cd build
cmake ..
make
ctest

The last command deserves some talking about.

Testing, Testing, Testing

When working with binding development there’s a myriad of things that can go wrong, and a number of them go wrong in complete silence. With this in mind, I tell you that having unit tests makes the binding developer life bearable.

To see detailed results from the tests, run ctest with -V (verbose) option.

Check the tests directory for examples on how to use the python-epub bindings.

UI Example (or else it would be boring)

But the UI itself is very boring, although it was made with the amazing PySide I did it as simple as possible, it doesn’t even show images. On the other hand, with very little code I can now see what’s inside an ePub ebook.

From python-epub/build directory call the ePub viewer like this:

python ../simple-ui/bookviewer.py ../tests/beyond-the-wall-of-sleep.epub

It always expects a parameter with the path to an ePub file, in this case “Beyond the Wall of Sleep” by Lovecraft.

Screenshots:

Simplest ePub viewer (made with PySide)