Articles tagués “boost

Boost pour appeler du C++ depuis Python

Il y a plusieurs années, je m’étais intéressé à la manière d’appeler du code C depuis Python. C’était tellement compliqué que je n’ai jamais eu envie d’essayer. J’ai récemment découvert l’existence de Boost Python, et là, j’ai eu envie d’essayer ! C’est parti !

Créer son module Python

La première étape est bien sûr d’écrire quelques fonctions ou classes C++ et de faire un peu de magie pour faire les wrappers pour Python. Pour créer un module Python, il suffit de créer une bibliothèque dynamique avec ce code et CMake est bien sûr l’outil de choix pour cela. Voici un exemple :

On builde la bibliothèque :

$ mkdir build
$ cd build/
$ cmake ..
-- The C compiler identification is GNU 7.3.0
-- The CXX compiler identification is GNU 7.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Warning at /usr/share/cmake-3.10/Modules/FindBoost.cmake:1626 (message):
  No header defined for python3; skipping header check
Call Stack (most recent call first):
  CMakeLists.txt:5 (find_package)


-- Boost version: 1.65.1
-- Found the following Boost libraries:
--   python3
-- Found PythonInterp: /usr/bin/python3 (found suitable version "3.6.7", minimum required is "3") 
-- Found PythonLibs: /usr/lib/i386-linux-gnu/libpython3.6m.so (found suitable version "3.6.7", minimum required is "3") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pierre/Documents/boost_python/build

$ cmake --build .
Scanning dependencies of target mylibrary
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
[100%] Linking CXX shared library mylibrary.so
[100%] Built target mylibrary

On peut maintenant exécuter Python et tester notre module :

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
>>> mylibrary.say_hello()
Hello
>>> p = mylibrary.Printer("B&W printer")
>>> p.print("bla bla")
[B&W printer]bla bla
>>>help(mylibrary)
Help on module mylibrary:

NAME
    mylibrary

CLASSES
    Boost.Python.instance(builtins.object)
        Printer
    
    class Printer(Boost.Python.instance)
     |  Method resolution order:
     |      Printer
     |      Boost.Python.instance
     |      builtins.object
     |  
     |  Methods defined here:
     |  
     |  __init__(...)
     |      __init__( (object)arg1, (str)arg2) -> None :
     |      
     |          C++ signature :
     |              void __init__(_object*,std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
     |  
     |  __reduce__ = (...)
     |  
     |  print(...)
     |      print( (Printer)arg1, (str)arg2) -> None :
     |      
     |          C++ signature :
     |              void print(Printer {lvalue},std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
     |  
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |  
     |  __instance_size__ = 32
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from Boost.Python.instance:
     |  
     |  __new__(*args, **kwargs) from Boost.Python.class
     |      Create and return a new object.  See help(type) for accurate signature.
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from Boost.Python.instance:
     |  
     |  __dict__
     |  
     |  __weakref__

FUNCTIONS
    compute(...)
        compute( (int)arg1, (int)arg2) -> int :
        
            C++ signature :
                int compute(int,int)
    
    sayHelloTo(...)
        sayHelloTo( (str)arg1) -> None :
        
            C++ signature :
                void sayHelloTo(std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
    
    say_hello(...)
        say_hello() -> None :
        
            C++ signature :
                void say_hello()

FILE
    /home/pierre/Documents/boost_python/build/mylibrary.so

Notez que import mylibrary fonction.ne parce que mylibrary.so est dans le dossier d’où Python est lancé. Il y a moyen de l’installer dans le dossier dédié de Python, comme expliqué ici, pour y avoir accès depuis n’importe où.

Pour aller plus loin

Il existe le GitHub parfait pour aller plus loin : boost::python examples.

Ah ! Si ça marchait toujours du premier coup…

Évidemment, tout n’a pas marché du premier coup… Je me suis tapé quelques erreurs sympas avant d’arriver à quelque chose de fonctionnel.

Il faut bien sûr que Boost et Python soient installés :

sudo apt install libboost-all-dev python3-dev

J’ai eu une erreur magnifique de compilation à cause d’un header Python :

$ make
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
In file included from /usr/include/boost/python/detail/prefix.hpp:13:0,
                 from /usr/include/boost/python/args.hpp:8,
                 from /usr/include/boost/python.hpp:11,
                 from /home/pierre/Documents/boost_python/functions.cpp:1:
/usr/include/boost/python/detail/wrap_python.hpp:50:11: fatal error: pyconfig.h: No such file or directory
 # include 
           ^~~~~~~~~~~~
compilation terminated.

Ce fichier est apporté par le paquet python3-dev et il était bien présent sur mon PC :

$ locate pyconfig.h
/usr/include/i386-linux-gnu/python2.7/pyconfig.h
/usr/include/i386-linux-gnu/python3.6m/pyconfig.h
/usr/include/python2.7/pyconfig.h
/usr/include/python3.6m/pyconfig.h

Le problème était donc d’ajouter le dossier /usr/include/python3.6m/ à l’include path. Une solution est de l’ajouter au path avant de compiler. Il y a beaucoup mieux en s’appuyant que les capacités de CMake à trouver Python : si Python est trouvé, la variable PYTHON_INCLUDE_DIRS est renseignée et il suffit de l’ajouter en tant qu’include directory comme fait dans mon CMakeLists.txt ci-dessus.

La première fois que j’ai tenté d’importer mon module, j’ai eu une erreur de version de Python :

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
Traceback (most recent call last):
  File "", line 1, in 
ImportError: /usr/lib/i386-linux-gnu/libboost_python-py27.so.1.65.1: undefined symbol: PyClass_Type
>>>
[3]+  Stopped                 python3

En effet, find_package(Boost COMPONENTS python) trouvait la variante pour Python 2. Il m’a fallu rajouter un 3, ce qui donne find_package(Boost COMPONENTS python3). La documentation de FindBoost a un paragraphe à ce sujet :

Note that Boost Python components require a Python version suffix (Boost 1.67 and later), e.g. python36 or python27 for the versions built against Python 3.6 and 2.7, respectively. This also applies to additional components using Python including mpi_python and numpy. Earlier Boost releases may use distribution-specific suffixes such as 2, 3 or 2.7. These may also be used as suffixes, but note that they are not portable.