Всем доброго {daytime}!

Сегодня пришла пора рассказать вам о фундаментальной проблеме перекодировки при взаимодействии проекта собранного на MS Visual C++ на платформе Windows и наиболее приятной скриптовой обвязки для языка C++, благодаря библиотеке Boost.Python, собственно написанной для языка Python.

Вы ведь хотите использовать для вашего приложения на C++ под ОС Windows хорошую скриптовую обвязку на последней версии Python 3.x, либо вы хотите использовать для вашего приложения на Python максимально ускоренный участок кода вашего модуля, переписанный на C++. В обоих случаях, если вы знаете оба языка как минимум хорошо, вам стоит это прочитать.

Не буду вас утомлять многочасовыми выкладками о проблеме перекодировки текста в принципе. Все мы знаем что эта проблема не новая, везде и всюду решается по разному, в большинстве случаев никак, то есть перекладывается на плечи программиста.

В языке Python, начиная с версии 3.0 принято решение считать строками только сам текст. Не важно как сам текст закодирован, а закодирован он в Юникоде, само понятие строки навсегда оторвано от его кодировки. То есть нет никакой возможности понять какое число соответствует символу в строке иначе, как перекодировать его в массив байт, указав кодировку.
"Привет!".encode('cp1251')

Пример выше показывает, что сама строка «Привет!» останется таковой как вы её набрали, вне зависимости от того, смотрят на неё в России, США или в Китае, на Windows, Linux или MacOS, она останется строкой «Привет!». Декодировав её в массив байт методом строки str.encode( encoding ) мы всегда получим одно и то же значение элементов массива байт, вне зависимости от того в какой точке земного шара мы находимся и какой платформой мы пользуемся. И это замечательно!
Однако вернёмся на Землю. Есть такая ОС Windows…

Вся проблема заключается в замечательной среде разработки MS Visual Studio. А более всего она замечательна тем, что все строки в C++ в ней гарантированно в кодировке кодовой страницы Windows. То есть для России все строки всегда будут в 'cp1251'. И всё бы ничего, но для вывода на веб-страницу, сохранения в XML, вывода в интернациональную БД и прочего данная кодировка не подходит. Предлагаемый Microsoft вариант со строками вида L«Привет» приемлем чуть более, но мы ведь знаем как замечательно в C++ работается с такими строками. Кроме того мы будем исходить из того, что к нам попал проект уже с кучей строк в виде cp1251. Гигабайты кода, работающие с std::string и char* и работающие с ними прекрасно: быстро и качественно.

Если вы идёте со стороны Python в C++, просто помните, что строки Python отлично конвертируются в char* используя внутреннюю память Python, поскольку все строки в Python 3.x как минимум в UTF-8 уже хранятся и за ними зорко следят GC и счётчик ссылок. Поэтому опять же: не надо этого UCS-2 от Microsoft выдаваемого за Юникод, используйте обычные строки. Ну и кроме того, помните, что локальная для России БД вашей компании не скажет вам спасибо за удвоенный размер данных, при переходе с WIN-1251 на UTF-8, поскольку наверняка под завязку набита кириллицей.
В общем проблема обозначена.

Теперь решение.

У вас уже наверняка есть последняя версия Python 3.x (в настоящий момент это — Python 3.3), если ещё нет, ставьте последний отсюда: www.python.org/download/releases
Также у вас наверняка стоит MS Visual Studio (в настоящий момент последняя — это VS2012, но всё нижесказанное будет верно и для предыдущей версии VS2010).
Для связки ваших классов на C++ с Python потребуется библиотека Boost.Python. Поставляется в составе уже почти стандартной библиотеки Boost: www.boost.org (в настоящий момент последней версией является 1.52, но проверено и верно вплоть до 1.44).
К сожалению, в отличие от всего остального, Boost.Python нужно собрать. Если он у вас ещё не собран вместе с остальными библиотеками собрать только Boost.Python можно следующей командой Boost.Build (в более старых версиях через bjam):
b2 --with-python --build-type=complete
Если вы выкачали Python 3.x для x64, то нужно указать ещё и address-model=64.
Более подробно в документации Boost.Build.
В результате в {BoostDir}\stage\lib\ у вас должна появится куча библиотек вида boost-python*. Они нам уже вот-вот понадобятся!..

Итак собственно воспроизводим проблему. Пишем простой класс:
    class MY_EXPORT Search
    {
    public:
        static string That( const string& name );
    };

С вот такой реализацией его единственного метода:
    string Search::That( const string& name )
    {
        if( name == "Это Я!" )
            return "Я";
        else
            throw runtime_error( "Я ничего не нашёл!" );
    }

В реальности всё значительно сложнее: у вас скорее всего запись из БД с полями кириллицей, да и сами значения тоже кириллицей, и всё в кодировке Windows-1251. Но нам чтобы отладится хватит этого тестового примера. Здесь есть конвертация строк туда и обратно из С++ и даже передача исключений в Python.

Используя Boost.Python обернём нашу маленькую библиотеку:
BOOST_PYTHON_MODULE( my )
{
    class_<Search>( "Search" )
        .def( "That", &Search::That )
        .staticmethod( "That" )
    ;
}

Не забываем про зависимости от Boost и исходной библиотеки в настройках проекта!
Полученную библиотеку переименовываем в my.pyd (да-да, просто меняем расширение).

Пробуем поработать с ней из Python. Можно прямо из консоли, если нет под рукой IDE вроде Eclipse+PyDev, просто импортируем и используем в две строки:
import my
my.Search.That( "Это Я!" )

Не забываем, что это всё-таки .dll и ей наверняка требуется .dll исходной библиотеки с классом Search, кроме того новой библиотеке-обёртке потребуется .dll Boost.Python соответствующей сборки из {BoostDir}\stage\lib\, например для MS VS2012 и Boost 1.52 для сборки Debug (Multi-thread DLL) это boost_python-vc110-mt-gd-1_52.dll.
Если непонятно чего не хватает вашей .dll посмотрите её зависимости с помощью того же Dependency Walker: www.dependencywalker.com — просто откройте depends.exe вашу .dll с библиотекой-обёрткой.
Итак вам удалось импортировать библиотеку my и выполнить my.Search.That( "Это Я!" )

Если всё хорошо, вы увидите пришедшее исключение из С++ с пустым текстом. То есть мало того, что мы не попали в нужную ветку if, так ещё и текст исключения перекодировался не так, как мы его отправили!

Если вы присоединитесь к процессу Python через "Attach to process" из MS Visual Studio, то увидите что в Search::That( const string& name ) приходит name в UTF-8. Boost.Python не знает о том в какой кодировке отдавать строку, поэтому отдаёт по умолчанию в UTF-8.
Само собой наш код в Visual Studio полностью ориентирован на Windows-1251, поэтому понять что «Р­С‚Рѕ РЇ!» на самом деле «Это Я!» он также не может. Получаем разговор слепого с глухим. По той же причине не видно текста исключения пришедшего из C++ в Python.

Ну что же, будем исправлять.

Первое что приходит в голову: унаследовать/завернуть исходный класс в другой, который умеет перекодировать.
Ага, теперь посмотрим на остальные классы, сиротливо шаркающих ножкой в ожидании своей очереди. Вы готовы потратить полжизни? Даже если это не так, первые же замеры производительности покажут насколько вы не правы оборачивая потомков. Ну и в конце у вас будут адовые проблемы при попытке достать обёрнутые классы обратно в объекты C++. Они у вас будут, поверьте! Мы строим мост по которому будем ходить в обе стороны, и обёртки классов должны напрямую ссылаться на методы и свойства нужного класса. Смотри в сторону extract<T&>(obj) из boost::python на стороне C++.

Анализируем всё, что делается в Boost.Python когда строка путешествует между C++ и Python. Видим несколько замечательных мест в которых используются функции PyUnicode_AsString и PyUnicode_FromString. Немного зная родное для Python API для чистого Си (если не знаем, то читая документацию) понимаем, что это и есть корень всех зол. Boost.Python отлично различает Python 2 и 3 версий, но понять самостоятельно что строку юникода нужно преобразовать в строку закодированную кодовой страницей файловой системы он не может, однако предоставляет для этого альтернативные функции, которые предлагается использовать самостоятельно:

PyUnicode_DecodeFSDefault — перекодирует строку в кодировке файловой системы (в нашем случае это как раз Windows-1251) и возвращает уже готовый объект строки, отлично подходит вместо PyUnicode_FromString в {BoostDir}\libs\python\src\ в файлах str.cpp и converter\builtin_converters.cpp.

PyUnicode_DecodeFSDefaultAndSize – то же самое, но с указанием размера строки. Подходит в качестве замены аналогичной PyUnicode_FromStringAndSize в тех же файлах.

PyUnicode_EncodeFSDefault — наоборот принимает объект строки из питона и перекодирует, возвращает результат в виде массива байт (объекта PyBytes), из массива байт уже после этого можно вытянуть обычную сишную строку функцией PyBytes_AsString. Требуется для обратного преобразования вместо функции PyUnicode_AsUTF8String, а в паре
PyBytes_AsString( PyUnicode_EncodeFSDefault(obj) ) заменяют макрос _PyUnicode_AsString( obj ) делающий фактически то же, но без переконвертации.

Итак, мы вооружены до зубов, знаем врага в лицо, осталось только его найти и обезвредить!

Нам нужны файлы использующие PyUnicode_* в коде {BoostDir}\libs\python\src\ и заголовочных файлах внутри {BoostDir}\boost\python\, кроме того, открою сразу тайну, нам потребуется ещё поправить исключения в файле error.cpp.

В общем список следующий:
builtin_converters.cpp — правим преобразования строк из Python в С++ и обратно
builtin_converters.hpp — надо поправить макрос преобразования в заголовочном файле
str.cpp — правим обёртку в C++ над классом Python str (обычная строка питона в C++).
errors.cpp – правим передачу текста исключения из C++ в Python

Изменений немного, они точечные, все перечислены ниже, в архиве приложенном к статье лежат патчи и отчёты об изменениях, как правило все изменения не превышают одной строки кода, чаще даже одной инструкции вызова, суммарно их ровно 13 на 4 файла. Вы ведь не суеверны, нет?..

После всех правок собираем только Boost.Python уже упомянутой выше командой:
b2 --with-python --build-type=complete
(Добавьте обязательно address-model=64 если сборка для x64, т. е. и ваш проект, и Python 3.x установленный на вашей машине собраны под 64-разрядную архитектуру адресации.)

После того как Boost.Python собран, соберите заново свой проект с обновлённой библиотекой, обновились не только .lib и .dll, но и один заголовочный файл.
Не забудьте подменить старый и унылые .dll на свежесобранные. Вы ведь наверняка не забудете их скопировать, так ведь?!

Момент истины!

import my
res = my.Search.That( 'Это Я!' )
print( res )

Всё тот же код теперь возвращает то, что и ожидалось: строку 'Я'.
Вполне себе кириллица, очень даже Юникод, если Python 3 считает этот объект строкой!
Теперь проверим как там придёт наше исключение:
import my
res = my.Search.That( 'Это Я!' )
print( res )
try:
    my.Search.That( 'Это кто-то другой!' )
except RuntimeError as e:
    print( e )

Наше исключение приходит замечательно, с нужным текстом, в виде RuntimeError — стандартного исключения Python.
Бонусом мы получили то, что на стороне C++ создавая объекты boost::python::str мы их сразу конвертируем в Юникод, что очень поможет нам когда мы на стороне C++ захотим какой-нибудь аттрибут объекта Python названного кириллицей:
object my = import( "my" );
object func = my.attr( str("Функция") )
int res = extract<int>( func( x * x ) );

Теперь в MS Visual C++ не будет никаких проблем с таким кодом. Всё отлично выцепится, позовётся и вернёт всё что надо.
Ну и раз уж речь зашла о вызове из C++ кода на Python, стоит упомянуть о том, как ловить оттуда исключения.
Все исключения из Python на уровне C++ будут ловиться типом error_already_set& всё из того же boost::python. Выцепить текст, тип и стэк исключения не представляется сложным и подробно описано вот здесь: wiki.python.org/moin/boost.python/EmbeddingPython — раздел Extracting Python Exceptions. В подавляющем большинстве случаев ничего большего нежели забрать текст исключения и не ��онадобится, если конечно вы не придумали своей специфической логики исключений. Но в этом случае вам лучше написать свой транслятор исключений, а это уже совсем другая история…

Итого

Мы подружили родной код MS Visual C++ с обычным кодом Python с помощью небольшого патча Boost.Python, фактически не меняя код, просто подменив в нескольких местах вызов одних функций API питона на другие, выполняющие дополнительную перекодировку. Поскольку всё сделано через API самого Python, он сам позаботится о памяти выделяемой для объектов, никаких std::string и прочих ужасов обращения к Heap через замечательные мьютексы, которые Microsoft так старательно заложила в механизм new своей стандартной библиотеки. Нет! Ничего такого! Всё за нас сделает Python, нам лишь надо было ему немного помочь.
Простые смертные всё так же могут писать код в Visual Studio не задумываясь о кодировках. Возможно даже и не зная о них. В принципе узкому специалисту в области той же транспортной части (протоколы, пакеты данных и т. п.) знать об этом не так уж и обязательно.
Особо пытливые могут замерить потери на перекодировке, они разумеется есть. По моим замерам, они настолько незначительные, что однажды переписав код очень медленной генерации веб-страницы с C++ на один join+format в Python ускорил его почти на 10%. Это с учётом перекодировки с вышеприведёнными правками. Соответственно можете представить незначительность подобных потерь, если в коде на C++ просто собиралась достаточно большая строка (даже с предварительным reserve).
По стабильности, уже полгода минимум как оболочка построенная на данных изменениях благополуч��о крутится на рабочих сайтах (правда версии Boost намного старше текущей). На сегодняшний день всё перекодируется стабильно, нареканий не вызывает, и не вызывало.

Обещанный архив с изменениями

Здесь собраны отчёты и патчи по изменениям в файлах библиотеки Boost.Python:
www.2shared.com/file/NFvkxMzL/habr_py3_cxx.html

Также прилагается бонусом маленький архив с тестовым проектом (собран под x64):
www.2shared.com/file/FRboyHQv/pywrap.html

Ссылки на полезное

Ссылка на документацию Python 3. Раздел Си-API перекодировки из кодовой страницы файловой системы и обратно:
docs.python.org/3/c-api/unicode.html?highlight=pyunicode#file-system-encoding

Ссылка на документацию Boost.Python:
www.boost.org/doc/libs/1_52_0/libs/python/doc