Опубликовано:
Исправлено:
Версия документа: 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 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-версию той же функции.