Опубликовано:
Исправлено:
Версия документа: 1
Юникод
Разрабатывая приложение, ты определённо должен использовать преимущества юникода. Даже если ты пока не собираешься локализовать программный продукт, разработка с прицелом на юникод упростит эту задачу в будущем. Юникод также позволяет:
- легко обмениваться данными на разных языках;
- распространять единственный двоичный EXE‐ или DLL‐файл, поддерживающий все языки;
- увеличить эффективность приложений (об этом мы поговорим чуть позже).
FreeBASIC и кодировки
Кракозябры в консоли
Поздней ночью ты садишься за компьютер, создаёшь пустой файл HelloWorld.bas и пишешь в нём хрестоматийный код:
Dim s As String = "Всем привет!"
Print sС чистой совестью ты компилируешь программу, запускаешь, но вместо желанного текста «Привет, мир!» получаешь кракозябры:
C:\FreeBASIC\Projects>HelloWorld.exe ┬ёхь яЁштхЄ! C:\FreeBASIC\Projects>_
Так происходит из‐за конфликта кодировок символьных данных:
- компилятор считает, что символьные данные в файле исходного кода сохранены в одной кодировке (например, 1251 или 1252), а ты сохраняешь их в другой (например, 866);
- при использовании ANSI‐функций WinAPI ты отдаёшь данные в одной кодировке (866), а функции WinAPI ожидают данные в другой кодировке (1251).
Неправильные решения
Сразу перечислим неправильные способы решения проблемы:
- набрать исходный текст в «правильной» кодировке 866;
- переключить кодовую страницу консоли командой
chcp 1251; - сменить кодировку функциями SetConsoleCP(1251) и SetConsoleOutputCP(1251);
- сменить шрифт консоли на какой‐нибудь Lucida Console или собственного производства;
- перед выводом на экран перекодировать строки «на лету» функцией CharToOem или какой-либо собственной.
Эти проблемы полностью решает отказ от вывода строк в 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‐ и широкими строками:
- InStr
- LCase
- Left
- Len
- LSet
- LTrim
- Mid
- Right
- RSet
- RTrim
- Trim
- UCase
- Val
- VALLNG
- VALINT
- VALUINT
- VALULNG
Эти стандартные функции работают только с ANSI‐строками:
- String
- Space
- BIN
- CHR
- HEX
- OCT
- STR
- INPUT
Эти стандартные функции работают только с широкими строками:
- WSpace
- WString
- WBIN
- WCHR
- WHEX
- WOCT
- WSTR
- WINPUT
Юникод и библиотека языка Си
Типы данных 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 можно писать в трёх вариантах: неюникодном, юникодном и обобщённом. Чтобы реализовать возможность компиляции обобщённого варианта:
- буфер под строки символов объявляются как массив
TCHAR; - указатель на строку как
LPTSTR; - строковые литералы оборачиваются в макрос
__TEXT.
Строковые типы данных
Для символов ANSI используется тип CHAR:
Type CHAR As Byte
Type PCHAR As CHAR PtrANSI‐строки:
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-версию той же функции.