Просмотр элементов TDictionary и TObjectDictionary
— Здесь вместо TStringList.AddObject было бы лучше использовать TObjectDictionary. -Не могу. Мне надо получить список всех объектов, а у TDictionary нельзя обратиться к его элементу по
Притча о правильных именах
Вот начитаешься Мартина и начинаешь функции называть просто, понятно и чтобы обязательно отражали суть выполняемого действия. GetFileType, – ну отличное же название. Всё по делу.
Притча о правильных именах
Вот начитаешься Мартина и начинаешь функции называть просто, понятно и чтобы обязательно отражали суть выполняемого действия. GetFileType, – ну отличное же название. Всё по делу.
Добавление EurekaLog в программу вызывает Access Violation?
К нам обратился человек, который пожаловался на то, что его приложение работало нормально, пока он не добавил в него EurekaLog. После включения в проекте EurekaLog стало появляться исключение AccessViolation с текстом Access violation at address 00E15025 in module ‘Project.exe’. Read of address 83EC8B69 и таким стеком:
- Contoso.pas TContosoEventMessage.BasePerform
- Vcl.Forms.pas TApplication.WndProc
- System.Classes.pas StdWndProc
- Vcl.Forms.pas TApplication.HandleMessage
- Vcl.Forms.pas TApplication.Run
- Project.dpr Initialization
(все имена заменены; Contoso — это некая известная библиотека для Delphi).
Воспроизводимый пример человек предоставить не захотел, но, к счастью, у нас был доступ к исходному коду библиотеки.
Адрес в сообщении об ошибке (83EC8B69 — «случайный») и короткий стек могут указывать на проблему «управление ушло по мусорному указателю и вылетело в случайном месте». К сожалению, в отчёте не были включены секции CPU и ассемблера (отключены в настройках). В этом случае диагностика по исходному коду была бы невозможной. Признаться, я сначала хотел так и ответить, дав рекомендацию по усилению контроля за памятью.
Но вот именно такой стек возможен также в случае, если BasePerform
вызывается главным потоком как процедура, поставленная в очередь для Synchronize
. И действительно — поиск по исходникам выдал такой код:
function TContosoEventMessage.Perform(AThread: TContosoThread): Boolean;
begin
FMsgThread := AThread as TContosoEventThread;
if FMsgThread.Active then begin
if FMsgThread.Options.Synchronize then
TContosoThread.Queue(nil, BasePerform)
else
BasePerform;
end;
Result := True;
end;
Т.е. у класса сообщения есть функция для обработки этого сообщения. И если в опциях чего-то там указан флаг Synchronize
, то непосредственно обработка сообщения (BasePerform
) будет выполнена не в текущем потоке, а будет отправлена на выполнение в главный поток — через TThread.Queue
.
И да, в отчёте также был виден этот фоновый поток от Contoso, который мог бы выполнить такой код.
Сам метод BasePerform
является просто обёрткой вокруг InternalHandle
:
procedure TContosoEventMessage.BasePerform;
begin
FMsgThread.InternalHandle(Self);
end;
И отчёт EurekaLog показывает, что вылет произошёл на первой же строчке.
Что мы имеем?
- Фоновый поток поставил в очередь метод, заполнив поле
FMsgThread
. - Главный поток попробовал выполнить этот метод и вылетел при попытке прочитать поле
FMsgThread
.
Вторая строка является предположением, поскольку, как я уже сказал, ассемблера в отчёте не было. Но гипотеза вполне себе, поскольку если иначе вылет был бы внутри InternalHandle
.
Т.е. вроде как налицо проблема с памятью. И, кстати говоря, это самая частая «проблема» проектов, которые включают EurekaLog. В самом деле, посмотрите на такой код:
procedure TForm1.Button1Click(Sender: TObject);
var
List: TList;
begin
List := TList.Create;
List.Free;
List.Clear; // - использование объекта после удаления
end;
Корректный ли это код? Нет, конечно. Но будет ли он «работать». Да, с большой вероятностью он корректно отработает. Происходит это по той простой причине, что «освобождённая» память (и, следовательно, «освобождённый» объект) в действительности не удаляются, а просто помечаются как свободные. Т.е. их память остаётся доступной в неизменном виде. Конечно, если между особождением памяти/объекта и повторным доступом к нему будет много операций, то есть какой-то шанс, что они испортят бывшую память. Но на коротких пробегах вы не заметите проблемы.
Добавление EurekaLog в программу меняет ситуацию на корню, поскольку EurekaLog по умолчанию включает проверки памяти и активно борется с указанными багами.
Что-ж, посмотрим может ли у нас сейчас быть такая ситуация. Для этого нужно понять где освобождается память для FMsgThread
. Поскольку он приходит из параметра метода — посмотрим, кто вызывает метод BasePerform
. Поиск по исходникам подсказал такой код (поиск показал несколько мест, но конкретное место было выбрано исходя из состояние фонового потока из отчёта):
procedure TContosoThread.Execute;
// ...
begin
// ...
for i := 0 to oReceivedMsgs.Count - 1 do
begin
oMsg := TContosoThreadMsgBase(oReceivedMsgs[i]);
try
if not oMsg.Perform(Self) then
Break;
finally
oMsg.Destroy;
end;
// ...
end;
Оп, а вот он и баг. Сценарий происходящего:
- Фоновый поток перебирает входящие сообщения.
- Каждое сообщение обрабатывается (
Perform
). - Какое-то сообщение указывает, что его нужно синхронизировать в главный поток.
- Фоновый поток ставит сообщение в очередь (
Queue
). - Фоновый поток удаляет сообщение (
Destroy
), но указатель на сообщение всё ещё сидит в очереди. - Главный поток приступает к обработке запланированных сообщений (
BasePerform
). - Обработка сообщения вызывает Access Violation, потому что сообщение уже удалено.
Т.е. баг тут либо в управлении памятью, либо в синхронизации потоков. Как возникла такая ошибка? Очевидно, изначально были написаны базовые классы, которые предполагали линейное выполнение: «взяли сообщение — обработали — освободили». Потом был написан особый класс-наследник, который переопределил поведение «обработать» на «поставить в очередь и выйти». А базовый класс (с циклом) про это не в курсе. Что и привело к багу.
По сути, класс-наследник явно нарушил неявный контракт: все аргументы функции валидны только на время вызова функции. Т.е. если вы сохраняете аргументы функции куда-то, где они будут доступны после завершения функции, то вы должны убедиться, что аргументы будут доступны. Класс-наследник этого не сделал.
Я подозреваю, что исправление проблемы могло бы быть в замене Queue
на Synchronize
.
Вот такое расследование получилось — исключительно по исходному коду незнакомой библиотеки и неполному отчёту о вылете, не имея перед собой рабочего примера.
Табличные переменные в динамическом SQL
Табличные переменные являются одной из интересных возможностей MS SQL Server. Эта удобная альтернатива временным таблицам, которую можно использовать для хранения небольших наборов данных в виде
Варианты подключения к Oracle в UniDAC
Лет 20-25 тому назад каждая программа для работы с базами данных написанная на Delphi таскала за собой десятки мегабайт Borland Database Engine (BDE). Программисты и
Ходячие мертвецы. Bold for Delphi теперь Open Source
В конце 20-го века стал набирать популярность модельно-ориентированный подход к разработке программного обеспечения. Концепция Мodel Driven Architecture (MDA) разрабатывается консорциумом Object Management Group (OMG). Среди
Ходячие мертвецы. Bold for Delphi теперь Open Source
В конце 20-го века стал набирать популярность модельно-ориентированный подход к разработке программного обеспечения. Концепция Мodel Driven Architecture (MDA) разрабатывается консорциумом Object Management Group (OMG). Среди
Ответ на задачку №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 не менялась. Возможно. Я не знаю.
В караоке-бар требуется официант со знанием Borland Delphi
В октябре 2019 в Минске была опубликована вакансия уборщика офисных помещений, которому требовались знания BPWin и Borland Delphi. Теперь караоке-бар в Солигорске ищет официанта