Компания
26,68
рейтинг
19 марта 2012 в 18:21

Разработка → Интеграция 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. Поправил код в соответствии с замечаниями из комментариев.
Автор: @rigeborod
PENXY
рейтинг 26,68
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Комментарии (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
    У вас однопоточное приложение?

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

Самое читаемое Разработка