Использование компилятора Delphi (dcc32.exe) в прикладных программах

Конструктор принимает имя файла и открывает файл на чтение, а деструктор закрывает файл. Класс TStringCompileOut хранит генерируемый текст в памяти:

public
 procedure Clear;
 procedure SaveToFile(const aFileName: String);
 procedure SaveToOut(aOut: TCompileOut);
 property Capacity: Integer ...
 property Items[aIndex: Integer]: String ... default;

Методы класса позволяют очистить поток, сохранить поток в файле и добавить его к другому потоку. Свойства позволяют изменить резервируемый объем памяти для списка строк и получить доступ на запись и чтение строк по индексу. Общее число строк определяет наследуемое свойство LinesCount. Примеры использования этих классов смотрите в DccExamples.pas.
Отметим, что часть неизменяемого или шаблонного кода может быть заготовлена заранее, располагаться в файлах и объединяться в нужных местах результирующего кода с помощью AddFile и AddFileTemplate. По ходу генерации кода может быть создано несколько потоков - для деклараций переменных и констант, деклараций и реализаций классов и так далее. После просмотра всей задачи, сформулированной технологом, эти потоки сшиваются в один результирующий поток. Для частных потоков можно использовать строковую реализацию, а для результирующего потока - файловую.
2. Компиляция
После того, как исходный код создан, требуется его откомпилировать. Компилятор dcc32 замечательно подходит для этой роли - он очень быстрый, качественный и объединяет в себе все, что необходимо для построения exe-файлов, dll-библиотек и пакетов. Размер файла dcc32.exe (версия 12.0, из Delphi 5) всего 545 Кб, ранние версии имеют еще меньший размер. К нему нужно добавить только три файла - rlink32.dll, sysinit.dcu и system.dcu (это минимум). Компилятор и указанные файлы можно разместить в подкаталоге прикладной программы, например, bin. Генерировать текст целесообразно в подкаталоге компилятора, например, bin\pas, чтобы использовать короткие пути файлов и не засорять каталог компилятора.
Для вызова dcc32.exe в библиотеке DccUsing определена функция ExecDcc32. Она устанавливает текущий каталог, создает файл для перехвата ошибок компиляции, вызывает компилятор, дожидается завершения компиляции и определяет наличие ошибок.

function ExecDcc32(const aDccDir, aOptions,

  aProjectPath, aErrorPath: String;

  aCheckPaths: Boolean = False): Boolean;

Функция принимает аргументы: aDccDir - каталог, в котором находится компилятор Dcc32, aOptions - опции компилятора (рекомендации по их использованию смотрите в файле DccUsing.pas), aProjectPath - путь файла проекта (обычно dpr), aErrorPath - путь файла, куда будут направляться сообщения об ошибках компиляции. Необязательный аргумент aCheckPaths позволяет разрешить или запретить контроль наличия каталога и файла dcc32.exe. Функция возвращает True, если компиляция была успешной и False в противном случае. Предупреждения (hints и warnings) ошибками не считаются - их выводом можно управлять с помощью опций -H и -W. Опуская детали, рассмотрим немного подробнее эту функцию:

// сохранение текущего каталога и установка нового

CurDir := GetCurrentDir;

if not SetCurrentDir(DccDir) then

 raise Exception.Create(SCantChangeDir + DccDir);

try

 hStdOut := INVALID_HANDLE_VALUE;

 try

  // установки атрибутов безопасности

  with SecurAtt do begin

  nLength := SizeOf(SecurAtt);

  lpSecurityDescriptor := nil;

  // разрешить наследование дочернему процессу

  bInheritHandle := BOOL(True);

  end;

  // создание файла, в который будут направляться ошибки

  hStdOut := CreateFile(PChar(aErrorPath), GENERIC_WRITE, 0,

  @SecurAtt, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);

  if hStdOut = INVALID_HANDLE_VALUE then

  raise Exception.Create(SCantCreateFile + aErrorPath);

  // заполнение структуры, специфицирующей создание процесса

  ZeroMemory(@StartupInfo, SizeOf(StartupInfo));

  with StartupInfo do begin

  cb := SizeOf(StartupInfo);

  // скрывать окно компилятора и наследовать потоки ввода-вывода

  dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;

  wShowWindow := SW_HIDE;

  hStdOutput := hStdOut;

  end;

  // создать и стартовать процесс компилятора

  s := 'dcc32.exe ' + aOptions + ' ' + aProjectPath;

  if not CreateProcess('dcc32.exe', PChar(s), @SecurAtt, @SecurAtt,

  BOOL(True), 0, nil, PChar(DccDir), StartupInfo, ProcessInfo) then

  raise Exception.Create(SCantCreateProcess + 'dcc32.exe');

  // ждать завершение компиляции неопределенное время

  WaitForSingleObject(ProcessInfo.hProcess, INFINITE);

  // получить результат компиляции

  ResultCode := 0;

  GetExitCodeProcess(ProcessInfo.hProcess, ResultCode);

  result := ResultCode = 0;

 finally

  // закрыть файл ошибок

  if hStdOut <> INVALID_HANDLE_VALUE then

  CloseHandle(hStdOut);

 end;

finally

 // восстановить прежний каталог по умолчанию

 SetCurrentDir(CurDir);

end;

Установка каталога компилятора, как текущего, позволяет не заботиться о мелочах, связанных с назначением путей. Компилятор направляет сообщения об ошибках в стандартный файл вывода. Для его перехвата создаем свой файл, дескриптор которого передаем компилятору. Для того, чтобы процесс компилятора мог наследовать дескриптор открытого файла, устанавливаем его атрибут наследования. При заполнении структуры StartupInfo указываем, что окно компилятора должно быть скрытым и порождаемый процесс должен наследовать стандартные потоки ввода-вывода. Атрибуты безопасности, передаваемые функции создания процесса, нужны для правильной работы в NT, в Windows 95-98 их можно было бы опустить. Функция CreateProcess сохраняет параметры процесса в структуре ProcessInfo - мы используем дескриптор процесса, чтобы передать его функции ожидания системного события - в данном случае, завершения процесса. С помощью GetExitCodeProcess получаем значение, которое возвращает компилятор. Если компиляция была успешной, то возвращается 0, иначе - ненулевое значение. Операции закрытия файла ошибок и восстановления предыдущего каталога произойдут независимо от возможных исключительных ситуаций по ходу функции ExecDcc32.
Компилятору, вместе с исходным файлом (файлами), нужно также передать файл проекта (dpr) и уточнить в опциях, что же будет результатом компиляции. Возможных вариантов много - GUI или консольное приложение, dll, пакет, ActiveX (наверное, есть еще варианты). Выбор вида компиляции связан со спецификой задачи, требованиями пользователя и вкусами разработчика. К этому вопросу я еще раз вернусь в разделе Исполнение кода.
3. Диагностика ошибок
Идеальный вариант - это генерация синтаксически и семантически правильного кода. Но проверка семантики в большинстве случаев вряд ли возможна, поэтому желательно генерировать, по крайней мере, синтаксически правильный код. В этом случае компиляция всегда будет успешной. Если проверка синтаксической корректности затруднительна или невозможна, то приходится полагаться на диагностику, которую сформирует компилятор. Конечно, давать эту диагностику технологу - это самый последний случай, когда уже ничего не остается. Более спокойный вариант - это извлечь из файла ошибок номера ошибочных строк и определить, чему они соответствуют в том описании, который сделал технолог. Для разбора файла ошибок, библиотека DccUsing содержит класс TParseDcc32Errors. Класс весьма прост, поэтому я только обрисую его интерфейс:

TCompileMessageStatus = (cmsNone, cmsHint, cmsWarning,

  cmsError, cmsFatal);

public

 procedure ParseFile(const aFileName: String);

 function MessagesCount: Integer;

 function StatusCount(aStatus: TCompileMessageStatus): Integer;

 function MessageText(aIndex: Integer): String;

 function MessageStatus(aIndex: Integer): TCompileMessageStatus;

 function MessageFile(aIndex: Integer): String;

 function MessageLine(aIndex: Integer): Integer;

TCompileMessageStatus перечисляет все возможные статусы ошибок. Процедура ParseFile выполняет разбор файла ошибок и сохраняет результат в своем приватном списке. Функция MessagesCount возвращает общее количество сообщений, а StatusCount - количество сообщений с заданным статусом. Оставшиеся 4 функции разбирают строку сообщения компилятора на составляющие - текст сообщения, статус, имя файла, в котором обнаружена ошибка и номер строки.
Вот теперь можно вернуться к необъясненным методам TCompileOut. Метод AddPoint добавляет в поток контрольную точку. Контрольная точка - это просто целое число, которое помечает уникальным номером начало некоторой части генерируемого кода и жестко связывается с номером строки. Контрольная точка может служить, например, индексом в таблице ошибок. Расставив при генерации кода такие точки-метки, мы можем локализовать место ошибки. Для поиска ошибки нужно повторить генерацию кода без вызова компилятора (чтобы опять сформировать выходной поток), а затем, для результирующего выходного потока, вызвать функцию FindPoint, передав ей номер ошибочной строки. Эта функция определит ближайшую точку ошибки. Если генерируется несколько файлов исходных кодов, то выбор ошибочного файла сделать с помощью функции, возвращающей имя файла - MessageFile.

Отправить комментарий

Проверка
Антиспам проверка
Image CAPTCHA
...