打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Embedding Python in C++ Applications with boost::python

In the Introduction to this tutorial series, I took at look at the motivation for integrating Python code into theGranola code base. In short, it allows me to leverage all the benefits of the Python language and the Python standard library when approaching tasks that are normally painful or awkward in C++. The underlying subtext, of course, is that I didn’t have to port any of the existing C++ code to do so.

Today, I’d like to take a look at some first steps at using boost::python to embed Python in C++ and interact with Python objects. I’ve put all the code from this section ina github repo, so feel free to check the code out and play along.

At it’s core, embedding Python is very simple, and requires no C++ code whatsoever – the libraries provided with a Python distributioninclude C bindings. I’m going to skip over all that though, and jump straight into using Python in C++ via boost::python, which provides class wrappers and polymorphic behavior much more consistent with actual Python code than the C bindings would allow. In the later parts of this tutorial, we’ll cover a few things that you can’t do with boost::python (notably, multithreading and error handling).

So anyway, to get started you need to download and build boost, or retrieve a copy from your package manager. If you choose to build it, you can build just the boost::python library (it is unfortunately not header-only), though I would suggest getting familiar with the entire set of libraries if you do a lot of C++ programming. If you are following along with the git repo, make sure you change the path in the Makefile to point to your boost installation directory. And thus concludes the exposition. Let’s dive in!

First, we need to be able to build an application with Python embedded. With gcc this isn’t too difficult; it is simply a matter of including boost::python and libpython as either static or shared libraries. Depending on how you build boost, you may have trouble mixing and matching. In the tutorial code on github, we will use the static boost::python library (libboost_python.a) and the dynamic version of the Python library (libpython.so).

One of the soft requirements I had for my development efforts at MiserWare was to make the environment consistent across all of our support operating systems: several Windows and an ever-changing list of Linux distros. As a result, Granola links against a pinned version of Python and the installation packages include the Python library files required to run our code. Not ideal, perhaps, but it results in an environment where I am positive our code will run across all supported operating systems.

Let’s get some code running. You’ll need to include the correct headers, as you might imagine.

  1. Py_Initialize();  
  2. py::object main_module = py::import("__main__");  
  3. py::object main_namespace = main_module.attr("__dict__");  



Note that you must initialize the Python interpreter directly (line 1). While boost::python greatly eases the task of embedding Python, it does not handle everything you need to do. As I mentioned above, we’ll see some more shortcomings in future sections of the tutorial. After initializing, the __main__ module is imported and the namespace is extracted. This results in a blank canvas upon which we can then call Python code, adding modules and variables.

  1. boost::python::exec("print 'Hello, world'", main_namespace);  
  2. boost::python::exec("print 'Hello, world'[3:5]", main_namespace);  
  3. boost::python::exec("print '.'.join(['1','2','3'])", main_namespace);  


The exec function runs the arbitrary code in the string parameter within the specified namespace. All of the normal, non-imported code is available. Of course, this isn’t very useful without being able to import modules and extract values.

  1. boost::python::exec("import random", main_namespace);  
  2. boost::python::objectrand = boost::python::eval("random.random()", main_namespace);  
  3. std::cout << py::extract<double>(rand) << std::endl;  


Here we’ve imported the random module by executing the corresponding Python statement within the__main__ namespace, bringing the module into the namespace. After the module is available, we can use functions, objects, and variables within the namespace. In this example, we use theeval function, which returns the result of the passed-in Python statement, to create a boost::python object containing a random value as returned by therandom() function in the random module. Finally, we extract the value as a C++double type and print it.

This may seem a bit.. soft. Calling Python by passing formatted Python strings into C++ functions? Not a very object-oriented way of dealing with things. Fortunately, there is a better way.

  1. boost::python::object rand_mod = boost::python::import("random");  
  2. boost::python::object rand_func = rand_mod.attr("random");  
  3. boost::python::object rand2 = rand_func();  
  4. std::cout << boost::python::extract(rand2) << std::endl;  


In this final example, we import the random module, but this time using the boost::pythonimport function, which loads the module into a boost Python object. Next, therandom function object is extracted from therandom module and stored in a boost::python object. The function is called, returning a Python object containing the random number. Finally, the double value is extracted and printed. In general, all Python objects can be handled in this way – functions, classes, built-in types.

It really starts getting interesting when you start holding complex standard library objects and instances of user-defined classes. In thenext tutorial, I’ll take a full class through its paces and build a bona fide configuration parsing class around theConfigParser module discuss the details of parsing Python exceptions from C++ code.



In Part 1, we took a look at embedding Python in C++ applications, including several ways of calling Python code from your application. Though I earlier promised a full implementation of a configuration parser in Part 2, I think it’s more constructive to take a look at error parsing. Once we have a good way to handle errors in Python code, I’ll create the promised configuration parser in Part 3. Let’s jump in!

If you got yourself a copy of the git repo for the tutorial and were playing around with it, you may have experienced the wayboost::python handles Python errors – theerror_already_set exception type. If not, the following code will generate the exception:

  1. namespacepy = boost::python;  
  2. ...  
  3. Py_Initialize();  
  4. ...  
  5. py::object rand_mod = py::import("fake_module");  


…which outputs the not-so-helpful:

terminate called after throwing an instance of 'boost::python::error_already_set'Aborted

In short, any errors that occur in the Python code that boost::python handles will cause the library to raise this exception; unfortunately, the exception does not encapsulate any of the information about the error itself. To extract information about the error, we’re going to have to resort to using the Python C API and some Python itself. First, catch the error:

  1. try{  
  2.     Py_Initialize();  
  3.     py::object rand_mod = py::import("fake_module");  
  4. }catch(boost::python::error_already_setconst &){  
  5.     std::string perror_str = parse_python_exception();  
  6.     std::cout <<"Error in Python: " << perror_str << std::endl;  
  7. }  



Above, we've called the parse_python_exception function to extract the error string and print it. As this suggests, the exception data is stored statically in the Python library and not encapsulated in the exception itself. The first step in the parse_python_exception function, then, is to extract that data using thePyErr_Fetch Python C API function:

  1. std::string parse_python_exception(){  
  2.     PyObject *type_ptr = NULL, *value_ptr = NULL, *traceback_ptr = NULL;  
  3.     PyErr_Fetch(&type_ptr, &value_ptr, &traceback_ptr);  
  4.     std::string ret("Unfetchable Python error");  
  5.     ...  


As there may be all, some, or none of the exception data available, we set up the returned string with a fallback value. Next, we try to extract and stringify the type data from the exception information:

  1. ...  
  2. if(type_ptr != NULL){  
  3.     py::handle<> h_type(type_ptr);  
  4.     py::str type_pstr(h_type);  
  5.     py::extract<std::string> e_type_pstr(type_pstr);  
  6.     if(e_type_pstr.check())  
  7.         ret = e_type_pstr();  
  8.     else  
  9.         ret ="Unknown exception type";  
  10. }  
  11. ...  


In this block, we first check that there is actually a valid pointer to the type data. If there is, we construct aboost::python::handle to the data from which we then create astr object. This conversion should ensure that a valid string extraction is possible, but to double check we create anextract object, check the object, and then perform the extraction if it is valid. Otherwise, we use a fallback string for the type information.

Next, we perform a very similar set of steps on the exception value:

  1. ...  
  2. if(value_ptr != NULL){  
  3.     py::handle<> h_val(value_ptr);  
  4.     py::str a(h_val);  
  5.     py::extract<std::string> returned(a);  
  6.     if(returned.check())  
  7.         ret += ": " + returned();  
  8.     else  
  9.         ret += std::string(": Unparseable Python error: ");  
  10. }  
  11. ...  


We append the value string to the existing error string. The value string is, for most built-in exception types, the readable string describing the error.

Finally, we extract the traceback data:

  1. if(traceback_ptr != NULL){  
  2.      py::handle<> h_tb(traceback_ptr);  
  3.      py::object tb(py::import("traceback"));  
  4.      py::object fmt_tb(tb.attr("format_tb"));  
  5.      py::object tb_list(fmt_tb(h_tb));  
  6.      py::object tb_str(py::str("\n").join(tb_list));  
  7.      py::extract<std::string> returned(tb_str);  
  8.      if(returned.check())  
  9.          ret +=": " + returned();  
  10.      else  
  11.          ret += std::string(": Unparseable Python traceback");  
  12.  }  
  13.  returnret;  


The traceback goes similarly to the type and value extractions, except for the extra step of formatting the traceback object as a string. For that, we import thetraceback module. From traceback, we then extract theformat_tb function and call it with the handle to the traceback object. This generates a list of traceback strings which we then join into a single string. Not the prettiest printing, perhaps, but it gets the job done. Finally, we extract the C++ string type as above and append it to the returned error string and return the entire result.

In the context of the earlier error, the application now generates the following output:

Error in Python: : No module named fake_module

Generally speaking, this function will make it much easier to get to the root cause of problems in your embedded Python code. One caveat: if you are configuring a custom Python environment (especially module paths) for your embedded interpreter, theparse_python_exception function may itself throw a boost::error_already_set when it attempts to load the traceback module, so you may want to wrap the call to the function in atry...catch block and parse only the type and value pointers out of the result.

As I mentioned above, in Part 3 I will walk through the implementation of a configuration parser built on top of theConfigParser Python module. Assuming, of course, that I don't get waylaid again.



In Part 2 of this tutorial, I covered a methodology for handling exceptions thrown from embedded Python code from within the C++ part of your application. This is crucial for debugging your embedded Python code. In this tutorial, we will create a simple C++ class that leverages Python functionality to handle an often-irritating part of developing real applications: configuration parsing.

In an attempt to not draw ire from the C++ elites, I am going to say this in a diplomatic way: I suck at complex string manipulations in C++. STLstrings andstringstreams greatly simplify the task, but performing application-level tasks, and performing them in a robust way, always results in me writing more code that I would really like. As a result, I recently rewrote the configuration parsing mechanism from Granola Connect (the daemon inGranola Enterprise that handles communication with the Granola REST API) using embedded Python and specifically theConfigParser module.

Of course, string manipulations and configuration parsing are just an example. For Part 3, I could have chosen any number of tasks that are difficult in C++ and easy in Python (web connectivity, for instance), but the configuration parsing class is a simple yet complete example of embedding Python for something of actual use. Grab the code from theGithub repo for this tutorial to play along.

First, let’s create a class definition that covers very basic configuration parsing: read and parse INI-style files, extract string values given a name and a section, and set string values for a given section. Here is the class declaration:

  1. class ConfigParser{  
  2.     private:  
  3.         boost::python::object conf_parser_;  
  4.   
  5.         void init();  
  6.     public:  
  7.         ConfigParser();  
  8.   
  9.         bool parse_file(const std::string &filename);  
  10.         std::string get(const std::string &attr,  
  11.                         const std::string &section = "DEFAULT");  
  12.         void set(const std::string &attr,  
  13.                  const std::string &value,  
  14.                  const std::string &section = "DEFAULT");  
  15. };  


The ConfigParser module offers far more features than we will cover in this tutorial, but the subset we implement here should serve as a template for implementing more complex functionality. The implementation of the class is fairly simple; first, the constructor loads the main module, extracts the dictionary, imports theConfigParser module into the namespace, and creates aboost::python::object member variable holding aRawConfigParser object:

  1. ConfigParser::ConfigParser(){  
  2.     py::object mm = py::import("__main__");  
  3.     py::object mn = mm.attr("__dict__");  
  4.     py::exec("import ConfigParser", mn);  
  5.     conf_parser_ = py::eval("ConfigParser.RawConfigParser()", mn);  
  6. }  


The file parsing and the getting and setting of values is performed using thisconfig_parser_ object:

bool ConfigParser::parse_file(const std::string &filename){    return py::len(conf_parser_.attr("read")(filename)) == 1;}std::string ConfigParser::get(const std::string &attr, const std::string &section){    return py::extract<std::string>(conf_parser_.attr("get")(section, attr));}void ConfigParser::set(const std::string &attr, const std::string &value, const std::string &section){    conf_parser_.attr("set")(section, attr, value);}

In this simple example, for the sake of brevity exceptions are allowed to propagate. In a more complex environment, you will almost certainly want to have the C++ class handle and repackage the Python exceptions as C++ exceptions. This way you could later create a pure C++ class if performance or some other concern became an issue.

To use the class, calling code can simply treat it as a normal C++ class:

  1. int main(){  
  2.     Py_Initialize();  
  3.     try{  
  4.         ConfigParser parser;  
  5.         parser.parse_file("conf_file.1.conf");  
  6.         cout << "Directory (file 1): " << parser.get("Directory", "DEFAULT") << endl;  
  7.         parser.parse_file("conf_file.2.conf");  
  8.         cout << "Directory (file 2): " << parser.get("Directory", "DEFAULT") << endl;  
  9.         cout << "Username: " << parser.get("Username", "Auth") << endl;  
  10.         cout << "Password: " << parser.get("Password", "Auth") << endl;  
  11.         parser.set("Directory", "values can be arbitrary strings", "DEFAULT");  
  12.         cout << "Directory (force set by application): " << parser.get("Directory") << endl;  
  13.         // Will raise a NoOption exception  
  14.         // cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;  
  15.     }catch(boost::python::error_already_set const &){  
  16.         string perror_str = parse_python_exception();  
  17.         cout << "Error during configuration parsing: " << perror_str << endl;  
  18.     }  
  19. }  


And that's that: a key-value configuration parser with sections and comments in under 50 lines of code. This is just the tip of the iceberg too. In almost the same length of code, you can do all sorts of things that would be at best painful and at worse error prone and time consuming in C++: configuration parsing, list and set operations, web connectivity, file format operations (think XML/JSON), and myriad other tasks are already implemented in the Python standard library.

In Part 4, I'll take a look at how to more robustly and generically call Python code using functors and a Python namespace class.


In Part 2 of this ongoing tutorial, I introduced code for parsing Python exceptions from C++. InPart 3, I implemented a simple configuration parsing class utilizing the PythonConfigParser module. As part of that implementation, I mentioned that for a project of any scale, one would want to catch and deal with Python exceptions within the class, so that clients of the class wouldn’t have to know about the details of Python. From the perspective of a caller, then, the class would be just like any other C++ class.

The obvious way of handling the Python exceptions would be to handle them in each function. For example, theget function of the C++ ConfigParser class we created would become:

  1. std::string ConfigParser::get(const std::string &attr, const std::string &section){  
  2.     try{  
  3.         return py::extract(conf_parser_.attr("get")(section, attr));  
  4.     }catch(boost::python::error_already_set const &){  
  5.         std::string perror_str = parse_python_exception();  
  6.         throw std::runtime_error("Error getting configuration option: " + perror_str);  
  7.     }  
  8. }  


The error handling code remains the same, but now the main function becomes:

  1. int main(){  
  2.     Py_Initialize();  
  3.     try{  
  4.         ConfigParser parser;  
  5.         parser.parse_file("conf_file.1.conf");  
  6.         ...  
  7.         // Will raise a NoOption exception  
  8.          cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;  
  9.     }catch(exception &e){  
  10.         cout << "Here is the error, from a C++ exception: " << e.what() << endl;  
  11.     }  
  12. }  


When the Python exception is raised, it will be parsed and repackaged as a std::runtime_error, which is caught at the caller and handled like a normal C++ exception (i.e. without having to go through theparse_python_exception rigmarole). For a project that only has a handful of functions or a class or two utilizing embedded Python, this will certainly work. For a larger project, though, one wants to avoid the large amount of duplicated code and the errors it will inevitably bring.

For my implementation, I wanted to always handle the the errors in the same way, but I needed a way to call different functions with different signatures. I decided to leverage another powerful area of theboost library: the functors library, and specifically boost::bind andboost::function. boost::function provides functor class wrappers, andboost::bind (among other things) binds arguments to functions. The two together, then, enable the passing of functions and their arguments that can be called at a later time. Just what the doctor ordered!

To utilize the functor, the function needs to know about the return type. Since we're wrapping functions with different signatures, a function template does the trick nicely:

  1. template <class return_type>  
  2. return_type call_python_func(boost::function<return_type ()> to_call, const std::string &error_pre){  
  3.     std::string error_str(error_pre);  
  4.     
  5.     try{  
  6.         return to_call();  
  7.     }catch(boost::python::error_already_set const &){  
  8.         error_str = error_str + parse_python_exception();  
  9.         throw std::runtime_error(error_str);  
  10.     }  
  11. }  



This function takes the functor object for a function calling boost::python functions. Each function that callsboost::python code will now be split into two functions: the private core function that uses the Python functionality and a public wrapper function that uses thecall_python_func function. Here is the updated get function and its partner:

  1. string ConfigParser::get(const string &attr, const string &section){  
  2.     return call_python_func<string>(boost::bind(&ConfigParser::get_py, this, attr, section),  
  3.                                     "Error getting configuration option: ");  
  4. }  
  5.     
  6. string ConfigParser::get_py(const string &attr, const string &section){  
  7.     return py::extract<string>(conf_parser_.attr("get")(section, attr));  
  8. }  


The get function binds the passed-in arguments, along with the implicit this pointer, to theget_py function, which in turn calls the boost::python functions necessary to perform the action. Simple and effective.

Of course, there is a tradeoff associated here. Instead of the repeated code of thetry...catch blocks and Python error handling, there are double the number of functions declared per class. For my purposes, I prefer the second form, as it more effectively utilizes the compiler to find errors, but mileage may vary. The most important point is to handle Python errors at a level of code that understands Python. If your entire application needs to understand Python, you should consider rewriting in Python rather than embedding, perhaps with some C++ modules as needed.

As always, you can follow along with the tutorial by cloning the github repo.



本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Boost.Python
c – 当某些结构字段被省略或与结构声明中的顺序不一样时,如何实现正确的解析?
python模块介绍- argparse:命令行选项及参数解析
python之parser.add
(原创)compile cortex-vfx on Ubuntu10.04
Python扩展方法及工具比较 歪歪虫
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服