18 сентября 2020 г.

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

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

Не сказать, что было много вариантов ответов, но один ответ был достаточно близок к истине. Да, задачка была сильно неочевидная.

Напомню код функции:
function InitDeflate(const ACompressionLevel: Byte): TZStreamRec;
var
  Code: Integer;
begin
  FillChar(Result, SizeOf(Result), 0);
  Code := System.ZLib.deflateInit_(Result, ACompressionLevel, zlib_version, SizeOf(TZStreamRec)));
   
  // ... далее идёт анализ Code
  // в данном случае Code = Z_OK
end;
Запись TZStreamRec выглядит так:
type
  TZStreamRec = record
    next_in: PByte;      // следующий входной байт
    avail_in: Cardinal;  // число байт в next_in
    total_in: LongWord;  // сколько уже байт прочитали
    next_out: PByte;     // сюда будет записан следующий выходной байт
    avail_out: Cardinal; // сколько осталось свободного места в next_out
    total_out: LongWord; // сколько всего байт записали
    msg: PAnsiChar;      // последнее сообщение об ошибке, nil если ошибки не было
    state: Pointer;      // недокументировано
    zalloc: alloc_func;  // для выделения памяти для state
    zfree: free_func;    // для освобождения памяти state
    opaque: Pointer;     // "непрозрачный" параметр для zalloc и zfree
    data_type: Integer;  // возможный тип данных (двоичные/текст) для сжатия, 
                         // или состояние процесса для распаковки 
    adler: LongWord;     // контрольная сумма распакованных данных
    reserved: LongWord;  // зарезервированно, должно быть 0
  end;
Короче говоря, это достаточно простая функция-обёртка. И обычно она будет работать без проблем. Вернее, она-то всегда будет работать нормально - в том смысле, что она всегда будет возвращать Z_OK. Но иногда любая попытка воспользоваться результатом функции (например, в вызове deflate) вернёт ошибку Z_STREAM_ERROR в первом же вызове!

Проблема кроется в недокументированном поле state. Если мы откроем исходники ZLib, то увидим такой код (сокращено):
typedef struct internal_state {
    z_streamp strm;      /* указатель на TZStreamRec, которому принадлежит это состояние */
    int   status;        /* как следует из названия */
    /* ... */ 
Иными словами, поле state - это указатель на запись, в поле которой записывается указатель на родительскую запись (TZStreamRec). Что-то вроде Owner-а у классов. Проблема в том, что записи - это не классы. Их экземпляры передаются по значению, а не по ссылке (как объекты). Это значит, что если мы присвоим TZStreamRec другой переменной - это скопирует все поля записи в новую область памяти (переменную). У этой переменной будет свой собственный адрес, не совпадающий со старым. В то же время операция присваивания ничего не знает про поле state, поэтому оно не будет изменено и продолжит указывать на старую запись. Вызов deflate увидит, что указатель strm не указывает на валидную запись TZStreamRec и вернёт Z_STREAM_ERROR.

Но где же проблема? Действительно, при вызове функции компилятор передаст в неё указатель на переменную (обычно - стек), внутри функции этот указатель будет передан сначала в FillChar, а затем и в deflateInit. Поскольку всюду запись передаётся по указателю - проблем быть не должно:
; ZLIBStream := InitDeflate(9);
  lea ecx,[ebp-$4d]
  mov al,$09
  call InitDeflate
Но иногда компилятор может решить сделать такое:
; ZLIBStream := InitDeflate(9);
  mov rcx,rbp
  lea rdx,[rbp+$48]
  mov r8b,$09
  call InitDeflate
  lea rdi,[rbp+$000000b0]
  lea rsi,[rbp+$48]
  mov ecx,$0000000c
  rep movsq
Т.е. на псевдокоде:
Tmp := InitDeflate(9);
ZStream := Tmp;
Вот вам и копирование. Вот вам и проблема.

Не сказать, что это прям совсем баг. Да, это лишняя операция. Да, её делать не нужно. Но она технически корректна, и компилятор имеет полное право её выполнить.

Чья же здесь ошибка? Я считаю, что программиста. Мы неявно использовали допущение о внутренней реализации. Это примерно как задачка №13.

Как это можно исправить? Передавать ссылку явно:
procedure InitDeflate(const ACompressionLevel: Byte; out Result: TZStreamRec);
var
  Code: Integer;
begin
  FillChar(Result, SizeOf(Result), 0);
  Code := System.ZLib.deflateInit_(Result, ACompressionLevel, zlib_version, SizeOf(TZStreamRec)));
   
  // ...
end;
Разумеется, если вы делаете все операции в рамках одной функции, то эта проблема также не стоит.

P.S. Я ругался на "нововведения" ZLib, потому что "ранее этот код работал" (со старой ZLib) и упоминания о такой детали реализации или же требовании я в документации не нашёл. Но пока писал ответ, мне пришло в голову, что, возможно, дело в компиляторе Delphi. Возможно, где-то когда-то что-то поменяли. И ранее там не было скрытой временной переменной, а реализация в ZLib не менялась. Возможно. Я не знаю.

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

  1. Доброго времени суток!

    Подскажите, какие стадии выпуска программы должен пройти разработчик? Ну вот программа написана, а дальше? Указать версию - где? Добавить информацию о разработчике - где? Что делать, если Windows блокирует запуск из-за того, что программа не подписана? Покупать сертификат за $500+ каждый год? Как выпустить программу инди-разработчику? Как понять, правильно ли реализована функция (например, программа, которая появляется только по клике на иконку в трее, а затем исчезает, потеряв фокус)? Нужен ли установщик, если программа - это один только exe-файл? И каким должен быть установщик, иконка удаления? Как определить, считает ли система программу полноценной, а не скомпилированным куском скрипта (не знаю, как объяснить)?

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

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

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

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

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

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