Порядок финализации модуля для приложения, скомпилированного с пакетами времени выполнения?

Мне нужно выполнить мой код после завершения модуля SysUtils.

Я поместил свой код в отдельный модуль и сначала включил его в раздел использования dpr-файла, например:

project Project1;

uses
  MyUnit,    // <- my separate unit
  SysUtils,
  Classes,
  SomeOtherUnits;

procedure Test;
begin
  //
end;

begin
  SetProc(Test);
end.

MyUnit выглядит так:

unit MyUnit;

interface

procedure SetProc(AProc: TProcedure);

implementation

var
  Test: TProcedure;

procedure SetProc(AProc: TProcedure);
begin
  Test := AProc;
end;

initialization

finalization
  Test;
end.

Обратите внимание, что MyUnit не используется.

Это обычный Windows exe, без консоли, без форм и скомпилированный с пакетами времени выполнения по умолчанию. MyUnit не является частью какого-либо пакета (но я тоже пытался использовать его из пакета).

Я ожидаю, что раздел финализации MyUnit будет выполнен после раздела финализации SysUtils. Это то, что мне подсказала помощь Delphi.

Тем не менее, это не всегда так.

У меня есть 2 тестовых приложения, которые немного отличаются по коду в тестовой подпрограмме/dpr-файле и модулях, перечисленных в использовании. MyUnit, однако, во всех случаях указывается первым.

Одно приложение запускается как положено: Halt0 -> FinalizeUnits -> ...другие модули... -> финализация SysUtils -> финализация MyUnit -> ...другие модули...

Но второй нет. Финализация MyUnit вызывается до финализации SysUtils. Фактическая цепочка вызовов выглядит следующим образом: Halt0 -> FinalizeUnits -> ...другие модули... -> финализация SysUtils (пропущена) -> финализация MyUnit -> ...другие модули... -> финализация SysUtils (выполнено)

Оба проекта имеют очень похожие настройки. Я много пытался убрать/свести к минимуму их различия, но до сих пор не вижу причины такого поведения.

Я попытался отладить это и обнаружил следующее: кажется, что у каждого модуля есть какой-то подсчет ссылок. И кажется, что InitTable содержит несколько ссылок на один и тот же модуль. Когда секция финализации SysUtils вызывается в первый раз, она меняет счетчик ссылок и ничего не делает. Затем выполняется финализация MyUnit. Затем снова вызывается SysUtils, но на этот раз ref-count достигает нуля и выполняется секция финализации:

Finalization: // SysUtils' finalization
5003B3F0 55               push ebp          // here and below is some form of stub
5003B3F1 8BEC             mov ebp,esp
5003B3F3 33C0             xor eax,eax
5003B3F5 55               push ebp
5003B3F6 688EB50350       push $5003b58e
5003B3FB 64FF30           push dword ptr fs:[eax]
5003B3FE 648920           mov fs:[eax],esp
5003B401 FF05DCAD1150     inc dword ptr [$5011addc] // here: some sort of reference counter
5003B407 0F8573010000     jnz $5003b580     // <- this jump skips execution of finalization for first call
5003B40D B8CC4D0350       mov eax,$50034dcc // here and below is actual SysUtils' finalization section
...

Может ли кто-нибудь пролить свет на этот вопрос? Я что-то упускаю?


person Alex    schedule 13.04.2010    source источник


Ответы (4)


Единицы финализируются в порядке, обратном инициализации. Порядок инициализации определяется нециклическим (т. е. никогда не спускающимся в уже посещенный блок) обходом графа использования блока в постпорядке, начиная с пункта main uses (в программе или библиотеке). SysInit обычно инициализируется первым модулем, за которым следует System.

Динамическая загрузка пакетов все усложняет, потому что основной EXE или DLL должен указать порядок инициализации модулей, используемых основным образом. Таким образом, когда пакет загружается динамически, он будет запускать то, что, по его мнению, должно быть порядком инициализации, но уже инициализированные модули будут пропущены; когда пакет динамически выгружается, это происходит в обратном порядке.

Общие правила:

  • вещи более низкого уровня должны быть инициализированы перед вещами более высокого уровня
  • финализация должна быть в порядке, обратном инициализации

Эти правила почти всегда имеют смысл. Инициализация модулей более высокого уровня часто зависит от услуг, предоставляемых модулями более низкого уровня. Например, без SysUtils в Delphi нет поддержки исключений. Финализация в обратном порядке имеет смысл по той же причине: финализации высокого уровня полагаются на службы, предоставляемые модулями более низкого уровня, поэтому они должны запускаться до финализации модулей более низкого уровня.

Все сказанное в отношении вашей проблемы звучит так, будто где-то в компиляторе или RTL есть ошибка, если то, что вы говорите, верно: основной EXE-файл сначала использует MyUnit, а MyUnit не использует никаких других модулей в своем интерфейсе или реализация, и с динамически загружаемыми пакетами не бывает ничего смешного. Все, что я могу предложить, это продолжать урезать проект со странным поведением, пока у вас не будет минимального воспроизводимого образца; в этот момент должно быть ясно, что именно вызывает проблему.

person Barry Kelly    schedule 13.04.2010
comment
Все пакеты статически связаны и динамической загрузки нет - это точно. Спасибо за ответ, но я этого боялся :( Я надеялся, что кто-нибудь может дать несколько советов, чтобы искать конкретные вещи, потому что я не знаю, где еще искать. Я попробую еще несколько раз... - person Alex; 13.04.2010

Я смог найти причину, и теперь я чувствую себя немного глупо :)

Мое второе тестовое приложение имеет статическую ссылку на DLL, которая была скомпилирована с RTL.bpl (она пуста, за исключением ссылок на SysUtils и имеет 1 простую процедуру). Таким образом, поскольку DLL статически связана, она инициализируется до того, как любой код из exe сможет запуститься.

Вот и все:

System DLL -> SysUtils DLL -> System exe (пропущено) -> MyUnit -> exe SysUtils (пропущено) -> и т. д.

Завершения выполняются в обратном порядке, что приводит к выполнению MyUnit перед SysUtils.

Решение: требуется сначала включить MyUnit во все проекты.

(о, как я хочу иметь машину времени, чтобы путешествовать во времени и заставлять кого-нибудь добавить событие OnBeforeMMShutdown :D)

person Alex    schedule 20.04.2010

Вы ничего не упускаете. Это именно то, что происходит.

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

Есть ли какая-то конкретная причина, по которой ваш MyUnit должен быть завершен после SysUtils?

person Mason Wheeler    schedule 13.04.2010
comment
› Есть ли какая-то конкретная причина, по которой ваш MyUnit необходимо завершить после SysUtils? Ага. Это чеки с утечкой памяти. - person Alex; 13.04.2010
comment
@Alexander: Делает ли он что-то особенное, чего нельзя добиться с помощью FastMM в FullDebugMode? - person Mason Wheeler; 13.04.2010
comment
› Делает ли он что-то особенное, чего нельзя добиться с помощью FastMM в режиме FullDebugMode? Это не тот вопрос, который я задаю, так что давайте отложим его. Во втором моем приложении FastMM не работал ровно по той же причине: слишком рано его назвали. - person Alex; 13.04.2010
comment
(в качестве превентивного комментария: я вообще не пытаюсь использовать FastMM, поэтому не предлагайте AQTime или что-то в этом роде - я просто пытался использовать FastMM в качестве дополнительной проверки, если я полностью глуп или нет; исходная проблема - неожиданное выполнение порядок; я пытаюсь понять, что это такое, и посмотреть, смогу ли я это контролировать) - person Alex; 13.04.2010

Я не уверен, но разве не существует старой доброй глобальной переменной ExitProc, известной как Turbo/BorlandPascal? Если да, это может решить вашу проблему.

person dummzeuch    schedule 13.04.2010
comment
Да, это. К сожалению, эта подпрограмма вызывается перед вызовом любого раздела завершения. Так что, это слишком рано. Есть еще событие ExitProcess, но оно вызывается непосредственно перед завершением процесса - слишком поздно. (Я не пишу менеджер памяти - только устанавливаю фильтр; поэтому для меня важно выполнить мой код до финализации системы, когда менеджер памяти по умолчанию может освободить всю память) Черт, я думаю об установке хука на финализацию системы :( Просто шутя. - person Alex; 14.04.2010
comment
@Alexander: Возможно, привязка к финализации SysUtils не такая уж плохая идея, если вы не можете решить ее иначе. - person dummzeuch; 14.04.2010