Интеграция Python и C++

    Всем доброго времени суток!

    Недавно при прототипировании одной из частей разрабатываемого нами продукта возникла одна интересная задача: нужно было проверить склейку Python и C++. Связано это было с тем, что основной код был написан на плюсах, и необходимо было подключить внешнюю библиотеку Websockets, написанную на Python (на тот момент не было соответствующей библиотеки на C++). Схема взаимодействия при такой задаче достаточно простая. Из C++ вызывается функция подключения к серверу (на python), в качестве параметра передается его адрес. Соответственно, при получении сообщния Python передавает его обратно в метод C++.

    При написании кода использовалась питоновская библиотека Websocket от Autobahn (http://www.tavendo.de/autobahn/clientlibraries.html), которую было необходимо вызывать из C++. Для этих целей в Python предусмотрен Python C-API (http://docs.python.org/extending/index.html), однако многие простые действия, например, вызов функций делается несколькими действиями. После небольшого гугления был найден ряд библиотек, позволяющих упростить подобные действия: Boost.Python (http://www.boost.org/doc/libs/1_39_0/libs/python/doc/index.html), SWIG(http://www.swig.org/), Py++, Pybindgen, Pyrex… В результате был выбран Boost.Python как наиболее популярное решение.

    Для начала напишем простенький эхо клиент на Python, который будет раз в секунду посылать сам себе сообщение “Hello world”, принимать его и отдавать в C++. cppMethods будет объявлен в C++ коде, cppMethods.printMessage(msg) — как раз место склейки со стороны Python, непосредственно вызов функции C++, которая будет печатать полученное сообщение.

    Вот код Python — echo-client.py:
    from twisted.internet import reactor
    from autobahn.websocket import WebSocketClientFactory, WebSocketClientProtocol, connectWS
      
    import cppMethods 
      
    class EchoClientProtocol(WebSocketClientProtocol):
      
       def sendHello(self):
          self.sendMessage("Hello, world!")  
      
       def onOpen(self):
          self.sendHello()
      
       def onMessage(self, msg, binary):
          cppMethods.printMessage(msg)
          reactor.callLater(1, self.sendHello)
      
    def Connect(addressStr):
        factory = WebSocketClientFactory(addressStr)
        factory.protocol = EchoClientProtocol
        connectWS(factory)
        reactor.run()
    


    Теперь напишем на C++ код, в котором опишем нашу функцию, вызываемую из питона. Для использования Python C-API нужно проинклудить Python.h. Обратите внимание, что на этом этапе мы еще не используем Boost.Python, лишь собственно родной Python C-API.

    PrintEmb.cpp:
    #include <Python.h>
    
    #include <iostream>
    #include <string>
    
    static PyObject * 
    printString(PyObject * self, PyObject* args)
    {
        const char * toPrint;
        if(!PyArg_ParseTuple(args, "s", &toPrint))
        {
            return NULL;
        }
        std::cout << toPrint << std::endl;
        Py_RETURN_NONE;
    }
    
    static PyMethodDef EmbMethods[] = {
        {"printMessage", printString, METH_VARARGS, "Return the string recieved from websocket server"},
        {NULL, NULL, 0, NULL}
    };
    


    В последнем объявлении мы описали, что при вызове функции printMessage из Python будет вызван C++ метод printString.

    Ну и наконец свяжем все это вместе. Для проверки работы websockets, помимо эхо-клиента, была использована ссылка на html5labs.

    WebSocketConnect.cpp:
    #include <Python.h>
    #include <boost/python.hpp>
      
    #include <iostream>
    #include <string>
    #include "PrintEmb.cpp" 
      
    void WebSocketConnect()
    {
        using namespace boost::python;
         
        Py_Initialize();
         
        Py_InitModule("cppMethods", EmbMethods);
         
        PyObject * ws = PyImport_ImportModule("echo_client");
        std::string address = "ws://html5labs-interop.cloudapp.net:4502/chat";
        call_method<void>(ws, "Connect", address);
         
        Py_Finalize();
    }
    

    В этом месте мы все-таки воспользовались возможностями Boost, а именно функцией call_method, иначе бы нам понадобилось написать существенно больше кода.

    Ну вот как-то так. Здесь мы поинициализировали EmbMethods для питона, назвав их cppMethods, а затем вызвали из Python метод Connect и передали в него строку “address”. В результате наше приложение раз в секунду печатает строку «Hello World» (которую посылает сам себе питон), а также любое сообщение, приходящее с сервера вебсокетов.

    Вот таким образом можно связать Python и C++. Буду благодарен за комментарии по теме.

    Upd. Поправил код в соответствии с замечаниями из комментариев.
    PENXY 26,56
    Компания
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 13
    • +1
      У вас там неправильно разбираются аргументы в printString(): PyObject * args это всегда types.TupleType, и переданную при вызове строку оттуда надо доставать через PyArg_ParseTuple(), плюс возвращать что-то из такого метода — лишняя ненужна работа, можно просто делать Py_RETURN_NONE.
      • +2
        Хотя, у вас там тип метода опущен, и получается что это deprecated METH_OLDARGS, так что работать оно, конечно, будет, но плохо так делать совсем.
        • +7
          image
          • +1
            Спасибо, юный падаван.
            • +1
              Спасибо за все замечания, поправил код в соответствии с ними.
              • +1
                Вы неправильно поправили =)

                1) Если PyArg_ParseTuple() не смог что-нибудь распарсить, потому что функции были переданые неверные параметры, то он возвращает false и бросает исключение, поэтому в этом месте из функции нужно выходить через return NULL, чтобы интерпретатор понял, что не всё прошло гладко. Проще говоря, возвращение NULL сообщает интерпретатору, что при вызове было брошено исключение и с ним надо что-то делать.

                2) Спецификатор формата «s» означает const char *, а не std::string, тем более вот так вот писать в объект, вообще говоря, нельзя, потому что это его превратит в кровавое месиво. Плюс, совсем не обязательно объявлять переменную для строки статично в namespace scope.

                3) Просто так возвращать Py_None нельзя, это такой же питонячий объект как и все остальные, ему тоже надо делать Py_INCREF(). Но, поскольку возвращение None это идиоматическое действие, есть макрос Py_RETURN_NONE(), который за вас всё сделает сам.

                Вот так будет правильно:
                static PyObject * 
                printString(PyObject * self, PyObject* args)
                {
                    const char * toPrint;
                
                    if(!PyArg_ParseTuple(args, "s", &toPrint))
                    {
                        return NULL;
                    }
                    std::cout << toPrint << std::endl;
                    Py_RETURN_NONE;
                }
                
                • +1
                  Ужас. Все вы верно говорите (ну только PyRETURN_NONE я и сам исправил, видимо, уже после того, как вы открыли статью). Извините за дурацкие ошибки. :)
      • +1
        Мне одному кажется, что тема статьи раскрыта во втором абзаце, а остальное немного лишнее?
        • 0
          В целом, вы, конечно, правы. С другой стороны тогда почти любую статью по .net можно заменить на соответствующую ссылку msdn. Мне кажется, наличие работающего кода в тексте статьи упрощает восприятие информации и позволяет быстрее освоить предложенный способ.
        • 0
          Зачем вы возвращаете пустую строку из ф-ии printString? return Py_BuildValue("");
          • 0
            Похоже это оттого, что просто не знали как вернуть «None», но статью написать хотелось :) Есть макрос Py_RETURN_NONE.
            • 0
              Да, на эту несуразность уже обратили внимание. Все исправил.
          • 0
            У вас однопоточное приложение?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое