Не сказать, что было много вариантов ответов, но один ответ был достаточно близок к истине. Да, задачка была сильно неочевидная.
Напомню код функции:
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 не менялась. Возможно. Я не знаю.
Доброго времени суток!
ОтветитьУдалитьПодскажите, какие стадии выпуска программы должен пройти разработчик? Ну вот программа написана, а дальше? Указать версию - где? Добавить информацию о разработчике - где? Что делать, если Windows блокирует запуск из-за того, что программа не подписана? Покупать сертификат за $500+ каждый год? Как выпустить программу инди-разработчику? Как понять, правильно ли реализована функция (например, программа, которая появляется только по клике на иконку в трее, а затем исчезает, потеряв фокус)? Нужен ли установщик, если программа - это один только exe-файл? И каким должен быть установщик, иконка удаления? Как определить, считает ли система программу полноценной, а не скомпилированным куском скрипта (не знаю, как объяснить)?