пятница, 31 августа 2012 г.

UTF-16 to UTF-8

 В статье речь пойдет о кодировках и об их преобразовании 


 Юрий «yurembo» Язев
ведущий программист компании GenomeGames

Суть дела такова: есть игровой движок Torque, имеющий порты под все распространенные операционные системы, как то: Windows NT, Linux, Mac OS X. С другой стороны есть сервер, работающий под ОС Windows 7, на нем крутится БД MS SQL Server 2008 R2.
При отображении движком текстовых данных извлеченных из БД, эти данные выводились иероглифами. Очевидно, что проблема в кодировках. Так и есть: названные выше программные системы работают с разными кодировками:

1)      Windows NT kernel mode использует UTF-16
2)      Windows user mode обрабатывает UTF-16 и win-1251
3)      SQL Server хранит текстовые данные в кодировке UCS-2, это та же UTF-16
4)      Torque работает только с UTF-8

Из-за наличия у Torque портов, он напрямую не совместим со стандартной кодировкой Windows NT. Данная статья – не место для обзора различий и преимуществ каждой из кодировок (гугл в помощь). Поэтому посмотрим, как можно их преобразовать.
Первое время, когда в БД хранилось довольно-таки небольшое количество данных, с помощью Notepad++ я преобразовывал их в формат UTF-8, в результате чего из них получались крокозябры, и в таком виде вставлял в БД. После извлечения и обработки их в движке, в игре они выводились в виде читабельного русского текста. Для преобразования я использовал следующий код на C++ (приведены только строки, относящиеся к преобразованию данных):

String weaponClass;
_variant_t vWeaponClass;//класс оружия на русском
vWeaponClass = pRecordset->GetCollect("ClassRusName");//получаем данные из БД
_bstr_t WeaponClass = _bstr_t(vWeaponClass),//конвертируем variant в BSTR
weaponClass = _com_util::ConvertBSTRToString(WeaponClass);//преобразуем BSTR в
//String
..
string weap= objName + ' ' + type + ' ' + weaponClass;//собираем строку
Con::executef("UpdateWeaponData", clientID.c_str(), weap.c_str());//передаем данные в
//скриптовый код, для вывода

Впоследствии, по мере развития проекта, количество данных в базе данных стало возрастать по экспоненциальной прогрессии. Поэтому хранить текст в иероглифах стало невозможно. Тогда я вновь задумался над решением проблемы. Мной было испробовано несколько решений, в том числе имеющая win-порт юниксовая утилита iconv. Однако, мало того, что установка под Windows последней содержит в себе изрядную долю гемора, поскольку компилировать объектные файлы надо самостоятельно через компилятор командной строки, так еще она не оправдала возложенных на нее надежд.
Как я рассказывал ранее, чтобы получить на выходе нормальный русский текст, надо его заранее преобразовать и хранить в БД иероглифы. Тогда я задумался: если из БД я получаю UTF-16, тогда variant тоже хранит UTF-16, следовательно, BSTR тоже - UTF-16.
Значит, все обламывается на следующем шаге – при преобразование в String.
Тут я погуглил интернет в поисках алгоритма преобразования UTF 16 -> UTF8, но не один из них мне не понравился: все какие-то громоздкие :) .
Тут меня осенила идея: воспользоваться типом wchar_t в качестве посредника при преобразовании. Так я и сделал: заменил прямое преобразование из BSTR в String (функция COM ConvertBSTRToString) таким кодом:

const size_t newsize1 = (WeaponClass.length());
wchar_t *wc = new wchar_t[newsize1];
wcscpy(wc,WeaponClass);
weaponClass = wc;

В результате вышеприведенный код принял такой вид:

String weaponClass;
_variant_t vWeaponClass;//класс оружия на русском
vWeaponClass = pRecordset->GetCollect("ClassRusName");//получаем данные из БД
_bstr_t WeaponClass = _bstr_t(vWeaponClass),//конвертируем variant в BSTR
//weaponClass= _com_util::ConvertBSTRToString(WeaponClass);//старый код для 
//преобразования BSTR в String

const size_t newsize1 = (WeaponClass.length());//новый код
wchar_t *wc = new wchar_t[newsize1];//для преобразования
wcscpy(wc,WeaponClass);//BSTR в широкие символы
weaponClass = wc;//присваиваем значение переменной широких символов переменной
//String
string weap= objName + ' ' + type + ' ' + weaponClass;//собираем строку из элементов,
//кстати два типа String и string различают тем, что у последнего в отличие от первого
//есть перегруженный оператор +

Con::executef("UpdateWeaponData",clientID.c_str(), weap.c_str());//передаем данные в
//скриптовый код

Этот код прекрасно компилируется, но содержит ошибку времени исполнения: утечка памяти! Изначально, я забыл о том, что строка завершается конечным нулем - '0', поэтому во время преобразования BSTR в wide char буфер приемника на 1 символ меньше буфера источника:

wcscpy(wc, WeaponClass);

я воспользовался безопасной версией приведенной функции:

wcscpy_s(wc, newsize1, WeaponClass);
 
дебаггер сразу сообщил мне что буфер мал!
тогда код преобразования я исправил следующим образом:

const size_t newsize1 = (WeaponClass.length()+1);//+1 – конечный ноль
wchar_t *wc = new wchar_t[newsize1]; 
wcscpy_s(wc, newsize1, WeaponClass); 
weaponClass = wc;

Теперь, код не только успешно компилируется, но и прекрасно работает!

P.S. Данное решение изначально было написано мною на английском и выложено на форум сайта компании GarageGames – разработчика движка Torque.