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

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

Исправлено:

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

Юникод

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

FreeBASIC и кодировки

Кракозябры в консоли

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

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

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

C:\FreeBASIC\Projects>HelloWorld.exe
┬ёхь яЁштхЄ!

C:\FreeBASIC\Projects>_

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

Неправильные решения

Сразу перечислим неправильные способы решения проблемы:

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

FreeBASIC и строки

Начнём с того, какие вообще типы символьных данных существуют в языке.

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

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

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

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

Тип данных String

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

Type String
	data As ZString Ptr
	len As Integer
	size As Integer
End Type

По своей природе String могут содержать только ANSI‐строки, в этом случае они ничем не отличаются от ZString.

FreeBASIC и юникод

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

Строка с фиксированной ёмкостью

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

Const BufferCapacity As Integer = 99
Dim wszBuffer As WString * (BufferCapacity + 1)

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

Длина такой строки будет известна компилятору, поэтому он может отслеживать выход за границы буфера.

Строка с переменной ёмкостью

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

' В переменной nCharacters хранится количество символов в строке
Dim nCharacters As Integer = 511

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

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

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

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

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

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

' Для строки с фиксированной ёмкостью
wszBuffer = "Error"

' Для строки с динамической ёмкостью
*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:

' ANSI-строка
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 не определенаОписание
TCHARWCHARByteСимвол
PTCHARWCHAR PtrByte PtrУказатель на символ
TBYTEWCHARUByteСимвол
PTBYTEWCHAR PtrUByte PtrУказатель на символ
PTSTR, LPTSTRLPWSTRLPSTRУказатель на строку
PCTSTR, LPCTSTRLPCWSTRLPCSTRУказатель на константную строку

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

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

ПрефиксОписание
szANSI‐строка с нулевым символом
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-версию той же функции.