boost.python: Nabelschnur zu Python Ein Erfahrungsbericht mit Rezepten PyCon 2013, Köln Reinhard Wobst. [email protected] R.Wobst, PyCon 2013 Köln 1/44 1. Was ist embedded Python? Hat nichts mit embedded Software zu tun, sondern ist ein PythonInterpreter, der von einer C-Schnittstelle aus gesteuert wird. BEISPIEL: Editor vim, der mit --with-features=big (oder mit python enabled) übersetzt wurde: $ vim ... :py a=3 # vim-Befehl ... :py print a**3 # vim-Befehl 27 # Ausgabe R.Wobst, PyCon 2013 Köln 2/44 Der Interpreter "lebt" innerhalb von vim und merkt sich seinen Zustand. Das Beispiel nützt wenig, aber:Der Interpreter kann auf Daten von vim zugreifen, etwa so: vimdemo.py: import vim def _revlist(lst): for i in range(len(lst)-1, -1, -1): yield lst[i] def reverse(): s = vim.current.line vim.current.line = ''.join(_revlist(s)) R.Wobst, PyCon 2013 Köln 3/44 Ein Aufruf von reverse() dreht die aktuelle Zeile (in der der Cursor steht) um - in vim: $ vim _textfile_ ... :py execfile('vimdemo.py') :reverse() "define reverse() Damit kann man sich beliebig komplexe Editorfunktionen in Python selbst programmieren (sofern erforderlich ☺). Ermöglicht wird dies durch die mit Python gelieferte Python-C-Api, die nicht ganz so schwierig wie beim ersten Anschein ist. Dokumentation findet sich hier: R.Wobst, PyCon 2013 Köln 4/44 R.Wobst, PyCon 2013 Köln 5/44 Einfaches Beispiel aus dem Tutorial: #include <Python.h> int main(int argc, char *argv[]) { Py_Initialize(); PyRun_SimpleString( "from time import time,ctime\n" "print 'Today is',ctime(time())\n"); Py_Finalize(); return 0; } R.Wobst, PyCon 2013 Köln 6/44 Arbeit mit Listen: PyObject* PyList_New(Py_ssize_t len); int PyList_Append(PyObject *list, PyObject *item); Problem: Referenzen müssen selbst verwaltet werden, Makros Py_INCREF(x), Py_DECREF(x) Chronische Fehlerquelle, sehr schwer zu lokalisieren! R.Wobst, PyCon 2013 Köln 7/44 2. Wozu braucht man das? Test eines Sternsensors (http://de.wikipedia.org/wiki/Sternsensor) - solch ein Gerät bestimmt anhand von Sternkarten seine Lage im Weltall auf 1...10 Bogensekunden innerhalb von Sekunden. (Quelle: http://www.jena-optronik.de/de/lageregelungssensoren/sternsensor-astro-aps.html) R.Wobst, PyCon 2013 Köln 8/44 Das Gerät muss z.B. 15 Jahre lang im Orbit funktionieren.Tests sind daher extrem wichtig - aber die Testsoftware ist teils in C++ geschrieben und läuft auf großen Multicore-Systemen (um Datenmengen zu beherrschen und vorzufiltern), auch sind nicht alle Treiber frei. Entwickler möchten Tests gern in Python schreiben (Begründung in diesem Rahmen überflüssig), brauchen jedoch "irgendwie" Zugang zu Treibern und Daten, möglichst sogar zu Klasseninstanzen. R.Wobst, PyCon 2013 Köln 9/44 3. boost.python boost ist eine Sammlung leistungsfähiger C++ - Bibliotheken (Release 1.52 enthält etwa 80), u.a. zu linearer Algebra, Bildverarbeitung, Multithreading, regulären Ausdrücken u.v.a.m. boost.python ist Teil des Boost-Projekts: eine C++ - Bibliothek zur "nahtlosen Kopplung" von C++ mit Python, die letztendlich auf der PythonC-API aufsetzt. Homepage: http://www.boost.org/doc/libs/1_44_0/libs/python/doc/index.html Wiki (nützlich für den Anfang): https://wiki.python.org/moin/boost.python > Embedding Python Hier wird nur auf die Anbindung an ein embedded Python eingegangen. R.Wobst, PyCon 2013 Köln 10/44 3.1. Fähigkeiten einfacheres Interface als bei Python-C-API BEISPIEL: Funktionalität des Python-Skripts import random print random.random() nachbilden: R.Wobst, PyCon 2013 Köln 11/44 #include <boost/python.hpp> #include <iostream> #include <Python.h> int main() { Py_Initialize(); boost::python::object rand_mod = boost::python::import("random"); boost::python::object rand_func = rand_mod.attr("random"); boost::python::object rand2 = rand_func(); std::cout << boost::python::extract<double>(rand2) << std::endl; return 0;} R.Wobst, PyCon 2013 Köln 12/44 Referenzen werden automatisch verwaltet (wichtiger Vorteil) grundlegende Python-Datentypen wie Listen, Tupel, Dictionaries können einfach von boost aus erzeugt und verwaltet werden für obige Anwendung entscheidend: Klassentypen und sogar Klasseninstanzen von C++ lassen sich nach Python exportieren, und C++ - Klasseninstanzen lassen sich von Python aus verändern! Wir sehen weiter unten, wie das geht. Die Python-C-API lässt sich parallel dazu nutzen und muss sogar mit verwendet werden (bei Py_Initialize() z.B.) - boost.python deckt (noch?) nicht den vollen API-Umfang ab. R.Wobst, PyCon 2013 Köln 13/44 3.2. Probleme Mangelhafte, komplizierte Dokumentation - Web ist voll von Hilferufen und mehr oder weniger guten Beispielen. Hier: Auch nur mehr oder weniger gute Beispiele, hoffentlich nah am Optimum ☺. boost.python basiert wie auch boost viel auf Templates, die Anwender zur Verzweiflung treiben können. Beispiel einer nicht so seltenen Fehlermeldung: R.Wobst, PyCon 2013 Köln 14/44 /home/wobst/buss/.../third_party/boost/boost/python/object/value_holder.hpp: In constructor ‘boost::python::objects::value_holder<Value>::value_holder(PyObject*, A0) [with A0 = boost::reference_wrapper<const GlobPy::VfsReadNode>, Value = GlobPy::VfsReadNode, PyObject = _object]’: /home/wobst/buss/.../third_party/boost/boost/python/object/make_instance.hpp:71:48: instantiated from ‘static Holder* boost::python::objects::make_instance<T, Holder>::construct(void*, PyObject*, boost::reference_wrapper<const T>) [with T = GlobPy::VfsReadNode, Holder = boost::python::objects::value_holder<GlobPy::VfsReadNode>, PyObject = _object]’ /home/wobst/buss/.../third_party/boost/boost/python/object/make_instance.hpp:45:13: instantiated from ‘static PyObject* boost::python::objects::make_instance_impl<T, Holder, Derived>::execute(Arg&) [with Arg = const boost::reference_wrapper<const GlobPy::VfsReadNode>, T = GlobPy::VfsReadNode, Holder = boost::python::objects::value_holder<GlobPy::VfsReadNode>, Derived = boost::python::objects::make_instance<GlobPy::VfsReadNode, boost::python::objects::value_holder<GlobPy::VfsReadNode> >, PyObject = _object]’ /home/wobst/buss/.../third_party/boost/boost/python/object/class_wrapper.hpp:29:51: instantiated from ‘static PyObject* boost::python::objects::class_cref_wrapper<Src, MakeInstance>::convert(const Src&) [with Src = GlobPy::VfsReadNode, MakeInstance = boost::python::objects::make_instance<GlobPy::VfsReadNode, boost::python::objects::value_holder<GlobPy::VfsReadNode> >, PyObject = _object]’ /home/wobst/buss/.../third_party/boost/boost/python/converter/as_to_python_function.hpp:27:9: instantiated from ‘static PyObject* boost::python::converter::as_to_python_function<T, ToPython>::convert(const void*) [with T = GlobPy::VfsReadNode, ToPython = boost::python::objects::class_cref_wrapper<GlobPy::VfsReadNode, boost::python::objects::make_instance<GlobPy::VfsReadNode, boost::python::objects::value_holder<GlobPy::VfsReadNode> > >, PyObject = _object]’ /home/wobst/buss/.../third_party/boost/boost/python/to_python_converter.hpp:87:5: instantiated from ‘boost::python::to_python_converter<T, Conversion, has_get_pytype>::to_python_converter() [with T = GlobPy::VfsReadNode, Conversion = boost::python::objects::class_cref_wrapper<GlobPy::VfsReadNode, boost::python::objects::make_instance<GlobPy::VfsReadNode, boost::python::objects::value_holder<GlobPy::VfsReadNode> > >, bool has_get_pytype = true]’ /home/wobst/buss/.../third_party/boost/boost/python/object/class_wrapper.hpp:26:1: instantiated from ‘static void boost::python::objects::class_metadata<T, X1, X2, X3>::maybe_register_class_to_python(T2*, mpl_::false_) [with T2 = GlobPy::VfsReadNode, T = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified, mpl_::false_ = mpl_::bool_<false>]’ /home/wobst/buss/.../third_party/boost/boost/python/object/class_metadata.hpp:229:9: instantiated from ‘static void boost::python::objects::class_metadata<T, X1, X2, X3>::register_aux2(T2*, Callback) [with T2 = GlobPy::VfsReadNode, Callback = boost::integral_constant<bool, false>, T = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified]’ R.Wobst, PyCon 2013 Köln 15/44 /home/wobst/buss/.../third_party/boost/boost/python/object/class_metadata.hpp:219:9: instantiated from ‘static void boost::python::objects::class_metadata<T, X1, X2, X3>::register_aux(void*) [with T = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified]’ /home/wobst/buss/.../third_party/boost/boost/python/object/class_metadata.hpp:205:9: instantiated from ‘static void boost::python::objects::class_metadata<T, X1, X2, X3>::register_() [with T = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified]’ /home/wobst/buss/.../third_party/boost/boost/python/class.hpp:497:9: instantiated from ‘void boost::python::class_<T, X1, X2, X3>::initialize(const DefVisitor&) [with DefVisitor = boost::python::init_base<boost::python::init<std::basic_string<char> > >, W = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified]’ /home/wobst/buss/.../third_party/boost/boost/python/class.hpp:209:9: instantiated from ‘boost::python::class_<T, X1, X2, X3>::class_(const char*, const boost::python::init_base<DerivedT>&) [with DerivedT = boost::python::init<std::basic_string<char> >, W = GlobPy::VfsReadNode, X1 = boost::python::detail::not_specified, X2 = boost::python::detail::not_specified, X3 = boost::python::detail::not_specified]’ /home/wobst/buss/.../ut-ng/src/PythonApi/PythonApi.cpp:34:67: instantiated from here /home/wobst/buss/.../third_party/boost/boost/python/object/value_holder.hpp:137:13: error: no matching function for call to ‘GlobPy::VfsReadNode::VfsReadNode(const boost::reference_wrapper<const GlobPy::VfsReadNode>::type&)’ /home/wobst/buss/.../ut-ng/src/PythonApi/pyvfs.hpp:72:9: note: candidates are: GlobPy::VfsReadNode::VfsReadNode(const std::string&) /home/wobst/buss/.../ut-ng/src/PythonApi/pyvfs.hpp:59:1: note: GlobPy::VfsReadNode::VfsReadNode(GlobPy::VfsReadNode&) /home/wobst/buss/.../third_party/boost/boost/system/error_code.hpp: At global scope: /home/wobst/buss/.../third_party/boost/boost/system/error_code.hpp:214:36: warning: ‘boost::system::posix_category’ defined but not used /home/wobst/buss/.../third_party/boost/boost/system/error_code.hpp:215:36: warning: ‘boost::system::errno_ecat’ defined but not used /home/wobst/buss/.../third_party/boost/boost/system/error_code.hpp:216:36: warning: ‘boost::system::native_ecat’ defined but not used R.Wobst, PyCon 2013 Köln 16/44 Templates sind zwar typsicher, aber der Programmierer weiß trotzdem meist nicht, was hinter den Kulissen passiert - Nebeneffekte machen den Nutzen oft fraglich. boost.python verwendet sogar C-Makros, z.B. das wichtige BOOST_PYTHON_MODULE, das etwa so expandiert: R.Wobst, PyCon 2013 Köln 17/44 BOOST_PYTHON_MODULE(WriteNode) void init_module_WriteNode(); extern "C" __attribute__ \ ((visibility("default"))) \ void initWriteNode() { boost::python::detail::init_module \ ("WriteNode",&init_module_WriteNode); } void init_module_WriteNode() { ... } R.Wobst, PyCon 2013 Köln 18/44 Fazit: Wenn man einen Modulnamen WriteNode angibt, wird impliziert eine neue Funktion initWriteNode() definiert, die später verwendet werden muss - das ist in der Dokumentation zunächst nicht zu finden. Compilezeiten: Insbesondere Templates treiben Compiler oft an ihre Grenzen, auch beim Speicherbedarf (der Autor verbrauchte damit zum ersten Mal mehr als 4 GB RAM, obwohl er sonst extensiv Gimp, Firefox und LibreOffice nutzt). Die Compilezeiten wachsen für PythonProgrammierer auf nervige Längen. Kein Wunder: Der Präprozesser generiert aus #include <iostream> etwa 437 KB Code; zum Vergleich bei C: #include <stdio.h> erzeugt 16 KB Code. Templates generieren i.a. weitaus komplexeren Code. Fehlerbehandlung: Bei Python-Exceptions wird immer nur die Ausnahme boost::python::error_already_set geworfen; wie man sich den Fehlertext holt: s.u. R.Wobst, PyCon 2013 Köln 19/44 4. Praktisches Beispiel Wir erzeugen eine Instanz der C++Klasse PythonApi::Mylog, von der wir die Methoden write() und save() nutzen wollen, und übergeben sie als Attribut eines Python-Moduls CPPlog (der nur als Namensraum existiert) dem embedded Python. Eine mögliche (hoffentlich fast optimale) Lösung kann so aussehen: R.Wobst, PyCon 2013 Köln 20/44 4.1. Python-Modul definieren namespace Py = ::boost::python; BOOST_PYTHON_MODULE(CPPlog) { Py::class_<PythonApi::Mylog>("Pylog") .def("write", &PythonApi::Mylog::write) .def("save", &PythonApi::Mylog::save); } Damit werden u.a. die Ausnahmebehandlung vorbereitet und die beiden Methoden boost.python "bekannt gemacht". Pylog ist der Name der Klasse in Python. Achtung - dieses Makro muss auf File-globaler Ebene noch vor main() gerufen werden! R.Wobst, PyCon 2013 Köln 21/44 4.2. Python-Modul initialisieren if(PyImport_AppendInittab( "CPPlog", &initCPPlog) == -1) { // error } Das Modul CPPlog wird zum builtin-Modul von Python und kann später importiert werden, ohne dass ein File CPPlog.py existiert. Die von BOOST_PYTHON_MODULE implizit definierte Funktion initCPPlog() wird gerufen und damit die Fehlerbehandlung initialisiert. PyImport_AppendInittab() wird in der Python-C-Api definiert. R.Wobst, PyCon 2013 Köln 22/44 4.3. Embedded Python initialisieren Py_Initialize(); Das ist der übliche Start des embedded Interpreters in der Python-C-Api und darf erst jetzt passieren! Achtung - Py_Finalize() sollte von boost.python aus nicht gerufen werden; ohnehin werden dadurch nicht sicher alle Ressourcen freigegeben (z.B. indirekt von Extensions reservierte). R.Wobst, PyCon 2013 Köln 23/44 4.4. Python-Namensraum nach boost.python exportieren Py::object mainModule = Py::import("__main__"); mainNamespace = mainModule.attr("__dict__"); Damit wird der globale Python-Namensraum in C++ bekannt. Wir brauchen ihn im folgenden ständig. R.Wobst, PyCon 2013 Köln 24/44 4.5. Klassischer Modulimport Py::object pylog(Py::handle<> \ (PyImport_ImportModule("CPPlog"))); mainNamespace["CPPlog"] = pylog; Wieder wird eine Funktion aus der Python-C-Api gerufen, und der in C++ definierte Python-Modul CPPlog wird unter diesem Namen auch Python bekannt. R.Wobst, PyCon 2013 Köln 25/44 4.6. Klasseninstanz exportieren PythonApi::Mylog pylogger = Mylog(); scope(pylog).attr("pylogInst") = \ Py::ptr(&pylogger); Damit wird die C++ - Klasseninstanz pylogger in Python unter dem Namen CPPlog.pylogInst verfügbar. R.Wobst, PyCon 2013 Köln 26/44 4.7. Klasseninstanz in Python verwenden oder erzeugen #!/usr/bin/env python CPPlog.pylogInst.write("this is a message") CPPlog.pylogInst.save() Damit nutzen wir die in C++ definierte Klasseninstanz pylogger, können ihre Methoden rufen und sogar ihre Daten verändern, wie bei einer normalen Python - Klasseninstanz! Um Klassen zu erzeugen, ruft man den Konstruktor normal auf und könnte dann in C++ per modulname.attr() darauf zugreifen oder einfach die Instanz an eine in C++ bekannten Liste anhängen, vgl.a. 6.2. R.Wobst, PyCon 2013 Köln 27/44 4.8. Python-Funktion von C++ aus rufen Py::object pystart = mainNamespace["start"]; Py::list parmlist; parmlist.append(Py::make_tuple(...)); ... Py:: object testret = pystart(parmlist); Damit kann man eine Funktion im eingebetteten Interpreter starten, oder wenn nur ein Skript gestartet werden soll - man verwendet einfach PyRun_SimpleString(). R.Wobst, PyCon 2013 Köln 28/44 4.9. Die perfekte Lösung? Es geht auch anders, vielleicht sogar einfacher - aber dieses Vorgehen funktionierte in der Praxis und reicht für viele Zwecke aus. Wenn man einen oben beschriebenen Schritt auslässt, gibt es jedenfalls Fehler. Bei Problemen: Google is your friend ☺ R.Wobst, PyCon 2013 Köln 29/44 5. Behandlung von Python-Fehlern Das bereitet anfangs Jedem arge Kopfzerbrechen, denn es kommt bei boost immer nur die Ausnahme Py::error_already_set an, ohne Text. Und PyErr_Print() schreibt auf sys.stderr - basta. Mein Ausweg: R.Wobst, PyCon 2013 Köln 30/44 5.1. Ausgaben puffern PyRun_SimpleString( "import sys, cStringIO\n" "sys.stdout = cStringIO.StringIO()\n" "sys.stderr = cStringIO.StringIO()"); Damit werden alle Ausgaben im RAM gepuffert. Das muss aus irgendeinem Grund vor dem Start der Python-Anwendung erfolgen! R.Wobst, PyCon 2013 Köln 31/44 5.2. Fehler in boost abfangen try { // start Python script or call function } catch(Py::error_already_set const&) { ... PyErr_Print(); } Damit landen die Fehlerausgaben in sys.stderr, das gepuffert ist. R.Wobst, PyCon 2013 Köln 32/44 5.3. Fehler auswerten Py::object sys = mainNamespace["sys"]; Py::object out = sys.attr("stdout"); std::string outTxt = \ Py::extract<std::string>( \ out.attr("getvalue")()); Py::object err = sys.attr("stderr"); std::string errTxt = \ Py::extract<std::string>( \ err.attr("getvalue")()); Mit outTxt und errTxt kann man nun nach Herzenslust verfahren. R.Wobst, PyCon 2013 Köln 33/44 Das Verfahren hat natürlich den Nachteil, dass sys.stderr auch noch anderen Text enthalten kann. Experimente zeigten jedoch, dass die Umleitung von sys.stderr nicht erst vor dem PyErr_Print() erfolgen darf. R.Wobst, PyCon 2013 Köln 34/44 6. Weitere Problemstellungen 6.1. Typechecks Werden Python-Objekte an C++ übergeben, so muss man deren Typ noch mittels der Python-C-Api überprüfen, also etwa mit int PyInt_Check(PyObject *o) In boost.python gibt es noch keine entsprechenden Funktionen. R.Wobst, PyCon 2013 Köln 35/44 6.2. Funktionen überladen, KonstruktorArgumente, Standardargumente Der Aufruf überladener Methoden einer C++ - Klasse ist etwas umständlich. Zunächst muss man sich Hilfsfunktionen in C++ definieren, etwa so: R.Wobst, PyCon 2013 Köln 36/44 void WriteNode::write_int(const int value) {valueNode->write(value);} void WriteNode::write_double( \ const double value) {valueNode->write(value);} void WriteNode::write_strg( \ const std::string& value) {valueNode->write(value);} Die Methode valueNode.write() ist überladen. Danach deklariert man R.Wobst, PyCon 2013 Köln 37/44 BOOST_PYTHON_MODULE(WriteNode) { Py::class_<WriteNode>("WriteNode", \ Py::init<std::string>()) .def("write_int", &WriteNode::write_int) .def("write_double", \ &WriteNode::write_double) .def("write_strg", &WriteNode::write_strg); } Der Konstruktor erhält ein std::string als Argument. Standardargumente von Funktionen/Methoden müssen analog behandelt werden, da nur Funktionspointer übergeben werden. R.Wobst, PyCon 2013 Köln 38/44 6.3. Abbruch des Interpreters von innen oder außen Der eingebettete Interpreter ist kein gesonderter Prozess. Das heißt: Ein sys.exit() in Python reißt auch das aufrufende C++ Programm mit ins Grab! Aus dem gleichen Grund ist es auch nicht möglich, eine Endlosschleife in Python (oder ein Warten auf ein Ereignis) von C++ aus abzubrechen. Einziger Ausweg: Programmarchitektur von Anfang an entsprechend planen. R.Wobst, PyCon 2013 Köln 39/44 6.4. boost.any vs. Python-Objekte Um eine Python-Liste mit unterschiedlichen Typen aufzubauen, liegt es nahe, boost::any zu verwenden, denn das ist ja gerade "der variable Typ". Aber: C++ will alle Typen schon zur Compilezeit wissen, Python erst zur Laufzeit! Man braucht dann Funktionen wie folgende: R.Wobst, PyCon 2013 Köln 40/44 Py::object any2pyobj(const boost::any& value) { if(value.type() == typeid(int)) return Py::object(boost::any_cast<int> (value)); else if(value.type() == typeid(double)) return Py::object(boost::any_cast<double> (value)); else if(value.type() == typeid(std::string)) return Py::object( boost::any_cast<std::string>(value)); else // error } R.Wobst, PyCon 2013 Köln 41/44 7. Alternativen Wenn "nur" Treiber in C/C++ von Python aus verwendet werden müssen, reicht vielleicht das Modul ctypes. Wenn die Steuerung von C/C++ aus erfolgen muss, reichen vielleicht schon die callback-Funktionen von ctypes (s.dort). So wird z.B. bei Fuse.py verfahren. Theoretisch leistet die Python-C-Api alles, aber sie ist schwerfällig, und die "manuelle Referenzzählung" ist eine üble Fehlerquelle. R.Wobst, PyCon 2013 Köln 42/44 8. Fazit boost.python kann sehr nützlich sein, wenn man die "Umkehrung" des ctypes-Modul braucht, insbesondere C++ - Klasseninstanzen verwenden muss. Die Schwierigkeiten sind heftig, aber lösbar - dieser Vortrag will eine Hilfe sein. Meine Erfahrungen aus der Parallelentwicklung in C++ und Python (wie einst schon bei Qt): erstaunlich, wie viele Laufzeitfehler doch im ach so (typ)sicheren C++ auftreten; erschreckend, wie aufwändig die Entwicklung in C++ (auch für "geborene C/C++'ler" wie der Autor) erscheint, wenn man parallel dazu das gleiche Problem in Python bearbeitet. R.Wobst, PyCon 2013 Köln 43/44 C++ ist wirklich phantastisch, um als Programmierer seinem Hobby zu frönen und ordentlich Geld zu verdienen. Sprachen wie Python sind nur dazu da, schnell ein Problem gelöst zu bekommen. R.Wobst, PyCon 2013 Köln 44/44