Ответ на задачку №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 не менялась. Возможно. Я не знаю.

Читать на сайте автора.