Автор: mabu (Корпорация «Пакетные файлы»)

Опубликовано:

Исправлено:

Версия документа: 1.0

Юникод

Разрабатывая приложение, ты определённо должен использовать преимущества юникода. Даже если ты пока не собираешься локализовать программный продукт, разработка с прицелом на юникод упростит эту задачу в будущем. Юникод также позволяет:

FreeBASIC и кодировки

В чём проблема?

Поздней ночью ты садишься за компьютер, создаёшь пустой файл HelloWorld.bas и пишешь в нём хрестоматийный код:

Dim s As String = "Всем привет!"
Print s

С чистой совестью ты компилируешь программу, запускаешь, но вместо желанного текста «Привет, мир!» получаешь кракозябры:

┬ёхь яЁштхЄ!

Так происходит из‐за конфликта кодировок символьных данных:

Вот несколько способов неправильного решения проблемы:

Эти проблемы полностью решает отказ от вывода строк в ANSI кодировках и переход на юникод.

FreeBASIC и юникод

Типы данных ZString и WString

В язык FreeBASIC введены новые типы данных: ZString и WString.

ZString
Стандартный тип данных, содержащий 8‐битные символы, ANSI‐строки
WString
Стандартный тип данных, содержащий широкий (юникодный) символ, Wide‐строки

Для обычных символьных данных — символов ANSI — в языке FreeBASIC используется тип ZString; переменная этого типа занимает в памяти один байт. Для хранения символов юникода необходимо больше байт, из-за чего их называют широкими символами (Wide Characters). В языке FreeBASIC широким символам соответствует тип WString.

Тип данных String

Тип данных для строк, пришедший из QuickBasic. Является удобной объектной оболочкой для ZString с автоматическим управлением памятью. По своей природе String могут содержать только ANSI‐строки.

Выделение памяти для строки

Если ты хочешь, скажем, создать буфер для хранения юникодной строки длиной до 99 символов с нулевым символом в конце, сделай так:

Dim wszBuffer As WString * (99 + 1)

Эта конструкция создаст строку известного на этапе компиляции размера из ста значений WString: 99 для данных и 1 для нулевого символа. Мы могли бы сразу написать «100», однако запись «99 + 1» нагляднее показывает сколько компилятор выделит памяти под значимые символы и сколько под нулевой символ.

Если длина строки будет известна только во время выполнения программы или когда строка очень длинная, то её создают динамически:

' В переменную nCharacters записываем количество символов в строке
Dim nCharacters As Integer = 512

' Выделяем память
Dim pwszBuffer As WString Ptr = Allocate((nCharacters + 1) * SizeOf(WString))

' Выделенная память не инициализируется нулями
' Необходимо вручную установить нулевой символ
pwszBuffer[0] = 0

Функция Allocate выделяет память в байтах, а строка работает с символами, поэтому для расчёта точного количества байт под строку вместо Allocate(nCharacters) необходимо использовать формулу Allocate((nCharacters + 1) * SizeOf(WString)).

Правило вычисления количества байт для строки запомнить труднее всего. Если ты ошибёшься, то и компилятор не выдаст предупреждений, и программа будет работать неправильно.

Строковые литералы

Используя WString можно заполнять строки данными:

' Для строки с фиксированной длиной
wszBuffer = "Error"

' Для указателя на WString
*pwszBuffer = "Error"

Правда, в этом операторе есть проблема. По умолчанию FreeBASIC транслирует строковые литералы в зависимости от кодировки исходного файла. Если кодировка ANSI, то и литерал будет состоять из символов ANSI, а не из широких символов, присваивание широкой строке ANSI‐строки приведёт к неявной конвертации символов.

(Замечание: для строк фиксированной длины компилятор сам следит чтобы буфер под строку не переполнился. Для динамических строк ты должен следить за переполнением самостоятельно.)

Создание литерала с широкими символами через функцию WStr

Чтобы литерал состоял из широких символов, необходимо обернуть его в функцию WStr:

' Для строк фиксированной длины
wszBuffer = WStr("Error")

' Для динамической строки
*pwszBuffer = WStr("Error")

Если в литерале встречаются символы с кодами больше 127 (например, кириллица), компилятор может их неправильно распознать. Мы можем явно указать компилятору коды символов через экранированную строку. Для этого перед литералом ставим восклицательный знак:

' «Текст строки»
Const SOME_STRING = WStr(!"\u0422\u0435\u043A\u0441\u0442 \u0441\u0442\u0440\u043E\u043A\u0438")

Здесь могут встречаться последовательности:

\uXXXX
шестнадцатеричный код символа
\&hXX
шестнадцатеричный код символа
\nnn
десятичный код символа

Коды нужных символов можно смотреть через утилиту charmap.exe.

Создание литерала с широкими символами через кодировку исходного файла

Если ты сохранишь файл в одной из юникодных кодировок: UTF-8 с BOM, UTF-16 LE, UTF-16 BE, то все строковые литералы станут широкими. Тогда компилятор будет правильно распознавать символы с кодами больше 127:

' «Текст строки»
Const SOME_STRING = "Текст строки"

Однако не все редакторы умеют работать с файлами исходного кода сохранёнными в юникодных кодировках. Например, FBEdit считает кодировку UTF-8 как ANSI и выдаёт кракозябры, но может читать файлы в кодировке UTF-16.

Создание литерала с ANSI‐символами

Иногда требуется получить литерал только с ANSI‐символами независимо от юникодности исходного кода. Для этого литерал оборачивают в функцию Str:

Const SOME_STRING = Str("ANSI")

Но в этом случае может произойти потеря информации при конвертации кодов символов с кодами больше 127.

Функции стандартной библиотеки работающие со строками

Эти стандартные функции работают с ANSI‐ и широкими строками:

Эти стандартные функции работают только с ANSI‐строками:

Эти стандартные функции работают только с широкими строками:

Юникод и библиотека языка Си

Типы данных char и wchar_t

В языке Си существуют типы данных: char и wchar_t.

char
Стандартный тип данных, содержащий 8‐битные символы, ANSI‐строки
wchar_t
Стандартный тип данных, содержащий широкий (юникодный) символ, Wide‐строки

Функции работающие со строками

Эти стандартные функции работают только с ANSI‐строками:

char * strcat(char *, const char *);
char * strchr(const char *, int);
int strcmp(const char *, const char *);
char * strcpy(char *, const char *);
size_t strlen(const char *);

Эти стандартные функции работают только с широкими строками:

wchar_t * wcscat(wchar_t *, const wchar t *);
wchar_t * wcschr(const wchar_t *, wchar_t);
wchar_t * wcscpy(wchar_t *, const wchar_t *);
int wcscmp(const wchar_t *, const wchar_t *);
size_t wcslen(const wchar_t *);

Имена всех новых функций начинаются с wcs — это аббревиатура «wide character set» (набор широких символов). Таким образом, имена юникодных функций образуются простой заменой префикса str соответствующих ANSI‐функций на wcs.

Дефиниция _UNICODE

Макрос _UNICODE используется в заголовочных файлах библиотек Си.

Юникод и WinAPI

Под WinAPI можно писать в трёх вариантах: неюникодном, юникодном и обобщённом. Чтобы реализовать возможность компиляции обобщённого варианта:

Строковые типы данных

Для символов ANSI используется тип CHAR:

Type CHAR As Byte
Type PCHAR As CHAR Ptr

ANSI‐строки:

Type LPSTR As ZString Ptr
Type PSTR As ZString Ptr

Константные ANSI‐строки:

Type LPCSTR As Const ZString Ptr
Type PCSTR As Const ZString Ptr

Широким символам соответствует тип WCHAR:

Type WCHAR As wchar_t
Type PWCHAR As WCHAR Ptr

Широкие строки:

Type LPWSTR As WString Ptr
Type PWSTR As WString Ptr

Константные широкие строки:

Type LPCWSTR As Const WString Ptr
Type PCWSTR As Const WString Ptr

Дефиниция UNICODE

Макрос UNICODE используется в заголовочных файлах Windows. Этот макрос включает использование широких символов.

Указываем, что будем использовать юникодные версии строковых функций:

#define UNICODE
#include once "windows.bi"

Но лучше передавать дефиницию в параметрах компилятора:

fbc -d UNICODE file.bas

Обобщённые типы данных

В заголовочных файлах Windows есть обобщённые типы данных для символов и строк. Посмотрим, как некоторые из них раскрываются в зависимости от дефиниции UNICODE:

Тип данных UNICODE определена UNICODE не определена Описание
TCHAR WCHAR Byte Символ
PTCHAR WCHAR Ptr Byte Ptr Указатель на символ
TBYTE WCHAR UByte Символ
PTBYTE WCHAR Ptr UByte Ptr Указатель на символ
PTSTR, LPTSTR LPWSTR LPSTR Указатель на строку
PCTSTR, LPCTSTR LPCWSTR LPCSTR Указатель на константную строку

Префиксы переменных

Ты также можешь встречать префиксы у переменных:

Префикс Описание
sz ANSI‐строка с нулевым символом
psz, lpsz указатель на ANSI‐строку с нулевым символом
pcsz, lpcsz указатель на сонстантную ANSI‐строку с нулевым символом
wsz широкая строка с нулевым символом
pwsz, lpwsz указатель на широкую строку с нулевым символом
pcwsz, lpcwsz указатель на константную широкую строку с нулевым символом
tsz обобщённая строка с нулевым символом
ptsz, lptsz указатель на обобщённую строку с нулевым символом
pctsz, lpctsz указатель на константную обобщённую строку с нулевым символом

Префиксы P и LP означают указатели. Разница между ними объясняется исторически: в 16‐битном мире P — короткий указатель, LP — длинный. В настоящее время эти указатели равнозначны.

Префиксы не являются абсолютной истиной, например, поле lpszClassName в структуре WNDCLASSEX может ссылаться как на ANSI, так и на широкую строку.

Строковые константы

Текст литерала обрамляем макросом __TEXT:

Const SOME_STRING = __TEXT("String Text")

Строки символов

Буфер для хранения символов делаем массивом обобщённого типа TCHAR:

' Буфер
Dim tszString(0 To 265) As TCHAR

' Указатель на первый символ в буфере
Dim ptszString As LPTSTR = @tszString(0)

Для вывода на консоль приводим указатель к LPTSTR и разыменовываем:

Print *CPtr(LPTSTR, @tszString(0))

Динамические строки символов

Указатели на строки символов делаем обобщённым типом LPTSTR или LPCTSTR:

' Длина строки
Dim StringLength As Integer = 265
' Выделяем память для строки и нулевого символа
Dim ptszString As LPTSTR = Allocate(SizeOf(TCHAR) * (StringLength + 1))

Ресурсы

Строки, которые требуют локализации, следует выносить в ресурсы программы.

Строки в ресурсах (таблицы строк, шаблоны диалоговых окон, меню и др.) всегда записываются в юникоде. Такой файл рекомендуют сохранять в одной из юникодных кодировок: UTF-8 с BOM или UTF-16 LE.

#define IDS_CAPTION    20001
#define IDS_HELLOWORLD 20002

STRINGTABLE LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
BEGIN
	IDS_CAPTION    "Hello world program"
	IDS_HELLOWORLD "Hello world"
END

STRINGTABLE LANGUAGE LANG_RUSSIAN, SUBLANG_NEUTRAL
BEGIN
	IDS_CAPTION    "Программа Hello world"
	IDS_HELLOWORLD "Здравствуй, мир"
END

Извлекаем строку из ресурсов функцией LoadString:

Dim StringLength As Integer = 265
Dim ptszString As LPTSTR = Allocate((StringLength + 1) * SizeOf(TCHAR))

Dim ret As Long = LoadString( _
	GetModuleHandle(NULL), _
	IDS_CAPTION, _
	ptszString, _
	StringLength _
)

Print *tszString

Юникод и COM

Все методы СОМ‐интерфейсов, работающие со строками, должны принимать только строки типа BSTR, которые содержат только символы типа OLECHAR.

OLECHAR
псевдоним для WCHAR

Для инициализации COM‐строки можно использовать WString, так как она совместима с OLECHAR:

Dim wszString As WString * (15 + 1)
Dim b As BSTR = SysAllocString(StrPtr(wszString))

Второй вариант — использовать массив OLECHAR:

Dim oleString(15) As OLECHAR
Dim b As BSTR = SysAllocString(@oleString(0))

DLL и строки

Если ты пишешь динамически подключаемую библиотеку работающую со строками, то тебе просто необходимо поступать так, как делает это Windows: экспортировать два вида функций, первые будут принимать ANSI‐строки, вторые широкие строки.

Например, мы создаём функцию FindCrLfIndex, которая будет искать в строке символы CrLf. Для этого мы определяем две функции FindCrLfIndexA и FindCrLfIndexW, принимающие ANSI‐строки и широкие строки соответственно:

#include once "windows.bi"

Declare Function FindCrLfIndexA Alias "FindCrLfIndexA"( _
	ByVal pszBuffer As LPCSTR, _
	ByVal pFindIndex As Long Ptr _
)As WINBOOL

Declare Function FindCrLfIndexW Alias "FindCrLfIndexW"( _
	ByVal pwszBuffer As LPCWSTR, _
	ByVal pFindIndex As Long Ptr _
)As WINBOOL

Ключевое слово Alias здесь обязательно, иначе FreeBASIC переведёт название функции в верхний регистр вроде «FINDCRLFINDEXA».

Затем мы создаём макрос FindCrLfIndex, раскрывающийся в вызов нужной функции в зависимости от установки UNICODE:

#ifdef UNICODE
Declare Function FindCrLfIndex Alias "FindCrLfIndexW"( _
	ByVal pwszBuffer As LPCWSTR, _
	ByVal pFindIndex As Long Ptr _
)As WINBOOL
#else
Declare Function FindCrLfIndex Alias "FindCrLfIndexA"( _
	ByVal pszBuffer As LPCSTR, _
	ByVal pFindIndex As Long Ptr _
)As WINBOOL
#endif

В ANSI-версии функции просто выделяй память, преобразуй ASNI‐ в широкую строку функцией MultiBytoToWideChar и вызывай Unicode-версию той же функции.