![]() |
![]() |
"Надежда" работает под контролемПоказания счётчика числа подписчиков ненавязчиво, но неумолимо рекомендуют мне написать очередную статью. От читателей я стал получать вежливые письма-напоминания о том, что я являюсь автором рассылки... всё, словом, идёт к тому, что писать-таки придётся - от судьбы не уйдёшь. Я напомню, что в предыдущей статье мы рассмотрели структуру кода HRESULT и обращение с ошибкой, которую мы ещё ранее классифицировали как "ошибку второго рода", а сейчас должны рассматривать обработку ошибки "первого рода". И мы её и рассмотрим. Но, раз уж от судьбы не уйдёшь, то я должен сказать, что нечасто, но получаю письма и другого характера. В которых их авторы разными словами, но выражают одну, в сущности, мысль – я занимаюсь ерундой, поскольку платформа .NET "отменяет COM", который в данном случае "учить не нужно", а в юниксе COM и вовсе нет... К сожалению таких авторов, я должен их дважды разочаровать. Во-первых, и сама излагаемая ими проблема не нова. Она была отмечена ещё Д.И. Фонвизиным и в те времена формулировалась так: "Зачем географию учить, когда извозчики и так довезут?". Во-вторых -.NET построена поверх, или, поменьшей мере - с использованием COM... :) А вот, с другой стороны, меня радует появление таких писем – они означают, что в нашу читательскую аудиторию влились и продолжают вливаться свежие силы, как раз те, которым "рассылка по COM" и была бы особенно полезна. И, если читатель не просто молча отвернулся, а всё же сочёл нужным высказать мне своё, пусть пока и такое, мнение – ещё не всё потеряно. Я не буду больше вступать в дискуссии по таким вопросам – на протяжении всего изложения я уже высказался немало по этому поводу, поэтому заинтересованные читатели просто отсылаются к архиву статей. Мы же возвращаемся к теме изложения – существует механизм передачи "ошибки первого рода". Ранее отмечалось, что его необходимость возникает по той причине, что программисту в COM одновременно видимы два концептуальных уровня – уровень служебной функциональности самого COM и уровень абстракций пользовательской программы. И их желательно как-то разделять. А вот как пользоваться таким разделением и что в него должно попадать мы сейчас попробуем догадаться, рассматривая саму концепцию деления системы на уровни абстракции. Я понимаю, что сообщаю очевиднейшую вещь, но это – правда: любая более-менее сложная система не может быть сконструирована человеком из однородных частей. Так или иначе, человек начнёт их группировать "по уровням" следуя психологии своего восприятия. А на границах этих групп будет происходить преобразование уровня абстракции – из секторов диска будет образовываться кластер, из кластеров – файл, из файла – бесконечная непрерывная строка байтов. Понятно, что преобразование уровня абстракции позволяет справиться со сложностью системы (и за это нужно платить – не желая иметь дело с секторами диска напрямую, мы вынуждены конструировать специальный программный компонент - "файловую систему", которая имеет уже меньшую производительность, чем контроллер диска). Тем не менее, не очевидно, что все наши программные системы и конструируются только для того, чтобы преобразовывать эти уровни абстракции – программный продукт невидим, его невозможно потрогать и вообще, одни и те же видимые результаты, которые произведёт компьютер, могут быть достигнуты программами совершенно разной организации. Но это действительно так, поэтому преобразования уровня абстракции и имеют значение только "для нас". Именно по упомянутым причинам сложные системы конструируются "послойно по вертикали", причём, - независимо от того выделяются ли слои в отдельные модули или нет, т.к. принцип инкапсуляции применяется так же и в разработке программ, которые порождают обычный монолитный исполнимый код. Допустим, что сказанное - тривиально. Тогда вопрос – одно и то же первичное событие, например, "сигнал контроллера диска об ошибке чтения сектора", должно ли оно интерпретироваться одинаково слоем чтения секторов, слоем компоновки из них кластеров и слоем обработки файла в целом? Конечно – нет! Это – разные абстракции и в каждой из них это одно событие будет иметь свою семантику, т.е. одновременно с преобразованием уровня абстракции мы должны преобразовывать уровень и событий, обрабатываемых данным слоем. Вообще любых событий, с которыми имеет дело данный слой, т.е. некоторые из них мы можем не поднимать на уровень выше, некоторые – заменять другими событиями, третьи - генерировать программно. Обработка событий может быть произвольной и определяется семантикой слоя-обработчика. Естественно, что сказанное в полной мере относится и к "событиям ошибки". А... вот как это сделать? На этот вопрос нет однозначного ответа – системы обработки ошибок могут иметь разную и произвольную конструкцию. Но кое-какие соображения высказать можно. Во-первых, абстракции, с которыми имеет дело пользователь, разительно отличаются от абстракций, которые являются источником информации об ошибке. Например, отказ в чтении сектора, возвращённый контроллером диска, может быть интерпретирован на уровне пользователя, как "попытка чтения счёта N 123 из БД оказалась неудачной" - контроллер не обязан знать, смысл того, что именно он читает в данный момент, а компонент пользователя не должен знать особенностей хранения данных в постоянном хранилище. Во-вторых, по мере возрастания абстракции, семантика сообщения становится всё более сложной. И если контроллер все свои ошибки, без всякого сомнения, способен перенумеровать и обойтись при возврате состояния только кодом возврата, то, как мы видели из предыдущей статьи, уже на уровне отдельного интерфейса COM этот механизм обнаруживает свою кардинальную недостаточность. Иными словами, одновременно с преобразованием абстракции события ошибки должна изменяться и форма представления сигнала о ней, на уровне пользователя она никак не может быть "просто кодом". Этого можно достичь, например, табличным преобразованием – в интерфейс пользователя "из глубин" приходит соответствующий код, интерфейс смотрит по таблице и выводит пользователю текстовую строку сообщения об ошибке, соответствующую коду. Но, как в предыдущей же статье и было замечено, даже составить просто непротиворечивую и уникальную систему кодов масштаба всей программной системы – задача весьма трудная. В качестве же приемлемого решения напрашивается весьма простой подход – объединить преобразование уровня абстракции события ошибки и сопоставление этому событию некоторого текстового описания в одном месте. Тогда, получив событие ошибки, объект (компонент, интерфейс) не просто транслирует его, а подготовит всё, чтобы это событие можно было непосредственно показать пользователю. Этим радикально решаются завязанные в единый узел проблемы. Во-первых, пользовательская оболочка не обязана уметь транслировать коды ошибок в сообщения пользователю, а, значит, её теперь действительно может касаться только "наличие ошибки исполнения метода – отсутствие ошибки". Во-вторых, "код ошибки" обязан быть уникальным только в пределах даже не всего компонента, а только слоя или даже объекта или интерфейса объекта (FACILITY_ITF из предыдущей статьи), что, естественно, реализуется намного легче, чем разработка глобально непротиворечивой системы кодов ошибок. В-третьих, если данный объект/компонент – не самый верхний, т.е. вызывается не пользователем, а каким-то объектом/компонентом ещё более высокого уровня, то тот компонент по-прежнему располагает, пусть и преобразованным, но технологическим кодом ошибки типа HRESULT. И может построить на нём анализ и преобразование абстракций своего уровня, а текстовую строку просто проигнорировать, если она ему не требуется. Вот, собственно, и всё... осталось только рассмотреть детали реализации описанного механизма, который в COM просторечно называется "механизм IErrorInfo". Итак, в COM существует т.н. "объект ошибки", который экспонирует интерфейс IErrorInfo. Объект этот обеспечивается слоем поддержки COM, поэтому реализовывать его самому не нужно. То, что это именно "объект" - артефакт реализации, поскольку давно подмечено, что в COM значительно легче организовать передачу специального объекта, нежели передачу обычной заполненной данными структуры. В рассматриваемом же нами случае "объект ошибки" по своему содержанию как раз такая структура и есть - достаточно только взглянуть на состав методов интерфейса IErrorInfo (методы 8 - 12):
Сам интерфейс IErrorInfo наследует пока не изученному нами интерфейсу автоматизации IDispatch (методы 4 - 7), поскольку описанная выше проблема "перевода на человеческий язык" возникла именно в эпоху господства в эволюции COM автоматизации. Интерфейс IDispatch – сам по себе есть отдельный и очень большой раздел COM, но, если вы используете интерфейсы, от него унаследованные, "как обычно", то вы можете просто проигнорировать это знание – ничего специфического для обращения с интерфейсом просто сам факт наследования от IDispatch не вносит. Кроме, конечно, дополнительно занимаемых четырёх ячеек vtbl. Интерфейс IErrorInfo – интерфейс, предназначенный для извлечения клиентом уже установленной сервером в "объект ошибки" информации. Изменить её в объекте ошибки клиент не может. А вот для того, чтобы её первично установить, "объект ошибки" реализует и второй, парный к описанному, интерфейс ICreateErrorInfo. Он тоже является наследником интерфейса IDispatch:
Работает вся механика так. Объект ошибки реализован в Oleaut32.dll, т.е. его сервис доступен в операционной системе всегда. Существует функция системного APICreateErrorInfo: HRESULT CreateErrorInfo(ICreateErrorInfo **pperrinfo); которая создаёт новый объект ошибки и возвращает вызывающей процедуре ссылку на интерфейс ICreateErrorInfo этого объекта. Пользуясь методами этого интерфейса сервер заполняет объект значениями данных. Далее, сервер должен получить у объекта ошибки ссылку на интерфейс IErrorInfo и должен вызвать другую функцию системного API – SetErrorInfo: HRESULT SetErrorInfo(DWORD dwReserved,IErrorInfo *perrinfo); которая "отцепляет" объект ошибки от сервера и "прицепляет" его к системному механизму передачи этого объекта на сторону клиента. Естественно, что это "отцепление/зацепление" рассматривается как удерживание ссылок на объект – сервер передаёт ссылку системе и освобождает свою, т.е. всё делается примерно таким образом: ICreateErrorInfo *pcerrinfo; IErrorInfo *perrinfo; HRESULT hr; $nbsp; 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 и состоит из одного своего метода:
Полезный метод данного интерфейса описывается так: 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 - 2004, Михаил Безверхов Публикация требует разрешения автора |
|