Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Мастерская программиста" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Что такое "технология COM" и как с ней бороться?
Информационный Канал Subscribe.Ru |
М. Безверхов
vasilisk@clubpro.spb.ru
Что такое "технология COM" и как с ней бороться? №46
"Надежда" работает под контролем.
Показания счётчика числа подписчиков рассылки ненавязчиво, но неумолимо рекомендуют мне написать очередной номер. От читателей я стал получать вежливые письма-напоминания о том, что я являюсь автором рассылки... всё, словом, идёт к тому, что писать-таки придётся - от судьбы не уйдёшь.
Я напомню, что в прошлом номере рассылки мы рассмотрели структуру кода HRESULT и обращение с ошибкой, которую мы ещё ранее классифицировали как "ошибку второго рода", а сейчас должны рассматривать обработку ошибки "первого рода". И мы её и рассмотрим.
Но, раз уж от судьбы не уйдёшь, то я должен сказать, что нечасто, но получаю письма и другого характера. В которых их авторы разными словами, но выражают одну, в сущности, мысль – я занимаюсь ерундой, поскольку платформа .NET "отменяет COM", который в данном случае "учить не нужно", а в юниксе COM и вовсе нет... :)
К сожалению таких авторов, я должен их разочаровать – излагаемая ими проблема не нова. Она была отмечена ещё Д.И. Фонвизиным и в те времена формулировалась так – "зачем географию учить, когда извозчики и так довезут?". Правда, у нас не извозчики и не география, но это только лишь оттого, что не было в его время ещё компьютеров... А вот, с другой стороны, меня радует появление таких писем – они означают, что в нашу читательскую аудиторию влились и продолжают вливаться свежие силы, как раз те, которым "рассылка по COM" и была бы особенно полезна. И, если читатель не просто молча отвернулся, а всё же сочёл нужным высказать мне своё, пусть пока и такое, мнение – ещё не всё потеряно. Я не буду больше вступать в дискуссии по таким вопросам – на протяжении нашей рассылки я уже высказался немало по этому поводу, поэтому заинтересованные читатели просто отсылаются к архиву.
Мы же возвращаемся к теме изложения – существует механизм передачи "ошибки первого рода". Ранее отмечалось, что его необходимость возникает по той причине, что программисту в COM одновременно видимы два концептуальных уровня – уровень служебной функциональности самого COM и уровень абстракций пользовательской программы. И их желательно как-то разделять. А вот как пользоваться таким разделением и что в него должно попадать мы сейчас попробуем догадаться, рассматривая саму концепцию деления системы на уровни абстракции.
Я понимаю, что сообщаю очевиднейшую вещь, но это – правда, любая более-менее сложная система не может быть сконструирована человеком из однородных частей. Так или иначе, человек начнёт их группировать "по уровням" следуя психологии своего восприятия. А на границах этих групп будет происходить преобразование уровня абстракции – из секторов диска будет образовываться кластер, из кластеров – файл, из файла – бесконечная непрерывная строка байтов. Понятно, что преобразование уровня абстракции позволяет справиться со сложностью системы (и за это нужно платить – не желая иметь дело с секторами диска напрямую, мы вынуждены конструировать специальный программный компонент - "файловую систему", которая имеет уже меньшую производительность, чем контроллер диска). Однако, не очевидно, что все наши программные системы и конструируются только для того, чтобы преобразовывать эти уровни абстракции – программный продукт невидим, его невозможно потрогать и вообще, одни и те же видимые результаты, которые произведёт компьютер могут быть достигнуты программами совершенно разной организации. Но это действительно так, поэтому преобразования уровня абстракции и имеют значение только "для нас".
Именно по упомянутым причинам сложные системы конструируются "послойно по вертикали", причём, - независимо от того выделяются ли слои в отдельные модули или нет, т.к. принцип инкапсуляции применяется также и в разработке программ, которые порождают обычный монолитный исполнимый код.
Допустим, что сказанное - тривиально. Тогда вопрос – одно и то же первичное событие, например, "сигнал контроллера диска об ошибке чтения сектора", должно ли оно интерпретироваться одинаково слоем чтения секторов, слоем компоновки из них кластеров и слоем обработки файла в целом? Конечно – нет! Это – разные абстракции и в каждой из них это одно событие будет иметь свою семантику, т.е. одновременно с преобразованием уровня абстракции мы должны преобразовывать уровень и событий, обрабатываемых данным слоем. Вообще любых событий, с которыми имеет дело данный слой, т.е. некоторые из них мы можем не поднимать на уровень выше, некоторые – заменять другими событиями, третьи - генерировать программно. Обработка событий может быть произвольной и определяется семантикой слоя-обработчика. Естественно, что сказанное в полной мере относится и к "событиям ошибки". А... вот как это сделать?
На этот вопрос нет однозначного ответа – системы обработки ошибок могут иметь разную и произвольную конструкцию. Но кое-какие соображения высказать можно. Во-первых, абстракции, с которыми имеет дело пользователь, разительно отличаются от абстракций, которые являются источником информации об ошибке. Например, отказ в чтении сектора, возвращённый контроллером диска, может быть интерпретирован на уровне пользователя, как "попытка чтения счёта N 123 из БД оказалась неудачной" - контроллер не обязан знать, смысл того, что именно он читает в данный момент, а компонент пользователя не должен знать особенностей хранения данных в постоянном хранилище. Во-вторых, по мере возрастания абстракции, семантика сообщения становится всё более сложной. И если контроллер все свои ошибки, без всякого сомнения, способен перенумеровать и обойтись при возврате состояния только кодом возврата, то, как мы видели из прошлого выпуска рассылки, уже на уровне отдельного интерфейса COM этот механизм обнаруживает свою кардинальную недостаточность. Иными словами, одновременно с преобразованием абстракции события ошибки должна изменяться и форма представления сигнала о ней, на уровне пользователя она никак не может быть "просто кодом".
Этого можно достичь, например, табличным преобразованием – в интерфейс пользователя "из глубин" приходит соответствующий код, интерфейс смотрит по таблице и выводит пользователю текстовую строку сообщения об ошибке, соответствующую коду. Но, как в прошлом выпуске было замечено, даже составить просто непротиворечивую и уникальную систему кодов масштаба всей программной системы – задача весьма трудная.
В качестве же приемлемого решения напрашивается весьма простой подход – объединить преобразование уровня абстракции события ошибки и сопоставление этому событию некоторого текстового описания в одном месте. Тогда, получив событие ошибки, объект (компонент, интерфейс) не просто транслирует его, а подготовит всё, чтобы это событие можно было непосредственно показать пользователю.
Этим радикально решаются завязанные в единый узел проблемы. Во-первых, пользовательская оболочка не обязана уметь транслировать коды ошибок в сообщения пользователю, а, значит, её теперь действительно может касаться только "наличие ошибки исполнения метода – отсутствие ошибки". Во-вторых, "код ошибки" обязан быть уникальным только в пределах даже не всего компонента, а только слоя или даже объекта или интерфейса объекта (FACILITY_ITF из предыдущего номера рассылки), что, естественно, реализуется намного легче, чем разработка глобально непротиворечивой системы кодов ошибок. В-третьих, если данный объект/компонент – не самый верхний, т.е. вызывается не пользователем, а каким-то объектом/компонентом ещё более высокого уровня, то тот компонент по-прежнему располагает, пусть и преобразованным, но технологическим кодом ошибки типа HRESULT. И может построить на нём анализ и преобразование абстракций своего уровня, а текстовую строку просто проигнорировать, если она ему не требуется.
Вот, собственно, и всё... осталось только рассмотреть детали реализации описанного механизма, который в COM просторечно называется "механизм IErrorInfo".
Итак, в COM существует т.н. "объект ошибки", который экспонирует интерфейс IErrorInfo. Объект этот обеспечивается слоем поддержки COM, поэтому реализовывать его самому не нужно. То, что это именно "объект" - артефакт реализации, поскольку давно подмечено, что в COM значительно легче организовать передачу специального объекта, нежели передачу обычной заполненной данными структуры. В рассматриваемом же нами случае "объект ошибки" по своему содержанию как раз такая структура и есть - достаточно только взглянуть на состав методов интерфейса IErrorInfo (методы 8 - 12):
1. | QueryInterface | |
2. | AddRef | |
3. | Release | |
4. | GetTypeInfoCount | |
5. | GetTypeInfo | |
6. | GetIDsOfNames | |
7. | Invoke | |
8. | GetDescription | Возвращает текстовое описание ошибки |
9. | GetGUID | Возвращает GUID интерфейса, определившего ошибку |
10. | GetHelpContext | Возвращает контекст подсказки, относимый к данному случаю возникшей ошибки |
11. | GetHelpFile | Возвращает спецификацию файла подсказки, которая содежит требуемый раздел, описывающий ошибку |
12. | GetSource | Возвращает зависящий от языка идентификатор объекта или программы, вызвавших ошибку |
Сам интерфейс IErrorInfo наследует пока не изученному нами интерфейсу автоматизации IDispatch (методы 4 - 7), поскольку описанная выше проблема "перевода на человеческий язык" возникла именно в эпоху господства в эволюции COM автоматизации. Интерфейс IDispatch – сам по себе есть отдельный и очень большой раздел COM, но, если вы используете интерфейсы, от него унаследованные, "как обычно", то вы можете просто проигнорировать это знание – ничего специфического для обращения с интерфейсом просто сам факт наследования от IDispatch не вносит. Кроме, конечно, дополнительно занимаемых четырёх ячеек vtbl.
Интерфейс IErrorInfo – интерфейс, предназначенный для извлечения клиентом уже установленной сервером в "объект ошибки" информации. Изменить её в объекте ошибки клиент не может. А для того, чтобы её первично установить, "объект ошибки" реализует и второй, парный к описанному, интерфейс ICreateErrorInfo. Он тоже является наследником интерфейса IDispatch:
1. | QueryInterface | |
2. | AddRef | |
3. | Release | |
4. | GetTypeInfoCount | |
5. | GetTypeInfo | |
6. | GetIDsOfNames | |
7. | Invoke | |
8. | SetDescription | Устанавливает текстовое описание ошибки |
9. | SetGUID | Устанавливает GUID интерфейса, определившего ошибку |
10. | SetHelpContext | Устанавливает контекст подсказки, относимый к данному случаю возникшей ошибки |
11. | SetHelpFile | Устанавливает спецификацию файла подсказки, которая содежит требуемый раздел, описывающий ошибку |
12. | SetSource | Устанавливает зависящий от языка идентификатор объекта или программы, вызвавших ошибку |
Работает вся механика так. Объект ошибки реализован в Oleaut32.dll, т.е. его сервис доступен в операционной системе всегда. Существует функция системного API CreateErrorInfo:
HRESULT CreateErrorInfo(ICreateErrorInfo **pperrinfo);
которая создаёт новый объект ошибки и возвращает вызывающей процедуре ссылку на интерфейс ICreateErrorInfo этого объекта. Пользуясь методами этого интерфейса сервер заполняет объект значениями данных. Далее, сервер должен получить у объекта ошибки ссылку на интерфейс IErrorInfo и должен вызвать другую функцию системного API – SetErrorInfo:
HRESULT SetErrorInfo(DWORD dwReserved,IErrorInfo *perrinfo);
которая "отцепляет" объект ошибки от сервера и "прицепляет" его к системному механизму передачи этого объекта на сторону клиента. Естественно, что это "отцепление/зацепление" рассматривается как удерживание ссылок на объект – сервер передаёт ссылку системе и освобождает свою, т.е. всё делается примерно таким образом:
ICreateErrorInfo *pcerrinfo;
IErrorInfo *perrinfo;
HRESULT hr;
hr = ::CreateErrorInfo(&pcerrinfo);
. . .
//заполнить объект через ICreateErrorInfo
hr = pcerrinfo->QueryInterface(IID_IErrorInfo, (LPVOID FAR*) &perrinfo);
if(SUCCEEDED(hr)){
::SetErrorInfo(0, perrinfo);
perrinfo->Release();
}
pcerrinfo->Release();
после чего сервер может возвратить управление клиенту как обычно - завершив исполнение метода с возвратом HRESULT. На клиентской же стороне программа обращается к функции системного API GetErrorInfo, которая извлекает переданный сервером объект ошибки:
HRESULT GetErrorInfo(DWORD dwReserved,IErrorInfo **pperrinfo);
Возможно, что в уже "промаршалированном виде", но это будет ссылка на тот самый объект ошибки, который передавал сервер... После того, как надобность в полученном объекте ошибки у клиента минет, он должен просто отпустить на него ссылку. Вот так:
IErrorInfo *perrinfo;
HRESULT hr;
hr = ::GetErrorInfo(&perrinfo);
if(SUCCEEDED(hr)){
. . .
//прочитать значения через IErrorInfo
perrinfo->Release();
}
Следует обратить особенное ваше внимание на одно подразумеваемое при использовании данного механизма передачи ошибки обстоятельство – этот механизм в точности повторяет механизм errno из CRT. А errno обеспечивается в контексте каждого потока, а не процесса в целом. Понятно почему – необходимо предотвратить вмешательство параллельных потоков между установкой и чтением errno. COM, посредством механизма proxy/stub эмулирует т.н. "логический поток исполнения программы" (как мы видели ранее, фактически в этом принимает участие два разных физических потока), который, с точки зрения клиента, аналогичен системному физическому потоку. Естественно, что система предохраняет объект ошибки от вмешательства параллельных потоков, поэтому никаких специальных мер по синхронизации потоков при использовании SetErrorInfo/GetErrorInfo не требуется – с этим механизмом можно работать одинаково во всех потоковых моделях.
Но и это - отнюдь не всё. Механизм IErrorInfo – опционален, т.е. сервер может использовать его, а может обходиться только возвратом кода ошибки через HRESULT. Как клиенту узнать – HRESULT с кодом ошибки, который вернул метод сервера, это – всё, чем располагает клиент или же клиент может выяснить и больше? Существует специальное соглашение, которому должен следовать интерфейс, если он реализует механизм передачи ошибки посредством IErrorInfo.
Во-первых, все методы данного интерфейса либо всегда пользуются SetErrorInfo, когда возвращают код ошибки, либо – никакой метод данного интерфейса этим не пользуется. Во-вторых, объект, имеющий реализации интерфейсов использующие SetErrorInfo должен вести у себя реестр интерфейсов, которые в состоянии предоставить клиенту IErrorInfo. В-третьих, объект должен реализовывать в составе своих интерфейсов специальный интерфейс ISupportErrorInfo, пользуясь которым клиент может выяснить – получив через HRESULT состояние ошибки от метода некоего интерфейса может ли клиент рассчитывать на то, что этот интерфейс предоставил и заполненный IErrorInfo?
Интерфейс ISupportErrorInfo также наследует интерфейсу IDispatch и состоит из одного своего метода:
1. | QueryInterface | |
2. | AddRef | |
3. | Release | |
4. | GetTypeInfoCount | |
5. | GetTypeInfo | |
6. | GetIDsOfNames | |
7. | Invoke | |
8. | InterfaceSupportsErrorInfo | Возвращает признак, поддерживает ли данный интерфейс механизм ISupportInfo |
Полезный метод данного интерфейса описывается так:
HRESULT InterfaceSupportsErrorInfo(REFIID riid);
он принимает в качестве аргумента ссылку на IID интерфейса, вернувшего состояние ошибки, а возвращает значение S_OK, если этот интерфейс передаёт объекты ошибок и S_FALSE в противном случае. Т.е. действия клиента по извлечению всей доступной ему информации, возвращаемой сервером при возникновении ошибки выглядят примерно таким образом:
IErrorInfo *perrinfo;
ISupportErrorInfo *psupeinfo;
HRESULT hr_obj,hr;
hr_obj = obj->Method(...);//вызываем метод интерфейса с IID == IID_MyItf
if(FAILED(hr_obj)){
hr = obj->QueryInterface(IID_ISupportErrorInfo, (LPVOID FAR*) &psupeinfo);
if(SUCCEEDED(hr)){
hr = psupeinfo->InterfaceSupportsErrorInfo(IID_MyItf);
if(SUCCEEDED(hr)){
hr = ::GetErrorInfo(&perrinfo);
if(SUCCEEDED(hr)){
. . .
//прочитать значения через IErrorInfo
perrinfo->Release();
}
}
psupeinfo->Release();
}
}
Вот почти и всё об обработке ошибки в COM. Но следует добавить, что и обработка ошибки тоже не стоит на месте. В OLE DB появился новый интересный объект - семейство ошибок. Оно состоит из объектов ошибок (немного модифицированных, по сравнению с описанным объектом), которые возвращаются на всех слоях преобразования абстракции, которые проходит событие вверх, к клиенту. Так, что когда клиент получит сообщение самого верхнего уровня "счёт N 123 не может быть прочитан" он в состоянии выяснить в какой БД это произошло, в каком resultset и в какой строке... - это новый, весьма, как мне кажется мощный подход к трансформации абстракций о событиях ошибки в COM. Во всяком случае он позволяет не только "возвести ошибку вверх", но и заглянуть "вниз", на уровень всех тех обработчиков, которые имели отношение к ней.
На сегодня - всё, а в следующем номере мы рассмотрим необходимую для возврата к теме о
библиотеке типа информацию о наследовании в COM...
Авторские права © 2001 - 2002, М. Безверхов
Публикация требует разрешения автора.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||