21 апреля 2015 г.

Ответ на задачку №17

Ответ на задачку №17.

Передача строки во внешнюю функцию - это достаточно типичное действие. Мы постоянно так делаем при вызове Windows API. Например:
MessageBox(0, PChar(Message), 'Error.', MB_OK or MB_ICONERROR);
В чём же проблема?
Упреждающее примечание: код задачки построен по аналогии с вызовом функции Windows API. Поэтому он использует тип PChar, подразумевая, что AddToList - это, на самом деле, AddToListA или AddToListW - в зависимости от того, компилируете ли вы в Unicode или нет. Но да, как указали в ответах к задачке, если вы вызываете не классический Windows API (который представлен в двух вариантах), а просто некий API (который обычно представлен в одном варианте), то вместо PChar нужно использовать PWideChar. Ну или PAnsiChar - если API уже написан (не вами) и использует однобайтовые строки.

Иными словами, проблема (задачки) не в ANSI/Unicode (как на это указывали некоторые ответы), хотя это, действительно, важное уточнение.
Итак, проблема задачки заключается в семантике функции. Одно дело, если функция использует данные из своих параметров в работе и выйдет. Другое дело, если функция сохранит эти данные (указатель) для последующего использования. Иными словами, вопрос здесь во владении памятью.

Очевидно, что параметры функции должны быть доступны на протяжении выполнения этой функции. И это условие у нас выполняется: под данные параметров (ListBox1.Items[X]) выделена память и они доступны всё время, пока идёт выполнение функции AddToList. Напомню, что PChar двоично совместим с StringPWideChar - с UnicodeString и WideString; а PAnsiChar - c AnsiString, AnsiString[CodePage] и RawByteString). Иными словами, в строке PChar(ListBox.Items[X]) не происходит выделения памяти для хранения PChar. Вместо этого, берётся строка из ListBox.Items[X], и она передаётся вместо PChar в AddToList. Иными словами, проблема не в том, что в DLL передаётся PChar от строки (как указывали некоторые ответы к задачке).

Хорошо, т.е. вроде как передача данных в функцию у нас проходит без проблем. Но что произойдёт, когда функция завершится?

Что ж, где-то в SomeDLL.dll будет сохранён указатель на строку. Что это за строка? Это строка, которую вернул ListBox.Items[X]. А что за строку вернул нам ListBox.Items[X]? Именно это - и есть вопрос задачки №17.

Возможно, вы уже почуяли подвох этой задачки. "Что возвращает Items[X]" - звучит знакомо, не так ли?

Да, задачка №17 - это переформулированная задачка №10. ListBox.Items - это TListBoxStrings. TListBoxStrings не хранит строки в array of String. Вместо этого TListBoxStrings записывает/читает строки из внутреннего USER-объекта ListBox через Windows API (конкретно - через сообщения Windows).

В итоге, это означает, что строка не хранится в памяти на длительном прогоне программы, она создаётся только в момент вызова ListBox.Items[X], возвращается через Result и сохраняется во временную (скрытую) переменную. В результате, если код DLL попробует обратится к сохранённым через AddToList данным, то он получит Access Violation, поскольку данные будут из памяти удалены (окей, он может и не получить Access Violation, т.к. на этом же месте могут быть созданы другие данные).

Но, постойте, это ещё не всё. Поскольку в AddToList передаётся временно созданная переменная, а временная переменная у нас всего одна (т.е. в цикле от 0 до 999 будет одна временная переменная, а не 1000 временных переменных), то это означает, что в каждый вызов AddToList передаётся одно и то же значение! Действительно, мы выделяем память под строку, получаем её (из ListBox), вызываем AddToList, удаляем строку, выделяем строку (на том же самом месте), получаем её (из ListBox), вызываем AddToList, удаляем строку, ... и т.д. И хотя на каждой итерации данные в памяти по указателю строки будут разными (ведь в ListBox хранятся разные строки), но сам указатель будет (почти) всегда одним и тем же!

Как можно исправить этот код? Легко! Для этого нужно просто прогарантировать наличие данных в памяти до окончания работы с ними SomeDLL.dll. Например:
var
  Buffer: array of String;
begin
  SetLength(Buffer, ListBox1.Items.Count);
  for X := 0 to ListBox1.Items.Count - 1 do
  begin
    Buffer[X] := ListBox1.Items[X];
    AddToList(PChar(Buffer[X]));
  end;

  // ... SomeDLL.dll работает с сохранёнными данными 

  // <- SomeDLL.dll закончила работу с сохранёнными данными
  Finalize(Buffer);
end;
Здесь, однако, возникает такой подводный камень:
var
  Buffer: array of String;
begin
  SetLength(Buffer, ListBox1.Items.Count);
  for X := 0 to ListBox1.Items.Count - 1 do
  begin
    Buffer[X] := ListBox1.Items[X];
    AddToListA(PAnsiChar(AnsiString(Buffer[X])));
  end;

  // ...
end;
Это - ещё один вариант наткнуться на обсуждаемую нами проблему. Хотя в этом примере мы пытаемся сделать всё правильно (храня буфер строк), но проблема остаётся. Действительно, A-вариант функции требует преобразования строк, что означает использования временной переменной для хранения результата конвертации. Правильный вариант этого кода использовал бы array of AnsiString.

Примечание: управление памятью - не проблема этой задачки. Действительно, вам нужно быть внимательным и следить за управлением памятью. Владелец памяти всегда должен знать как её освободить. Если вы передаёте память на владение кому-то, вам нужно выделять память согласно API владельца. Иными словами, вы не можете передать через API внешней библиотеки память, выделенную менеджером памяти Delphi (а автоматически выделяемая под строки память, определённо, выделяется именно менеджером памяти Delphi) - потому что внешний код понятия не имеет о менеджере памяти Delphi. Однако, API вовсе не обязательно может требовать передачу прав на владение. Например:
Attachment.Title := PChar(Caption);
Attachment.FileName := PChar(FileName);
AddAttachment(Message, Attachment);
SendMessage(Message);
(да, этот гипотетический API сделан "по мотивам" Simple MAPI).

Здесь API не принимает на владение никаких данных. Да, вы передаёте в API кучу указателей, но всё, что требует API - чтобы указатели просто "жили" до конца SendMessage. Да, вы передаёте в API строки, но вы не передаёте права на владение!

1 комментарий :

  1. ..две недели пролетели не заметно..

    ОтветитьУдалить

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

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

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

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

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