четверг, 3 сентября 2009 г.

PChars: сами строки не включены

Это перевод PChars: no strings attached. Автор: Rudy Velthuis.
The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information. — Alan Perlis
В общедоступных newsgroup-ах на сервере Embarcadero я часто вижу, что по-прежнему есть много проблем с пониманием как типа PChar, так и типа String. В этой статье я хотел бы обсудить общие моменты и различия между обоими типами, а также вещи, которые вы можете и которые вы не должны делать с ними.

Общие принципы, описанные в этой статье, применимы ко всем Win32 версиям Delphi, включая Delphi 2009 и выше. В конце, однако, есть специальный раздел для тех, кто использует Delphi 2009 и выше.
Примечание: поскольку PChar является указателем, а String - ссылочным типом данных, то первым делом вам нужно бы разобраться с понятием указателя.

Содержание


to topPChar

Trying to outsmart a compiler defeats much of the purpose of using one. — Kernighan and Plauger, The Elements of Programming Style.
Идея типа PChar взята из строк языка C. Большинство функций Windows API имеют интерфейс в стиле C и принимают строки в стиле C. Чтобы можно было использовать эти API функции, Borland пришлось ввести тип, который бы подражал им, ещё в предшественнике Delphi: Turbo Pascal-е.

В C на самом деле не существует специального типа для строк, как это имеет место в Delphi. Строки в C – это просто массивы символов, а конец текста отмечается символом, ASCII код которого равен 0. Это позволяет строкам быть большими (по сравнению со строками в Turbo Pascal-ле, где они были ограничены 255 символами из-за счётчика длины в виде байта – теперь это тип ShortString в Delphi), но несколько неудобными в использовании. Начало массива просто отмечается указателем на символ, который и стал определением типа в PChar в Delphi. Чтобы пройтись по строке в C, вы должны использовать этот указатель, как указатель на массив символов (вообще, это общее правило для всех указателей в C), и использовать s[20] для указания 21-го символа (отсчёт начинается с 0). Но арифметика указателей в C позволяет делать не только инкременты и декременты, но также допускает сложение указателя с числом или вычисление разницы между двумя указателями. В C, *(s + 20) эквивалентно s[20] (* в C является оператором разыменования, это аналог ^ в Delphi). Для типа PChar Borland сделала возможным практически тот же самый синтаксис.

Итак, PChar – это просто указатель, ровно как в C. И, снова как и в C, вы можете использовать его, как если бы это был массив (т.е. указатель, указывающий на первый элемент массива). Но на самом деле он им не является! Тип PChar не имеет автоматически управляемого хранилища данных, как это есть у обычных строк в Delphi. Если вы копируете текст в PChar-"строку", вы должны всегда быть уверенными, что этот ваш PChar действительно указывает на допустимый массив символов, и что массив достаточно велик, чтобы вместить весь текст.
var
  S: PChar;
begin
  S[0] := 'D';
  S[1] := '6';
Код выше не выделяет никакого хранилища для строки, поэтому он пытается сохранить символы в какое-то случайное место в памяти (адрес S неопределён и содержит какой-то мусор, см. мою статью про указатели). Это вызовет проблемы или даже вылет программы. Это ваша ответственность за гарантию наличия массива (прим. пер.: для типа String в Delphi ответственность за хранилище лежит на библиотеке поддержки языка Delphi). Простейший способ сделать это – использовать локальный массив:
var
  S: PChar;
  A: array[0..100] of Char;
begin
  S := A;
  S[0] := 'D'; // эквивалентно A[0] := 'D';
  S[1] := '6'; // вы могли бы также написать: (S + 1)^ := '6';
Код выше записывает символы в массив. Но если вы попробуете показать строку S на экране, вы, вероятно, увидите много другого мусора или даже вылет программы (*). Это потому что мы не завершили нашу строку символом #0. OK, тогда мы можем добавить ещё одну строку:
S[2] := #0; // или: (S + 2)^ := #0;
и тогда при выводе S вы получите текст "D6". Но записывать символы по-одному – это весьма неудобно. Чтобы показать текст через PChar, можно поступить проще: вы просто указываете PChar-ом на уже готовый массив символов с текстом в нём. К счастью, строковые константы типа 'Delphi' также являются такими массивами, поэтому они могут быть использованы как PChar:
var
  S: PChar;
begin
  S := 'Delphi';
Вам только следует понимать, что код выше просто меняет указатель S. Сам текст никуда не копируется и не перемещается. Текст строковых констант хранится где-то в программе (и имеет терминатор #0), а S теперь указывает на его начало – вот и всё. Если вы сделаете:
// ПРЕДУПРЕЖДЕНИЕ: ПЛОХОЙ ПРИМЕР
var
  S: PChar;
  A: array[0..100] of Char;
begin
  S := A;
  S := 'Delphi';
это не скопирует текст 'Delphi' в массив A. Первая строка после begin нацеливает указатель S на массив A, но тут же мы, во второй строке, изменяем S так, что он теперь указывает на строковую константу. Если вы хотите скопировать текст в массив, вам нужно указать это явно, используя, например, StrCopy или StrLCopy:
var
  S: PChar;
  A: array[0..100] of Char;
begin
  S := A;
  StrCopy(S, 'Delphi');
или
StrLCopy(S, 'Delphi', Length(S));
В конкретно этом случае нам очевидно, что строка 'Delphi' влезет в массив (101 символ как-никак), так что использование StrLCopy выглядит немного перебором, но в других случаях, когда вы не знаете наперёд размера строки, вы должны использовать StrLCopy для избежания переполнения буфера (да, ТОГО самого переполнения буфера – прим.пер.).

Массив типа A полезен как буфер для небольших строк или строк, ограниченных сверху (т.е. для которых известен максимальный потенциальный размер), но часто у вас будут строки, размер которых вам неизвестен во время компиляции. В этом случае вам нужно использовать динамическое выделение буфера для текста. Вы можете использовать, например, StrAlloc или StrNew для создания буфера, или же GetMem (а также динамические массивы или даже строки – прим.пер.), но тогда вы должны не забывать освобождать память, когда она вам станет не нужна, используя StrDispose или FreeMem. Вы также можете использовать тип String в Delphi в качестве буфера, но, прежде чем я опишу, как это сделать, я бы сперва хотел обсудить этот тип.

to topString

A world without string is chaos — Randolf Smuntz, Mouse Hunt
Позвольте мне вас запутать: String или, более точно, AnsiString (в Delphi 2009 и выше: UnicodeString) фактически является PChar-ом. Точно так же, как и PChar, строка представляет собой указатель на массив символов, заканчивающихся символом #0. Но есть одно большое отличие: обычно вам не нужно думать, как работают строки. Их можно использовать не задумываясь, почти как любую другую переменную. Компилятор сам заботится о вызове кода для выделения, копирования и освобождения текста строк. Поэтому вместо ручного вызова подпрограмм типа StrCopy, вы просто позволяете компилятору сделать это за вас.

Но это ещё не всё. Хотя текст, несомненно, всегда заканчивается символом #0 – сделано это только для того, чтобы сделать строки Delphi совместимыми со строками C, сам компилятор не нуждается в терминаторе. Перед текстом строки в памяти, по отрицательному смещению указателя, хранится длина строки, как число Integer. Так что, чтобы узнать длину строки, компилятор просто читает этот Integer, экономя на поиске первого #0 в строке. Это означает, что вы можете хранить и сам символ #0 в середине строки, и это будет работать. Но некоторые подпрограммы, которые работают с терминатором, воспримут только часть строки.

Обычно, каждый раз, когда вы присваиваете одну строку другой, компилятору надо бы выделять память и копировать текст из одной переменной в другую. Поскольку строки в Delphi могут быть очень большими (теоретически до 2 Гб максимум в 32-х разрядных приложениях), это может быть весьма медленно. Чтобы избежать лишнего копирования, Delphi использует концепцию, которая известна под названием "копирование по требованию" ("copy on demand"). Каждая строка имеет, помимо длины, и другое служебное поле: счётчик ссылок (reference count). Он содержит количество строковых переменных, которые ссылаются на конкретно эту строку в памяти. Если счётчик опускается до 0, то это значит, что на текст строки никто больше не ссылается, и он может быть удалён из памяти.

Компилятор гарантирует вам, что счётчик ссылок всегда будет содержать правильное значение (но вы можете и обмануть компилятор: приведениями типов – подробнее об этом ниже). Если строка объявлена в секции var или как поле класса или записи, она начнёт свою жизнь как nil – внутреннее представление пустой строки ('').  Когда текст строки только создаётся и присваивается переменной, счётчик ссылок становится равным 1. Каждое дополнительное присваивание этой строки другой переменной будет увеличивать счётчик ссылок (никаких данных при этом не копируется). Если строковая переменная покидает область видимости (когда заканчивается функция или удаляется объект), или же ей присваивают другую строку, то счётчик ссылок уменьшается.

Простой пример:
function PlayWithStrings: String;
var
  S1, S2: String;
begin
  S1 := IntToStr(123456);
Теперь S1 указывает на текст '123456' и имеет счётчик ссылок равный 1.
S2 := S1;
Текст не копируется, S2 просто указывает на тот же адрес, что и S1, но только счётчик ссылок текста '123456' теперь равен 2.
S2 := 'The number is ' + S2;
Теперь выделяется новый, больший буфер, в него копируется текст 'The number is ' и туда же добавляется текст '123456'. Но т.к. S2 более не указывает на текст '123456', то счётчик ссылок этого текста снова уменьшается до 1.
Result := S2;
Result теперь указывает на тот же адрес, что и S2, а счётчик ссылок текста 'The number is 123456' увеличивается до 2.
end;
Теперь S1 и S2 выходят из области видимости. Счётчик ссылок текста '123456' будет уменьшен до 0, поэтому буфер этого текста освобождается (**). Счётчик ссылок текста 'The number is 123456' также уменьшается на единицу, становясь равным 1. Сам буфер не удаляется, поскольку счётчик ссылок не равен 0 (у нас на него ещё указывает Result).

Сложно? Да, весьма запутанно. И это становится ещё более запутанным с введением в игру var, const и out параметров. Но, к счастью, обычно вам не нужно заморачиваться этими вопросами. Такие вещи важно понимать только если вы обращаетесь к строкам из ассемблера, напрямую через PChar или с помощью подпрограмм прямого доступа к памяти. Но использование строк с приведением к PChar не является чем-то необычным. Тем не менее, если вы хотите заглянуть чуть дальше под капот языка - вы можете прочитать эту статью.

Самыми важными вещами, которые вам надо запомнить для работы со строками, являются:
  • что текст копируется в новый буфер только при изменении строки;
  • что счётчик ссылок и длина текста не привязаны к строковой переменной, а только к конкретному тексту, на который могут указывать несколько (равноправных) строковых переменных;
  • что счётчик ссылок всегда верен, если только вы не наврёте компилятору, используя приведения типов;
  • что присваивание чего-то строковой переменной уменьшит счётчик ссылок текстового буфера, на который она указывала до присваивания;
  • что, если счётчик ссылок опускается до 0, то строковый буфер удаляется.

to topИспользования вместе String и PChar

If you can't be a good example, then you'll just have to be a horrible warning. — Catherine Aird
PChar и символьные массивы довольно тяжело использовать. В большинстве случаев вы должны выделять память и не забывать её освобождать. Если вы хотите добавить текст, то вы должны сперва посчитать длину получающейся строки, затем увеличить буфер, если он слишком мал, и использовать StrCat или StrLCat, чтобы, наконец, добавить текст. Вы должны использовать StrComp или StrLComp для сравнения строк и т.д. и т.п.

Строки (String), с другой стороны, намного проще использовать. Большинство вещей выполняется автоматически, само собой. Но множество API функций Windows (или Linux) требуют нуль-терминированных строк PChar, а не строк Delphi String. К счастью, тип String специально устроен так, что они фактически тоже являются указателями на строки, завершающиеся нулём (этот терминатор никак не используется Delphi и компилятор постоянно таскает его со строками только на этот случай). Так что любую строку String вы можете использовать и как PChar – просто приводя тип:
var
  S: string;
begin
  S := ExtractFilePath(ParamStr(0)) + 'MyDoc.doc';
  ShellExecute(0, 'open', PChar(S), nil, nil, SW_SHOW);
end;
Не забывайте, что переменная AnsiString является указателем на текст, а не самим текстовым буфером. Если текст изменяется, то он будет скопирован в другое место, а адрес в переменной соответствующим образом изменится. Это означает, что вы не должны использовать PChar для указания на строки, а затем изменять строку. Лучше всего избегать таких вещей:
// ПРЕДУПРЕЖДЕНИЕ: ПЛОХОЙ ПРИМЕР
var
  S: string;
  P: PChar;
begin
  S := ParamStr(0); // например, нам вернули 'C:\Test.exe';
  P := PChar(S);
  S := 'Something else';
Если S изменяется на 'Something else', указатель P не будет изменён и будет продолжать указывать на 'C:\Test.exe'. Поскольку P не является строковой (в смысле String) ссылкой на этот текст, и у нас нет никакой другой переменной, ссылающейся на него, то его счётчик ссылок станет равным 0, и текст будет удалён из памяти. Это означает, что P теперь указывает на недоступную память (invalid memory).

Будет мудрым решением не путать компилятор смешением переменных PChar и String, только если вы точно не знаете, что вы делаете. Компилятор не воспринимает PChar как String, поэтому он не будет менять счётчик ссылок строковых буферов, если вы нацелите на них PChar. Часто наилучшим решением будет отказ от подобного использования PChar. Просто используйте обычные строки, и делайте приведение типов только в последний момент. Функции, принимающие параметр PChar, должны копировать текст параметра в свои собственные переменные.

Обычно строковые буферы имеют размер достаточный только для того, чтобы вместить лежащий в них (присвоенный им) текст. Но используя SetLength, вы можете установить произвольный размер строки (в символах). Это делает строки полезными для использования в качестве буферов. Например, вызов функции Windows API, которая возвращает текст в символьном массиве, может выглядеть так:
function WindowsDirectory: string;
begin
  SetLength(Result, MAX_PATH);
  GetWindowsDirectory(PChar(Result), Length(Result));
  SetLength(Result, StrLen(PChar(Result)));
end;
Альтернативно, вы можете присвоить PCharString, и в результате у вас получится новая строка с копией текста. Поэтому вы можете установить длину строки с помощью такого эквивалентного кода:
Result := PChar(Result);
Последняя строка в функции устанавливает размер строки равный размеру нуль-терминированной C-строке, записанной в ней (длина C-строки всегда меньше или равна длине её String-хранилища – прим.пер.). Если вам нужен результат как PChar-строка для дальнейших действий (например для передачи в другие API-функции), вы могли бы попробовать использовать такой код:
// ПРЕДУПРЕЖДЕНИЕ: ПЛОХОЙ ПРИМЕР
function WindowsDirectoryAsPChar: PChar;
var
  Buffer: array[0..MAX_PATH] of Char;
begin
  GetWindowsDirectory(Buffer, MAX_PATH);
  Result := Buffer;
end;
Однако, это не будет работать. Потому что Buffer является локальной переменной, хранящейся целиком в локальной памяти (процессорном стеке). Как только вы покидаете эту функцию, память, которая была выделена под Buffer, начинает использоваться другими подпрограммами, так что текст, который лежал в буфере, становится мусором. Вы никогда не должны использовать локальные переменные для возвращаемых значений PChar.

Вы могли бы обойти это, делая динамическое выделения памяти для возвращаемого PChar, с помощью, например, StrAlloc, но тогда вызывающему пришлось бы руками освобождать выделенный вами буфер. Обычно, это не самый лучший подход. Лучше следовать примеру GetWindowsDirectory, и позволить вызывающему указать свой буфер и его размер. Тогда вы просто заполните уже готовый буфер (используя StrLCopy) своими данными до допустимого размера.

Есть и альтернативная реализация функции WindowsDirectory, которая может использовать локальный буфер. Она основывается на том, что вы можете присваивать PChar строке String напрямую. Чтобы сделать текст именно строкой Delphi (с служебными полями длины и счётчиком ссылок), будет создан строковый буфер требуемой длины, а текст будет скопирован в него. Поэтому, даже если локальный буфер будет удалён, то текст в строковом буфере всё ещё будет с нами:
function WindowsDirectory: string;
var
  Buffer: array[0..MAX_PATH] of Char;
begin
  GetWindowsDirectory(Buffer, MAX_PATH);
  Result := Buffer; // Копируется StrLen(Buffer) символов!
end;
Но как вам написать функцию, например в DLL, которая должна возвращать данные как PChar? Я думаю, что вы снова можете следовать примеру GetWindowsDirectory. Вот пример простой функции из DLL, возвращающей строку версии:
// Иметь отдельную функцию для получения длины проще, 
// чем просить GetDLLVersion вернуть длину при nil аргументе.
function GetDLLVersionLength: Integer;
begin
  Result := Length(DLLVersion + IntToStr(VersionNum));
end;

// Возвращаем длину скопированных символов, исключая терминатор
function GetDLLVersion(Buffer: PChar; MaxLen: Integer): Integer;
begin
  if (Buffer <> nil) and (MaxLen > 1) then
begin
  StrLCopy(Buffer, PChar(DLLVersion +IntToStr(VersionNum)), MaxLen - 1);
  Result := StrLen(Buffer);
end
else
  Result := 0;
end;
Как вы можете видеть, строка просто копируется в предоставленный вызывающим буфер с помощью StrLCopy. Поскольку вызывающий обязан подготовить буфер, вы избегаете любых проблем с управлением памятью. Если вы предоставляете буфер, то вы и знаете, как его удалять. FreeMem не будет работать через границу DLL (в общем случае). Но даже если бы работала (например, с использованием общего менеджера памяти – прим.пер.), то пользователь вашей DLL, работающий в C или Visual Basic, не знал бы, как освободить буфер в своём языке, т.к. управления памятью индивидуально в каждом языке. Позволяя вызывающему указывать свой буфер, вы делаете его или её независимыми от вашей реализации.

Прим.пер.: а вот ещё обсуждение этого вопроса с несколькими альтернативными решениями.

to topDelphi 2009 и выше

В Delphi 2009 строки были значительно изменены. До Delphi 2009 (т.е. с Delphi 2 по Delphi 2007) строки были, фактически, типом AnsiString, а каждый символ был однобайтовым AnsiChar. Тип PChar был псевдонимом для PAnsiChar. Но в Delphi 2009 строки стали использовать Unicode, а ещё точнее – UTF-16, что означает, что потребовался новый тип строк: UnicodeString. Этот тип строк состоит уже из двух-байтовых WideChar. Он стал умалчиваемым типом строк, что означает, что String теперь псевдоним для UnicodeString, Char для WideChar, а PChar для PWideChar.

Delphi for Win32 уже имела строковый тип WideString (также состоящий из WideChar), но это всегда лишь псевдоним для системного типа строк BSTR, используемом в основном в COM. Этот тип управляется ОС (и поэтому является идеальным средством для обмена строками между границами модулей – прим.пер.) и не имеет счётчика ссылок и “копирования по требованию”, так что каждое присваивание означает создание новой уникальной строки с полным копированием текстового буфера. Поэтому тип WideString не отличается особой производительностью – вот почему был введён новый тип UnicodeString.

Кроме длины и счётчика ссылок, каждый строковый тип данных (т.е. AnsiString и UnicodeString) теперь имеют дополнительные служебные поля: Word, содержащий кодировку (encoding) строки (в основном используется в однобайтовых строках типа AnsiString), и Word, содержащий размер символа в байтах. Кодировка строки AnsiString управляет интерпретацией и конвертацией символов с кодами от 128 до 255, а размер символа в основном используется для взаимодействия с кодом на C++.

Кроме того, также было введено несколько вспомогательных типов строк: RawByteString (= AnsiString($FFFF)) и UTF8String (= AnsiString(65001)) (а также огромного количества любых других пользовательских типов строк на базе AnsiString, например, Win1251String = AnsiString(1251) – прим.пер.). Подразумевается, что строки UTF8Strings используются для хранения данных в формате UTF-8, что означает, что каждый элемент строки является AnsiChar-ом, но каждый "символ" может быть представлен несколькими элементами AnsiChar. Заметьте, что я поставил слово "символ" в кавычки, потому что в контексте Unicode более правильно будет говорить о кодовых позициях (code point) (а также это не приведёт к путанице с символом в смысле один Char - прим.пер.).

Как вы можете видеть из статьи Википедии о UTF-16, также возможно, что некоторые кодовые позиции UTF-16 требуют нескольких WideChar-ов – так называемых "суррогатных пар" (surrogate pairs). Так что длина UnicodeString или UTF8String не обязательно напрямую соответствует числу кодовых позиций в них. Однако если в UnicodeString суррогатные пары относительно редки (суррогатные пары используются только для записи символов вне базовой плоскости: Basic Multilingual Plane (BMP) – именно там находятся все символы для всех современных языков и множество специальных символов – прим.пер.), то в то же время редкая UTF8-строка обходится без мультибайтовых символов.

Ещё одним новым типом является RawByteString. Если вы присвоите AnsiString с одним типом кодировки другой AnsiString с другой кодировкой, то будет выполнена автоматическая конвертация (потенциально: с возможной потерей данных, если символы из одной кодировки не имеют эквивалента в другой кодировке). Собственно AnsiString используют кодировку по-умолчанию, управляемую системой (“Кодировка для не-Unicode приложений” в региональных настройках системы – прим.пер.). Пользовательские типы строк на базе AnsiString всегда имеют фиксированную кодировку (например, UTF8String всегда имеет кодировку 65001). А RawByteString – это специальная строка без кодировки, так что вы можете быть уверены, что при присваивании ей другой AnsiString-строки (собственно AnsiString или пользовательской типа UTF8String), никакой конвертации не будет, а все текстовые буфера будут скопированы “как есть”.

Справка Delphi 2009 так говорит о RawByteString:
RawByteString позволяет передачу строковых данных с любой кодовой страницей без выполнения конвертации данных. Обычно это означает, что параметры подпрограмм, которые обрабатывают строки без учёта кодовой страницы, должны иметь тип RawByteString. Объявление переменных типа RawByteString должно происходить редко, если происходить вообще. Потому что это может приводить к неопределённому поведению и потере данных.

Так что же с этим делать?

Как вы можете видеть из текста статьи, я ни разу не делал ссылок на размер Char. Так что всё, что я написал выше, полностью применимо и к Delphi 2009 и выше без изменений. Большинство кода, использующего эти техники, может быть просто перекомпилировано в Delphi 2009 и выше, и будет работать корректно, но теперь уже используя Unicode, т.е. вместо AnsiString, код будет работать с UnicodeString, WideChar и PWideChar.

Функции Win32 API часто поставляются в двух вариантах: один из которых принимает Ansi (т.е. однобайтовые) символы и (C-) строки, а второй принимает Wide (Unicode, двухбайтовые) символы и (C-) строки. Эти варианты обычно отличаются друг от друга окончаниями A или W в имени функции соответственно. Заголовочные модули Delphi для таких API функций, типа Windows.pas, обычно также определяют третий вариант, без окончаний A или W (точно так же поступает Microsoft в своих заголовочниках C), и отождествляет этот вариант с Ansi вариантом. Вот один пример из Windows.pas до Delphi 2009:
function GetShortPathName(lpszLongPath: PChar; lpszShortPath: PChar; cchBuffer: DWORD): DWORD; stdcall;
{$EXTERNALSYM GetShortPathName}  
function GetShortPathNameA(lpszLongPath: PAnsiChar; lpszShortPath: PAnsiChar; cchBuffer: DWORD): DWORD; stdcall;
{$EXTERNALSYM GetShortPathNameA}
function GetShortPathNameW(lpszLongPath: PWideChar; lpszShortPath: PWideChar; cchBuffer: DWORD): DWORD; stdcall;
{$EXTERNALSYM GetShortPathNameW}
...
function GetShortPathName;  external kernel32 name 'GetShortPathNameA';
function GetShortPathNameA; external kernel32 name 'GetShortPathNameA';
function GetShortPathNameW; external kernel32 name 'GetShortPathNameW';
Как вы можете видеть, GetShortPathName проецируется на функцию 'GetShortPathNameA'. Вы также можете видеть, что -A версия объявляется с PAnsiChar-ами, а -W версия принимает строки PWideChar. Нейтральная же работает с нейтральным типом PChar.

В Delphi 2009 и выше, такие нейтральные функции теперь ассоциируются с -W вариантом, так что вторая часть вышеприведённого кода теперь становится:
function GetShortPathName;  external kernel32 name 'GetShortPathNameW';
function GetShortPathNameA; external kernel32 name 'GetShortPathNameA';
function GetShortPathNameW; external kernel32 name 'GetShortPathNameW';
Это означает, что в Delphi 2009 и выше, даже если вы вызываете функцию из Windows API, а также если вы вызываете функцию RTL или VCL, то в большинстве случаев вам не нужно беспокоиться о размере символов в строке. Строки теперь у нас Unicode, функции API стали тоже Unicode, так что если вы продолжите использовать нейтральные типы данных String, Char и PChar, то вам не придётся изменять свой код. А если у вас есть код, который работает с неверным размером символа (некоторые функции API, типа GetProcAddress, существуют только в ANSI варианте), то вы получите красивое предупреждение или ошибку от компилятора, так что вы тут же сможете решить, как вам реагировать (прим.пер.: вообще-то, у GetProcAddress есть перегруженный Unicode-вариант, который делает преобразование строк к ANSI-варианту).

SizeOf или Length?

Конечно же, вам нужно быть осторожным с кодом, особенно если этот код использует низко-уровневые подпрограммы типа Move или FillChar (которая теперь по-хорошему должна была бы называться FillByte, т.к. работает она с байтами, но старое название было сохранено по соображениям совместимости кода – прим.пер.), которые предполагают, что символы имеют размер в один байт (ну, на самом деле они просто меряют размеры в байтах, а не в символах – прим.пер.). Так, чтобы очистить массив из Char, не делайте так:
var
  Buffer: array[0..MAX_PATH] of Char;
begin
  FillChar(Buffer, MAX_PATH + 1, 0);
потому что теперь буфер состоит из WideChar, что означает размер в 2 * (MAX_PATH + 1) байт. Намного проще при этом использовать SizeOf:
FillChar(Buffer, SizeOf(Buffer), 0);
Заметьте, что SizeOf можно применять только к статическим массивам. Для динамических массивов она всегда возвращает размер указателя (см. статью про указатели). В этом случае вам надо писать:
SetLength(MyCharArray, MAX_PATH + 1);
FillChar(MyCharArray[0], Length(MyCharArray) * SizeOf(Char), 0);
Для ситуаций же, когда важно число символов, используйте просто Length:
StrLCopy(Buffer, PChar(MyString), Length(Buffer));

Информация для дальнейшего чтения

Тут есть whitepaper от Marco Cantù, который обширно и ясно описывает различные новые строковые типы и расширения. Я рекомендую скачать её и прочитать хотя бы один раз.

Больше советов по адаптации ваших проектов к Delphi 2009 и выше вы найдёте в статьях от Nick Hodges, менеджера R&D Delphi: Delphi в мире Unicode, Часть 1, Часть 2 и Часть 3.
Практическое руководство по миграции на unicode от Embarcadero.
На Embarcadero Developer Network (EDN) есть куча статей и документов, связанных с Unicode.
Джоэль Спольски объясняет, почему нет такой вещи, как “просто текст”.

to topЗаключение

The open secrets of good design practice include the importance of knowing what to keep whole, what to combine, what to separate, and what to throw away. — Kevlin Henny
Хотя оба типа String и PChar являются строковыми типами, они весьма различны. String проще использовать, а для PChar вам нужно всё делать вручную. Вы можете использовать их вместе и приводить String к PChar, и присваивать PChar строке, но поскольку String меняет свой адрес при изменениях, вы не должны долго держаться за этот адрес при использовании PChar. Присваивание PChar-а строке намного безболезненней.

Как показано в тексте выше, выделение текста в функции и возвращение PChar на новый буфер не является очень хорошей идеей. Это ещё хуже в ситуациях межмодульного обмена данными, поскольку вызывающий может даже не сумеет освободить возвращаемую память – DLL и вызывающий, скорее всего, будут иметь различные менеджеры памяти – каждый со своей кучей. Также не очень хорошей идеей является использование локального буфера для возвращаемого текста.

Если вам обязательно нужно использовать PChar, потому что этого требует функция, вы должны использовать String настолько, насколько это возможно. И приводить строку к PChar только когда вы готовы передать строку параметром. Использование строк намного проще и менее подвержено ошибкам, чем использование функций строк в C-стиле.
A little inaccuracy sometimes saves a ton of explanation. — H. H. Munro (Saki)
Я надеюсь, что я немного приподнял завесу тумана над PChar. Я не рассказал всё, что известно, и даже, возможно, немного переврал правду (например, не любая Delphi-строка типа String управляется счётчиком ссылок – к примеру, строковые константы всегда имеют счётчик ссылок равный –1 (***)), но эти мелкие детали не столь важны для общей, большой картины и не оказывают влияния на использование и взаимодействие между String-ми и PChar-ми.

Дополнительный материал для чтения: тонкости работы со строками.

Примечания переводчика:
(*) Вот ещё интересный пример.
(**) Чтобы гарантировать обязательное выполнение очистки строк (уменьшения счётчика ссылок), компилятор неявно вставляет в подпрограмму скрытый try/finally. Поэтому, фактически, код любой подпрограммы со строковой переменной (а также любой другой авто-финализируемой переменной) выглядит примерно так:
begin
  [Подготовка переменных (обнуление локальных авто-финализируемых переменных)]
  try
    begin
      // Ваш код, который вы реально написали в подпрограмме
    end;
  finally
    [Очистка (финализация) переменных]
  end;
end;
(***) См. также пример ситуации, где это имеет значение.

См. также: обсуждение внутренней реализации авто-финализируемых типов.

24 комментария:

  1. Спасибо. Некоторые моменты стали более ясными.

    ОтветитьУдалить
  2. Спасибо за перевод! Хотел было сам перевести, но теперь просто сошлюсь у себя в блоге, как будет время :)
    З.Ы. По-моему, у А. Григорьева в его книге есть об этом. Плюс части этой книги есть на Королевстве Delphi.

    ОтветитьУдалить
  3. > З.Ы. По-моему, у А. Григорьева в его книге есть об этом. Плюс части этой книги есть на Королевстве Delphi.
    Вообще-то я дал ссылку:

    > Дополнительный материал для чтения: тонкости работы со строками.

    ;)

    ОтветитьУдалить
  4. спасибо за перевод(ы)

    ОтветитьУдалить
  5. Спасибоо)

    PS:
    StrLCopy(S, 'Delphi', Length(S));
    наверное должно быть length('Delphi')?

    ОтветитьУдалить
  6. Нет, там всё верно.

    Предварительно замечу, что функция копирует нуль-терминированную строку - т.е. она остановится на #0 в 'Delphi'. Передавать отдельно длину 'Delphi' не имеет смысла, т.к. информация о длине будет дублироваться. Длину источника вы бы этим задали, а длина буфера у вас никак не указывалась - поэтому функция не смогла бы узнать, когда остановиться, чтобы не вылезти за буфер.

    Ну и по сути: третий параметр указывает ограничение по размеру копирования. Мы ставим его в Length(S), чтобы указать, чтобы функция не копировала больше символов, чем есть места в буфере (S) - это и есть защита от переполнения буфера.

    Если вы укажете Length('Delphi') - то в данном примере всё будет нормально (S больше 'Delphi'), но если длина копируемой строки будет больше буфера, то, указав Length('строка'), вы скопируете строку целиком, переполнив меньший буфер.

    ОтветитьУдалить
  7. Добавлю, что строки Delphi могут содержать #0 в качестве обычного символа внутри строки, и потому необходимо быть осторожным, передавая такие строки кому-то ещё (в C++, WinAPI etc.).

    Это также может потенциально представлять большие проблемы и внутри самого Delphi: некоторые функции работы со обычными String рассчитывают на то, что в конце существует нулевой символ. Некоторые приводят их в PChar. Соответственно, нулевой символ там, где его не ожидается, может привести к неожидаемому поведению.

    ОтветитьУдалить
  8. Спасибо за перевод! В моей программе надо передать API функции (ReadFile) буфер. Я передавал pchar, а потом пытался преобразовать в string чтобы вернуть как результат функции. Ну естественно указатель указывал на стек и программа показывала ошибку доступа к памяти. SetLength решил проблему. Жалко, что в документации не написано это. И в интернете запаришься пока найдешь...

    ОтветитьУдалить
  9. MyType = packed record
    UID: Dword;
    NickLen: byte;
    Nick: String;
    Other: Byte;
    end;

    type
    PMyType = ^MyType;

    .....................................

    var
    t : PMyType;
    buf : TByteArray;
    begin
    buf:=''; //тут данные
    t := PMyType(@buf[0]);
    len:=t^.NickLen;

    как мне прально доставать строки?? стринг, pchar , но чтоб после прочтении строки следущие данные парсились дальше, тоесть Other это был следующий байт после строки, я уже мучаюсь почти сутки нервы все дела( помоги мне пджлста

    ОтветитьУдалить
  10. В статье описан случай, когда функция возвращает PChar и написано, что вызывающему нужно самому освобождать память. Понятно, что решение не самое лучшее, но все же, как освободить память, на которую ссылается PChar? Функция возвращает PChar с получением данных проблем нет, но происходит утечка, nil, как я понял, обнуляет ссылку а не данные, находящиеся по ссылке. Нужно искать функции, в том же модуле, для обнуления? Нет других вариантов?

    ОтветитьУдалить
  11. Любая функция должна описывать (в документации), как правильно освобождать память после её вызова, если это необходимо. Читайте описание конкретной функции.

    В целом же могут быть такие варианты:
    - Функция возвращает указатель на глобальный буфер. Память освобождать не нужно, она будет использована заново при повторном вызове.
    - Для освобождения результата функции нужно вызвать специальную функцию освобождения из DLL/модуля, откуда импортируется функция.
    - Для освобождения результата функции нужно вызвать одну из системных функций (например, HeapFree, LocalFree и т.п.).
    - Функция возвращает результат в буфере, который выделили вы. Память нужно освобождать способом, обратным к тому, которым вы выделяли память.

    ОтветитьУдалить
  12. Подскажите пожалуйста, что я пропустил. Это рабочая процедура, но до версии Delphi 2009. Заменил все: LV_ITEM на LV_ITEMW, PChar на PWideChar, sendMessage на sendMessageW

    (процедура считывания с чужого листа ListView или syslistview32 в свой TStringGrid)
    procedure GetListViewGrid(ALVHandle: HWND; AColumnCount, AItemCount: Integer;
    ADataGrid: TStringGrid);
    const
    cchTextMax=255;
    var
    hProcess: THandle;
    dwProcessID: DWORD;
    dwWriten: DWORD;
    LVItemCount: Integer;
    i, j, nTextLength: Integer;
    pLVItem: ^LV_ITEMW;
    LVItem: LV_ITEMW;
    pszText: PWideChar;
    svText: ShortString;
    begin
    if ALVHandle=0 then
    Exit;
    // Получаем количество строк
    LVItemCount := ListView_GetItemCount(ALVHandle);
    if AItemCount>LVItemCount then
    Exit;
    if AItemCount>0 then
    LVItemCount := AItemCount;
    // Получаем ID процесса, которому принадлежит найденное окно
    dwProcessID := 0;
    GetWindowThreadProcessId(ALVHandle, @dwProcessID);
    if dwProcessID=0 then
    ExitProcess(GetLastError);
    // Открываем процесс
    hProcess := 0;
    hProcess := OpenProcess(PROCESS_ALL_ACCESS, True, dwProcessID);
    if hProcess=0 then
    ExitProcess(GetLastError);
    // Выделяем в нем память под текстовый буффер
    pszText := VirtualAllocEx(hProcess, nil, cchTextMax, MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE);
    // Выделяем в нем память под структуру LVITEM
    pLVItem := VirtualAllocEx(hProcess, nil, SizeOf(LV_ITEMW), MEM_COMMIT or MEM_TOP_DOWN,
    PAGE_READWRITE);
    // Устанавливаем колич строк и столбцов в TStringGrid
    ADataGrid.RowCount := LVItemCount;
    ADataGrid.ColCount := AColumnCount;
    // Заполняем структуру
    ZeroMemory(@LVItem, SizeOf(LV_ITEMW));
    LVItem.Mask := LVIF_TEXT;
    LVItem.pszText := pszText;
    LVItem.cchTextMax := cchTextMax;
    // Считываем строки
    for i := 0 to LVItemCount do
    begin
    LVItem.iSubItem := 0;
    // Пишем ее в память удаленного процесса
    if not WriteProcessMemory(hProcess, pLVItem, @LVItem, SizeOf(LV_ITEMW), dwWriten) then
    Exit;
    nTextLength := sendMessageW(ALVHandle, LVM_GETITEMTEXT, i, Integer(pLVItem));
    // Читаем результат
    ZeroMemory(@svText, cchTextMax);
    ReadProcessMemory(hProcess, LVItem.pszText, @svText[1], nTextLength, dwWriten);
    // заполняем строки TStringGrid
    ADataGrid.Cells[1, i+1] := StrPas(PWideChar(@svText[1]));
    // Считываем столбцы
    for j := 0 to AColumnCount do
    begin
    LVItem.iSubItem := j;
    // Пишем ее в память удаленного процесса
    if not WriteProcessMemory(hProcess, pLVItem, @LVItem, SizeOf(LV_ITEMW), dwWriten) then
    Exit;
    nTextLength := sendMessageW(ALVHandle, LVM_GETITEMTEXT, i, Integer(pLVItem));
    // Читаем результат
    ZeroMemory(@svText, cchTextMax);
    ReadProcessMemory(hProcess, LVItem.pszText, @svText[1], nTextLength, dwWriten);
    // заполняем столбцы TStringGrid
    ADataGrid.Cells[j, i] := StrPas(PWideChar(@svText[1]));
    end;
    end;
    // Освобождаем ранее выделенную память
    VirtualFreeEx(hProcess, pszText, 0, MEM_RELEASE);
    VirtualFreeEx(hProcess, pLVItem, 0, MEM_RELEASE);
    // Закрываем описатель процесса
    CloseHandle(hProcess);
    end;

    Но StringGrid по прежнему пустой

    ОтветитьУдалить
  13. Всем спасибо, вопрос решен. Пропустил замену LVM_GETITEMTEXT на LVM_GETITEMTEXTA и все через PAnsiChar.

    ОтветитьУдалить
  14. Здравствуйте, уважаемый Александр, позвольте поблагодарить Вас за Ваши примеры! Знания полученные от их прочтения, мне очень пригодились.

    Но у меня возник ряд вопросов, не затруднил ли Вас пролить свет:

    можно ли поступать так:

    var
    p:pchar;
    begin
    p:='some string'; // или таким же образом присваиваем строковую переменную

    ведь в таком случае данные запишутся в неясном месте в программе, или я не прав?

    с уважением, S-or.

    ОтветитьУдалить
  15. > ведь в таком случае данные запишутся в неясном месте в программе, или я не прав?

    Нет, неверно. В этом случае 'some string' будет сохранена как константа компилятором, ещё при компиляции программы и подобным присваиванием вы просто получите адрес на неё. В дальнейшем ей можно пользоваться, как обычной строкой, за исключением того, что она неизменяемая.

    Рассмотрим пример:

    var
    Str: String;
    P: Pchar;

    begin
    Str := 'Mutable string';
    P := PChar(Str);

    PByte(P)^ := 49;

    Writeln(P);

    результат: 1utable string

    теперь уберём обычную строку:

    var
    P: Pchar;

    begin
    P := 'Immutable string';

    PByte(P)^ := 48;

    Writeln(P);

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

    ОтветитьУдалить
  16. Подскажите, есть ли возможность в Delphi 2009 и последующих включить "режим совместимости" на уровне проекта? Или директивами компилятора.

    ОтветитьУдалить
  17. Штатно - нет. Есть вот такой хак, но сам автор говорит, что он больше проблем создаёт, чем решает.

    По факту проще исправить код (читай: перевести его на Unicode).

    ОтветитьУдалить
  18. По факту, команда PChar(S), где S : String ничего плохого не сделает, даже если мы после присвоения переменной типа PChar, поменяем строку. Наша куча символов не измениться, и будет указывать на живую страницу.
    Var S : String;
    p : PChar;
    Begin
    S := 'Hallo!';
    p := PChar(S);
    ShowMessage(p);
    Delete(S,2,1);
    ShowMessage(p);
    end;
    Результат -- на экране дважды Hello!, хотя по Вашим словам будет ЖОПА!
    Учим мат часть! В начале выделяется новая страница, в ней образуется копия строки но без лидирующих 4х байт, затем дается ссылка.
    PChar это не просто преобразователь типов, но и копировщик строки!!!

    ОтветитьУдалить
    Ответы
    1. Это настолько замечательный комментарий, что я даже сделал на него развёрнутый статью-ответ. Спасибо.

      Удалить
  19. Подскажите пожалуйста! Делфи Рио. Почему-то вылетает в трубу:

    function GenTempFileName(): String;
    var Buffer: array [0 .. MAX_PATH] of Char;
    begin
    repeat
    GetTempPath(SizeOf(Buffer) - 1, Buffer);
    GetTempFileName(Buffer, '~', 0, Buffer);
    Result := Buffer;
    until
    not FileExists(Result);
    end;

    с этим тоже вылетает:

    Result := String(Buffer);

    и с этим тоже:

    SetString(Result, PChar(@Buffer), StrLen(PChar(@Buffer)));

    ОтветитьУдалить
    Ответы
    1. nBufferLength - The size of the string buffer identified by lpBuffer, in chars.

      Удалить
  20. Подскажите пожалуйста! Как корректнее всего, учитывая что Text типа String и длина этого Text может быть ощутимо большой?:
    * SendMessage( ... LPARAM(@Text));
    * SendMessage( ... LPARAM(@Text[1]));
    * SendMessage( ... LPARAM(@Text[Low(Text)]));
    * SendMessage( ... LPARAM(PChar(Text)));
    * SendMessage( ... LPARAM(Pointer(Text)));
    Будет ли разница для древних Делфи?
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. Если строка - точно не пустая, то особой разницы между этими вариантами нет.

      Если строка может быть пустой, то очевидно не подойдут варианты два и три. Остальное зависит от того, куда вы это передаёте: в частности, в каком формате ожидается пустая строка. Допустимо ли там nil, или же нужен именно указатель на #0. А если можно и то, и другое - то нет ли семантической разницы. Это надо смотреть по документации.

      Для Windows API в большинстве случаев используют LPARAM(PChar(Text))).

      Длина строки здесь ни на что не влияет.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.