Передача строки во внешнюю функцию - это достаточно типичное действие. Мы постоянно так делаем при вызове 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
двоично совместим с String
(а PWideChar
- с 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 строки, но вы не передаёте права на владение!
..две недели пролетели не заметно..
ОтветитьУдалить