25 сентября 2013 г.

Задачка №17

Что не так с этим кодом?

procedure AddToList(Item: PChar); stdcall; external 'SomeDll.dll';

...

procedure TForm1.Button1Click(Sender: TObject);
var
  X: Integer;
begin
  for X := 0 to ListBox1.Items.Count - 1 do
    AddToList(PChar(ListBox1.Items[X]));
end;
Объяснить поведение кода и как его нужно исправить.

Ответ будет опубликован через две недели.

Ответ.

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

  1. а) а оно скомпилируется? функция без указания результирующего типа..
    б) а версия компилятора юникодная? или это в данном случае не имеет значения?..

    ОтветитьУдалить
  2. а, точно, скомпилируется.. такое допускается

    ОтветитьУдалить
  3. не, всё-таки надо либо отдельно написать прототип вызова (с параметрами и результатом) и далее связь с библиотекой без указания параметров в функции, либо должен быть указан результирующий тип. либо идти спать...
    (сорри :з)

    ОтветитьУдалить
  4. Прошу прощения, это ошибка, конечно же.

    ОтветитьУдалить
  5. pchar это поинтер в длл передаваемый адрес будет смотреть не тудой

    ОтветитьУдалить
  6. "pchar это поинтер в длл передаваемый адрес будет смотреть не тудой"

    Это вряд ли... "тудой"... Мы такое используем...

    ОтветитьУдалить
  7. удивляюсь дотошности и интеллекту Александра...
    ведь как всегда окажется - либо фреймы не такие.. либо стек...
    откуда вы это берёте?

    ОтветитьУдалить
  8. Если уж мы передаём указатель в dll, то лучше писать явно: PAnsiChar или PWideChar а то может быть очень весело, если в dll мы ожидаем анси, а прилетит юникод.

    ОтветитьУдалить
  9. Все что касается передачи строковых параметров во внешние функции в DLL всегда вызывало у меня дикий бургурт. Предположу, что для того, чтобы нормально все передалось, лучше завести отдельный массив из Char, складывать строку из ListBox1 сначала туда, а потом уже передавать указатель на этот массив внутрь DLL. В таком варианте все явно, нигде ничего не течёт, указатель в конце прибиваем.

    Чувствую, что подвох с хранением строкового значения Result для неявно вызываемой функции Strings.GetItem(X).

    И я, конечно же, искренне надеюсь на то, что сферическая функция AddToList складывает в куда-то _не_ указатели на строки.

    ОтветитьУдалить
  10. Указать явно PAnsiChar вместо PChar

    ОтветитьУдалить
  11. Соглашусь со Степаном. GetItem создает локальную строку в Result. Далее, поскольку она ничему не присваивается, то к моменту вызова счетчик ссылок уже будет обнулен и строка освобождена.

    Максим

    ОтветитьУдалить
  12. Надо явно выделять память под передаваемый PChar.
    Если бы было что-то типа
    procedure JustShow(Item: PChar); stdcall; external 'SomeDll.dll';
    с выводом, например, MessageBox с текстом, то приведённый код рабочий.

    ОтветитьУдалить
  13. Согласен с двумя ораторами, отписавшимися выше, именно так.

    ОтветитьУдалить
  14. >>GetItem создает локальную строку в Result. Далее, поскольку она ничему не присваивается, то к моменту вызова счетчик ссылок уже будет обнулен и строка освобождена.
    И тем не менее даже в самом LIstBox такой подход используется.
    procedure TCustomListBox.CopySelection(Destination: TCustomListControl);
    ...
    Destination.AddItem(PChar(Items[ItemIndex]), Items.Objects[ItemIndex]);
    ..
    на момент вызова AddToList - указатель будет гарантированно валидный. а вот если этот указатель где то запоминается, тогда да. Будет бадабум

    ОтветитьУдалить
  15. Название процедуры в dll - напрягает. Есть вероятность, что кто-то захочет удалить элемент из листа. А память чистит вызываемая подпрограмма.

    ОтветитьУдалить
  16. Если листбокс пустой, процедура попробует обратиться к несуществующему ийтему.

    Должно быть так:
    begin
    if ListBox1.Items.Count >0 then
    for X := 0 to ListBox1.Items.Count - 1 do
    AddToList(PChar(ListBox1.Items[X]));
    end;

    ОтветитьУдалить
  17. >Если листбокс пустой, процедура попробует обратиться к несуществующему ийтему.
    Бред. Идите читайте работу с циклами.

    ОтветитьУдалить
  18. помоему все дело в pchar и в чьей области памяти будет выделятся область под нее

    ОтветитьУдалить
  19. Судя по названию импортированной функции, она добавляет строку типа Pchar в какой-то список. Отсюда можно предположить, что данный список будет потом как-то используется. Проблема состоит в том, что типы string и pchar не совместимы, а это значит, что в момент преобразования будет создана локальная переменная типа PChar, которая и будет передана AddToList. Далее, поскольку переменная локальная, то она будет уничтожена после вызова. Следовательно, в итоге наш список строк PChar будет пуст. А если точнее, то будет содержать неверные ссылки.

    ОтветитьУдалить
  20. >Судя по названию импортированной функции, она добавляет строку типа Pchar в какой-то список
    А с чего вы взяли, что она (функция) добавляет именно указатель? Это вообще тогда будет детская ошибка. Но наверняка предполагается что в dll обработка сделана как положено, без ошибок (через копирование строки), а проблемный код - перед нами.

    Повторюсь - реально что здесь возможно, так это путаница с Ansi/Wide Char, если используются разные версии компиляторов.

    ОтветитьУдалить
  21. >А с чего вы взяли, что она (функция) добавляет именно указатель?
    А что такое по Вашему тип PChar? Это и есть указатель.

    ОтветитьУдалить
  22. >А что такое по Вашему тип PChar? Это и есть указатель.
    Ну так это всего лишь параметр функции. И в Delphi есть такая замечательная функция - StrPas (http://docwiki.embarcadero.com/Libraries/XE5/en/System.SysUtils.StrPas) которая из этого указателя сделает полноценную строку, которую уже можно хранить сколь угодно долго.

    ОтветитьУдалить
  23. >И в Delphi есть такая замечательная функция - StrPas
    Т.е., Вы предполагаете, что библиотека, из которой вызывается функция AddToList, тоже написана на Delphi и собирается хранить строки в TStrings? А вот мне кажется, что библиотека может быть написана на С++, к примеру, и работает она именно с указателями, поскольку строки могут быть очень длинными, и копирование строк может приводить к неоправданному расходу вычислительных ресурсов.

    ОтветитьУдалить
  24. Думаю, что с кодом всё в порядке. Можно что-то испортить в AddToList, но при грамотной реализации данной функции ошибок не будет.

    ОтветитьУдалить
  25. Алексей Гаибов
    В условиях задачи написано: "Объяснить поведение кода и как его нужно исправить". Кода функции в dll мы не видим, а значит и исправлять там ничего не надо. И не важно, на каком языке он написан. Функцию StrPas я привёл как пример копирования строки. Аналогичные функции будут и в других языках программирования.

    Если вы убеждены, что библиотека оперирует указателем и никак не копирует строку во внутреннее представление, то как по вашему нужно исправить представленный код на Delphi, чтобы всё работало и небыло бы утечек памяти? Думаю понятно, что изменять API интерфейс dll и добавлять функцию выделения памяти нельзя? Хотя, я думаю, что даже добавив такую функцию, но оставив тип PChar и не беря в расчёт его изменчивый размер в Delphi, у вас всё равно останется код с ошибкой.

    ОтветитьУдалить
  26. Оформлю полный ответ:
    1. Нужно выяснить какой тип строк ожидает dll (ansi или wide) и исправить соответствующим образом прототип функции.
    2. В соответствии с новым прототипом функции из dll, привести нашу строку к ожидаемому типу (если у нас юникодные строки, а dll ожидает ansi, то мы столкнёмся с потенциальной потерей информации, т.к. однозначно конвертировать wide в ansi не всегда возможно).
    3. Передать в dll указатель на подготовленную строку правильного типа.

    ОтветитьУдалить
  27. Если вы убеждены, что библиотека оперирует указателем и никак не копирует строку во внутреннее представление, то как по вашему нужно исправить представленный код на Delphi, чтобы всё работало и небыло бы утечек памяти? Думаю понятно, что изменять API интерфейс dll и добавлять функцию выделения памяти нельзя?

    А я ничего в DLL исправлять не предлагаю. Достаточно явным образом выделять память для строки, которую передаем в импортированную функцию:
    StrNew(PChar(ListBox1.Items[X]))

    В этом случае строка будет размещена в куче, и останется там после вызова импортированной функции.

    Конечно, потом нужно будет освободить память, которую использует строка, но это забота уже другой части программы. Ведь, как я уже говорил: судя по названию функции список где-то используется.

    Ну сами-то подумайте - стоило ли приводить такой код только для того, чтобы проиллюстрировать возможные проблемы с PANSIChar/PWideChar? (хотя они, конечно, тоже могут быть)

    ОтветитьУдалить
  28. >Конечно, потом нужно будет освободить память

    А вы уверены, что dll написана таким образом, что у неё есть соответствующий метод, который вызывает нашу функцию освобождения памяти? Мне кажется, что это гораздо более смелое предположение по поводу поведения кода в dll, нежели предположение, что там выполняется копирование строки. По-моему как раз наоборот, раз нам ничего не сказано, что такая функция есть, то мы должны считать, что её нет, и всё взаимодействие dll с нашим кодом - это эта одна-единственная функция.

    >Ну сами-то подумайте - стоило ли приводить такой код только для того, чтобы проиллюстрировать возможные проблемы с PANSIChar/PWideChar? (хотя они, конечно, тоже могут быть)

    Судя по ответам - таки стоило :)

    ОтветитьУдалить
  29. А вы уверены, что dll написана таким образом, что у неё есть соответствующий метод, который вызывает нашу функцию освобождения памяти?

    Нашу функцию? Зачем? Я предполагаю, и это вобщем-то достаточно стандартный подход, что Список, который создается при помощи Dll, уничтожает стоки при своем уничтожении (либо ему можно указать как поступать, как например в TObjectList). В любом случае здравый смысл подсказывает, что я в контексте данной программы буду иметь доступ к элементам списка, и следовательно, смогу корректно освободить память.

    Осталось только узнать - что предполагал автор :)

    ОтветитьУдалить
  30. >Нашу функцию? Зачем?

    Как зачем? Если мы выделили память при помощи StrNew, то dll самостоятельно эту память никаким способом освободить не сможет, поскольку строкой заведует наш менеджер памяти.

    ОтветитьУдалить
  31. >В любом случае здравый смысл подсказывает, что я в контексте данной программы буду иметь доступ к элементам списка, и следовательно, смогу корректно освободить память.

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

    ОтветитьУдалить
  32. AddToList - предполагает что? ЧТЕНИЕ из переданного указателя? Или ЗАПИСЬ в него?

    ОтветитьУдалить
  33. AddToList предполагает передачу строки из приложения в dll.

    ОтветитьУдалить
  34. Александр, а можно сначала?

    Мне так кажется, что идеологически dll-ка в плагино-подобном стиле должна выполнять действия по обработки "глобального" хранилища данных, который управляется из ядра?
    Или как, dll-ка для выполнения своей операции вынуждена "откачивать" себе данных немного?
    Руки-то чешутся не входить в эту ситуацию, заполнить контейнер в ядре, а dll-ке дать на обработку (блин) указатель на (под)множество хранимых данных?

    А если посмотреть дальше в сторону разбиения "ядро-dll-ки" на концепцию сервисов с "бесшовным" преодолением адресного пространства, то "транспортное" проталкивание элементов списка по указателю вообще не поедет.

    Короче :) Можно прикладной контекст дать?

    ОтветитьУдалить
  35. Если AddToList сразу обрабатывает строку, или делает из нее локальную копию и нет путаницы с ansi - widechar, то никаких проблем в коде нет. У меня прекрасно подобный код работает с библиотеками без всяких ошибок.

    Условие задачи явно неполное, непонятно что происходит внутри dll.

    ОтветитьУдалить
  36. > откуда вы это берёте?
    жизнь подкидывает примеры: http://www.sql.ru/forum/1049047/kak-v-funkci-peredat-massiv-pchar :о)

    ОтветитьУдалить
  37. Так что не так с этим кодом-то, две недели вроде бы как бы прошло уже :)

    ОтветитьУдалить
  38. Хм.. предположу следующее:
    PChar(ListBox1.Items[X]) по-сути создает локальную переменную внутри функции, куда закидывает сей pchar, при выходе из функции эта память станет "неиспользуемой", т.к. в отличии от string, pchar не содержит ссылок использования (или как они там называются))). Соответственно эта память может быть перезаписана чем угодно и при обращении внутри dll, там может оказаться любой мусор)).
    Я достаточно давно программировал на delphi, но, насколько помню, нужно выделить память отдельно, скопировать туда строку, а потом уже передавать указатель... Ну, а очистку памяти оставим на усмотрение разработчика ;)

    ОтветитьУдалить
  39. При переводе String в PChar может потеряться часть строки из-за #0

    ОтветитьУдалить
  40. Судя по всему, блог заброшен, печально ;(

    ОтветитьУдалить
  41. Поспешный вывод, с последней публикации всего месяц прошел.

    ОтветитьУдалить
  42. >Что не так с этим кодом?
    Всё не так: написан на мертвом языке, вызывается перерисовка ListBox на каждом добавлении из-за отсутствия BeginUpdate/EndUpdate, противоречащие сути ООП антипаттерны.

    >как его нужно исправить
    Переписать на C#, избегая костыльных решений

    ОтветитьУдалить
  43. Да все тут вроде нормально. Само собой разумеется, что DLL-ка имеет тот же тип кодировки строк и при вызове AddToList выделит у себя память и скопирует туда текст, а не будет складировать указатели 'на потом' )

    ОтветитьУдалить
  44. Анонимный комментирует...

    >Что не так с этим кодом?
    Всё не так: написан на мертвом языке, вызывается перерисовка ListBox на каждом добавлении из-за отсутствия BeginUpdate/EndUpdate, противоречащие сути ООП антипаттерны.

    Вам и живой язык не поможет с такими понятиями интересными =)
    Причем тут перерисовка ListBox и ООП ?

    ОтветитьУдалить
  45. var i: word;
    begin
    for i:=0 to List1.Count-1 do
    ShowMessage(List1.Items[i]);
    ...
    Если в списке 0 элементов, появится исключение о недопустимом индексе
    http://lurkmore.to/Быдлокод

    ОтветитьУдалить
  46. Исправте свой косяк var i: word; на то, как показано в примере var i: integer; и уже в новом свете перечитайте лурк ;)

    ОтветитьУдалить
  47. В тонкостях я не мастер, но мне кажется что с PChar всё нормально, подразумеваться же PAnsiChar по умолчанию. Есть и в Windows.pas строки вида:
    function MessageBox(hWnd: HWND; lpText, lpCaption: PChar; uType: UINT): Integer; stdcall;
    Насчёт procedure вроде тоже сойдёт, тоже что и void функция. Остальное тоже вроде без помарок, только не понятно направление данных - не dll заполняет лист, а наоборот, странно.
    Может с ним всё нормально? С:

    ОтветитьУдалить
  48. Не знаю каков правильный ответ, но я создал из примера Чудищще... И оно живое... Память как ни странно не ест, но Винда уходит просто в Астрал...

    library aSomeDll;
    uses Windows;
    var w: HWND;
    procedure SetWND(h: HWND); stdcall;
    begin
    w:=h;
    end;
    procedure AddToList(Item: PChar); stdcall;
    begin
    SetWindowText(w,Item);
    end;
    exports SetWND, AddToList;
    begin
    end.

    . . .

    procedure AddToList(Item: PChar); stdcall; external 'aSomeDll.dll';
    procedure SetWND(w: HWND); stdcall; external 'aSomeDll.dll';
    procedure TForm1.FormCreate(Sender: TObject);
    begin
    SetWND(Handle);
    ListBox1.Items.Add('0100');
    ListBox1.Items.Add('1001');
    ListBox1.Items.Add('011-');
    end;
    procedure TForm1.Button1Click(Sender: TObject);
    var X: Integer;
    begin
    for X:=0 to ListBox1.Items.Count-1 do AddToList(PChar(ListBox1.Items[X]));
    end;
    procedure TForm1.Button2Click(Sender: TObject);
    var X: Integer;
    begin
    for X:=0 to MaxInt do Button1.Click;
    end;

    Ни в коем случае не запускайте! >:3

    ОтветитьУдалить
  49. var X: Integer; // по ГОСТу имена переменных с мелкой буквы нужно! :D

    ОтветитьУдалить
  50. Анонимный1 июля 2014 г., 23:40

    Александр, не томи, por favore) Что там с памятью?

    ОтветитьУдалить
  51. В чём подвох? Код-то работает...

    for x:=0 to listbox1.Items.Count-1 do
    MessageBox(0,pchar(listbox1.Items[x]),nil,0);

    Кстати, кто может объяснить по какой причине listbox1.Items[x] на каждой итерации возвращает одну и ту же строку (указатель на один и тот же блок меняющихся данных)? И почему счётчик ссылок на этот блок равен единице и остаётся таким до самого выхода из процедуры, сколько бы ни было дальше кода?

    ОтветитьУдалить
  52. PChar(s) возвращает указатель на (null-terminated) строку, но не изменят счетчик ссылок исходной строки. Исходная строка будет жить, пока на неё есть ссылки, т.е. как минимум до момента, пока из списка не будет удален соответствующий элемент. А потом - как повезет. Если на строку была только одна ссылка, то область памяти, где размещалась строка, может быть использована повторно. В этом случае при обращении к строки из DLL можно получить просто мусор, а можно и ошибку доступа к памяти.
    Подробнее здесь: http://www.transl-gunsmoker.ru/2009/09/pchars.html#mixing (Использования вместе String и PChar)

    ОтветитьУдалить
  53. PChar(s) возвращает указатель на (null-terminated) строку, но не изменят счетчик ссылок исходной строки. Исходная строка будет жить, пока на неё есть ссылки, т.е. как минимум до момента, пока из списка не будет удален соответствующий элемент. А потом - как повезет. Если на строку была только одна ссылка, то область памяти, где размещалась строка, может быть использована повторно. В этом случае при обращении к строки из DLL можно получить просто мусор, а можно и ошибку доступа к памяти.
    Подробнее здесь: http://www.transl-gunsmoker.ru/2009/09/pchars.html#mixing (Использования вместе String и PChar)

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

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

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

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

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

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