    ГЛАВА      26 

    Программирование DLL: сложный материал 

    Эрик Абер 

    
    И—— Линейные функции 
    Применение линейных функций для доступа к объектам и их методам. 

    ——— Запись форм и доступ к ним 
    Как записывать в DLL модальные и немодальные ^юрмы и обеспечивать к ним доступ. 

    ——— Совместное использование объектов 
    Непосредственный доступ к объектам через таблицу виртуальных методов и 
совместное использование классов модулями DLL и ЕХЕ. 

    ——— Совместное использование данных 
    Выход за границы процесса и обмен данными между программами. 

    ——— Структура DLL-файла 
    Применение утилиты Turbo Dump для поиска полезной информации в файле РЕ. 

    SJ Delphi реализована архитектора программирования DLL, прекрасно 
дополняющая требования операционной системы Win32. Эта архитектура и способ ее 
использования с Win32 рассматривались в главе 25, "Программирование DLL: 
введение". Основное внимание было уделено синтаксису, импортированию и 
экспортированию подпрограмм, загрузке и выгрузке DLL, переносимости и 
всевозможным нестандартным ситуациям, в которые можно попасть при 
программировании DLL. В этой главе речь пойдет об эффективном использовании 
DLL и некоторых связанных с этим тонкостях. 
    В основном эффективность Delphi связана с применением объектов, 
большинство которых являются визуальными (например, с формами). Есть, однако, 
много и невизуальных (например, списки и потоки). Рассмотрим случаи, когда 
родственные объекты удобнее группировать и . компилировать в независимых 
модулях DLL. 
    В главе 25 рассматривалось экспортирование обычных функций, выполняющих 
какие-либо операции. Подобное применение типичное для DLL, однако не 
единственно возможное. В DLL можно помещать объявления классов, затем 
использовать их в модуле ЕХЕ почти так же, как если бы они были для этого 
модуля локальными. К методам , расположенного в DLL класса можно обращаться 
непосредственно или через линейные функции, в которые "завернуты" эти методы. 
В следующем разделе рассматривается простейший вариант такого '"обволакивания" 
методов невизуального класса обычными функциями. 

    610 

    Функции-оболочки классов 

    Класс или иерархию классов можно скомпилировать в один многократно 
используемый модуль DLL. В этом нет ничего сложного, 
(10~Т?аТГТШТГТТо"том"воспользоваться? Ответ — определить обычные функции, 
"обволакивающие" класс. Это означает, что DLL должна*экспортировать~^5унюдиюг^ 
_инициализиЕующую ,этот класс, а также функцию, удаляющую его. И такие функции 
придется создать для каждого методами свойства, к которым нужен доступ. 
    "*~~Дан"нь1ТГ"под1Год предоставляет широкие возможности, поскольку между 
экспортируемыми функциями и объектами можно создать еще один слой сопряжения. 
Есть и небольшая ложка дегтя: приходится создавать функции только для 
то-дчх,,ч.тобы-цызывать.другие функции. К тому же система становится менее 
гибкой: изменение одного метода может привести к необходимости изменить 
функцию обращения к нему. Лрб^вление метода к классу требует новой функции. 
Удалив метод из класса, надо удалить и соответствующую функцию. Ниже будет 
гюказано, что заключение класса в оболочку наиболее удобно в тех случаях, 
когда мы имеем дело с законченным набором классов либо^если_мддж^икация этих 
клас-COBJI будущем маловероятна.	"' 
    Рассмотрим DLL-проект, содержащий один класс и функции экспортирования для 
доступа к этому классу. Проект именуется ClassWrap и находится на 
дополнительно распространяемой дискете. 

    •  Листинг 26.1. \UDELPHI3\CHP26\ CLASSWRAP. DPR. Для этого проекта не 
нужна реализация DLLMain - ничего не надо ни инициализировать, ни удалять при выходе 

    library ClassWrap; 
    uses 
    NumAccum in 'NumAccum.pas', wraplib in 'wraplib.pas1; 
    end. 

    DPR-файл этого проекта довольно скуден. Он содержит только раздел uses, в 
котором указаны два pas-файла. В файле NumAccum.pas определен демонстрационный 
класс, в файле wraplib.pas— функции-оболочки со списком exports. 

    На заметку 
    Создание DLL-проекта в Delphi рассматривалось в главе 25, 
"Программирование DLL: введение". Там же описано экспортирование с помощью 
директивы exports. 


    Реализация класса в DLL 

    В созданный DLL-проект можно внести модули, содержащие необходимые классы. 
Объявление и определение демонстрационного класса для DLL-проекта ClassWrap 
представлено в листинге 26.2. 

    Листинг 26.2. \UDELPHI3\CHP26\ NUMACCUM. PAS. Демонстрационный класс (в 
реальном проекте классы будут описаны более подробно) 

    unit NumAccum; interface 
    type 
    TOrdAccum = class private 
    FNumber: integer; protected 
    procedure SetAdd(i: integer); virtual; 
    procedure SetSubtract(i: integer); virtual; 
    procedure SetDivide(i: integer); virtual; 


    Глава 26. Программирование DLL: сложный материал 611 


    procedure SetMultiply (i: integer); virtual/public 
    constructor Create(InitalValue: integer); virtual; 
    property Add: integer read FNuraber write SetAdd; 
    property Subtract: integer read FNumber write SetSubtract; 
    property Divide: integer read FNumber write SetDivide; 
    property Multiply: integer read FNumber write SetMultiply; end; 
    var 
    NoAccum: TOrdAccum; 
    implementation 
    constructor TOrdAccum.Create(InitalValue: integer); begin 
    FNumber := InitalValue; end; 
    procedure TOrdAccum.SetAdd(i: integer); begin 
    FNumber := FNumber + i; end; 
    procedure TOrdAccum.SetSubtract(i: integer); begin 
    FNumber := FNumber — i; end; 
    procedure TOrdAccum.SetDivide(i: integer); begin 
    FNumber := FNumber div i; end; 
    procedure TOrdAccum.SetMultiply(i: integer); begin 
    FNumber := FNumber * i; end; 
    end. 

    В листинге 26.2 представлен модуль NUMACCUM. PAS, в котором объявлен тип 
TOrdAccum. Этот тип содержит единственное поле данных FNumber типа integer. 
Все методы этого объекта работают с полем FNumber. Интерфейсом для данного 
класса служит набор свойств, предназначенных как для чтения, так и для записи. 
При присвоении значения каждому свойству производится математическая операция, 
выполняемая неявно вызываемым методом (с префиксом Set). Сразу после 
присвоения можно считать из свойства новое значение. 
    Класс инициализируется начальным значением, передаваемым в качестве 
параметра конструктору Create, т.е., если новый экземпляр объекта создается с 
указанием конструктору в качестве параметра 1, полю FNumber присваивается 
начальное значение 1. Теперь предположим, что свойству Add присваивается 
значение 2. Если после этого прочесть это свойство (или любое другое), 
полученное значение окажется равным 3. Если затем присвоить значение 4 
свойству Multiply и снова прочесть, значение окажется равным 12. Последующее 
присвоение свойству Divide значения 3 и считывание даст значение 4. Как видим, 
класс работает наподобие некоего накопителя, поскольку в каждой операции 
участвует поле FNumber, и в нем всегда содержится результат последней операции. 
    Конечно же, в DLL можно разместить и более сложные классы, имеющие 
реальное практическое применение. Класс TOrdAccum выбран для примера ввиду его 
компактности, благодаря которой (будем надеяться) принцип "обволакивания" 
класса обычными функциями удалось продемонстрировать достаточно наглядно. 
Класс, скомпилированный в DLL-проект, остается недоступным модулю ЕХЕ, 
загрузившему DLL. Поэтому необходимо применять обычные функции — оболочки 
класса, и эти функции должны экспортироваться для использования вне DLL, что 
можно сделать в отдельном модуле.	' 

    612	Часть VI. Сложный материал 


    Определение функций-оболочек в модуле экспортирования 

    В листинге 26.3 представлен модуль WRAPLIB.PAS DLL-проекта CLASSWRAP. Этот 
модуль экспортирует шесть функций. Обратите внимание, что в каждой из 
экспортируемых функций принято соглашение о вызовах StdCall — стандартное 
соглашение для Win32. Для обеспечения совместимости с операционными системами 
Win95 и WinNT и независимости от языка программирования следует задавать в 
DLL-модулях именно это соглашение. Более подробно соглашения о вызовах описаны 
в главе 25, "Программирование DLL: введение". 

    Листинг 26.3. \UDELPHi3\CHP26\WRAPLiB. PAS. В этом модуле объявлены и 
определены все экспортируемые функции 

    unit wraplib; interface uses NumAccum; 
    procedure FreeOrdAccumlnst(Inst: TOrdAccum); stdcall; function 
GetOrdAccumlnst(InitialVal: integer): TOrdAccum; stdcall; function  
OrdAdd(Inst: TOrdAccum; iVal: integer): integer; stdcall; function  
OrdDivide(Inst: TOrdAccum; iVal: integer): integer; stdcall; function  
OrdMultiply(Inst: TOrdAccum; iVal: integer): integer; stdcall; function OrdSubt 
    ract(Inst: TOrdAccum; iVal: integer): integer; stdcall; 
    exportis 
    FreeOrdAccumlnst, 
    GetOrdAccumlnst, 
    OrdAdd, 
    OrdDivide, 
    OrdMultiply, 
    OrdSubtract; 
    implementation 
    function GetOrdAccumlnst(InitialVal: integer): TOrdAccum; begin 
    result := TOrdAccum.Create(InitialVal); end; 
    procedure FreeOrdAccumlnst(Inst: TOrdAccum); begin 
    Inst.Free; end; 
    function OrdAdd(Inst: TOrdAccum; iVal: integer): integer; begin 
    Inst.Add := iVal; 
    result := Inst.Add; end; 
    function OrdSubtract(Inst: TOrdAccum; iVal: integer): integer; begin 
    Inst.Subtract := iVal; 
    result := Inst.Subtract; end; 
    function OrdDivide(Inst: TOrdAccum; iVal: integer): integer; begin 
    Inst.Divide := iVal; 
    result := Inst.Divide; end; 


    Глава 26. Программирование DLL: сложный материал	613 


    function OrdMultiply(Inst: TOrdAccum; iVal: integer): integer; begin 
    Inst.Multiply := iVal; 
    result := Inst.Multiply;г end; 
    end. 

    Среди экспортируемых функций есть GetOrdAccumlnst: function     
GetOrdAccumlnst(InitialVal:   integer):   TOrdAccum;   stdcall; 

    Функция GetOrdAccumlnst имеет один целый параметр, с помощью которого 
передается начальное значение конструктору класса TOrdAccum, и возвращает 
инициализированный экземпляр объекта. Модуль ЕХЕ может обращаться к этой 
функции многократно, каждый раз получая новый экземпляр объекта типа 
TOrdAccum. Закончив работу с данным экземпляром, модуль ЕХЕ должен удалить его 
из памяти. Для этого в модуле WRAPLIB. PAS есть экспортируемая функция 
FreeOrdAccumlnst, удаляющая из памяти объект, передаваемый ей в качестве параметра: 

    procedure  FreeOrdAccumlnst(Inst:   TOrdAccum);   stdcall; 

    В загружающем модуле проекта каждый объект, полученный с помощью функции 
GetOrdAccumlnst, должен сохраняться. Затем каждый объект независимо от 
остальных освобождается с помощью процедуры FreeOrdAccumlnst. Таким образом, 
если функция GetOrdAccumlnst вызывалась, скажем, пять раз, то ровно столько же 
раз придется обратиться и к процедуре FreeOrdAccumlnst. 
    При импортировании тип функции GetOrdAccumlnst можно объявить как Thandle 
вместо TOrdAccum. Немного поразмыслив, нетрудно понять, что модуль ЕХЕ не 
знает, что собой представляет тип TOrdAccum, да и знать ему это, собственно, 
незачем. Переменная THandle имеет тот же объем (32 бит), что и адрес 
экземпляра типа TOrdAccum. Таким образом, различие межд у этими типами не 
выходит за рамки интерпретации,	j 
    Точно так же при импортировании процедуры FreeOrdAccumlnst тип THandle 
можно задать для параметра inst. Применение типа THandle при импортировании 
обеспечивает единообразие обращений к экземплярам объектов без сортировки по 
типам. Пользователь DLL рассматривает дескриптор как ссылку на объект, имея 
дело с указателем на данные этого экземпляра. 
    Модуль WRAPLIB. PAS экспортирует еще четыре функции. 
    function	OrdAdd(Inst:   TOrdAccum;   iVal:   integer):   integer;   stdcall; 
    function	OrdDivide(Inst:   TOrdAccum;   iVal:   integer):   integer;   stdcall; 
    function	OrdMultiply(Inst:   TOrdAccum;   iVal:   integer):   integer;   stdcall; 
    function	OrdSubtract(Inst:   TOrdAccum;   iVal:   integer):   integer;   stdcall; 
    Эти функции обеспечивают доступ к соответствующим свойствам экземпляра 
типа TOrdAccum, передаваемого в качестве первого параметра. Значение второго 
параметра присваивается соответствующему свойству. Каждая функция возвращает 
новое значение свойства. 
    За объявлением всех подпрограмм модуля следует список exports. 
    exports 
    FreeOrdAccumlnst, 
    GetOrdAccumlnst, 
    OrdAdd, 
    OrdDivide, 
    OrdMultiply, 
    OrdSubtract; 
    Имена функций из этого списка заносятся в специальный раздел образа 
DLL-файла. Когда модуль ЕХЕ или другая DLL, загружающая данную, обращается 
(прямо или косвенно) к функции LoadLibrary, последняя находит в образе 
DLL-файла все эти функции. Если они найдены, к ним можно будет обращаться, в 
противном случае вызов LoadLibrary завершается с ошибкой. 
    Более подробно о функции LoadLibrary и экспортировании рассказывается в 
главе 25. "Программирование DLL: введение". 

    614	Часть VI. Сложный материал 

    В оставшейся части модуля WRAPLIB.PAS размещены лишь определения всех 
экспортируемых подпрограмм. Данный пример также достаточно прост, находится он 
на дополнительно распространяемой дискете. 

    Модальные формы в DLL 

    Как правило, с программами работать намного удобнее и легче, если в них 
используются стандартные элементы пользовательского интерфейса (User Interface 
— UI). Диалоговые и другие однотипные формы можно помещать в DLL. Вместо того 
чтобы создавать одни и те же формы и компоновать в программу один и тот же 
код, лучше создать собственный пак-fr ап?'м"н тгтWp.flP'KHH.f' ° ф"р"р DLL. В 
данном разделе рассматривается размещение в DLL модальных диалоговых элементов 
(позже будет рассказано и о немодальных). 

    Преимущества способа размещения форм в DLL 

    Посмотрим, что же все-таки дает размещение модальных диалоговых элементов 
в DLL. Допустим, вы подписали договор с компанией Tyler's Toys на написание 
для нее серии программ. Одна из программ — сетевой каталог, доступный в 
Internet через протокол TCP/IP. Аналогичную версию программы-каталога надо 
создать для пользователей, не имеющих доступа к Internet. Эта версия 
распространяется по обычной почте. Основные формы и логика программирования в 
обеих версиях существенно различаются, поскольку одна из них работает в 
коммуникационной среде, а другая — локально. Однако пользовательские 
интерфейсы обеих версий не должны сильно различаться. Размещение кода и форм, 
общих для обеих версий, в DLL позволяет избежать дублирования программных 
текстов и существенно* упростить''сопровождение. Такую DLL могут использовать 
обе версии каталога. Некоторые общие для них формы, которые могут содержаться 
в DLL, показаны на рис. 26.1-26.4. 

    Рис. 26.1. Заставка —это немодальная форма, рассматриваемая в следующем 
разделе данной главы 

    Рис. 26.2. Данные о разработчике — это модальная форма с информацией об 
авторском праве и версии продукта 

    Рис. 26.3. Окно вывода сообщений — это модальная форма для    

    Рис. 26.4. Окно ввода — это модальная форма для ввода текста вывода любого текста с 
подсказкой                                                            " 


    Глава 26. Программирование DLL: сложный материал 615 

    Формы, представленные на рисунках — типичные элементы пользовательского 
интерфейса, применяемые практически в каждой программе, пользующейся 
средствами графического интерфейса Windows. И если можно создавать формы 
общего назначения, в которые затем вносятся конкретный текст и строки ввода, 
то почему бы всем программам, заказанным компанией Tyler'sToys, не 
пользоваться общей DLL? 
    Вот тут и сказываются преимущества такого подхода. Допустим, что 
программа, ориентированная на Internet, загружает в машину клиента DLL с этими 
стандартными формами. Когда этой программе необходимо открыть диалоговое окно, 
это происходит достаточно быстро, поскольку форма уже находится на диске 
клиента. Естественно, при первом обращении пользователя к каталогу через 
Internet для загрузки DLL потребуется некоторое время, но это произойдет 
только один раз. 
    Теперь предположим, что в одной из форм возникает ошибка, скажем, форма 
слишком велика для разрешения 640x480. Если подобная форма находится в DLL, 
совместно используемой сетевой и локальной версиями программы, достаточно 
устранить ошибку один раз. Что касается конечного пользователя, то высылать 
ему полностью перекомпилированную программу необязательно; достаточно выслать 
исправленную DLL. 

    Создание подпрограмм доступа 

    Чтобы обеспечить доступ к модальным формам в новой DLL, необходимо создать 
и экспортировать подпрограмму доступа. Загружающий модуль вызывает 
подпрограмму доступа, которая инициализирует экземпляр переменной формы и 
выводит его на экран в модальном режиме. Если в форму вводятся данные или в 
ней содержатся элементы управления, например кнопки OK, Cancel и Abort, 
подпрограмма^доступа может возвратить соответствующую информацию. 
    В листинге 26.4 представлена процедура доступа к простому модальному окну 
вывода сообщений, подобному тому, которое показано на рис. 26.3. 

    Листинг 26.4. \UDELPHi3\CHP26\EXPORTU. PAS. Процедура доступа к форме 
TToyinfoDlg, открывающейся в модальном режиме 

    procedure ShowToylnfoDlgModal(AppHand: THandle; Prompt: PChar); begin 
    Application.Handle := AppHand; 
    with TToyinfoDlg.Create(Application) do 
    try 
    SetPrompt(Prompt); 
    ShowModal;	; finally 
    Free;	j end;                                                               
    ! end; 

    Процедура ShowToylnfoDlgModal создает, выводит на экран и удаляет 
экземпляр объекта типа TToyinfoDlg. Параметр AppHand — это дескриптор главного 
окна программы. Если процедура вызывается программой Delphi, этому параметру 
присваивается значение Application. Handle. Внутри DLL процедура 
ShowToylnfoDlgModal присваивает значение этого параметра локальному экземпляру 
глобального объекта Application. Таким образом, при сворачи-вании окна 
TToyinfoDlg сворачивается и главное окно программы, поскольку данное 
диалоговое окно связано с главным, из которого оно было открыто. Если у 
программы нет главного окна, параметру AppHand присваивается значение О, что 
не вызывает никаких побочных эффектов. Параметр Prompt передается методу 
SetPrompt нового экземпляра типа TToyinfoDlg. Метод SetPrompt просто 
присваивает текст, на который указывает параметр Prompt, свойству Caption 
локального объекта TLabel внутри формы. Ввиду возможности задавать текст, 
выводимый окном типа TToyinfoDlg, этот тип можно счита ть типом общего 
назначения и применять в более чем одной программе. 
    На рис. 26.2 показано диалоговое окно общего назначения с данными о 
разработчике — компании Tyler's Toys. Подпрограмма доступа к этому окну 
практически аналогична подпрограмме, представленной в листин-*     ге 26.4, но 
для задания большего числа полей — таких, как дата регистрации авторского 
права или номер версии — требуется больше параметров. 
    Итак, функция ShowToylnfoDlgModal создает экземпляр объекта TToyinfoDlg, 
которому в качестве владельца задается локальный объект Application. Затем она 
задает текст, выводимый диалоговым окном. Чтобы открыть диалоговое окно, 
функция ShowToylnfoDlgModal обращается к методу ShowModal объекта TToyinfoDlg. 
Затем она удаляет этот объект из памяти. Возвращать какие-либо значения 
процедуре нет необходимости, так как все действия пользователя сводятся к 
щелчку на кнопке ОК. 

    616	Часть VI. Сложный материал 

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

    Листинг 26.5. \UDELPHI3\CHP2 6XEXPORTU. PAS. Набрав текст, пользователь 
может отменить операцию либо 1 продолжить работу 

    function ShowToylnputDlgModal(AppHand: THandle; Prompt, pBuf: PChar; 
    Length: integer): Integer; begin 
    result := -1; 
    Application.Handle := AppHand; ---with TToylnputDlg.Create(Application) do try 
    SetPrompt(Prompt); result := ShowModal; 
    STrTTcbpy (pBuf, GetlnputBuf, Length) finally         ""  "———~~ 
    Free; end; end; 

    Параметр AppHand в этом примере применяется точно так, как и в предыдущем. 
Функция ShowToylnputDlgModal также имеет параметр Prompt для задания в ходе 
выполнения программы текста-подсказки, поэтому данное диалоговое окно можно 
отнести к разряду диалоговых окон общего назначения. Имеется также параметр 
pBuf. Это заранее выделенный буфер типа PChar, в который помещается введенный 
пользователем текст. Предполагается, что объем этого буфера соответствует 
значению параметра Length. Обратите также внимание, что ShowToylnputDlgModal — 
это уже функция, а не процедура. В открываемом окне пользователь может выбрать 
кнопку ОК или Cancel. Соответствую-щеё1т'Г№нйе*будет возвращено функцией 
ShowToylnputDlgModal. 
    После создания экземпляра объекта TToylnputDlg вызывается метод 
TToylnputDlg. SetPrompt, устанавливающий свойство Caption локального элемента 
управления IbMessageText типа TLabel. Затем вызывается метод ShowModal, 
открывающий форму в модальном режиме. Пользователь может ввести текст и 
закрыть окно, щелкнув на кнопке ОК или Cancel. При выборе ОК метод ShowModal 
возвращает значение 1, при выборе Cancel — 2. Значение метода ShowModal в 
конце концов возвращается функцией ShowToylnputDlgModal. 
    После открытия и закрытия формы вызывается метод GetlnputBuf. Как и метод 
SetPrompt, он объявлен директивой public и является локальным по отношению к 
экземпляру типа TToylnputDlg. Этот метод возвращает текст, набранный 
пользователем в поле edlnputEdit типа TEdit. С помощью библиотечной функции 
StrLCopy из буфера, указатель на который возвращен функцией GetlnputBuf, в 
буфер, указываемый параметром pBuf функции ShowToylnputDlgModal, копируется 
количество байтов, указанное параметром Length. После этого экземпляр типа 
TToylnputDlg удаляется из памяти с помощью метода Free. 
    Посмотрим на раздел интерфейса модуля, в котором объявлены две 
экспортируемые подпрограммы доступа, рассмотренные в данном разделе. 
    В листинге 26.6 показан типичный раздел interface модуля, экспортирующего 
подпрограммы доступа в DLL. Каждая подпрограмма объявлена директивой public с 
соглашением о вызове StdCall. За объявлением следует список exports, в который 
внесены обе подпрограммы. Фактические определения экспортируемых подпрограмм 
должны следовать за директивой implementation. Объявления, соглашения о 
вызовах и разделы exports подробно рассмотрены в главе 25, "Программирование 
DLL: введение". 

    ЛИСТИНГ 26.6. \UDELPHI3\CHP26\EXPORTU. PAS. Интерфейс МОДУЛЯ 
Экспортирования ДЛЯ DLL-проекта FormsLib 

    interface 
    uses 
    Forms, SysUtils, WinTypes, TTInfoDlg, TTAboutDlg, TTInputDlg, TTSplashDlg; 
    procedure ShowToylnfoDlgModal(AppHand: THandle;	Prompt: PChar); StdCall; 
    function ShowToylnputDlgModal(AppHand: THandle;	Prompt, pBuf: PChar; 
    '-        Length: integer):	Integer; StdCall; 


    Глава 26. Программирование DLL: сложный материал 617 


    exports 
    ShowToylnfoDlgModal, ShowToylnputDlgModal; 
    implementation 


    Немодальные формы в DLL 

    Немодальные формы реализуются в DLL диалогично модальным, но для них нужны 
две функции доступа: одна — для создания, другая — для удаления формы. Это 
результатного, что после создания немодальной формы выполнение программы 
продолжается обычным образом,.в отличие от,модальных форм. После открытия 
модальной формы методом ShowModlal_Bbn^HeHHe.nporpaM 
    Mbi можно продолжить, лишь закрыв форму. В приведенных выше примерах 
модальные формы вскоре после открытия вызывали метод Free, удалявший их из 
памяти. Что произойдет, если таким же образом поступить с немодальнрй формой? 
Работа программы возобновится сразу после вызова метода Show. Тут же 
вызывается метод Free, форма удаляется, и ее появление на экране едва удается 
заметить. Проблема решается путем разделения процесса между двумя 
подпрограммами доступа: 

    function     OpenToySplashDlg(AppHand:   THandle):   THandle;   StdCall; 
procedure   CloseToySplashDlg(hSplashlnst:   THandle);   StdCall; 

    Каждая из подпрограмм объявляется с соглашением о вызовах StdCall. Имена 
должны быть внесены в список exports модуля или проекта. 

    Создание и открытие немодальных форм 

    Функция OpenToySplashDlg создает и выводит на экран экземпляр типа 
TToySplashDlg. Эта форма проста, не взаимодействует с пользователем и 
закрывается в процессе выполнения программы путем вызова процедуры 
CloseToySplashDlg. Применять подобную заставку очень удобно, поскольку, открыв 
ее с помощью функции OpenToySplashDlg, JVJO»CHO_ Г1?0должить инициализацию 
других частей программы. После окончания инициализации заставка удаляется 
процедурой CloseToySplashDlg. Реализация функции OpenToySplashDlg представлена 
в листинге 26.7 

    1 Листинг 26.7. \UDELPHI3\CHP26\EXPORTU.PAS. Данный пример - часть 
проекта-примера, демонстрирующего использование модальных форм 

    function OpenToySplashDlg(AppHand: THandle): THandle; begin 
    Application.Handle := AppHand; 
    result := THandle(TToySplashDlg.Create(Application)); 
    with TToySplashDlg(result) do 
    try 
    Show; except 
    Free; 
    Result := 0; v. Raise; end; end; 

    В начале функции OpenToySplashDlg значение параметра AppHand присваивается 
полю handle локального объекта Application. Параметр AppHand— это дескриптор 
окна, с которым должен быть связан экземпляр объекта TToySplashDlg. Это важно 
^случаях, кргда^формаГдбТЖна быть сворачиваемой. Сворачивание главной формы 
должно приводить к сворачиванию всех форм" программы, что и обеспечивает 
данное присвоение. 
    Переменной result присваивается экземпляр, возвращаемый конструктором 
TToySplashDlg. Create. Ему приходится задавать тип Thandle — тип переменной 
result. Такое присвоение типа в данном случае безопасно, поскольку и 
переменная экземпляра объекта, и дескриптор имеют размер 32 бит. 
    После создания экземпляра TToySplashDlg присвоенное переменной result 
значение используется для вызова метода Show, выводящего форму на экран. 
Возврат из метода Show, в отличие от ShowModal, происходит сразу. Если при 
выводе формы на экран возникает исключительная ситуация, выполняются 
операторы, следующие за директивой except. При этом вызывается метод Free, 
переменной result присваивается значение 0, после чего исключительная 
ситуация возникает на новом уровне. 

    618	Часть VI. Сложный материал 


    Закрытие и удаление немодальных форм -и^М' 

    Программа, вызвавшая функцию OpenToySplashDlg, должна_?охр^нять 
возвращенное ею значение типа THandle. Это значение представляет экземпляр 
объекта и используется процедурой CloseToySplashDlg при удалении экземпляра. 
Реализация процедуры CloseToySplashDlg представлена в листинге 26.8. 

    Листинг 26.8, \UDELPHI3\CHP26\EXPORTU.PAS. Процедура CloseToySplashDlg 
вызывает исключительную ситуацию при возникновении ошибки, поэтому обращение к 
ней необходимо помещать в блок try... except 

    procedure  CloseToySplashDlg(hSplashlnst:   THandle); begin 
    if hSplashlnst  <   1  then Raise  Exception.Create('hSplashlnst   is  not 
 valid1); with  TToySplashDlg(hSplashlnst)   do begin Close; Free; end; end; 

    Дескриптор, возвращенный функцией OpenToySplashDlg, передается процедуре 
CloseToySplashDlg в пара-MeTpe~K5pTasTiThst. Процедура начинается с проверки 
равенства hSplashlnst нулю (признак ошибки). Надежного способа определения 
некорректности параметра hSplashlnst не существует, но по крайней мере он не 
должен быть равным нулю, поэтому в случае равенства вызывается исключительная 
ситуация с соответствующим сообщением. 
    Затем параметру hSplashlnst присваивается тип TToySplashDlg. Вызывается 
метод Close объекта и метод Free. При ошибочном hSplashlnst обращение к Close 
вызовет исключительную ситуацию и возврат из процедуры CloseToySplashDlg в 
вызвавший ее модуль. 

    Таблица виртуальных методов 

    Объекты Delphi, создаваемые в синтаксисе языка Object Pascal, представляют 
собой структуры данных вполне определенного формата. Объекты Object Pascal 
совместимы по внутреннему формату с объектами, определяемыми,^ Component 
Object Model (COM) фирмы Microsoft. С помощью этого формата объекты Delphi 
могут непосредственно поддерживать интерфейс, совместимый с объектами СОМ. 
    Компиляторы Delphi и Borland C++ разработаны одной фирмой, поэтому их 
объекты совместимы. Стандартизация и унификация объектных форматов 
способствует реализации разработок, независимых от языка программирования. 
Преимущества такой гибкости очевидны. 

    Формат объекта СОМ 

    СОМ-совместимая часть формата Delphi довольно проста. DK^ejjnjwrJjcjmuca, 
создаваемый конструктором, — это просто указатель на блок Шмяти. В_пе?врм_поле 
этого блока содержится адрес указателяi jjajwofiraigy виртуальных методов 
(Virtual Method Table— VMT). Второе поле блока памяти— это адрес первого 
"чл*?на данных, объявленного в базовом классе, а последнее поле — это 
последний член данных, объявленный в классе, на основе которого создан 
экземпляр. Таким образом, адреса членов данных по мере их объявления в 
иерархии классов представляются в виде последовательных полей в блоке памяти, 
содержащем экземпляр класса. 

    На заметку 
    Термин таблица виртуальных методов применяется только в Delphi, из чего, 
однако, не следует, что сама таблица не существует в других системах. Только в 
Delphijma называется именно_так. 

    Широко распространено название виртуальная таблица (Virtual Table, VTable 
— VT). Эти названия обычно ijil  применяются в документации по C++ и СОМ. При 
чтении такой документации указанные выше термины вполне можно отождествлять с 
рассматриваемой в данном разделе таблицей VMT. Следует иметь в виду, что 
именно положение VMT в общелузасположении объекта определяетегоСОМ-совместимдсть. 
    Таблица виртуальных методов (VMT) — это массив их адресов. Каждый адрес 
указывает на метод, объявленный в иерархии данного класса. Более ясно это 
показано на рис. 26.5. 

    Глава 26. Программирование DLL: сложный материал 619 

    
    Данные экземпляра			>	VMT 

    Addr Field!	Pointer			И т.д. 
    -8	vmtFreeln... 
    Addr Field2	Member Data 
    -4	vmtDestroy 
    Addr Field3	Member Data 
    0	Метод 1 
    Addr Field4	Member Data 
    4	Метод 2 
    И т.д. 
    8	Метод 3 
    12	Метод 4 
    И т.д. 
    
    
    Адреса виртуальных методов в VMT расположены последовательно, в порядке 
объявления методов в иерархии класса. Переопределенные методы также включаются 
в VMT, что обеспечивает возможность применения директивы inherited. 

    Рис. 26.5. СОМ-совместимый формат объектов Object Pascal 


    Формат объектов Delphi 

    Формат объектов Delphi сложнее СОМ-формата, поскольку включает ряд 
дополнительных элементов: динамические методы, информацию о типах времени 
выполнения (RTTI) и прочее, для чего требуются указатели на распределяемые в 
памяти структуры различных типов. Поскольку структура объекта определяется 
спецификацией СОМ, нужен способ присоединения данных к экземпляру объекта 
элементов Delphi без нарушения этой спецификации. Такой способ найден: ад-._ 
реса дополнительных элементов размещаются с отрицательным смещением от начала 
VMT. В табл. 26.1 перечислены константы, представляющие смещения элементов 
Delphi в VMT. Полностью эта информация приведена в модуле System, pas, 
расположенном в каталоге \DELPHI3\SOURCE\RTL\SYS. 
    Р"'Т 

    Таблица 26.1. Адреса таблицы виртуальных методов 

    Метод	Смещение	Метод	Смещение 

    vmtDestroy	-4	vmtDynamicTable	-36 
    vmt Free Instance	-8	vmtMethodTable	-40 
    vmtNewInstance	-12	vmtFieldTable	-44 
    vmtDefaultHandler	-16	vmtTypelnfo	-48 
    vmtSafeCallException	-20	vmtlnitTable	-52 
    vmtParent	-24	vmtAutoTable	-56 
    vmtlnstanceSize	-28	vmtlntfTable	-60 
    vmtClassName	-32	vmtSelfPtr	-64 


    Таблица виртуальных методов и программирование DLL 

    Информация о VMT, приведенная в данном разделе, не оказывала существенного 
влияния на программирование в Delphi 1.0; не становится она жизненно важной и 
с переходом к Delphi 2.0 или 3. Но она помогает понять одну из сторон 
двойственного интерфейса в СОМ и принципы совместного использования классов 
при программировании DLL. В последнее время обе эт и темы обсуждаются довольно 
активно. Фирма Microsoft успешно стимулировала потребность в DLL на основе 
СОМ, выпустив спецификации ActiveX. Программисты на C++ в последнее время 
пристрастились к программированию графических интерфейсов в Delphi, ускоряя 
тем самым цикл разработки, но им приходится применять и старые модули, 
написанные на языке C++. 
    Ну и никак нельзя забывать о "третьей силе", желающей на единой языковой 
основе разрабатывать библиотеки, которые можно было бы использовать в 
программах, написанных на разных языках. 
    С недавним выпуском фирмой Borland пакета C++ Builder проблема совместного 
использования кода программами разных языков стала еще более острой. Ключ к 
языковой независимости кода — возможность доступа из каждого языка к основным 
методам класса. Основными являются методы, объявленные виртуальными (их адреса 
помещены в VMT). В следующем разделе подробно рассматривается совместное 
использование объектов с помощью VMT.	~* 

    620 Часть VI. Сложный материал 


    Совместное использование объектов в DLL с помощью виртуальных классов 

    В этой главе рассматривались преимущества программ на основе DLL и, что 
более важно, их содержимое. Вы научились применять обычные функции и 
подпрограммы доступа для манипуляции объектами общего назначения и формами 
Delphi. В данном и следующих разделах описывается более простой способ 
обращения к объектам, размещенным в DLL. 
    Доступ к определенному методу экземпляра объекта, размещенного в DLL, 
возможен как теоретически, так и практически, однако при обязательном 
соблюдении следующих пяти условий. 
    И	Экземпляр объекта должен быть инициализирован в DLL. 
    •	Методы доступа к функциям DLL должны быть виртуальными. 
    •	Порядок объявления виртуальных методов в модулях DLL и ЕХЕ должен совпадать. 
    •	Должны совпадать соглашения о вызовах виртуальных методов в модулях DLL 
и ЕХЕ. 
    •	Объявляемые в модуле ЕХЕ классы не должны быть потомками классов, 
объявленных в DLL. 
    Каждое из этих условий подробно рассматривается в данном разделе. Начнем с 
простого примера модуля, который можно применить для передачи отладочных строк 
на другой компьютер или просто как средство набора телефонных номеров. 

    Класс TCommDebug 

    Предположим, при разработке программы возникла необходимость переслать 
отладочные строки на другой компьютер для трассировки выполнения. Часто в 
подобных случаях программисты вводят ряд подпрограмм, отображающих отладочную 
информацию в консольном окне. Однако проблема в том, что консольное окно часто 
закрывается главным окном отлаживаемой программы. С помощью класса TCommDebug 
отладочные сообщения можно направить через коммуникационный порт в другую 
систему, из которой их можно затем прочесть с помощью любой программы — 
терминала ASCII. 

    На заметку 
    В данном примере показано использование класса в простой демонстрационной 
коммуникационной программе. В ней не указан конкретный протокол, и она не 
может принимать информацию. Класс имеет общее назначение. Для него не важно, 
куда поступают данные: на модем или другой компьютер. В нашем примере 
рассматривается передача данных на другой компьютер. Чтобы выполнить пример 
при отсутствии другого компьютера, класс можно применить без какой-либо 
модификации как средство набора телефонных номеров. Для этого следует открыть 
с его помощью порт, используемый модемом, и вызвать метод StringOut со строкой 
'ATDP'+555-5555 (555-5555 заменяется реальным номером) в качестве параметра. 
Мы еще вернемся к этому примеру, так что читайте внимательно! 

    Прежде всего, рассмотрим исходный текст файла SHARED! . PAS, 
представленный в листинге 26.9. 

    Листинг 26.9. \UDELPHI3\CHP26\SHAREU1. PAS. Класс, в котором используется 
коммуникационный программный интерфейс Win32 

    unit shareul; interface 
    uses 
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, 
Menus, StdCtrls, Mask, ComCtrls, Buttons, ExtCtrls; 
    type 
    TCommDebug = class (TObject) private 
    FhPort: THandle; public 
    procedure  StringOut(sValue: string); virtual; Stdcall; 
    constructor Connect(sComPort: String); 
    destructor  Destroy; override; end; 


    Глава 26. Программирование DLL: сложный материал 621 


    function GetComDebuglnstance(iComPort: integer): TCommDebug; StdCall; 
    exports 
    GetComDebuglnstance; 
    implementation 
    function GetComDebuglnstance(iComPort: integer): TCommDebug; begin    "* • 
    result := TCommDebug.Connect(Format('COM%d',[iComPort+1])); end/procedure 
TCommDebug.StringOut(sValue: string); var 
    wCtr,wLen,wOffset : word; 
    TxBuf : array[0..255] of byte; 
    dwSent: DWord; begin 
    if strlen(PChar(sValue)) > 255 then 
    Raise Exception.Create('Value passed to TCommDebug.StringOut exceeds' + 
    ' 255 bytes limit'); 
    dwSent  := 0; 
    wOffset := 0; 
    if LongBool(Length(sValue))  then begin 
    wLen := Length( sValue ) ; 
    if wLen > 0 then 
    begin 
    for wCtr := 0 to wLen-1 do begin 
    TxBuft wCtr ] := ord( sValue[ wCtr +1] ); wOffset := wCtr; end; 
    TxBuf[ wOffset+1 ]	:= Ord(#13); 
    TxBuf[ wOffset+2 ]	:= Ord(#10); 
    TxBuf[ wOffset+3 ]	:= Ord(#0); 
    WriteFile( FhPort, TxBuf, wLen+2, dwSent, nil ); end; end; end; 
    constructor TCommDebug.Connect(sComPort: String); 
    var DCB: TDCB; 
    begin 
    inherited Create; 
    if Length(sComPort) = 0 then 
    Raise Exception.Create('Could not retrieve port settings.'); 
    FhPort := CieateF,ile(PChar(sComPort+':'),(Generic_Read + Generic_Write), 
0, nil, Open_Existing,0,0); 
    if FhPort = INVALID_HANDLE_VALUE then begin 
    FhPort := 0; 
    Raise Exception.Create('Open failed') end; 
    _GetCommState(FhPort,DCB); 
    with DCB do 
    begin 


    622	Часть VI. Сложный материал 


    BaudRate  := 9600; 
    ByteSize  := 8; // Databits : 4-8 
    Parity    := 0; // Parity   : 0-4 = none,odd,even,mark,space StopBits  := 
0; // StopBits : 0,1,2 = 1, 1.5, 2 end; 
    if not SetCommState( FhPort, DCB ) then 
    Raise"™Sxception.Create('SetState error: '+IntToStr ( GetLastError() ) ); end; 
    destructor TCommDebug.Destroy; begin 
    if FhPort <> 0 then CloseHandle(FhPort); 
    inherited Destroy; end; 
    end. 

    В разделе interface модуля SHAREU1.PAS определены разделы uses и type. В 
разделе type объявлен класс TCommDebug, в котором директивой private определен 
член FhPort, содержащий дескриптор коммуникационного порта после его открытия. 
Этот класс имеет единственный виртуальный метод: 

    procedure       StringOut(sValue:   string);   virtual;   Stdcall; 

    После инициализации объекта TCommDebug и его связи с портом процедура 
StringOut записывает в порт данные, содержащиеся в параметре sValue. Кроме 
того, в классе TCommDebug объявлен_новый_крнструктор с именем Connect: 

    constructor  Connect(sComPort:   String); 

    Единственный параметр этого конструктора определяет порт СОМ 
(последовательный порт; не путать с интерфейсом. СОМ!), который надо открыть. 
Формат строки — СОМп, где п — номер коммуникационногб'портаТ например 

    Inst   :=  TCommdebug.Connect('Com2');   //  Подключение  к 
коммуникационному порту 2. 

    Наконец, в классе TCommDebug объявлен и переопределен деструктор Destroy: 
destructor     Destroy;   override; 
    Поскольку модуль SHAREU1. PAS должен быть включен в DLL-проект, DLL 
должна_экспрртировать подпрограмму инициализации объекта TCommDebug. Эта 
подпрограмма определена в файле SHAREU1. PAS: 

    function GetComDebuglnstance(iComPort:   integer):   TCommDebug;   StdCall; 

    Вместо сложного строкового параметра в функции определен целый параметр 
iComPort. Значение 0 этого параметра соответствует порту СОМ1, 1 — СОМ.2 и 
т.д. В функции GetComDebuglnstance этот параметр преобразуется в COOT---• 
ветствующую строку, как и требуется для конструктора TCommDebug. 

    На заметку 
    Реализация модуля Shareul .pas, представленного в листинге 26.9, крайне 
проста. Возможно, коммуникационные операторы окажутся не всем понятными, но 
применять их совсем не сложно. В Win32 для 
комму-никационных^опе^ауийиспользуются.^с^адддртные _средстШ,фаДд0вого 
ввода-вывода из программного интерфейса. Тем, кто пока недостаточно знаком с 
этими средствами, следует обратиться к справочной системе по Win32 API. 


    Применение класса TCommDebug 

    После компиляции класса TCommDebug и модуля Shareul .pas в DLL-проекте 
этим классом можно воспользоваться из модуля ЕХЕ или другой DLL. Читатель 
может спросить, в чем тут, собственно, проблема. Ведь все, что делалось до сих 
пор, могло быть сделано в любом другом DLL-проекте. Созданы класс и программа, 
возвращающая экземпляр этого класса. Ну и что? 
    Вот здесь-то и начинается^амое интересное. Внимательный читатель, конечно, 
обратил внимание на то, что процедура SffringOut объявлена с директиво^птТгЬиа!: 

    procedure  StringOut(sValue:   string);   virtual;   Stdcall; 

    Сделано это вовсе не для того, чтобы переопределить эту процедуру в 
классе-потомке. Просто после создания экземпляра TCommDebug адрес этой 
процедуры должен оказаться в VMT (в первой позиции). 

    Глава 26. Программирование DLL: сложный материал 623 

    Когда загружающая программа вызывает экспортируемую функцию 
GetComDebuglnstance, последняя возвращает экземпляр типа TCommDebug, точнее— 
указатель на блок памяти, содержащий этот экземпляр. Выше уже было показано, 
что в первом поле этого блока содержится указатель на VMT. В данном случае 
первая позиция VMT — это адрес метода StringOut. 

    Неявный параметр экземпляра 

    Теоретически в качестве альтернативного способа использования объекта вне 
DLL можно присвоить значению, возвращаемому функцией GetComDebuglnstance, 
указательный тип. После этого можно обратиться к переменной, на которую 
указывает указатель, присвоив ей, опять же, тип указателя, и уже по этому 
указателю получить адрес функции StringOut в VMT. По этому адресу можно 
вызывать саму функцию. 
    Одно из семантических следствий такого подхода состоит в необходимости 
переменной-функции с двумя параметрами (в Delphi нельзя обращаться 
кподпрограммам по их адресам). В первом параметре указывается экземпляр, 
вШвр'ащаёмый функцией GetcomDebuginstance, во втором*— отладочная строка. Это 
необходимо, потому что методу в качестве первого параметра неявно передается 
параметр self, представляющий экземпляр объекта. Ведь в данном случае метод 
вызывается как обычная функция, так что передачу этого параметра приходится 
программировать вручную. Если такой подход кажется туманным, значит, так оно и 
есть. Применять этот метод следует лишь в языках, не поддерживающих модели 
объектов Delphi (или СОМ). 
    Итак, на данный момент имеется DLL, которая экспортирует функцию 
GetComDebuglnstance, возвращающую экземпляр класса. Модуль, получающий этот 
экземпляр (предположим, ЕХЕ), может по нему вызывать методы объекта. Однако 
класс TCommDebug определен в DLL, поэтому модуль ЕХЕ его не знает. 
Столкнувшись с попыткой импортиро-вать""функцию GetComDebuglnstance, 
компилятор выдаст сообщение об ошибке, поскольку он не знает о типе TCommDebug 
возвращаемого значения: 

    function GetComDebuglnstance(iComPort:   integer):   TCommDebug;   
StdCall;   external 'Shareit.dll'; 

    Проблема решается путем переобъявления класса в модуле ЕХЕ с абстрактными 
методами (в данном случае — StringOut). Объявление метода StringOut 
абстрактным позволяет избежать ошибок при компиляции. Обращение к абстрактному 
методу, с точки зрения компилятора, вполне законно. 
    При переобъявлении виртуальной части класса в модуле ЕХЕ, как показано в 
следующем примере, имена класса и метода значения не имеют. Однако соблюдение 
максимального сходства объектов упрощает как ™    программирование, так и 
сопровождение программы. 
    Не следует забывать, что все методы обязательно должны объявляться в том 
порядке, в котором они объявлены в DLL, так как вызовы из модуля 
ЕХЕ_разрешаются в порядке их объявления, а не по именам. Демонстрационный 
класс TCommDebug — простой класс, непосредственный потомок класса TObject. Это 
гарантирует, что функция StringOut будет представлена первой в VMT. Однако VMT 
содержит также адреса виртуальных методов классов-предков (если таковые 
имеются) в порядке их объявления, как и адреса всех методов, которые могли 
быть переобъявлены. 
    Таким образом, если бы класс TCommDebug был потомком некоторого класса 
TCustomComm, имеющего собственнь1е_виртуальные методы, то метод TCommDebug. 
StringOut был бы представлен не первой позицией в VMT, а, скажем, пятой. 
    Экземпляр, возвращаемый функцией GetComDebuglnstance, имеет собственную 
VMT с адресом функции StringOut, поэтому работает правильно. Рассмотрим модуль 
в листинге 26.10, в котором представлен данный способ. 
    В разделе interface модуля LoadComul .pas переобъявлен класс TCommDebug. 
Обратите, однако, внимание, что член данных FhPort, конструктор Connect и 
деструктор Destroy не переобъявляются. Они в модуле ЕХЕ не нужны, но 
существуют и работают в оригинальном объекте в DLL. Объявление класса 
TCommDebug в модуле ЕХЕ нужно только для того, чтобы компилятор знал о его 
существовании и мог разрешать обращения к методу StringOut. 

    Листинг 26.10. \UDELPHI3\CHP2 6\LOADCOMUl. PAS. В форме из этого модуля 
содержится только один элемент типа TButton. Функцию StringOut вызывает метод Buttoniciick 

    unit LoadComul; interface 
    uses 
    Windows, Messages, SysUtils, Classes, Graphics, 


    624	Часть VI. Сложный материал 


    Controls, Forms, Dialogs, StdCtrls; 
    type 
    TForml = class(TForm) 
    Buttonl: TButton; 
    procedure ButtonlClick(Sender: TObject); private 
    { Объявления private. } public 
    { Объявления public. } end; 
    TCommDebug = class(TObject) public 
    procedure  StringOut(sValue: string); virtual; Stdcall; abstract; 
end;	'"'""             " 
    var 
    Forml: TForml; 
    function GetComDebuglnstance(iComPort: integer): TCommDebug; StdCall; 
    external 'Shareit.DLL' implementation 
    {$R *.DFM} 
    procedure TForml.ButtonlClick(Sender: TObject); 
    var CommDebug: TCommDebug; 
    begin 
    CommDebug := GetComDebuglnstance (2); ~ try	'' {•^define PhoneDialer} 
//Следуе1._у_брать А, 
    // чтобы класс работал как номеронабиратель. {$ifdef PhoneDialer} 
    //Строка для набора номера, паузы и прерывания связи. 
CommDebug.StringOut('ATDP5555555, ,АТН'); {$else} 
    CommDebug.StringOut('Возможно, это отладочная строка'); {$endif} finally 
    CommDebug.Free; end; end; 
    end. 

    Реализация, представленная в листинге 26.10, содержит единственный метод— 
обработчик событий мыши для компонента Buttonl соответствующей формы. В методе 
ButtonlClick объявлена локальная переменная CommDebug типа TCommDebug. Этой 
переменной присваивается значение, возвращаемое импортируемой функцией 
GetComDebuglnstance. Следующий оператор — вызов функции StringOut, помещенный 
в блок try. . . finally, со строковым параметром для коммуникационного порта. 

    На       1 заметку. 
    В данном примере использована директива компилятора, ifdef, чтобы 
определить, для чего функция StringOut применяется в данном проекте— для 
набора телефонных номеров или передачи отладочных сообщений на другой компьютер. 

    Если второго компьютера нет, но есть модем, следует выбрать вариант с 
ATDP-текстом. Часть 'ATDP' текста— это команда модему набрать следующий 
телефонный номер. Часть строки ' 5555555 ' надо заменить настоящим номером. 
Последовательность запятых — это команда модему сделать паузу_, ' АТН' — 
повесить трубку. Во время экспериментов я воспользовался собственным 
телефонным номером: номер набирался, следовала пауза до сигнала занятости (я 
ведь находился на линии), после чего связь прерывалась. 

    Глава 26. Программирование DLL: сложный материал 625 

    После вывода строки в порт вызывается метод Free, который, в свою очередь, 
вызывает деструктор экземпляра TCommDebug. Деструктор не присутствует в 
экземпляре явно, но вызов его произойдет правильно даже из модуля ЕХЕ, 
несмотря на переопределение в DLL. 

    Совместное использование объектов: краткие выводы 

    Только что были рассмотрены очень важные методы программирования. Подведем 
краткие итоги. 
    1.   Создан класс TCommDebug с единственным виртуальным методом StringOut. 
Метод записывает в коммуникационный порт строку для отладки или набора 
телефонного номера. 
    2.   Класс TCommDebug компилируется в DLL-проекте. 
    3.   DLL экспортирует единственную функцию GetComDebuglnstance, которая 
вызывает конструктор TCommDebug . Connect и возвращает экземпляр объекта в 
вызывающий модуль (обычно в ЕХЕ). 
    4.   В модуле ЕХЕ объявляется класс TCommDebug, содержащий только 
виртуальный метод StringOut. 
    5.   Мсщль ЕХЕ импортирует из DLL функцию GetComDebuglnstance. 
    6.   Метод ButtonlClick в модуле ЕХЕ вызывает функцию GetComDebuglnstance, 
чтобы получить из DLL экземпляр типа TCommDebug. 
    7.   Вызывается метод StringOut этого экземпляра, после чего экземпляр 
удаляется из памяти с помощью метода Free. 
    Однако один вопрос остался без ответа. Как модуль ЕХЕ определяет 
местоположение метода StringOut в памяти? Очень просто. Компилятор знает, что 
адрес этого метода указан в первой позиции VMT, как он и был переобъявлен в 
модуле ЕХЕ. Когда~п]роисход"йт вызов, сгенерированный компилятором код находит 
адрес метода в VMT экземпляра, возвращенного функцией GetComDebuglnstance. 
Имея указатель на VMT, остается обратиться к ее первой позиции и выполнить 
вызов по расположенному в ней адресу. 

    Совместное использование данных 

    В главе 25 рассматривались проблемы, связанные с переносом программ из Win 
16 в Win32. Самое большое изменение в архитектуре DLL Win32 состоит в том, что 
сегмент данных DLL включается в процесс, в котором отражена эта DLL. Это 
означает, что, если процесс А загружает DLL, глобальные и динамически 
распределяемые данные и указатели недоступны для процесса Б, который 
попытается загрузить одновременно ту же DLL. Такая архитектура логична, но не 
совпадает с архитектурой Win 16. 
    В Win 16 каждая DLL имеет собственный сегмент данных с глобальным доступом 
к нему. Задание А может распределить и инициализировать блок памяти, а задание 
Б может обращаться к нему посредством тех же указателей. Программисты быстро 
освоили это средство для обмена данными между программами. 
    В Win32 также есть несколько способов передачи данных за пределы процесса, 
но ни в одном из них DLL не требуется. Если в Win 16 DLL предназначались 
только для совместного пользования данными, то для Wiri32'taKOfl подход уже 
устарел. С другой стороны, во многих случаях оказывается, что код совместного 
использования данных B^Vin32 дублируется в'каждом процессе, так что есть смысл 
помещать его в DLL. Рассмотрим простой способ распределения глобально 
доступного блока памяти в пределах системного страничного файла, а затем 
перейдем к способам его размещения в DLL-проекте и создания загрузчика для его 
загрузки и демонстрации. 

    Применение объекта отображения файла ядра Windows 

    В основе архитектуры памяти Win32 лежит системный страничный файл. Этот 
файл располагается на жестком диске и используется наряду с физической памятью 
(RAM). Объединение RAM с дисковым пространством предоставляет операционной 
системе и программам возможность распределять больше памяти, чем позволяет RAM. 
    Для выполнения программы в""стра"нйчном'"файле необходимо распределить 
лишь небольшой блок памяти. В нем содержится таблица адресов, отображающих 
ЕХЕ-файл вместе с зависящими от него DLL. В ядре Windows есть объект 
отображения файла, отображающий расположенные на жестком диске файлы в 
адресное пространство программы. 
    Что касается совместного использования данных программами, можно прийти к 
выводу, что процесс, описываемый в данной книге, очевиден: создать объект 
отображения файла для некоторого файла данных, записать в этот файл данные в 
одной программе и отобразить его в другую программу, в которой данные могут 
быть считаны. Но это не совсем так. Объект отображения файла можно создать 
непосредственно для системного страничного файла. При этом можно указать, 

    626	Часть VI.''Сложный материал 

    сколько понадобится адресного пространства физической памяти. Затем файл 
можно отобразить в адресное пространство программы, где непосредственно 
считывать из него данные либо записывать их. 
    Объект отображения файла может манипулировать глобальным для системы 
страничным файлом. Записанные в этот файл данные доступны для любого 
количества программ без каких-либо ограничений границ процессов. 

    Создание объекта отображения файла 

    Объект отображения файла (File-Mapping Object) создается с помощью функции 
CreateFileMapping. 

    function CreateFileMapping(hFile:   THandle; 
    IpFileMappingAttributes:   pSecurityAttributes; flProtect,   
dwMaximumSizeHigh,   dwMaximumSizeLow:   DWORD; IpName:   PChar):   THandle;   stdcall; 

    Параметр hFile указывает имя файла, связанного с объектом. Если это 
системный страничный файл, указывается 
магическое_щалени?_$1:РРРРРРР„(консддщ-а,MAXDWQRD, определенная в модуле 
WINDOWS. РАЗ)ГТ1арамётру IpFile-MappingAttributes можно присвоить 
зТП!чТгнШ~п11* что соответствует атрибутам секретности, устанавливаемым по 
умолчанию. Параметр flProtect задает атрибуты защиты, применяемые при 
отображении файла. В следующем примере этому параметру присваивается значение 
PAGE_READWRITE. Параметры dwMaximumSizeHigh и dwMaximumSizeLow представляют 
соответственно верхнее и нижнее двойные слова 64-битового численного значения, 
задающего максимальный объем отображаемого файла. При отображении системного 
страничного файла эти параметры можно рассматривать как количество байтов 
памяти, которые необходимо распределить. 
    Параметр IpName — это уникальное имя, по которому к объекту отображения 
файла можно обратиться из другого процесса (не из того, в котором этот объект 
создан). Скажем, если процесс А вызывает функцию CreateFileMapping. присвоив 
этому параметру значение _GMem, а затем то.же.самое делает процесс. Б, 
,'Ш,.оба,лроцессаабу,щт обращаться к одному^ блоку памяти. Именно так 
происходит совместное использование .данных. При успешном выполнении функция 
CreateFileMapping возвращает дескриптор объекта отображения файла, в противном 
случае возвращается нуль. 

    Отображение файла 

    После создания объекта отображения файла можно отобразить блок памяти в 
адресное пространство программы. Для этого вызывается функция MapViewOf File. 

    function  MapViewOfFile(hFileMappingObject:   THandle; 
    dwDesiredAccess:   DWORD; dwFileOffserHigh,   dwFileOffserLow, 
dwNumberOfBytesToMap:   DWORD):   Pointer;   stdcall; 

    Параметр hFileMappingObject— это дескриптор, возвращенный функцией 
CreateFileMapping. Параметр dwDesiredAccess задает вид доступа к экземпляру. В 
следующем примере это значение равно FILE_MAP_WRITE. Параметры 
dwFileOffserHigh и dwFileOffserLow образуют 64-битовое численное значение, 
задающее смещение внутри файла, с которого должно начинаться отображение. При 
работе с системным страничным файлом оба эти параметра должны быть равны нулю. 
Параметр dwNumberOfBytesToMap указывает количество байтов, которые должны быть отражены. 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Если параметру dwNumberOfBytesToMap присвоить значение 0, функция 
MapviewOfFile попытается отобразить всю память, указанную параметрами 
dwMaximumSizeHigh и dwMaximumSizeLow при вызове функции CreateFileMapping. 
Значения этих параметров могут быть довольно большими, что может привести к 
ошибке при выполнении функции MapviewOfFile. Если объем отображаемой памяти 
заранее неизвестен, параметру dwNumberOfBytesToMap следует присвоить значение 1. 

    В случае успешного выполнения функция MapviewOfFile возвращает адрес 
начала образа, в противном случае возвращается nil. Адрес начала содержится в 
указателе без типа, который можно непосредственно считывать или записывать. 

    Удаление образа файла и объекта отображения 

    После окончания работы с образом файла следует удалить его из памяти и 
закрыть дескриптор объекта отображения этого файла. Образ файла удаляется с 
помощью функции UnMapViewOfFile. В модуле WINDOWS. PAS эта функция объявлена 
следующим образом: 
    function UnMapViewOfFile(IpBaseAddress:   Pointer):   BOOL;   stdcall; 


    Глава 26. Программирование DLL: сложный материал 627 

    Параметр IpBaseAddress — это указатель, возвращенный (при успешном 
выполнении) функцией MapViewOf-File. В случае успешного выполнения функция 
UnMapViewOf File возвращает True. Удалив образ, следует закрыть дескриптор 
объекта отображения файла. Это делается с помощью функции CloseHandle, 
объявленной в модуле WINDOWS . PAS следующим образом: 

    function CloseHandle(hObject:   THandle):   BOOL;   stdcall; 

    Параметру hObject задается значение, возвращенное (при успешном 
выполнении) функцией CreateFileMapping. В случае успешного выполнения функция 
CloseHandle возвращает True. 

    Применение объекта отображения файла в DLL 

    Предположим, необходимо создать DLL, распределяющую блок памяти из 
системного страничного файла, доступ к которому должно иметь несколько 
программ. DLL может сначала распределить большую область памяти, внутри 
которой распределить области поменьше по собственной схеме. Программы, 
использующие данную DLL, могут запрашивать блоки памяти разных объемов, 
управляемые из DLL. 
    Рассмотрим простой пример, в котором для отображения некоторого объема из 
системного страничного файла применены средства отображения файлов из 
программного интерфейса Windows. DLL экспортирует одну функцию, возвращающую 
указатель на глобально распределенную область памяти. Рассмотрим листинг 26.11. 

    Листинг 26.11. \UDELPHI3\CHP26\SHAREHEAP.DPR. Инициализация и 
деинициализация объекта отображения файла с помощью процедуры DLLMain 

    library ShareHeap; 
    uses 
    Forms, 
    SysUtils, 
    Windows; 
    const Key: String = '_GMem'; MaxSize = $FFFF; 
    var 
    hObjHand: THandle; pGMem: pointer; 
    procedure UnmapMemory; begin 
    if Assigned(pGMem) then begin 
    UnMapViewOfFile(pGMem); pGMem := nil; end; 
    if hObjHand > 0 then CloseHandle(hObjHand); 
    hObjHand := 0; end; 
    procedure MapMemory(dwAllocSize: DWORD); begin 
    hObjHand := CreateFileMapping(MAXDWORD,nil,PAGE_READWRITE, 
    0,dwAllocSize,PChar(Key)); 
    if (hObjHand = 0) then 
    Raise Exception.Create('Could not create file-mapping object1); 
    pGMem := MapViewOfFile(hObjHand,FILE_MAP_WRITE, 0,0,1); 
    if not Assigned(pGMem) then 
    begin 
    UnmapMemory; 


    628	Часть VI. Сложный материал 


    Raise  Exception.Create('Could not map   file'); end; end; 
    procedure  DLLMain(dwReason:   DWORD); begin 
    case  dwReason of dll_Process_Attach: begin 
    pGMem   :=  nil; hObjHand   :=  0; MapMemory(MaxSize); end; dll_Process_Detach: 
    UnmapMemory; end; end; 
    function GetGBlock:   Pointer;   StdCall; begin 
    result   :=  pGMem; end; 
    exports 
    GetGBlock; 
    begin 
    DLLProc   :=   SDLLMain; 
    DLLMain(dll_Process_Attach) end. 

    В конце листинга 26.11 расположены инициализационные операторы. Адрес 
локальной процедуры DLLMain присваивается глобальной переменной DLLProc. Это 
обеспечивает уведомление DLL о каждом ее подключении к процессу и отсоединении 
от него. После этого DLL вызывается явно с константой dll_Process_Attach в 
качестве параметра. Если вы незнакомы с реализацией в Delphi процедуры 
DLLMain, то, прежде чем изучить данный пример, обратитесь к главе 25, 
"Программирование DLL: введение". 
    В процедуре DLLMain параметр dwReason оценивается в операторе case. . .of. 
Если его значение оказывается равным dll_Process_Attach, DLL подключается к 
еще одной программе. При этом инициализируются две глобальные переменные, по 
которым вызывается локальная функция MapMemory (описываемая ниже). 
    Если значение параметра dwReason оказывается равным dll_Process__Detach, 
DLL выгружается из памяти; при этом должны быть удалены все блоки, выделенные 
для объектов отображения файлов. В приведенном примере для этого вызывается 
локальная процедура UnmapMemory. 
    Так что же здесь происходит? DLL необходимо знать, как распределить память 
и отобразить в нее некоторую часть системного страничного файла. В данном 
примере все это происходит при загрузке DLL. При выгрузке DLL образ удаляется 
из памяти вместе с объектом отображения файла. В нашем случае использована 
лишь одна DLL, но та же последовательность событий должна иметь место при 
загрузке и выгрузке DLL другими процессами. 
    Наконец, модуль ЕХЕ вызывает экспортируемую функцию GetGBlock, 
возвращающую указатель. Если процесс А загружает DLL и вызывает функцию 
GetGBlock, значит, это указатель на образ в адресном пространстве процесса А. 
Если затем ту же DLL загружает процесс Б, который также вызывает функцию 
GetGBlock, возвращается указатель на образ, помещенный в процесс Б. 
Теоретически образ может существовать в каждом процессе по отдельному адресу, 
так как отображение выполняется заново при каждой загрузке DLL. 

    Доступ к глобально распределенной памяти 

    С помощью рассмотренного выше модуля SHAREHEAP. DLL можно распределить 
блок памяти из системного страничного файла, а затем с помощью экспортируемой 
функции GetGBlock получить указатель на него. После первоначальной загрузки 
модуль SHAREHEAP.DLL могут загрузить еще несколько программ. При этом функция 
GetGBlock возвращает каждой из них указатель на блок, распределенный 
первоначально. Однако значения указателя в каждом случае могут различаться, 
поскольку блок памяти отображается в адресное пространство каждого процесса в 
отдельности. ТГЗТйстинге 26.12 представлена простая программа с использованием 
модуля SHAREHEAP. DLL. 

    Глава 26. Программирование DLL: сложный материал 629 


    Листинг 26.12. \UDELPHi3\CHP26\LoadShareHeapUl. PAS. Программа-загрузчик с 
применением классов TBut-   ! В ton и TTimer для демонстрации использования 
модуля SHAREHEAP . DLL 

    unit LoadShareHeapUl; interface 
    uses 
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, 
StdCtrls, ExtCtrls; 
    type 
    TForml = class(TForm) 
    Memol: TMemo; 
    GroupBoxl: TGroupBox; 
    WriteButton: TButton; 
    Editl: TEdit; 
    Timerl: TTimer; 
    procedure FormCreate(Sender: TObject); 
    procedure WriteButtonClick(Sender: TObject); 
    procedure TimerlTimer(Sender: TObject}; private 
    { Объявления private. } public 
    { Объявления public. } end; 
    var 
    Forml: TForml;      ft  A pcBuf: PChar;     •, •• &.?,, 
    function GetGBlock: Pointer; StdCall; external 'SHAREHEAP.DLL' 
    implementation 
    ($R *.DFM} 
    procedure TForml.FormCreate(Sender: TObject); begin 
    pcBuf := GetGBlock; end; 
    procedure TForml.WriteButtonClick(Sender: TObject); begin 
    Editl.GetTextBuf(pcBuf, Length(Editl.Text)+1); end; 
    procedure TForml.TimerlTimer(Sender: TObject); begin 
    Memol.SetTextBuf(pcBuf); end; 
    end. 

    При создании главной формы происходит обращение к функции GetGBlock из 
модуля SHAREHEAP.DLL, возвращающей указатель на глобально распределенный блок 
памяти из системного страничного файла. Этот указатель записан в глобальной 
переменной с именем pcBuf. 
    Когда форма выведена на экран, в элемент Edit можно ввести текст. После 
ввода текста и щелчка мышью на кнопке Write выполняется метод 
WriteButtonClick. Он извлекает данные из элемента Edit и записывает их в блок 
памяти совместного использования, на который указывает переменная pcBuf. Через 
несколько мгновений после щелчка на кнопке Write введенный текст появится в 
элементе Memo. Это происходит благодаря применению в программе компонен

    630	Часть VI. Сложный материал 

    та TTimer, вызывающего через каждые 1 000 мс метод TimerlTimer. Последний 
помещает в буфер элемента Memo то, что находит в совместно используемом блоке 
памяти, на который указывает переменная pcBuf. 
    Чтобы увидеть весь процесс в действии, надо загрузить и откомпилировать 
проект LoadShareHeap, включающий файл LoadShareHeapUl .pas, затем точно так же 
загрузить и откомпилировать DLL-проект ShareHeap и зап\/стит>.. 
два_экземпляр_а_Ьоас13ЬагеНеар. Если теперь в элементе Edit программы А ввести 
текст и щелкнуть на кнопке Write, этот текст появится в элементах Memo обеих 
программ (А и Б), То же самое произойдет, если текст ввести в программе Б. 

    Структура РЕ-файла 

    Выполняемые модули для платформ Win32 известны как переносимые выполняемые 
модули (Portable Executable — РЕ). РЕ — это файловый формат, содержащий 
различные структуры как для программы, так и для операционной системы. Если мы 
различаем модули DLL и ЕХЕ, то для операционной системы это одно и то же. 
Действительно, что касается файлового формата, единственное различие между ЕХЕ 
и DLL заключается в двух байтах флагов, записанных в файле по определенному 
адресу. ~~~ 
    Знание внутреннего формата выполняемого модуля (в дальнейшем под этим 
термином будем подразумевать модуль DLL или ЕХЕ) необязательно влечет за собой 
более эффективное программирование в Delphi. Однако оно помогает снять 
мистический ^алет с определенных установок и директив компилятора или проекта 
(как задаваемых самим программи-стомТтак и установленных по умолчанию). Кроме 
того, информация, излагаемая в данном разделе, — необходимая предпосылка к 
изучению утилиты Turbo Dump, которая будет рассмотрена в конце этой главы. 

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

    Выполняемый модуль (ЕХЕ или DLL) при загрузке отображается в адресное 
пространство конкретного процесса, начиная с первого байта файлового образа. 
    Модули ЕХЕ и DLL отображаются в адресное пространство загружающего 
процесса с помощью средств, пре-доставляемых операционной системой. Эти 
средства рассматривались в начале данной главы. Вспомните: сна-" чала был 
создан объект отображения файла (поставляемый ядром Windows), затем в память 
была отображена часть файла, которому он адресуется (в данном случае — 
страничного файла). Когда операционная система отображает модуль ЕХЕ или DLL в 
память, она всегда начинает отображение с первого байта образа. Область в 
процессе, в которую отображается модуль ЕХЕ или DLL, именуется базой или 
базовым адресом образа. Этот термин следует запомнить: в дальнейшем он 
употребляется довольно часто (и иногда вольно). 
    С первого байта образа выполняемого файла начинается заголовок DOS (или 
заголовок MZ). Заголовок DOS является первой структурой файла РЕ, поскольку в 
нем описывается следующая дальше вставка DOS — маленькая программа реального 
режима DOS, которая, как правило, лишь выводит на экран сообщение о том, что 
данный модуль должен выпол-няться,в Microsoft Windows. 
    За вставкой DOS следует сигнатура файла РЕ — операционная система, для 
которой предназначен модуль. Это может быть DOS, NT/Win95 или даже OS/2. 
Файловые форматы всех этих платформ схожи, поэтому данное поле предоставляет 
возможность точно определить, может ли данный модуль выполняться в среде NT 
или Win95. Альтернативой использования такого поля может быть предположение, 
что файл имеет правильный формат, после чего остается лишь запустить его, а 
там будь что будет. 
    За сигнатурой идет еще одна структура, именуемая загод&зкрм файла РЕ. Поля 
этой структуры содержат такие данные, как тип машины, время и дата, адрес 
таблицы символов, отладочных характеристик и т.п. 
    Затем следует еще одна структура — необязательный заголовок. В общем, 
необязательный заголовок не так уж необязателен. В нем среди прочего 
содержится очень важная информация: версия компоновщика, объем 
инициализированных и неинициализированных данных, размер кода, адрес точки 
входа в программу, флаги загрузчика и т.п. Одно из наиболее важных полей 
данной структуры именуется базой образа (это десятое поле структуры). 
    База образа задается компоновщиком и представляет собой предпочтительный 
базовый адрес отображения модуля в загрузочном процессе. Если база образа 
известна заранее, компоновщик мржех выполнить некоторые оптимизационные 
операции. Однако нет никакой гарантии, что заданная компоновщиком база образа 
совпадёт с фактическим адресом отображения файла при выполнении программы. 

    Глава 26. Программирование DLL: сложный материал	631 

    Фактическая база образа определяется во время выполнения программы и 
представляет собой адрес, по ко-торому начинается отображение файла модуля в 
адресном пространстве процесса. В результате вызова ^ функции LoadLibrary или 
LoadLibraryEx возвращается переменная типа THandle, обычно именуемая 
hlnstance. В Win32 это называется дескриптором экземпляра и представляет собой 
фактический адрес базы образа. 
    Предпочтительная база образа, установленная в Delphi по умолчанию, равна 
$00400000. Во время выполнения это значение считывается и используется для 
отображения файла. Если файл не удается отобразить по предпочтительном) адресу 
(возможно, он занят модулем ЕХЕ или другой DLL), файл будет отображен по 
другому адресу, для чего придется перераспределить другие области данных, 
например адреса, используемые определенными командами. Такое перераспределение 
замедляет загрузку программы, но, к счастью, незначительно. 
    В Delphi, кроме предпочтительной базы образа, могут задаваться минимальный 
и максимальный размеры стека во вкладке Linker диалогового окна Project 
Options. Это окно открывается с помощью команды Proi-заметку    ect^Options 
    Наряду с базой образа необязательный заголовок содержит поля максимального 
и минимального объемов стека программы (главной подзадачи). По умолчанию 
максимальный объем стека равен 1 Мбайт. В Win32 значение максимального объема 
стека используется операционной системой для фиксации его переполнения в 
случае бесконечной рекурсии. Если программа застревает в бесконечном цикле 
обращения подпрограммы к самой себе, заполнение стека скоро достигнет 
максимального значения I Мбайт, в результате чего возникнет исключительная 
ситуация. Если же объем стека будет ограничиваться только имеющейся в наличии 
виртуальной памятью, процесс будет продолжаться до занятия стеком всей памяти, 
что, скорее всего, при ведет к полной потере системных ресурсов и зависанию 
самой системы. 

    Разделы 

    За рассмотренными выше основными заголовками в файле формата РЕ 
размещается таблица заголовков раздела образа, в которой содержатся сведения о 
неформатных данных, располагающихся в конкретных разделах выполняемого модуля. 
Разделы в файле формата РЕ можно сравнить с сегментами 16-разрядного файлового 
формата. Следует понимать, что неформатные данные, составляющие код модуля, 
упакованы в одном из разделов образа файла. В другом разделе упакованы 
неформатные данные, составляющие ресурсы, еще в одном — данные модуля и т.д. 
    Заголовки раздела образа содержат информацию о неформатных данных в 
конкретных разделах, например положение каждого раздела по отношению к базе 
образа и его характеристики (атрибуты). 
    ~'"i В 16-разрядной версии Delphi можно было задавать характеристики 
сегмента кода с помощью директивы 4 компилятора $С. Можно было задать, 
например, атрибут PRELOAD (предварительная загрузка), который за-I носился в 
сегментную таблицу, в позицию, соответствующую данному сегменту. При загрузке 
модуля в па-j; мять сегменты, помеченные как PRELOAD, загружаются первыми. В 
файловом формате РЕ существуют атрибуты разделов, но среди них нет атрибута 
PRELOAD и их нельзя задавать извне. 
    Характеристики разделов файлового формата РЕ представляют сочетание одного 
или нескольких следующих атрибутов. 
    •	$00000020 (содержит код) 
    И	$20000000 (выполняемый) 
    8	$00000040 (содержит инициализированные данные) 
    •	$40000000 (для чтения) 
    •	$10000000 (для совместного использования) 
    К	$80000000(для записи) 
    Наиболее интересен, пожалуй, атрибут $10000000 (для совместного 
использования). Операционная система отображает файл таким образом, чтобы 
информация была доступна любому процессу. К сожалению, в Delphi нет директивы 
для установки этого флага программистом. 

    632	Часть VI. Сложный материал 

    В главе 25, "Программирование DLL: введение", указывалось, что функции 
LoadLibraryEx в качестве третьего параметра может быть задан флаг 
Load_Library_As_DataFile. Если этот флаг установлен, выполняемый модуль 
отображается в адресное пространство процесса как обычный файл данных. Чтобы 
во время выполнения манипулировать разделами образа выполняемог о файла (без 
его фактического выполнения), которые иначе помечены флагом только для чтения, 
можно установить для функции LoadLibraryEx флажок Load_Library_As_DataFile. 
Результатом выполнения функции будет значение Ып-stance, соответствующее 
базовому адресу образа. 
    Практически, если позволяет язык программирования, в выполняемом модуле 
может содержаться любое количество разделов, определенных программистом, с 
любыми именами и наборами атрибутов. Delphi, однако, на данный момент не столь 
гибка и не позволяет генерировать собственные разделы. Компоновщик Delphi 
генерирует следующие разделы. "" 
    •   CODE (в компоновщике Microsoft это, как правило, . text). Код 
программы. В Win 16 каждый модуль имел собственный сегмент кода. В Win32 код 
всех модулей объединяется в данном разделе. в   BSS (в компоновщике Microsoft 
это, как правило, .bss). Раздел данных, содержащий только неинициализированные данные. 
    •   DATA (в компоновщике Microsoft это, как правило, . data). Раздел 
данных, содержащий глобальные переменные. 
    •   .rsrc. Раздел ресурсов, содержащий строковые таблицы, растровые образы 
и пиктограммы. 
    •   .idata. В этом разделе находятся списки имен неявно загруженных 
модулей и имен функций каждого модуля. в   .edata. В этом разделе находятся 
списки имен и порядковых значений функций, экспортируемых модулем. 
    •   .reloc. Некоторые адреса переменных и команд определяются как смещения 
по отношению к предпочтительной базе образа. Если модуль не удалось загрузить 
по предпочтительному базовому адресу, информация из данного раздела 
используется для определения адресов, которые должны быть изменены в процессе 
выполнения программы. 
    Увидеть имена и атрибуты любого раздела выполняемого модуля можно с 
помощью утилиты Turbo Dump, которая будет рассмотрена в следующем разделе. В 
отношении программирования DLL наиболее интересны разделы . idata и . edata. 
Просмотр этих разделов помогает находить ошибки, связанные с импортированием и 
экспортированием функций (о чем пойдет речь в следующем разделе). Еще один 
(пусть возникающий не столь часто) повод изучить информацию, содержащуюся в 
разделах, — научиться писать программы, подобные Turbo Dump и Microsoft Quick 
Viewer. Можно даже написать проводник ресурсов, как в демонстрационной 
программе ResXplor, поставляемой с Delphi. Но для выполнения подобных задач 
нужно основательно знать файловый формат РЕ. 
    Чтобы получить сведения о структурах конкретных заголовков, смещениях или 
же подробную информацию о файловом формате РЕ, следует отослать запрос 
Portable Exe,cutable_Bi^ce™Microsoft Dj;velo?er_Network„(M.SDNJI. Есть также 
некоторая информация на странице Microsoft в Web. Тем пользователям, чьи 
интересы ограничиваются извлечением информации из ресурсов или кто просто 
хочет покопаться в файлах, расположенных на жестком диске, следует заглянуть в 
демонстрационную программу ResXplor, находящуюся в каталоге 
\Delphi3\Demos\ResXplor. Эта программа считывает информацию непосредственно из 
образа файла.	*...».«,-»—„^ 

    Turbo Dump 

    Все^версии Delphi и C++ поставляются с утилитой командной строки Turbo 
Dump. Это обычный выполняемый файл DOS Т DUMP. ЁХЕ, запускаемый из командной 
строки и расположенный в каталоге \bin. Последняя на момент написания книги 
версия Turbo Dump — 4.2.15.2. 
    Утилита Turbo Dump (или TDump— по имени выполняемого файла) может читать 
файлы нескольких двоичных форматов и выводить их содержимое на экран в 
упорядоченной форме. TDump может считывать файлы следующих форматов. 
    •	Выполняемые файлы для DOS 
    •	Файлы New Executable (NE) 
    •	Линейные выполняемые файлы 
    •	Переносимые выполняемые файлы (РЕ) 
    •	Файлы OMF (OBJ и LIB) 
    •	Файлы COFF (OBJ и LIB) 
    •	Символьная отладочная информация Borland и Microsoft 

    Глава 26. Программирование DLL: сложный материал 633 

    Тем, кто не знаком со всеми этими типами файлов, беспокоиться не стоит. 
Здесь мы рассматриваем только перемещаемые выполняемые файлы, а именно — DLL. 
    Утилита TDump запускается с несколькими опциями командной строки. Чтобы 
вывести на экран описание опций, надо набрать в командной строке TDump и 
нажать <Enter>. Простейшая команда запуска утилиты TDump имеет следующий вид: 

    TDUMP MyFile.exe 

    Вместо MyFile.exe следует указать имя файла, который надо просмотреть. При 
запуске утилиты с опциями командной строки эти опции необходимо указывать 
перед именем файла: 

    TDUMP  -h  c:\Win95\System\GDI32.DLL 

    В данной главе рассматриваются только три опции. Описание остальных можно 
без труда получить, запустив утилиту без параметров. Первая опция (-е) 
применяется для просмотра выполняемых файлов, вторая (-а) — для просмотра 
файлов ASCII и третья (-h) выводит содержимое файла в шестнадцатеричном представлении. 
    Запущенная с файлом ЕХЕ или DLL в качестве параметра утилита TDUMP по 
умолчанию пытается вывести данные о файловом формате. Если это просто 
текстовый или двоичный файл, его содержимое выводится на экран в 
шестнадцатеричном формате. Обычно этот формат, используемый по умолчанию, 
удобен, но иногда требуется другое представление. Тогда, конечно, лучше 
воспользоваться приведенными выше опциями. 
    Итак, какую же помощь может оказать Turbo Dump при программировании DLL? 
Здесь постоянно возникает одна и та же проблема. Модуль ЕХЕ пытается 
импортировать из DLL функцию, но операционная система (или функция 
GetPro-cAddress) не может эту функцию найти. Причина всегда одна: имя функции 
в модуле ЕХЕуказано неверно. 
    Это лишь одна проблема, но причин ее возникновения может быть множество. 
Вот некоторые из них. 
    •   В DLL нет раздела exports, поэтому функции не экспортируются. 
    •   Из-за ошибки при наборе имя, указанное в модуле ЕХЕ, отличается от 
действительного имени экспортируемой функции. 
    •   Имя, указанное в модуле ЕХЕ, совпадает с именем экспортируемой из DLL 
функции, но DLL написана на языке C++, поэтому имя функции при экспортировании изменено. 
    •   Имя, указанное в модуле ЕХЕ, совпадает с именем экспортируемой из DLL 
функции, но DLL написана на языке C++, а в экспортируемой функции действует 
соглашение о вызовах _cdecl без запрещения применения символов подчеркивания 
для экспортируемых функций. 
    Полный список предполагаемых причин, пожалуй, не исчерпывается четырьмя 
перечисленными вариантами. Каждый вариант нетрудно проследить, если мыслить 
достаточно ясно (тогда, правда, и ошибок не должно быть). Но с помощью утилиты 
TDump это делать намного проще. Так что овчинка стоит выделки, игра — свеч, а 
Париж — мессы. 
    Выше рассматривались разделы файлового формата РЕ. В двух из них 
содержится информация, имеющая прямое отношение к рассматриваемой теме. Это 
разделы . idata и . edata. Если не удается подключить один модуль к другом), 
можно просмотреть в разделе . idata вызывающего модуля имена модулей и 
импортируемых из них функций с помощью утилиты TDump. Затем можно просмотреть 
раздел . е da t ^вызываемого модуля. В нем содержатся имена всех 
экспортируемых функций (если таковые имеются). 
    В листинге 26.13 показан вывод утилитой TDump содержимого раздела . idata 
файла ядра Windows GDI32 . DLL в каталоге \WINDOWS\SYSTEM. 

    Листинг 26.13. GDI32. тхт. Утилита TDump выводит информацию 
последовательно сверху вниз 

    Imports from KERNEL32.dll 
    SMapLS_IP_EBP_24(hint = Olee) 
    38 
    89 
    FT_Exitl2(hint = 0076) 
    FT_Exit20(hint = 0078) 
    GetSystemDefaultLCID(hint = 012b) HeapCreate(hint = 0168) GetOEMCP(hint = 
0102) RtlUnwind(hint = Ole8) 
    Imports from ADVAPI32.dll 
    RegCreateKeyExA(hint = 0082) RegQueryInfoKeyA(hint = 0098) 


    634	Часть VI. Сложный материал 


    RegDeleteKeyA(hint  =   0085) 
    RegSetValueExA(hint   =   ООаЭ) 
    RegQueryValueExA(hint  =  009d) RegCloseKey(hint =  007e) RegOpenKeyA(hint 
 =  0094) 

    Обратите внимание, что в этом листинге показана лишь часть выводимых 
данных. Листинг начинается блоком Imports from (импортирует из) KERNEL32 .dll 
и заканчивается блоком Imports from ADVAPI32. dll. Если модуль GDI32 . DLL 
попытается неявно загрузить модуль KERNEL32 . DLL и имя хотя бы одной функции 
будет указано неверно, усечено или совсем опущено , загрузка не выполнится. С 
другой стороны, имея в виду, что GDI32.DLL— это DLL, она, видимо, экспортирует 
некоторые функции, используемые другими модулями. В листинге 26.14 показан 
вывод утилитой TDump содержимого раздела . edata файла ядра Windows GDI32 . 
DLL в каталоге \WINDOWS\SYSTEM. 

    Листинг 26.14. GDI32 .тхт. Утилита TDump перечисляет порядковые значения, 
которые надо добавлять к базе; указываются также относительное виртуальное 
смещение и символьные имена экспортируемых функций 

    Exports   from GDI32.dll 
    330  exported name(s),   335  export  addresse(s).     Ordinal base  is   
100. Ordinal   RVA	Name 
    0005	000024ab	AbortDoc 
    0006	0000245b	AbortPath 
    0007	000028be	AddFontResourceA 
    0008	00007549	AddFontResourceW 
    0324	000021a5	TextOutW 
    0325	00003208	TranslateCharsetlnfo 
    0326	00002493	UnrealizeObject 
    0327	0000210с	UpdateC-olors 
    0328	000088ba	UpdatelCMRegKey 
    0329	000088al	UpdatelCMRegKeyA 
    0330	00007564	UpdatelCMRegKeyW 
    0331	0000247f	WidenPath 
    0332	00006fe2	gdiPlaySpoolStream 
    0333	OOOlbOaS	pfnRealizePalette 
    0334	0001bOa4	pfnSelectPalette 

    Модуль GDI32.DLL экспортирует свыше 300 функций. В листинге перечислены 
лишь некоторые из них. Поэтому самый быстрый способ проверить, экспортирует ли 
DLL модулю ЕХЕ именно то, что нужно, — заглянуть с помощью утилиты TDump в 
раздел . idata модуля ЕХЕ, точно так же просмотреть раздел . edata модуля DLL 
и сравнить данные в обоих разделах. Имена функций в них должны совпадать. 
    Утилита TDump выводит на экран большой объем информации с такой скоростью, 
при которой едва ли что-нибудь удастся прочесть. Можно перенаправить вывод в 
текстовый файл, который затем просмотреть в простом текстовом редакторе, 
например в Notepad. Командная строка DOS для вывода в файл имеет вид 

    TDump  -e   GDI32.DLL  >  GDI32.TXT. 

    При возникновении проблем утилита TDump позволяет сэкономить массу 
времени. С ее помощью из файла формата РЕ можно извлечь очень много 
информации. Если предыдущий раздел о файловом формате РЕ пробудил в вас 
любопытство, утилита TDump— к вашим услугам. В листинге 26.15 показан вывод 
утилитой TDump содержимого файла ядра Windows GDI32 . DLL. Как нетрудно 
заметить, листинг неполон. Он обрывается в том месте, где начинается 
информация об импортировании, уже представленная выше (этим мы сэкономили 
немного бумаги). При этом текст получился далеко не лаконичный. 

    Листинг 26.15. \UDELPHI3\CHP26\GDI32 .тхт. Утилита TDump выводит многое из 
информации, рассмотренной при изучении файлового формата РЕ 

    Turbo Dump  Version 4.2.15.2 Copyright (с) 1988, 1996 Borland 
International Display of File C:\WINDOWS\SYSTEM\GDI32.DLL 
    Old Executable Header 


    Глава 26. Программирование DLL: сложный материал	635 


    DOS File Size	20000h	(131072.	) 
    Load Image Size	450h	(  1104.	} 
    Relocation Table entry count                        OOOOh	(    0.	) 
    Relocation Table address	0040h	(    64.	) 
    Size of header record	(in paragraphs)           0004h	(    4.	) 
    Minimum Memory Requirement	(in paragraphs)            OOOOh	(     0.	) 
    Maximum Memory Requirement	(in paragraphs)            FFFFh	( 65535.	) 
    File load checksum	OOOOh	(    0.	) 
    Overlay Number	OOOOh	(     0.	) 
    Initial Stack Segment  (SS:SP)	0000:0088 Program Entry Point    (CS:IP)    
             0000:0000 
    Portable Executable (PE) File Header base: 00000080 
    CPU type	80386 
    Flags	210E [ executable backwards 32bit library ] 
    DLL flags	0000 [ ] 
    Linker Version	2.32 
    Time stamp	2FF383BC 
    0/S Version	1.0 
    User Version	0.0 
    Subsystem Version	4.0 
    Subsystem	0002 [ Windows GUI ] 
    Object count	00000007 
    Symbols offset	00000000 
    Symbols count	00000000 
    Optional header size    OOEO 
    Magic #	10B 
    Code size	00019600 
    Init Data size	00006600 
    Uninit Data size	00000000 
    Entry RVA	00002B6C 
    Image base	BFF30000 
    Code base	00001000 
    Data base	0001BOOO 
    Object/File align	00001000/00000200 
    Reserved	00000000 
    Image size	00025000 
    Header size	00000400 
    Checksum	0002CB01 
    Stack reserve/commit    00100000/00001000 
    Heap reserve/commit     00100000/00001000 
    Number interesting RVAs  00000010 
    Name	RVA      Size 
    Exports	0001EOOO	00002099 
    Imports	0001DOOO	OOOOOA40 
    Resources	00022000	00000394 
    Exceptions	00000000	00000000 
    Security	00000000	00000000 
    Fixups	00023000	000016B4 
    Debug	00000000	00000000 
    Description	00000000	00000000 
    TLS	00000000	00000000 
    Callbacks	00000000	00000000 
    reserved	00000000	00000000 
    reserved	00000000	00000000 
    reserved	00000000	00000000 
    reserved	00000000	00000000 
    reserved	00000000	00000000 


    636	Часть VI. Сложный материал 


    Object table #  Name		VirtSize	RVA	PhysSize	Phys off	Flags 
    01	.text	00019580	00001000	00019600	00000400	60000020	[CER] 
    02	.data	00001910	0001BOOO	00001AOO	00019AOO	D0000040	[ISRW] 
    03	. idata	OOOOOA40	0001DOOO	OOOOOCOO	0001B400	50000040	[ISR] 
    04	. edata	00002099	0001EOOO	00002200	0001COOO	40000040	[IR] 
    05	GPFIX	0000011C	00021000	00000200	0001E200	D0000040	[ISRW] 
    06	. rsrc	00000394	00022000	00000400	0001E400	40000040	[IR] 
    07	. reloc	000017FO	00023000	00001800	0001E800	42000040	[IDR] 
    Key to section flags: С — contains code D — discardable E — executable 
    I — contains initialized data R — readable S — shareable W — writeable 
    Imports from KERNEL32.dll 
    SMapLS_IP_EBP_24(hint = Olee) 38 
    и  т.д. 

    Резюме 

    В этой главе рассматривался широкий спектр технических вопросов. Особое 
внимание было уделено применению объектов в модулях DLL. Это объясняется тем, 
что в основе среды пользователя и библиотеки компонентов Delphi лежат объекты. 
    Подведем краткие итоги. 
    1.   Сначала рассказывалось о компиляции класса в DLL и экспортировании 
обычных функций для доступа к этому классу. 
    2.   Затем речь шла о компиляции в DLL классов форм и функций доступа, с 
помощью которых эти формы выводились на экран в модальном и немодальном режимах. 
    3.   После этого рассматривалась таблица виртуальных методов. Информация о 
VMT, с точки зрения совместного использования объектов с помощью виртуальных 
классов, крайне важна в контексте данной главы. 
    4.   Далее описывалось использование области памяти несколькими процессами 
с помощью объекта отображения файла и был создан этот объект для системного 
страничного файла, независимого от процесса. 
    5.   Чтобы вы разобрались в структуре DLL, приводилась информация об 
элементах файла формата РЕ, в том числе о разделах импортирования, 
экспортирования, кода и данных. 
    6.   И наконец, вы узнали об утилите Turbo Dump (T Dump) и ее 
использовании для отладки DLL. 

    Глава 26. Программирование DLL: сложный материал 637 
