лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdfМультимедийный таймер |
511 |
|
|
В случае успешного завершения функция timeSetEvent возвращает идентифи катор таймерного события. Если при выполнении функции произошла ошибка, то функция возвращает значение NULL.
Как только работа с таймером будет завершена, следует вызвать функцию timeKillEvent для освобождения задействованных системных ресурсов. В качестве параметра функции timeKillEvent передается тот идентификатор таймерного со бытия, который был получен от функции timeSetEvent.
Тестирование мультимедийного таймера
Чтобы сравнить поведение мультимедийного таймера с поведением стандартного таймера, проведем эксперименты по измерению реального интервала вызова фун кции lpTimeProc. Тестовая программа приведена в листинге 10.6.
Листинг 10.6. Проект MmTimerTest
//////////////////////////////////////////////////////////////////////
// MmTimerTest.cpp #include <windows.h> #include <stdio.h> #include <Mmsystem.h> #include <fstream> using namespace std;
#include "KWnd.h" #include "KTimer.h"
ofstream flog; KTimer timer;
enum UserMsg { UM_READY = WM_USER+1 }; #define TIME_PERIOD 1
#define N 1000
double realTimeInterval[N];
void CALLBACK TimeProc(UINT, UINT, DWORD, DWORD, DWORD); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
//====================================================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
flog.open("flog.txt"); MSG msg;
KWnd mainWnd("MM timer - test", hInstance, nCmdShow, WndProc);
while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg);
}
flog.precision(3);
for (int i = 0; i < N; ++i)
flog << realTimeInterval[i] << endl; flog.close();
return msg.wParam;
}
//====================================================================
512 |
|
|
|
|
|
Глава 10. Таймеры и время |
|
||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
||||||
{ |
|
|
|
|
|
|
HDC hDC; |
|
|
|
|
||
PAINTSTRUCT |
ps; |
|
|
|||
char |
text[200]; |
|
|
|||
int i; |
|
|
|
|
|
|
const int i0 = 30; // номер первого обрабатывемого измерения |
||||||
double minT, maxT, amount; |
|
|||||
static |
MMRESULT mmResult; |
|
||||
switch |
(uMsg) |
|
|
|
||
{ |
|
|
|
|
|
|
case |
WM_CREATE: |
|
|
|||
//timeBeginPeriod(1); |
// #1 |
|
||||
mmResult = timeSetEvent(TIME_PERIOD, 0, TimeProc, reinterpret_cast<DWORD>(hWnd), |
||||||
TIME_PERIODIC); |
|
|
|
|||
if |
(!mmResult) |
|
|
|||
MessageBox(hWnd, "Мультимедийный таймер не создан", "Error", MB_OK); |
||||||
break; |
|
|
|
|
||
case |
WM_PAINT: |
|
|
|||
hDC |
= BeginPaint(hWnd, &ps); |
|
||||
// Определяем min и max значения |
||||||
minT = maxT = amount = realTimeInterval[i0]; |
||||||
for (i = i0 + 1; i < N; ++i) { |
||||||
if |
(realTimeInterval[i] < |
minT) |
||||
|
|
minT |
= |
realTimeInterval[i]; |
||
if |
(realTimeInterval[i] > |
maxT) |
||||
|
|
maxT |
= |
realTimeInterval[i]; |
||
amount |
+= |
realTimeInterval[i]; |
||||
} |
|
|
|
|
|
|
sprintf(text, "minT = %.3f, |
maxT = %.3f, average = %.3f\0", |
|||||
minT, maxT, amount / (N - i0)); |
||||||
TextOut(hDC, 10, 10, text, strlen(text)); |
||||||
EndPaint(hWnd, |
&ps); |
|
||||
break; |
|
|
|
|
||
case |
UM_READY: |
|
|
|||
MessageBox(hWnd, "Готово", "Завершение задания", MB_OK); |
||||||
SendMessage(hWnd, WM_DESTROY, 0, 0); |
||||||
break; |
|
|
|
|
||
case WM_DESTROY: |
|
|
||||
timeKillEvent(mmResult); |
|
|||||
//timeEndPeriod(1); |
// #2 |
|
||||
PostQuitMessage(0); |
|
|||||
break; |
|
|
|
|
||
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
//====================================================================
void CALLBACK TimeProc(UINT wTimerID, UINT msg, DWORD dwUser, DWORD dw1, DWORD dw2)
продолжение
Мультимедийный таймер |
513 |
|
|
|
|
Листинг 10.6 |
(продолжение) |
|
{ |
|
|
static int count = -1; |
|
|
HWND hwnd |
= reinterpret_cast<HWND>(dwUser); |
|
if (count >= 0 && count < N) realTimeInterval[count] = timer.GetTime();
timer.Start();
count++;
if (count == N) { InvalidateRect(hwnd, NULL, TRUE); SendMessage(hwnd, UM_READY, 0, 0);
}
}
//////////////////////////////////////////////////////////////////////
Обратите внимание на то, что при вызове функции timeSetEvent в качестве четвертого параметра передается дескриптор окна приложения с преобразованием его к типу DWORD. Внутри функции TimeProc этот дескриптор используется для обратной связи с оконной процедурой при вызове функций InvalidateRect и SendMessage.
Период срабатывания мультимедийного таймера задается макросом TIME_PERIOD. Вычисляя минимальное, максимальное и среднее значения периода срабатывания таймера, мы отбрасываем первые i0измерений. В программе константа i0определе на со значением 30. Это сделано, чтобы устранить погрешность измерений, обус ловленную особенностями функционирования конвейерной системы процессора.
Выполнение программы для периода, равного 1 мс, дает следующие итоговые результаты:
minT = 0.649, maxT = 1.973, average = 1.00.
Ну что ж, значение для средней величины периода срабатывания можно оце нить на «отлично». Но почему такой большой разброс между минимальным и мак симальным значениями? Чтобы выяснить это, заглянем в файл протокола flog.txt, в котором сохранены данные всех N измерений.
Изучение его содержимого показывает, что значения в диапазоне 1,94–1,966, превышающие заданный период почти на 1 мс, встречаются с завидным постоян ством — примерно один раз в 40 мс. Этот факт нетрудно объяснить. Система Windows каждые 40 мс отдает ресурсы процессора примерно на 1 мс системному диспетчеру потоков. Другое крайнее значение, 0,649, встретилось только один раз на 1000 измерений, причем оно идет сразу вслед за значением 1,3. Такое впечатле ние, что интервал 1,949, принадлежащий указанному выше диапазону, разбился на две неравные части: 1,3 и 0,649. В то же время подавляющее число измерений находится в диапазоне 0,971–0,999.
Итоговые результаты для этого и других значений TIME_PERIOD приведены в табл. 10.5.
Таблица 10.5. Замеры реального интервала срабатывания мультимедийного таймера
TIME_PERIOD, ìñ |
Реальный интервал, мс |
|
|
|
|
|
|
|
Минимальный |
Максимальный |
Средний |
|
|
|
|
1 |
0,649 |
1,973 |
1,000 |
2 |
1,177 |
3,424 |
2,000 |
|
|
|
|
514 |
|
Глава 10. Таймеры и время |
|
|
|
|
|
|
|
|
|
TIME_PERIOD, ìñ |
Реальный интервал, мс |
|
|
|
|
|
|
|
Минимальный |
Максимальный |
Средний |
|
|
|
|
5 |
4,061 |
6,289 |
5,000 |
10 |
9,220 |
11,278 |
10,000 |
50 |
49,165 |
51,288 |
50,001 |
100 |
99,065 |
100,589 |
99,999 |
1000 |
999,747 |
1000,256 |
1000,001 |
|
|
|
|
Теперь обратим внимание на закомментированные инструкции с номерами #1 и #2. Их можно раскомментировать, чтобы выяснить, как влияет вызов функции timeBeginPeriod(1) на работу мультимедийного таймера. Проведение повторных ис пытаний дает удивительный результат. Вызов функции timeBeginPeriod(1) никак не повлиял на работу мультимедийного таймера! Следовательно, с учетом сказан ного выше о влиянии timeBeginPeriod на загрузку ресурсов системы, лучше обой тись без вызова этой функции.
Проверим теперь работу мультимедийного таймера в жестких условиях «реаль ного мира». Под жесткими условиями здесь понимаются интенсивные действия пользователя, многократно перемещающего с помощью мыши окно тестового прило жения. Напомним, что стандартный таймер повел себя очень плохо в такой ситуа ции. Например, для заказанного интервала 100 мс был получен разброс значений реального интервала срабатывания от 0,52 до 503,38 мс.
Аналогичный эксперимент с приложением MmTimerTest дал следующий ре зультат:
minT = 99.57, maxT = 100.62, average = 100.00.
Таким образом, мультимедийный таймер позволяет создавать программы, фун кционирующие почти в режиме реального времени. «Почти» — потому что Windows не является операционной системой реального времени, о чем уже гово рилось в начале этой главы.
Осталось сделать два заключительных замечания. Когда вы будете применять мультимедийный таймер для решения реальных задач, очевидно, что вы замените код профилировки в теле функции TimeProc на некий реальный код. Не забудьте при этом учесть, что если время выполнения этого кода будет превышать период таймера TIME_PERIOD, то периодичность вызова функции TimeProcбудет определяться уже не таймером, а тормозными характеристиками вашего кода.
Прежде чем использовать мультимедийный таймер, хорошо подумайте, дей ствительно ли это вам нужно? В очень многих случаях характеристики стандарт ного таймера, несмотря на все его недостатки, позволяют решать те проблемы, которые должны быть решены в вашем приложении. Во всех таких случаях пред почтение следует отдавать стандартному таймеру.
515
Библиотеки 11 динамической
компоновки DLL
Библиотеки динамической компоновки (dynamic link libraries (DLL)) являются ис полняемыми файлами особого формата, которые содержат функции, данные или ресурсы, доступные для других приложений. Как следует из названия, библиоте ки DLL загружаются при необходимости либо на стадии загрузки, либо на стадии выполнения приложения.
Очевидно, что в рамках модели «клиент сервер» DLL исполняет роль сервера, предлагающего дополнительную функциональность вашему приложению. При ложения, использующие данную возможность, являются клиентами DLL.
Особый формат модулей DLL предполагает наличие в них так называемых раз делов импорта и экспорта. Раздел экспорта указывает те идентификаторы объек тов (функций, классов, переменных), доступ к которым разрешен для клиентов.
Подавляющее большинство DLL (за исключением DLL, содержащих только ресурсы) импортирует функции из системных DLL — kernel32.dll,user32.dll,gdi32.dll и других библиотек. Если ваш проект создается в среде Visual Studio, то его опции автоматически включают стандартный набор таких библиотек. Иногда в этот спи сок требуется добавить дополнительные DLL, содержащие функции, необходи мые вашему приложению. Например, в проектах, приведенных в главе 8, мы все гда подключали дополнительно библиотеку comctl32.dll, чтобы использовать элементы управления общего пользования.
Вэтой главе мы рассмотрим как можно создавать и использовать DLL модули
вваших приложениях.
Применение DLL может дать ряд преимуществ:
Расширение функциональности приложения. DLL можно загружать в адрес ное пространство процесса на этапе выполнения, что позволит программе, оп ределив, какие действия от нее требуются, подгружать нужный код. Поэтому, разрабатывая приложение, можно предусмотреть расширение его функциональ ности за счет DLL от других производителей программного обеспечения.
Более простое управление проектом. Обычно большие проекты разбиваются на модули, разрабатываемые разными группами. Использование DLL упроща ет отладку, тестирование и сопровождение проекта.
Экономия памяти. Если одну и ту же DLL используют несколько приложений, то в оперативной памяти хранится только один ее экземпляр, доступный этим приложениям. Например, использование DLL версии библиотеки С/С++
516 |
Глава 11. Библиотеки динамической компоновки DLL |
|
|
позволяет избежать многократного дублирования в памяти кода таких функ ций, как sprintf, strlen, fopen и других.
Разделение ресурсов. DLL могут содержать такие ресурсы, как строки, рас тровые изображения, шаблоны диалоговых окон. Этими ресурсами может вос пользоваться любое приложение.
Упрощение локализации. DLL идеально подходит для локализации приложе ний. Например, программа, содержащая только код без компонентов пользова тельского интерфейса, может загружать DLL, реализующую компоненты лока лизованного интерфейса.
Возможность использования разных языков программирования. Например, пользовательский интерфейс приложения вы можете реализовать на Microsoft Visual Basic, а прикладную логику — на С++. Программа на Visual Basic может загружать DLL, написанные на С++, Фортране и других языках. Однако если из DLL экспортируются классы, это может привести к дополнительным про блемам (например, из за декорирования имен компилятором), которые вы дол жны будете решать.
Добавим, что без применения DLL было бы невозможным построение прило жений по технологии COM1.
В то же время, решение о том, нужно ли создавать собственные DLL библио теки, должно приниматься с учетом всех плюсов и минусов технологии DLL. К ми нусам относятся:
Увеличение количества поставляемых компонент: кроме основного EXE фай ла потребуется отслеживать и все необходимые DLL модули.
Бо«льшее время загрузки приложения (особенно при неявном способе загрузки и большом количестве DLL модулей).
Необходимость синхронизации версий библиотек и версий использующих их клиентов. Пожалуй, это самая неприятная проблема в этой технологии.
Очевидно, что при разработке небольших проектов (утилиты, тестовые прило жения) вряд ли имеет смысл создавать DLL для реализации отдельных функций.
DLL и адресное пространство процесса
Исполняемый код в DLL не предполагает автономного использования. Содержи мое каждого DLL файла загружается приложением и проецируется на адресное пространство вызывающего процесса. Это достигается либо за счет неявного свя зывания при загрузке, либо за счет явного связывания в период выполнения.
Только после этого для вызывающего потока становятся доступными функции или другие ресурсы библиотеки. В свою очередь, DLL получает доступ ко всем ресурсам потока. Когда поток вызывает из DLL какую либо функцию, та считыва ет свои параметры из стека потока и размещает в нем же собственные локальные переменные. Кроме того, любые созданные кодом DLL объекты принадлежат вы зывающему потоку или процессу — DLL ничем не владеет.
1Component Object Model (COM) — спецификация для создания приложений с компонентной архитек турой.
Создание собственной DLL |
517 |
|
|
Другое приложение также может воспользоваться уже загруженной DLL, свя зав ее с адресным пространством своего процесса.
Важно понимать, что процесс, загрузивший DLL, получает собственную копию глобальных данных, используемых этой библиотекой. Это защищает DLL от оши бок приложений, а процессы, использующие DLL, от взаимного влияния друг на друга.
В тех случаях, когда реализация DLL требует работы с динамической памятью, будьте особенно внимательны к корректному освобождению ресурсов. Например, если DLL функция вызывает VirtualAlloc, система резервирует область в адресном пространстве того процесса, которому принадлежит поток, обратившийся к DLL функции. Предположим, что DLL выгружается из адресного пространства про цесса. В этом случае автоматического освобождения зарезервированной области не произойдет, так как система не фиксирует того, что она зарезервирована DLL функцией. Считается, что эта область принадлежит процессу и освободится толь ко если поток процесса вызовет VirtualFree или завершится сам процесс.
Чтобы отслеживать подобные ситуации, DLL должна содержать особую функ цию — DllMain, которая рассматривается ниже.
Создание собственной DLL
Во многих случаях создать библиотеку DLL проще, чем приложение, потому что она является лишь набором автономных функций, пригодных для использования любой программой или другой DLL. Зачастую в DLL нет кода, предназначенного для создания окон или для обработки сообщений.
Рассмотрим создание DLL на примере проектирования библиотеки MyLib.dll, содержащей единственную функцию
void Print(LPCTSTR text);
которая является оболочкой для вызова функции MessageBox. Сомнительно, что эта библиотека найдет какое либо практическое применение, но для объяснения рассматриваемой технологии она вполне подходит.
Однако если функция Print будет объявлена с прототипом записанным выше, то поток, загрузивший библиотеку MyLib.dll, не сможет вызвать данную функцию, так как она окажется для него невидимой. Чтобы «открыть» какие то из своих объектов для внешнего мира, DLL должна объявить их экспортируемыми с помо щью модификатора __declspec(dllexport). Кроме этого, в объявление функции сле дует добавить модификатор extern "C", запрещающий компилятору C++ осуществ лять так называемое декорирование имен.
ПРИМЕЧАНИЕ
Декорирование (mangling) имен — специфическое явление, присущее компиляторам языка C++. Дело в том, что компилятор С++ всегда добавляет к имени функции сокращенный список формальных параметров и тип возвращаемого значения. Компилятор делает это, чтобы решить проблему идентификации перегруженных функций и правильно связать вызовы функций в программе.
Таким образом, если вы забудете добавить модификатор extern "C", то в отком пилированном модуле MyLib.dll имя нашей функции Print превратится в нечто по хожее на ?Print@@YAXPBD@Z. В этом случае при явном связывании DLL с клиент ским приложением возникнут проблемы.
Вызов функций из DLL |
519 |
|
|
Когда проект создан, добавьте в его состав файлы MyLib.h и MyLib.cpp и откомпи лируйте (F7).
Что обеспечивает среда при построении проекта этого типа? Чтобы получить ответ на этот вопрос, загляните в папку Debug. В ней вы найдете два новых файла:
MyLib.dll — DLL файл, содержащий собственно библиотеку;
MyLib.lib — LIB файл, содержащий список идентификаторов, импортируемых из DLL. Этот LIB файл, называемый также библиотекой импорта, нужен при компоновке любого ЕХЕ модуля, ссылающегося на такие идентификаторы.
Компоновщик также вставляет в конечный DLL файл раздел экспорта, содер жащий список экспортируемых идентификаторов. В раздел экспорта помещаются также относительные виртуальные адреса этих идентификаторов.
Вызов функций из DLL
Существует три способа загрузки DLL: а) неявная, б) явная, в) отложенная.
Неявная загрузка DLL
Для построения приложения, рассчитанного на неявную загрузку DLL, необходи мо иметь:
Библиотечный H файл с описаниями используемых объектов из DLL (прото типы функций, объявления классов и типов). Этот файл используется компи лятором.
LIB файл со списком импортируемых идентификаторов. Этот файл нужно до бавить в настройки проекта (в список библиотек, используемых компонов щиком).
Компиляция проекта осуществляется обычным образом. Используя объект ные модули и LIB файл, а также учитывая ссылки на импортируемые идентифика торы, компоновщик собирает загрузочный ЕХЕ модуль. В этом модуле компо новщик помещает также раздел импорта, где перечисляются имена всех необходимых DLL модулей. Для каждой DLL в разделе импорта указывается, на какие символьные имена функций и переменных встречаются ссылки в коде ис полняемого файла. Эти сведения будет использовать загрузчик операционной системы.
Что же происходит на этапе выполнения клиентского приложения? После за пуска EXE модуля загрузчик операционной системы выполняет следующие опе рации:
Создает виртуальное адресное пространство для нового процесса и проецирует на него исполняемый модуль.
Анализирует раздел импорта, определяя все необходимые DLL модули и тоже проецируя их на адресное пространство процесса. Заметим, что DLL может им портировать функции и переменные из другой DLL. А значит, у нее может быть собственный раздел импорта, для которого необходимо повторить те же дей ствия. В результате на инициализацию процесса может уйти довольно длитель ное время.
520 |
Глава 11. Библиотеки динамической компоновки DLL |
|
|
После отображения EXE модуля и всех DLL модулей на адресное простран ство процесса его первичный поток готов к выполнению, и приложение начинает работу.
Еще следует отметить, что загрузчик ищет DLL на дисковых устройствах в сле дующей последовательности:
Каталог, содержащий ЕХЕ файл.
Текущий каталог процесса.
Системный каталог Windows.
Основной каталог Windows.
Каталоги, указанные в переменной окружения PATH.
Чтобы проверить, как работает неявная загрузка DLL, создадим клиентское приложение ImplClient, использующее функцию Print из библиотеки MyLib.dll. Текст тестовой программы приведен в листинге 11.2.
Листинг 11.2. Проект ImplClient
//////////////////////////////////////////////////////////////////////
// ImplClient.cpp #include <windows.h> #include "MyLib.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Print("Hello!");
Print("The experiment was a success!"); Print("By!");
return 0;
}
//////////////////////////////////////////////////////////////////////
Создайте проект типа Win32 Application и добавьте в его состав файлы MyLib.h
и ImplClient.cpp.
Скопируйте в папку проекта библиотеку импорта MyLib.lib, которая была пост роена в предыдущем проекте.
Добавьте в настройках проекта ссылку на нужную нам библиотеку импорта. Для этого выберите команду меню Project Settings и в появившемся диалоговом окне Project Settings выберите вкладку Link. Последующие действия таковы:
в списке Category выберите опцию General;
в конце списка Object/library modules добавьте имя нашей библиотеки — MyLib.lib;
нажмите кнопку OK.
Откомпилируйте проект (F7).
Для того чтобы EXE файл смог выполняться, в одной папке с ним должен на ходиться используемый DLL файл. Так как мы будем запускать приложение из среды Visual Studio, скопируйте файл MyLib.dll в папку Debug.
Запустите клиентское приложение. На экране должно появиться диалоговое окно, показанное на рис. 11.1 слева. Если вы будете продолжать нажимать кноп ку OK, то должны увидеть еще два окна, (рис. 11.1 по центру и справа). Это означа ет, что клиентское приложение успешно выполняет свою работу, пользуясь серви сом (функцией Print), предоставляемым сервером MyLib.dll.
