25 февраля 2010 г.

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

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

Проблема в приведённом коде кроется в управлении кодом ошибки. На первый взгляд - проблем нет.

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

Но, вроде бы, вызов SetLastError и есть последнее действие в программе?

Но тут на сцену выходит магия компилятора Delphi. В нашей процедуре мы используем переменную WideString, которая, как известно, относится к т.н. авто-финализируемым типам (их ещё называют managed-типами) - т.е. типам, временем жизни которых управляет компилятор.

Окей, тогда наводящий вопрос: а где в Delphi освобождаются авто-финализируемые типы данных? Очевидно, в нашем случае это произойдёт в строке с "end;" - т.е. после вызова SetLastError.

Вот вам и место проблемы. Финализация WideString означает безусловный вызов SysFreeString (упражнение: объяснить почему; подсказка: как связаны WideString и BSTR?). Вызов же SysFreeString сбрасывает код ошибки. Что означает, что ваша подпрограмма возвращает мусор, а не ERROR_INVALID_PARAMETER, как вы планировали. Ооопс.

Фактически, это означает, что вы не можете вернуть код ошибки из функции, где вы используете WideString.

Но это ещё не всё! Ведь помимо WideString у нас полно других (и даже более родных) управляемых типов данных. Что с ними? А с ними всё аналогично. Строки (String), дин. массивы, интерфейсы и т.п. - все они будут удалены в "end;". Что, фактически, означает как минимум вызов функций менеджера памяти, а как максимум - вызов деструктора объекта произвольной сложности (для интерфейсов). И плакали ваши коды ошибок.

Отсюда можно сделать вывод, что вы вообще не можете вернуть код ошибки из подпрограммы, где вы использовали хоть один (любой) управляемый тип данных. А любой такой работающий сегодня код работает исключительно благодаря случайности (*)!

Вообще-то это баг. И это баг Delphi. Кой следует сообщить на QC, что я, пожалуй и сделаю в ближайшее время. Кстати, это не единственный баг Delphi с GetLastError/SetLastError. Например, давно уже я сообщил о таком баге. Как видим, похоже, что Delphi вообще очень небрежно относится к кодам ошибок, что делает программирование на них исключительно сложным делом.

Как это можно исправить? Исключительно использованием wrapper-ов, например так:
function DoSomethingW(const AStr: PWideChar): Integer; stdcall;

function RealDoSomething(const AStr: PWideChar; out Rslt: Integer): Integer;
var
S: WideString;
begin
Result := ERROR_SUCCESS;
S := AStr;

if (S <> '') and SomeCondition then
Rslt := Length(S) // положим, это и есть реальная работа
else
begin
Result := ERROR_INVALID_PARAMETER;
Rslt := 0;
end;
end;

begin
SetLastError(RealDoSomething(AStr, Result));
end;

(*) Окей - не каждый такой вызов может приводить к смене кода ошибки. Например, освобождение памяти (строка или массив) могут привести только к записям в память, когда менеджер памяти делает правки в своих внутренних структурах учёта - случай, когда он решает придержать память. Но гарантий нет - это и называется "исключительно благодаря случайности".

10 комментариев :

  1. Супер. Когда смотрел задачку, никакого косяка сразу не увидел, а искать косяк, не зная, как он проявляется, трудно. Очень интересно, хоть и бесполезно: ни разу ещё не доводилось работать через коды ошибок.

    ОтветитьУдалить
  2. Справедливо. Задачка сложная, особенно, если учесть, что там участвует баг Delphi.

    Возможно, мне бы и стоило дать подсказку, но я не знал как: с одной стороны - см. выше, с другой - в первых же ответах дали решение (причём, кажется, слизанное). Так что я был "в непонятках": не то задача сложна, не то проста.

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

    P.S. В чём был тайный смысл постить чужое готовое решение - я так и не понял. Мои задачки - это аналог квинтаны. Читеря, вы не обманываете никого, кроме самого себя.

    ОтветитьУдалить
  3. >>> Очень интересно, хоть и бесполезно: ни разу ещё не доводилось работать через коды ошибок.
    Ну это вообще кому как. Не всем нужно писать DLL и общаться с другими языками (в качестве вызываемой стороны), но уж кому надо - у тех это типичная задача.

    ОтветитьУдалить
  4. А какова верояность что в SysFreeString будет вызов SetLastError, учитывая, что компилятор сам управляет выделением памяти? И есть ли он там?

    ОтветитьУдалить
  5. А почему бы просто не возвращать коды ошибок из функций?

    ОтветитьУдалить
  6. >>> А какова верояность что в SysFreeString будет вызов SetLastError
    Ну вы же не станете писать программы, опираясь на вероятность? Нужен чётко детерминированный способ.

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

    ОтветитьУдалить
  7. Бесполезная статья. У меня работает всё корректно
    код
    DoSomethingW('');
    ShowMessageFmt('%d', [GetLastError]);
    Показывает 87
    Это проверено на моих Delphi7, D2005, D2007, D2010, DXE, DXE3. У Вас от кривых компонентов был кривой менеджер памяти который можно подменять начиная с Delphi2 - читайте книгу Секреты Delphi2 Рея Лишнера.

    ОтветитьУдалить
  8. Хотя проверил строка '' равна nil по этому у меня работало корректно если поменять условие S='' и передавать DoSomethingW('test') то возвращается действительно не то что надо.

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

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

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

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

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

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