Сжатие DFM ресурсов в Delphi программах

в 9:40, , рубрики: Delphi, DFM

Захотелось мне как-то попробовать сжать ресурсы dfm форм своего приложения, плюсы довольно спорные (сложные формы могут содержать много графических ресурсов которые в dfm файле хранятся как буфер с bmp, который можно неплохо сжать, так же защита от просмотра и редактирования ресурсов форм), но ведь есть несколько программ позволяющих такое делать, значит кому-то нужно.

Напишем приложение DFMCompressor, которое будет извлекать dfm ресурсы из exe файла, сжимать их и записывать обратно заменяя оригиналы.

Алгоритм работы компрессора

Компрессор находит dfm ресурсы и сжимает их. Всю его работу можно разложить на шаги:

  • Извлечь все DFM ресурсы приложения
  • Сжать их
  • Удалить из приложения найденные ресурсы
  • Записать сжатые ресурсы в приложение

Для единообразия дальнейшего кода реализации указанных шагов введем специальный тип, словарь, который будет содержать имя ресурса и его тело:

type
  //Словарь содержащий имена DFM ресурсов и их содержимое 
  TDFMByNameDict = TObjectDictionary<string, TMemoryStream>;

Большая часть компрессора завязана на работу с ресурсами exe файла. Windows API содержит функции для работы с ресурсами, нам понадобятся две основные функции:

  • EnumResourceNames — получение имен ресурсов
  • UpdateResource — добавление/удаление ресурсов

Так как мы будем работать с ресурсами только в контексте Delphi DFM ресурсов, то, чтобы упростить код, сделаем следующие допущения:

  • Все операции относятся только к ресурсам типа RT_RCDATA
  • LangId ресурсов всегда используется 0, так как именно такой LangId у dfm форм

Поиск DFM ресурсов

Алгоритм простой, пройдем по всем ресурсам из RT_RCDATA, и проверим являются ли они DFM ресурсами.

DFM ресурсы имеют сигнатуру, первые 4 байта содержат строку 'TPF0', напишем функцию чтобы проверять:

function IsDfmResource(Stream: TStream): Boolean;
const
  FilerSignature: array [1..4] of AnsiChar = AnsiString('TPF0');
var
  Signature: LongInt;
begin
  Stream.Position := 0;
  stream.Read(Signature, SizeOf(Signature));

  Result := Signature = LongInt(FilerSignature);
end;

Теперь, умея отличать DFM ресурсы от остальных напишем функцию получения их:

function LoadDFMs(const FileName: string): TDFMByNameDict;

  //Callback-функция для перечисления имен ресурсов
  //вызывается когда найден очередной ресурс указанного типа
  function EnumResNameProc(Module: THandle; ResType, ResName: PChar;
    lParam: TDFMByNameDict): BOOL; stdcall;
  var
    ResStream: TResourceStream;
  begin
    Result := True;

    //Откроем ресурс
    ResStream := TResourceStream.Create(Module, ResName, ResType);
    try
      //Если это не DFM выходим
      if not IsDfmResource(ResStream) then
        Exit;

      //Если DFM ресурс, то скопируем его тело в результирующий список
      lParam.Add(ResName, TMemoryStream.Create);
      lParam[ResName].CopyFrom(ResStream, 0);
    finally
      FreeAndNil(ResStream);
    end;
  end;

var
  DllHandle: THandle;
begin
  Result := TDFMByNameDict.Create([doOwnsValues]);
  try
    DllHandle := LoadLibraryEx(PChar(FileName), 0, LOAD_LIBRARY_AS_DATAFILE);
    Win32Check(DllHandle <> 0);
    try
      EnumResourceNamesW(DllHandle, RT_RCDATA, @EnumResNameProc, Integer(Result));
    finally
      FreeLibrary(DllHandle);
    end;
  except
    FreeAndNil(Result);
    raise;
  end;
end;

Cжимаем содержимое найденных ресурсов

Жать будем с помощью Zlib, вот такая функция сжимает TMemoryStream:

procedure ZCompressStream(Source: TMemoryStream);
var
  pOut: Pointer;
  outSize: Integer;
begin
  ZCompress(Source.Memory, Source.Size, pOut, outSize, zcMax);
  try
    Source.Size := outSize;
    Move(pOut^, Source.Memory^, outSize);
    Source.Position := 0;
  finally
    FreeMem(pOut);
  end;
end;

Теперь легко написать процедуру которая будет сжимать все ресурсы из нашего списка:

procedure CompressDFMs(DFMs: TDFMByNameDict);
var
  Stream: TMemoryStream;
begin
  for Stream in DFMs.Values do
    ZCompressStream(Stream);
end;

Удаление ресурсов

Чтобы удалить ресурс нужно вызвать функцию UpdateResource и передать в нее пустой указатель на данные. Но штука в том, что удаление ресурсов реализовано так, что оно не уменьшает exe файл, Windows просто удаляет запись о ресурсе из таблицы ресурсов, при этом место который занимал ресурс остается и никуда не перераспределяется. У нас цель не просто зашифровать dfm'ки, но и уменьшить на их сжатии общий размер программы, поэтому Win API не поможет. Благо есть решение, библиотека madBasic из madCollection содержит модуль madRes.pas, в котором реализованы функции по работе с ресурсами, в том числе и удаление ресурсов, при этом авторы постарались и сделали вызов функций совместимым по синтаксису с Windows API, за что отдельное спасибо.

Зная все это процедура удаления ресурсов получилась такой:

procedure DeleteDFMs(const FileName: string; DFMs: TDFMByNameDict);
var
  ResName: string;
  Handle: THandle;
begin
  Handle := MadRes.BeginUpdateResourceW(PChar(FileName), False);
  Win32Check(Handle <> 0);
  try
    for ResName in DFMs.Keys do
      Win32Check(MadRes.UpdateResourceW(Handle, RT_RCDATA, PChar(ResName),
        0, nil, 0));
  finally
    Win32Check(MadRes.EndUpdateResourceW(Handle, False));
  end;
end;

Добавляем ресурсы в приложение

Добавить ресурсы не сложнее чем удалить, вот код:

//Добавление ресурсов в EXE файл
procedure AddDFMs(const FileName: string; DFMs: TDFMByNameDict);
var
  Handle: THandle;
  Item: TPair<string, TMemoryStream>;
begin
  Handle := BeginUpdateResource(PChar(FileName), False);
  Win32Check(Handle <> 0);
  try
    for Item in DFMs do
      Win32Check(UpdateResource(Handle, RT_RCDATA, PChar(Item.Key),
        0, Item.Value.Memory, Int64Rec(Item.Value.Size).Lo));
  finally
    Win32Check(EndUpdateResource(Handle, False));
  end;
end;

Я думаю код вопросов не вызовет. Мы разобрали и написали код для всех шагов нашего алгоритма, самое время собрать приложение реализующее нужный функционал.

Финальные штрихи компрессора

Напишем основную процедуру которая будет реализовывать все вышеописанные шаги вместе взятые:

//Основная рабочая процедура
procedure ExecuteApplication(const FileName: string);
var
  DFMs: TDFMByNameDict;
begin
  //Получим все DFM ресурсы из файла
  DFMs := LoadDFMs(FileName);
  try
    //Если таких не найдено, выходим
    if DFMs.Count = 0 then
      Exit;

    //Сожмем тело ресурсов
    CompressDFMs(DFMs);

    //Удалим найденные ресурсы из файла
    DeleteDFMs(FileName, DFMs);

    //Запишем вместо них новые, сжатые
    AddDFMs(FileName, DFMs);
  finally
    FreeAndNil(DFMs);
  end;
end;

Собственно уже вполне можно собрать приложение. Создадим в Delphi новый проект консольного приложения, сохраним его с именем dfmcompressor.dpr и сделаем программу:

program dfmcompressor;

{$APPTYPE CONSOLE}

uses
  Windows, SysUtils, Classes, Generics.Collections, ZLib,

  madRes;

  //
  // Тут должны располагаться все вышенаписанные процедуры
  //

begin
  try
    ExecuteApplication(ParamStr(1));
    Writeln('Done.')
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Собираем, натравливаем на какое-нить vcl приложение, и оно работает!

Ресурсы сжались, но программа теперь вылетает, не мудрено, ведь vcl не знает что ресурсы теперь сжаты.

Учим программу использовать сжатые DFM ресурсы

Пора создать тестовое приложение, на котором и будут проводится эксперименты. Создадим новый пустой VCL проект, в свойствах проекта пропишем чтобы он после компиляции обрабатывался dfmcompressor'ом, так же, чтобы можно было отлаживать модули delphi, нужно включить в свойствах проекта использование отладочных dcu.

Запускаем, умираем с исключением, и можем по стеку изучить как дошло управление до загрузки формы.

Собственно по стэку видно что вызывалась процедура classes.InternalReadComponentRes в которой и происходит загрузка ресурсов:

function InternalReadComponentRes(const ResName: UnicodeString; HInst: THandle; var Instance: TComponent): Boolean; overload;
var
  HRsrc: THandle;
begin                   { avoid possible EResNotFound exception }
  if HInst = 0 then HInst := HInstance;
  HRsrc := FindResourceW(HInst, PWideChar(ResName), PWideChar(RT_RCDATA));
  Result := HRsrc <> 0;
  if not Result then Exit;
  with TResourceStream.Create(HInst, ResName, RT_RCDATA) do
  try
    Instance := ReadComponent(Instance);
  finally
    Free;
  end;
  Result := True;
end;

Что же, попробуем внести изменения. Для этого скопируем classes.pas в каталог с нашим тестовым приложением (чтобы при компиляции подхватывался измененный файл), и модифицируем указанною процедуру так, чтобы происходила распаковка файла:

function InternalReadComponentRes(const ResName: UnicodeString; HInst: THandle; var Instance: TComponent): Boolean; overload;
var
  Signature: Longint;
  ResStream: TResourceStream;
  DecompressStream: TDecompressionStream;
begin
  Result := True;

  if HInst = 0 then
    HInst := HInstance;

  if FindResource(HInst, PChar(ResName), PChar(RT_RCDATA)) = 0 then
    Exit(False);

  ResStream := TResourceStream.Create(HInst, ResName, RT_RCDATA);
  try
    //Проверим, сжат ли стрим
    //Если есть стандартная DFM сигнатура, значит он не сжат
    ResStream.Read(Signature, SizeOf(Signature));

    //Восстановим указатель
    ResStream.Position := 0;

    //Если есть сигнатура, значит считем что поток не сжат
    if Signature = Longint(FilerSignature) then
      Instance := ResStream.ReadComponent(Instance)
    else
      begin
        //Ну а если нет сигнатуры, то распакуем DFM
        DecompressStream := TDecompressionStream.Create(ResStream);
        try
          Instance := DecompressStream.ReadComponent(Instance);
        finally
          FreeAndNil(DecompressStream);
        end;
      end;
  finally
    FreeAndNil(ResStream);
  end;
end;

Так же нужно не забыть добавить модуль Zlib в раздел uses секции implementation
Собираем, запускаем — все работает!

Развиваем идею

Вроде все работает — но таскать с приложением измененный classes.pas это крайняя мера, попробуем что-нибудь сделать. В идеале бы поставить хук на функцию InternalReadComponentRes и перенаправлять ее вызов на свою реализацию.

Хук делается очень просто формированием команды длинного jump'а на свою функцию, и вставкой его в начало InternalReadComponentRes. Да, таким подходом vcl не сможет больше вызвать  свой InternalReadComponentRes, но нам этого и не надо. Пишем функцию установки перехвата:

type
  PJump = ^TJump;
  TJump = packed record
    OpCode: Byte;
    Distance: Pointer;
  end;

procedure ReplaceProcedure(ASource, ADestination: Pointer);
var
  NewJump: PJump;
  OldProtect: Cardinal;
begin
  if VirtualProtect(ASource, SizeOf(TJump), PAGE_EXECUTE_READWRITE, @OldProtect) then
  try
    NewJump := PJump(ASource);
    NewJump.OpCode := $E9;
    NewJump.Distance := Pointer(Integer(ADestination) - Integer(ASource) - 5);

    FlushInstructionCache(GetCurrentProcess, ASource, SizeOf(TJump));
  finally
    VirtualProtect(ASource, SizeOf(TJump), OldProtect, @OldProtect);
  end;
end;

Вот только не получится так, ведь определение процедуры InternalReadComponentRes отсутствует в интерфейсной секции, а значит узнать указатель на нее мы не можем.

Вернувшись к стеку загрузки формы и изучив его, видно что InternalReadComponentRes вызвана из InitInheritedComponent, которая является публичной функцией, и на которую можно поставить перехват. Так же играет на руку то, что InitInheritedComponent не вызывает ни одной приватной функции из classes.pas (разумеется кроме той что мы меняем), а значит дублирование кода будет минимальным.

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

{
  Модуль добавляет поддержку сжатых DFM ресурсов в приложение
}
unit DFMCompressorSupportUnit;

interface

uses
  Windows, SysUtils, Classes, ZLib;

implementation

const
  //Скопировано из classes.pas
  FilerSignature: array[1..4] of AnsiChar = AnsiString('TPF0');


  //
  // Тут должны распологаться вышенаписанные ReplaceProcedure и 
  // наша реализация InternalReadComponentRes
  //

//Скопировано из classes.pas
function InitInheritedComponent(Instance: TComponent; RootAncestor: TClass): Boolean;

  function InitComponent(ClassType: TClass): Boolean;
  begin
    Result := False;
    if (ClassType = TComponent) or (ClassType = RootAncestor) then Exit;
    Result := InitComponent(ClassType.ClassParent);
    Result := InternalReadComponentRes(ClassType.ClassName,
      FindResourceHInstance(FindClassHInstance(ClassType)), Instance) or Result;
  end;

var
  LocalizeLoading: Boolean;
begin
  GlobalNameSpace.BeginWrite;  // hold lock across all ancestor loads (performance)
  try
    LocalizeLoading := (Instance.ComponentState * [csInline, csLoading]) = [];
    if LocalizeLoading then BeginGlobalLoading;  // push new loadlist onto stack
    try
      Result := InitComponent(Instance.ClassType);
      if LocalizeLoading then NotifyGlobalLoading;  // call Loaded
    finally
      if LocalizeLoading then EndGlobalLoading;  // pop loadlist off stack
    end;
  finally
    GlobalNameSpace.EndWrite;
  end;
end;

initialization
  ReplaceProcedure(@Classes.InitInheritedComponent, @InitInheritedComponent);
end.

Заключение

Все это работает и тестировалось на Delphi 2010, как будет работать на других версиях я не знаю, но думаю имея это руководство адаптировать не составит проблем.

Автор: vic85

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js