I. L'article original▲
Le Qt Developer Network est un réseau de développeurs utilisant Qt afin de partager leur savoir sur ce framework. Vous pouvez le consulter en anglais.
Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.
Cet article est la traduction de PySide Binding Generation Tutorial.
II. Introduction▲
Cette image montre les entrées nécessaires pour générer le code source du binding :
L'utilisateur fournit les en-têtes de la bibliothèque avec un fichier de description du système de types décrivant comment les classes seront exportées vers le langage cible, tout comme tout code source requis est à inclure dans les sources générées. Si aucune grosse modification n'est requise, ceci sera juste une liste déclarative de classes, d'espaces de noms, d'énumérations et de fonctions globales.
Ce tutoriel va passer en revue toutes les étapes nécessaires pour avoir un binding prêt à l'importation et à l'utilisation dans un programme Python. Le code est disponible. Il y a deux versions de l'exemple, le code source est le même, mais l'une utilise un Makefile général et l'autre des fichiers CMake ; la dernière est le système utilisé par Shiboken et PySide.
III. La bibliothèque▲
Est présentée ici la petite et simpliste bibliothèque qui sera utilisée pour la suite de l'article, ainsi que les instructions pour la compiler. Il n'y a qu'une classe C++ dans cette bibliothèque, donc deux fichiers, ainsi qu'un fichier de projet pour la compilation avec QMake.
Les fichiers suivants seront considérés dans le dossier libfoo ; si vous les changez de place, étant donné que le Makefile bientôt présenté utilise ce répertoire en dur, vous rencontrerez quelques problèmes.
#ifndef FOO_H
#define FOO_H
#include
<QtCore/QtCore>
class
Math : public
QObject
{
Q_OBJECT
public
:
Math() {}
virtual
~
Math() {}
int
squared(int
x);
}
;
#endif
// FOO_H
#include
"foo.h"
int
Math::
squared(int
x)
{
return
x *
x;
}
TEMPLATE = lib
TARGET = foo
DEPENDPATH += .
INCLUDEPATH += .
HEADERS += foo.h
SOURCES += foo.cpp
Pour compiler :
cd libfoo
qmake
make
IV. Le binding libfoo avec Shiboken▲
Pour mener à bien la création du binding, quelques outils doivent être installés sur le système :
- Qt (avec les en-têtes et les fichiers .pc de pkg-config, ce qui correspond à un paquet -dev pour les distributions Debian) ;
- PySide, le binding Python de Qt réalisé à l'aide de Shiboken ;
- les systèmes de type pour les bindings Python de Qt ;
- les en-têtes pour la bibliothèque à binder.
Avec ces outils, le développeur souhaitant réaliser un binding Python doit récolter ces informations, utilisées par le générateur pour le binding :
- un fichier de description de système de type, pour donner la manière dont le binding doit être fait ;
- global.h, un en-tête incluant tous les en-têtes de libfoo et définissant les macros requises ;
- un système de compilation pour diriger le processus de génération, compilation et liaison.
IV-A. Compilation avec Makefile▲
La structure des répertoires et le contenu pour le projet de binding par Makefile pourraient ressembler à ceci :
foobinding-makefile/
|-- foo/
|-- global.h
|-- Makefile
|-- pyside_global.h
`-- typesystem_foo.xml
Le répertoire foobinding-makefile devrait contenir le fichier global.h, le point central incluant tous les en-têtes de libfoo (dans ce cas, il appert que la bibliothèque n'a qu'un en-tête ; dans une bibliothèque plus réaliste, cependant, il devrait y en avoir beaucoup plus). Il pourrait aussi contenir les définitions de macros qui vont influencer le parsage des en-têtes par le générateur. Le fichier pyside_global.h sera expliqué plus tard. Comme dit précédemment, le fichier typesystem_foo.xml décrit la manière dont l'export de la bibliothèque C++ se fera en Python.
Le répertoire foo est l'endroit où les sources générées seront placées. Au départ, il est vide et son nom sera le même que le nom de paquet trouvé dans le fichier de système de type :
<typesystem
package
=
"foo"
>
S'il y a besoin de code source écrit à la main de plus de deux lignes (ce qui le rend impropre à l'inclusion dans le fichier XML de système de type), il devrait être placé dans un répertoire glue. Pour ce binding, aucun code de ce type n'est requis.
Lors de l'écriture du fichier de système de type, il n'y a pas besoin de faire référence aux autres fichiers de système de type avec des chemins absolus, les endroits où ils peuvent être trouvés devraient être passés en paramètre au générateur (--typesystem-paths=PATH1:PATH2:[...]) ou dans la variable d'environnement TYPESYSTEMPATH.
IV-B. Compilation avec CMake▲
L'arborescence est alors un peu plus élaborée :
foobinding-cmake/
|-- CMakeLists.txt
|-- foo/
| |-- CMakeLists.txt
| |-- global.h
| |-- pyside_global.h
| `-- typesystem_foo.xml
`-- tests/
`-- CMakeLists.txt
`-- math_test.py
Cette structure suit celle utilisée par PySide : un répertoire racine, un répertoire pour les informations sur le binding (comme dans l'exemple du Makefile) et un répertoire tests, un must-have.
V. La description de système de type▲
Le système de type est une spécification utilisée lors du mappage d'une bibliothèque C++ et du module Python correspondant. La spécification est un fichier XML écrit à la main, listant les types qui seront disponibles dans le binding généré, les modifications dans les signatures des classes et fonctions pour mieux correspondre au langage cible et la liste des composants qui devraient être rejetés du binding.
PySide utilise un format similaire à celui utilisé par Qt Jambi et Qt Script, déjà abondamment abordé dans la documentation de Qt Jambi.
V-A. Le cas de libfoo▲
Tout fichier de système de type débute avec la balise racine typesystem. L'attribut package contient le nom du paquet tel qu'il sera vu depuis Python.
Juste après, tous les fichiers fournissant les informations requises pour le processus de génération du binding en cours sont inclus, à la manière des en-têtes en C.
<?xml version="1.0"?>
<typesystem
package
=
"foo"
>
<load-typesystem
name
=
"typesystem_core.xml"
generate
=
"no"
/>
<object-type
name
=
"Math"
/>
</typesystem>
Si le binding créé dépend d'autres bindings (dans ce cas-ci, libfoo dépend de QtCore, le binding de libfoo dépendra du binding de QtCore), le fichier nouvellement créé de système de type doit se référer aux fichiers de système de type de la bibliothèque dont il dépend.
L'inclusion d'autres fichiers de système de type se fait avec la balise load-typesystem. L'attribut generate doit être mis à no, sinon le générateur va tenter de recréer du code source pour les bindings déjà existants. Ces fichiers sont généralement situés dans /usr/share/PySide/typesystem, l'endroit précis peut être vérifié avec pkg-config :
pkg-config pyside --variable=typesystemdir
Maintenant, un mot d'explication. Les classes C++ peuvent être déclarées dans la description comme des types d'objet ou de valeur. La différence est subtile et tient plus du C++ que du Python. Si une classe C++ a un constructeur de copie public et un opérateur d'attribution, si elle peut être passée par valeur (comme QRect, QPoint, QString), si deux instances d'un objet peuvent être comparées et décrétées égales en se basant sur la valeur qu'elles contiennent (comme deux QPoint différents avec les mêmes coordonnées x et y), alors elle doit être déclarée comme un type de valeur. Si une classe C++ a un constructeur par copie et un opérateur d'attribution privés, si elle est prévue pour être passée comme pointeur (comme les QObject et dérivés), si les instances de cette classe sont considérées comme uniques et égales uniquement à elles-mêmes, alors elle doit être déclarée comme objet de type.
Pour ce binding d'exemple, spécifier uniquement le nom de la classe suffit, puisque le système de génération va automatiquement repérer les méthodes avec des arguments et retourner la valeur de types connus. Ces types peuvent être décrits dans le même fichier ou dans un référencé par le biais de la balise load-typesystem.
Dans des situations plus complexes, les signatures des méthodes peuvent être changées ou rejetées avec d'autres balises, que vous pouvez consulter dans la référence.
V-B. Autres cas communs▲
Suivent maintenant des utilisations communes des capacités du système de type. Toutes sont utilisées dans les fichiers de système de type de PySide. Elles ne sont pas utilisées dans ce tutoriel.
V-B-1. Templates▲
Pour simplifier le processus d'écriture de code personnalisé pour le binding, des morceaux récurrents de code peuvent devenir plus génériques avec le mécanisme de template. Ils sont déclarés de cette manière :
<template
name
=
"only_bool*_fix"
>
bool ok;
%RETURN_TYPE retval = self.%FUNCTION_NAME(&ok);
</template>
Et utilisés comme ceci :
<inject-code
class
=
"native"
position
=
"beginning"
>
<insert-template
name
=
"only_bool*_fix"
/>
</inject-code>
Le fichier typesystem_template.xml des bindings Qt peut être utilisé comme une bonne ressource pour ce genre d'exemples. Regardez aussi du côté de la documentation de Qt Jambi sur les templates.
V-B-2. Les classes ne dérivant pas de QObject▲
Même dans une bibliothèque basée sur Qt, il est assez fréquent de rencontrer des classes qui n'appartiennent pas à la hiérarchie QObject, elles doivent être déclarées comme ceci :
<value-type
name
=
"RectOrSomethingLikeThat"
/>
V-B-3. Les fonctions globales▲
Les fonctions globales doivent être déclarées avec la balise function et identifiées avec leur signature :
<function
signature
=
"functionName(int, const Object&) />
VI. L'en-tête global▲
En plus des informations fournies par le système de type, le générateur doit récolter des informations supplémentaires des en-têtes de la bibliothèque contenant les classes à exporter en Python. S'il y a un en-tête qui inclut tous les autres, il peut être passé directement au générateur.
Si un tel fichier n'est pas disponible, si seulement une partie de la bibliothèque est à convertir, si certains drapeaux (flags) doivent être déclarés à l'aide d'instructions #define avant le parsage des en-têtes de la bibliothèque, alors un fichier global.h doit être fourni. Par exemple, si NULL n'est pas défini et est utilisé comme paramètre par défaut pour certains constructeurs ou méthodes, le parseur ne va pas le reconnaître.
#undef QT_NO_STL
#undef QT_NO_STL_WCHAR
#ifndef NULL
#define NULL 0
#endif
#include
"pyside_global.h"
#include
<foo.h>
pyside_global.h contient des inclusions et des définitions requises pour générer les bindings qui composent PySide. Il s'agit d'une copie du fichier résultant du traitement du fichier pyside/PySide/global.pc.in. La situation idoine serait d'avoir ce fichier fourni par PySide. Pour le moment, on se satisfera d'une copie dans les projets basés sur PySide.
VII. Compiler le binding▲
Comme mentionné précédemment, le système de compilation utilisé doit effectuer ces différentes tâches dans le bon ordre :
- récolter des informations sur les endroits des en-têtes et des systèmes de type requis d'autres projets ;
- lancer le générateur avec les bons paramètres ;
- compiler et lier le binding.
VII-A. Lancer le générateur▲
Le générateur est appelé avec les paramètres et options suivants :
generatorrunner --generatorSet=shiboken \
global_header.h \
--include-paths=$(PATHS_TO_HEADERS)) \
--typesystem-paths=$(PATHS_TO_TYPESYSTEMS) \
--output-directory=. \
typesystem.xml
Notez que les variables pour l'inclusion et les systèmes de type auraient pu être déterminées au moment de la compilation avec l'outil pkg-config ou avec les informations fournies par les fichiers de configuration de CMake.
VII-A-1. Récolter des informations avec pkg-config▲
Les bindings de Qt utilisent des informations pour la compilation à travers le mécanisme pkg-config. Le nom des bindings Python de Qt est pyside et un simple pkg-config pyside --cflags --libs récupérera les informations requises pour la compilation du nouveau binding.
Le fichier pyside.pc des bindings Qt à destination de pkg-config requiert les fichiers .pc de Qt. Si la bibliothèque est à un endroit inhabituel comme /opt/qt47, il faut exporter la variable d'environnement PKG_CONFIG_PATH :
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/opt/qt47/lib/pkgconfig
Ces informations sont aussi disponibles grâce à pkg-config et à la variable typesystemdir. On l'utilise comme ceci :
pkg-config pyside --variable=typesystemdir
Ceci fournit des informations sur la localisation des fichiers de système de type utilisés pour les bindings Qt. Comme dit précédemment, le binding en cours de création les requiert en complément de ses propres informations pour le processus de génération.
Des informations en droite ligne du générateur de binding Shiboken sont aussi requises pour la compilation, son nom pkg-config est shiboken.
VII-A-2. Récolter des informations avec CMake▲
Lors de la compilation de votre binding avec CMake, les informations nécessaires peuvent être incluses depuis le fichier CMakeLists.txt de votre projet en utilisant :
find_package(Shiboken REQUIRED)
find_package(PySide REQUIRED)
L'inclusion obligatoire des paquets Shiboken et PySide va définir un certain nombre de variables, selon le fichier PySideConfig.cmake :
- PYSIDE_INCLUDE_DIR : les répertoires à inclure pour utiliser PySide ;
- PYSIDE_LIBRARY : les fichiers à lier pour utiliser PySide ;
- PYSIDE_PYTHONPATH : l'endroit où les modules Python de PySide peuvent être trouvés ;
- PYSIDE_TYPESYSTEMS : les fichiers de système de type qui devraient être utilisés par les bindings étendant PySide.
De même, le fichier ShibokenConfig.cmake fournit d'autres informations requises :
- SHIBOKEN_INCLUDE_DIR : les répertoires à inclure pour utiliser Shiboken ;
- SHIBOKEN_LIBRARIES : les fichiers à lier pour utiliser Shiboken ;
- SHIBOKEN_BUILD_TYPE : précise si Shiboken a été compilé en Release ou en Debug ;
- SHIBOKEN_PYTHON_INTERPRETER : interpréteur Python (Release ou Debug) à utiliser avec les bindings ;
- SHIBOKEN_PYTHON_LIBRARIES : les bibliothèques Python (Release ou Debug) auxquelles Shiboken est lié.
VII-B. Compiler▲
Cette section présente les deux méthodes : Makefile et CMake.
VII-B-1. Makefile▲
Voici un Makefile à utiliser :
LIBFOO_DIR = `pwd`/../libfoo
LIBS = `pkg-config pyside --libs` \
-L$(LIBFOO_DIR) -lfoo
CXXFLAGS = -I/usr/share/qt4/mkspecs/linux-g++ -I. \
-I$(LIBFOO_DIR) \
-I`pwd`/foo \
-I`pkg-config --variable=includedir pyside`/QtCore/ \
-I`pkg-config --variable=includedir QtCore` \
-I`pkg-config --variable=includedir QtCore`/.. \
-I`pkg-config --variable=includedir QtGui` \
`pkg-config pyside --cflags`
QT4TYPESYSTEM_DIR = `pkg-config pyside --variable=typesystemdir`
QT4HEADER_DIRS = `pkg-config --variable=includedir QtCore`:`pkg-config --variable=includedir QtCore`/..
PYSIDE_PYTHONPATH = `pkg-config --variable=pythonpath PySide`
PYTHON_INTERPRETER = `pkg-config --variable=python_interpreter shiboken`
all: generate compile link
generate:
generatorrunner --generatorSet=shiboken \
global.h \
--include-paths=$(LIBFOO_DIR):$(QT4HEADER_DIRS):/usr/include \
--typesystem-paths=.:$(QT4TYPESYSTEM_DIR) \
--output-directory=. \
typesystem_foo.xml
compile:
g++ foo/foo_module_wrapper.cpp foo/math_wrapper.cpp -Wall -fPIC $(CXXFLAGS) -c
link:
g++ foo_module_wrapper.o math_wrapper.o $(LIBS) -fPIC -shared -Wl,-soname,foo.so -o foo.so
test:
LD_LIBRARY_PATH=$(LIBFOO_DIR):$(LD_LIBRARY_PATH) PYTHONPATH=$(PYSIDE_PYTHONPATH):$(PYTHONPATH) $(PYTHON_INTERPRETER) -c \
"import foo; m = foo.Math(); print '5 squared is %d' % m.squared(5)"
clean:
rm -rf *.o *.so *.?pp *.log *.log foo/*
Gardez en tête que ce Makefile attend que les répertoires libfoo et foobinding-makefile soient au même niveau dans l'arborescence des dossiers. Il faut adapter le code si vous avez fait autrement.
Maintenant, compilez et liez le binding avec make :
cd foobinding-makefile
make
make test
La commande make test va lancer l'interpréteur Python avec ce bout de code, qui va importer le module du binding, en instancier une classe, lancer une méthode et afficher le résultat :
import foo; m = foo.Math(); print ‘5 squared is %d' % m.squared(5)
VII-B-2. CMake▲
Voici un fichier CMakeLists.txt à utiliser :
project(foobinding)
cmake_minimum_required(VERSION 2.6)
find_package(PythonLibs REQUIRED)
find_package(Shiboken REQUIRED)
find_package(PySide REQUIRED)
find_package(Qt4 4.6.2 REQUIRED)
set(LIBFOO_DIR ${CMAKE_SOURCE_DIR}/../libfoo)
find_program(GENERATOR generatorrunner REQUIRED)
if (NOT GENERATOR)
message(FATAL_ERROR "You need to specify GENERATOR variable (-DGENERATOR=value)")
endif()
if(CMAKE_HOST_UNIX)
option(ENABLE_GCC_OPTIMIZATION "Enable specific GCC flags to optimization library size and performance. Only available on Release Mode" 0)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fvisibility=hidden -Wno-strict-aliasing")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
if(ENABLE_GCC_OPTIMIZATION)
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG -Os -Wl,-O1")
if(NOT CMAKE_HOST_APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--hash-style=gnu")
endif()
endif()
if(CMAKE_HOST_APPLE)
if (NOT QT_INCLUDE_DIR)
set(QT_INCLUDE_DIR "/Library/Frameworks")
endif()
endif()
endif()
include(${QT_USE_FILE})
enable_testing()
add_subdirectory(foo)
add_subdirectory(tests)
Ceci est le CMakeLists.txt principal du projet, un fichier CMake tout à fait régulier ; en cas de doute, la documentation CMake est toujours disponible. Remarquez que les tests ont été activés avec enable_testing().
project(foo)
set(foo_SRC
${CMAKE_CURRENT_BINARY_DIR}/foo/foo_module_wrapper.cpp
${CMAKE_CURRENT_BINARY_DIR}/foo/math_wrapper.cpp
)
set(foo_INCLUDE_DIRECTORIES
${SHIBOKEN_INCLUDE_DIR}
${PYTHON_INCLUDE_PATH}
${PYSIDE_INCLUDE_DIR}
${PYSIDE_INCLUDE_DIR}/QtCore
${QT_INCLUDE_DIR}
${QT_QTCORE_INCLUDE_DIR}
${LIBFOO_DIR}
)
set(foo_LINK_LIBRARIES
${QT_QTCORE_LIBRARY}
${SHIBOKEN_PYTHON_LIBRARIES}
${SHIBOKEN_LIBRARY}
${PYSIDE_LIBRARY}
${LIBFOO_DIR}/libfoo.so
)
include_directories(foo ${foo_INCLUDE_DIRECTORIES})
add_library(foo MODULE ${foo_SRC})
set_property(TARGET foo PROPERTY PREFIX "")
target_link_libraries(foo ${foo_LINK_LIBRARIES})
add_custom_command(OUTPUT ${foo_SRC}
COMMAND ${GENERATOR}
--generatorSet=shiboken --enable-parent-ctor-heuristic --enable-pyside-extensions --enable-return-value-heuristic
${CMAKE_SOURCE_DIR}/foo/global.h
--include-paths=${QT_INCLUDE_DIR}:${LIBFOO_DIR}
--typesystem-paths=${typesystem_path}:${PYSIDE_TYPESYSTEMS}
--output-directory=${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/typesystem_foo.xml
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Running generator for libfoo..."
)
Ceci est le fichier CMakeLists.txt du répertoire du binding, la commande add_custom_command est responsable de l'appel du générateur Shiboken avec les paramètres et variables appropriés. Remarquez que les options de la ligne de commande sont directement liées aux particularités des bindings Qt ; pour un binding C++ pur, aucune n'aurait été nécessaire :
--enable-parent-ctor-heuristic --enable-pyside-extensions --enable-return-value-heuristic
if(WIN32)
set(TEST_PYTHONPATH "${foo_BINARY_DIR};${PYSIDE_PYTHONPATH}")
set(TEST_LIBRARY_PATH "${LIBFOO_DIR};$ENV{PATH}")
set(LIBRARY_PATH_VAR "PATH")
string(REPLACE "\\" "/" TEST_PYTHONPATH "${TEST_PYTHONPATH}")
string(REPLACE "\\" "/" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}")
string(REPLACE ";" "\\;" TEST_PYTHONPATH "${TEST_PYTHONPATH}")
string(REPLACE ";" "\\;" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}")
else()
set(TEST_PYTHONPATH "${foo_BINARY_DIR}:${PYSIDE_PYTHONPATH}")
set(TEST_LIBRARY_PATH "${LIBFOO_DIR}:$ENV{LD_LIBRARY_PATH}")
set(LIBRARY_PATH_VAR "LD_LIBRARY_PATH")
endif()
add_test(math ${SHIBOKEN_PYTHON_INTERPRETER} ${CMAKE_CURRENT_SOURCE_DIR}/math_test.py)
set_tests_properties(math PROPERTIES ENVIRONMENT "PYTHONPATH=${TEST_PYTHONPATH};${LIBRARY_PATH_VAR}=${TEST_LIBRARY_PATH}")
Ce fichier peu évolué informe CMake des tests à effectuer et avec quelles variables.
La meilleure chose à faire quand on compile avec CMake est de créer un répertoire de compilation et de le lancer depuis ce dernier (compilation hors sources).
cd foobinding-cmake
mkdir build
cd build
cmake ..
make
Finalement, un petit test unitaire très simple :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''Test cases for foo bindings module.'''
import
unittest
import
foo
class
MathTest
(
unittest.TestCase):
def
testMath
(
self):
'''Test case for Math class from foo module.'''
val =
5
math =
foo.Math
(
)
self.assertEqual
(
math.squared
(
5
), 5
*
5
)
if
__name__
==
'__main__'
:
unittest.main
(
)
Pour lancer le test :
ctest
La sortie devrait ressembler à ceci :
Test project YOURPATH/binding-tutorial/foobinding-cmake/build
Start 1: math
1/1 Test #1: math ............................. Passed 0.10 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.11 sec
Pour plus de détails, utilisez le paramètre -V.
VIII. Conclusion▲
C'est à peu près tout. D'autres exemples de CMakeLists.txt et de tests unitaires sont disponibles dans les sources de PySide.
Merci à eusebe19 pour la relecture !