Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Фабрика классов

Что такое "фабрика класса"? Это понятие приходит из ООПи обозначает механизм, который "производит объекты". Проблема, в которой оно возникает состоит в следующем - до запуска программы у нас есть полный набор статических типов. И нет ни одного их экземпляра. Нам требуется процедура времени исполнения, которая бы из статического типа делала экземпляр, т.е. выполняла бы преобразование "имя статического типа - адрес экземпляра". Эта процедура и называется "фабрика класса", она существует в любой реализацииООП. Но в каждой реализации она существует по-своему, сообразно тому "набору артефактов реализации", который используется в данном средстве. ВCOMявно говорят об "объекте фабрики класса", вC++об этом не принято говорить. Но, не существующая в виде отдельной сущности, фабрика класса есть и вC++- это пара "операторnew- конструктор класса". Поскольку вC++возможно создание экземпляров класса не только в динамической, но в и статической и автоматической памяти, то точнее бы говорить, что на самом деле это пара "распределитель памяти - конструктор класса".

Назовём тот "COM-объект", который мы до сих пор изучали "элементарнымCOM-объектом". Он состоит из одного или более интерфейсов, которые разделяют одну и только одну реализациюIUnknown, и одной или более областей данных, обслуживаемых этимIUnknown. Всё определение строится вокруг того, что реализацияIUnknownу этой сущности - самостоятельная и одна. Не следует считать "элементарныйCOM-объект" ни "типом", ни "объектом" в смыслеООП, потому что он есть только та программная конструкция, тот примитив, посредством которого вCOM реализуетсямодельный, полезный объект. Когда перед вами встанет задача не определения, а реализации объекта, вы возьмёте этот универсальный примитив и в нём реализуете все те свойства, которые и отличают ваш объект. В определение же свойств объекта модели сам примитив не добавляет ничего.

Теперь помыслим в том направлении, подозрение в котором нам доставила цитата из MSDN- вызовDllGetClassObjectесть вызов статического метода. Что делает этот статический метод? Он вызывает к жизни механизм, который может создавать полноценные экземпляры. Как хотите, но это - в чистом виде пара "операторnew- конструктор класса". Ведь операторnew- тоже статический метод, а то, что механизм этот "завёрнут" в конструкцию "элементарногоCOM-объекта" - артефакт реализацииCOM, просто вCOMникакого другого способа взаимодействия нет. И интерфейс, который мы при этом получаем, вполне можно считать аналогом конструктора объекта. И нам никто не запрещает иметь полиморфный конструктор - несколько таких интерфейсов, каждый из которых делает экземпляры объекта каким-то отличным от других способом.

Итак, всё встало на свои места. При вызове DllGetClassObjectмы передаём ейCLSIDстатического типа и получаем в ответ ссылку на элементарныйCOM-объект, который реализует статические аспекты этого типа. Если нашCOM-тип уподоблять классуC++, то посредством этого объекта мы получаем доступ к статическим методам класса - для доступа к статическим методам класса не нужно иметь никакого экземпляра, достаточно при вызове назвать имя класса. И мы - пока ещё кромеCLSID, именующего тип, тоже ничего не назвали. Далее, мы можем вызвать статический метод названного класса, который "отведёт память - вызовет конструктор - создаст экземпляр" и вернёт нам адрес экземпляра - сущности, реализующей динамические аспекты этого типа. Конечно, поскольку у насCOM-, а неC++-объект, вернёт он ссылку на другой элементарныйCOM-объект, но ведь это к философии взаимодействия вООПотношения не имеет - это только артефакт данной реализации, которая отличается от реализацииC++.

Но, коль скоро мы и действительно имеем дело с сущностью, представляющей все статические аспекты типа, почему мы в наборе её статических методов ограничены только конструкторами? Что мешает реализовать в её составе интерфейс, который не будет выполнять "создание экземпляра", а будет выполнять какие-то другие действия, относимые ко всему типу сразу? Так обычно не делают, но ведь "не делают" не означает, что "невозможно сделать"? Значит у нас имеет место быть не какая-то особая внесистемная сущность "фабрика класса", а сущность, представляющая все статические аспекты типа. В том числе - реализующая и фабрики класса. Эта сущность является элементарным COM-объектом, и она - отличается от сущностей, которые представляют собой динамические аспекты (экземпляры) типа - те тоже являются элементарнымиCOM-объектами, но - другими, физически с первой сущностью никак не связанными. А всё вместе - "статический типCOM-объекта", именуемый однимCLSID.

Так почему из всех статических аспектов типа в COMи реализуются только фабрики класса? Почему возможность реализовать иные "статические методы типа" вCOMфактически не используется? Ответ прост - потому, что хотя бы раз без фабрики класса обойтись никак нельзя, а вот без статических методов можно. Т.е. можно реализовать их фактическую функциональность без них. Ведь то, что вы имеете дело с "COM-объектом" или "статическим типомCOM" - абстракция, или, если хотите - иллюзия клиента. Это клиент, феноменологически наблюдая за той сущностью сервера, с которой он взаимодействует посредством интерфейсов, может заключить, что она имеет все признаки объекта - идентичность, состояние и поведение. А также и отличается ими от других таких же - имеет и признаки статического типа. Но внутри сервера всё это реализуется отнюдь не одним классомC++и не одним его экземпляром! Внутри сервера может иметь место очень сложная модель в которой несколько "СOM-типов" разделяют одни данные, так что параметр, измененный уCOM-объектаXможет немедленно изменить состояниеCOM-объектаY, который внешне - с точки зрения клиента - никак сXне связан. Да иCOM-сервер может быть реализован не только на языке объектно-ориентированного программирования, но и на процедурном языке - наC, например. Клиент этого никогда не узнает - его "представления о сервере" и ограничиваются "сущностью с интерфейсами", которая ведёт себя так, как положено объекту.

Поэтому то решение, которое в C++экономит код, в данном случае имеет выигрыш только в "чистоте идеи", но не в удобстве реализации. Это лишний раз должно наводить читателя на мысль - "объект" есть модельное понятие, позволяющее нам с меньшими собственными усилиями строить модели окружающей реальности и воплощать их в виде программ. Но было бы очень большой ошибкой говорить об "объектах программы" не упоминая уровня абстракций, на каком эти объекты рассматриваются.COMэто очень прекрасно иллюстрирует - то, что клиентCOM-сервера считает "объектом" внутри сервера реализуется чем и как угодно, в том числе, но не обязательно, - и "объектами" меньшего уровня. Можно привести и другой пример такого же "скачка" - "объектные конструкции"C++транслируются компилятором в плоский процедурный код, работа которого, тем не менее оставляет нас в убеждении, что перед нами - взаимодействие объектов.

Интерфейсов фабрики класса в COMстандартно известно два - IClassFactory и IClassFactory2. Возможно также созданиеCOM-объекта "не путём использования фабрики класса", т.е. конечно, с использованием той же концепции, но без использования интерфейса, который стереотипно так называется. Возможно написание собственной фабрики класса - интерфейса, который как-то совершенно особенно будет создавать вам экземпляры объектов, именно так, как вам надо. Никакой ошибкой не является и создание экземпляров так, как мы до сих пор делали в примерах кода в нашей серии - ну не было у наших типов реализации статических аспектов и не было. Тем не менее, стандартные интерфейсы - это "стандартные клиенты", которые, в частности -Visual Basic,Delphi,FoxPro,J++, клиенты, создаваемыеMFCиATL- пользуются именно стандартными интерфейсами фабрики класса. И если вы намереваетесь сделать "стандартно совместимый статический тип" - вы обязаны в числе прочих фабрик класса реализовать и такой интерфейс. Он может быть и единственным из всех фабрик класса - этого минимально достаточно.

Надо также ответить и на вопрос - а что даёт, кроме дополнительного шага в создании экземпляра, использование концепции "фабрики класса"? Даёт оно, в общем, то же, что даёт использование перегруженного конструктора - возможность по разному (а не однотипно, как это было до сих пор в наших примерах) создавать экземпляры объектов. Ведь CLSID- один, но это не означает, что процедура создания экземпляра объекта тоже возможна только одна. Вообразите, например, что у вас есть реализация обработчика файла. Обработка производится так - вы "напускаете" обработчик на файл, а он организует вам небольшое "окно" позволяющее доступ к содержимому этого файла. И экземпляр объекта - как раз и представляет для вас это самое "окно". Ситуация, когда вы именно экземпляру указываете имя файла порождает вам всегда "одно окно на один файл". А если вам может потребоваться несколько окон к одному файлу? Указывая имя файла не "объекту экземпляра", а "объекту типа" вы несколько меняете ситуацию - одна и та же реализация теперь сможет организовать сколько угодно окон к одному файлу и сколько угодно разных файлов тоже смогут ей обслуживаться одновременно. Это полностью аналогично использованию вC++конструктора копирования, например.

Эта особенность используется в MTS- контекст транзакции в нём определяется именно фабрикой класса, но я должен заметить, что приёмы программирования вC++и вCOM- отличаются. Если вC++принято нагружение конструкторов разной полезной функциональностью, иногда даже до такой степени, что вся программа большей частью сводится к порождению объектов разными способами, то дляCOMтакой подход нехарактерен. ВCOM, обычно, используется только "конструктор по умолчанию", а вся функциональность по изменению внутреннего состояния объекта реализуется методами.

Сейчас мы рассмотрим интерфейс IClassFactory. Реализуется он "объектом типа", а назначение его - делать "объекты экземпляров" данного типа. Вот список его методов (в порядкеvtbl):

  1. QueryInterface

  2. AddRef

  3. Release

  4. CreateInstance

  5. LockServer

Интерфейс состоит из двух "своих" методов - CreateInstanceиLockServer, а первые три метода унаследованы им отIUnknown. МетодCreateInstance- "основной рабочий" метод этого интерфейса, именно он "создаёт экземпляры", методLockServer- вспомогательный и о нём чуть ниже. Повторюсь - реализован интерфейс должен быть в "объекте типа", поэтому последовательность действий, которую выполняет клиент для того, чтобы получить ссылку на "объект экземпляра" выглядит так:

IClassFactory *ptr1;

::CoGetClassObject(CLSID, ... ,IID_IClassFactory,ptr1);

IUnknown *ptr2;

HRESULT hr = ptr1->CreateInstance(pUnkOuter,IID_IUnknown,ptr2);

...

ptr1->Release();

При этом ptr1- указатель на "объект типа", аptr2- указатель на "объект экземпляра", который как раз и создаётся методомCreateInstance. После создания экземпляра клиент может освободить "объект типа" (ptr1), а может, если ему требуются и ещё экземпляры, продолжать вызыватьCreateInstance. Каждый вызов будет доставлять ему новуюнезависимуюссылку на объект экземпляра.

Слово "независимая" специально подчёркнуто. Очень давно мы говорили о том, что в ответ на запрос клиента о предоставлении ссылки на объект сервер может всякий раз создавать новый объект, а может - выдавать всем один и тот же адрес. Что именно сервер выдаёт - дело сервера, т.е. как мы теперь видим - дело метода CreateInstance. И, хотя в спецификации методаIClassFactory::CreateInstanceподчёркнуто, что "...создаёт неинициализированный объект указанногоCLSIDтипа...", интерфейс не определяет семантики -CreateInstanceможет выдавать всем ссылку на один и тот же (статический, по-сути) объект. Сказанное - только возможность, которая может понадобиться в каком-то случае, т.к. в подавляющем большинстве "нормальных" реализацийCreateInstanceдействительно создаёт новый экземпляр пользуясь операциейnew. Но и в том и в другом случае клиент должен считать, что он получил совершенно новую ссылку и что созданный объект - не проинициализирован.

Для вызова CreateInstanceклиент должен обеспечить три параметра - указатель наIUnknownагрегата (pUnkOuter),IIDинтерфейса, который реализует "объект экземпляра" и указатель, куда будет помещена ссылка. Напомню, что во всех случаях (и при вызовеCoGetClassObjectиCreateInstanceи в любом другом случае, когда "возвращается ссылка") счётчик ссылок продвигается "внутри", т.е. клиент не должен для этого указателя вызыватьAddRef- кто ссылку создаёт, тот её и первый раз "защёлкивает".

Смысл параметров должен быть понятен, а IUnknownагрегата мы рассмотрим, когда будем изучать агрегирование. Если создаётся не агрегированный, а независимый объект, этот параметр должен иметь значениеNULL.

CreateInstance- методCOM, и в этом качестве возвращает значение типаHRESULT, индицирующее успешность создания экземпляра. Как обычно, значениеS_OKсообщает о полной успешности, но метод также может возвращать и состояния ошибки. Вот некоторые из них:

E_UNEXPECTED

- неожиданная ошибка

E_OUTOFMEMORY

- нехватка памяти

E_NOINTERFACE

- запрошенный интерфейс не поддерживается объектом

E_INVALIDARG

- неверный аргумент

CLASS_E_NOAGGREGATION

- объект не поддерживает агрегацию

Очень правильно, с точки зрения правил хорошего тона, если при невозможности создать экземпляр, реализация CreateInstanceне только вернёт состояние ошибки, но и поместитNULLв тот указатель, в котором клиент ожидает предоставления ссылки на интерфейс. Создатели серверов должны об этом помнить. А вот создатели клиентов должны помнить о том, что весьма редко, но бывают реализации, построеные так, что не обращают внимания значение указателя, которое возвращают клиенту, если создание экземпляра провалилось. Поэтому полагаться на успешность только проверяя ненулевое ли значение возвращённого указателя на экземпляр объекта - не стоит.

Как должен быть реализован CreateInstance- понятно. В примерах в нашем изложении мы создали практически все программные конструкции, необходимые для этого. Так что их теперь осталось только объединить "под одним методом". Но я думаю, что не стоит торопиться с кодом. Давайте выясним, а что делает методLockServerи как он попал именно в этот интерфейс?

Метод LockServerвыполняет простую и вспомогательную задачу - он блокирует сервер в загруженном состоянии. Этим я хотел сказать, чтоLockServerесть аналогAddRef, только - на уровне всего сервера. Вызов егоLockServer(TRUE)приводит к тому, что "счётчик блокировок сервера продвигается вперёд на единицу", аLockServer(FALSE)- производит обратное действие. Когда "счётчик блокировок сервера" достигает нуля, то... сервер может быть выгружен.

Понять функциональность LockServerпроще всего исторически. Этот метод потребовался в реализации не столькоDLL, сколькоEXE-серверовCOM. Все подробностиEXE-сервера мы еще изучим, но интересующая нас проблема возникает вот откуда. В отличие отDLL EXEне имеет механизма экспортируемых функций. И при стартеEXE-модуля он занимается регистрацией себя в системе "я - активен", регистрацией, что он реализует типы с такими-тоCLSIDи т.д. - немалой, надо сказать, работой. А при деактивизации он, соответственно, проделывает всё в обратном порядке. Когда клиент "поднимает"EXE-сервер, путём запроса у него ссылки на объект, то сервер естественно блокируется в запущенном состоянии этим объектом. Но вот когда клиент отпустит последнюю ссылку на последний объект обеспечиваемый сервером, то сервер поймёт, что "он - свободен" и немедленно засобирается в обратный путь - завершить свою работу. А сборы эти - долгие, и клиенту может оказаться удобнее подержать некоторое время сервер в запущенном состоянии хотя никаких ссылок на объекты в настоящий момент сервер никому не обеспечивает. Просто потому, что клиент собирается в ближайшем будущем вновь обратиться к этому серверу и совершенно не хочет ожидать цикла "выгрузка-повторная загрузка". Вот для того, чтобы не делать блокировку сервера запросом фиктивной, по сути, ссылки на какой-то объект, в состав сервера и введен особый метод самоблокировки. Его не нужно вызывать, если не нужна "искусственная блокировка", т.е. если сервер в текущий момент выдал кому-то ссылку на объект. Естественно, что вCOMэта функция - метод интерфейса, не менее естественно, что этот метод - метод самого первого интерфейса, который получает клиент. Так что - всё просто.

Понимая это можно понять и то, что в DLL-сервере функциональность этого метода несколько избыточна -DLL-сервер сам не выгружается. И система его по своей инициативе тоже не выгружает, она дожидается, когда клиент из себя вызоветCoFreeUnusedLibraries. А клиент может её и не вызывать. Тем не менее, единообразия ради, иinproc-серверы обязаны реализовывать этот метод. Так же, как иEXE. Наверное, понятно, что в этом случае методLockServerпросто влияет на значение, которое возвращает системе функцияDllCanUnloadNow.

Поэтому реализации всех IClassFactory, в одном сервере по сути, будут разделять один и тот же счётчик блокировок сервера, а вот методыCreateInstanceу каждой из них будут свои. В примере я также покажу одно очень интересное решение по реализацииAddRef/LockServer- словами его излагать долго и топорно, но код - очень изящен.

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

Почему CreateInstance- конструктор именно по умолчанию? Разве нельзя было бы предусмотреть ещё один параметр? Предусмотреть-то можно, да... зачем? Ведь явное понятие "конструктор" есть вC++, а вот вVBэто реализуется совсем уже по-другому, а где-нибудь ещё и вовсе понятия конструктора нет (VBA). АCOM- двоичная технология,COM-сервер должен работать везде, с любым клиентом. Именно поэтому минимум и ограничен только таким конструктором. Ибо в том языке (из того клиента) где этого понятия нет его всё равно можно вызвать неявно, без указания каких-либо параметров. Следовательно, правильнее было бы считать, что понятие "конструктор объекта" относится не к методу, но к целому интерфейсу. И интерфейсIClassFactoryкак раз и есть интерфейс "конструктора по умолчанию". Если ваш клиент способен понимать другие конструкторы вы можете реализовать и другие интерфейсы фабрики класса - именно те, которые вам нужны.

Говоря об интерфейсе IClassFactoryневозможно пройти и мимо его проприетарного собрата - интерфейсаIClassFactory2. Он предназначен в точности для того же, для чего предназначен и интерфейсIClassFactory, но только - для программ-серверов, в которые встроены какие-то средства ограничения работоспособности в зависимости от того "лицензированная" или "нелицензированная" копия программы запускается клиентом.

Вообще говоря, зная бизнес-модель компании Microsoftбыло бы удивительнее не обнаружить наличия такого интерфейса или какого-то подобного средства, чем обнаружить его, но, насколько этот интерфейс полезен "вне самой Microsoft"?

Я думаю, что - полезен. Не потому, что "противника нужно знать в лицо", а потому, что "лицензирование программ" - не блажь, а вполне необходимое занятие. И наличие стандартного на данной платформе интерфейса, который бы позволял единообразно различать "чистых" и "нечистых" является достоинством, а не недостатком - клиенты у COM-серверов могут быть разными, а точнее - круг клиентов заранее не ограничивается и даже не устанавливается. В этом - одна из самых сильных сторонCOM. Но ведь и желание получить доход со своего продукта - тоже не последнее по значимости обстоятельство. Поэтому интерфейсIClassFactory2является своего рода компромиссом между необходимостью обеспечить минимальную работоспособность сервера с любым клиентом (хотя бы для того, чтобы клиент не рухнул, а имел возможность корректно обработать специфическую ошибку) и нежеланием "дать попользоваться на халявку". Плохо это или хорошо c "идейной" стороны - пусть останется за рамками нашего рассмотрения, мы в данном случае изучаем только техническую возможность и как ею пользоваться. А когда и в каком объеме эту возможность применять - пусть разработчик сервера определяет сам.

Итак, вот список методов интерфейса IClassFactory2(в порядкеvtbl):

  1. QueryInterface

  2. AddRef

  3. Release

  4. CreateInstance

  5. LockServer

  6. GetLicInfo

  7. RequestLicKey

  8. CreateInstanceLic

Видно, что интерфейсIClassFactory2является наследником интерфейсаIClassFactory- его собственные методы толькоGetLicInfo,RequestLicKeyиCreateInstanceLic.

Работает же вся эта механика так. Допустим, наш сервер работает в паре с каким-то механизмом, позволяющим по ходу исполнения программы выяснить - есть лицензия или нет. Это может быть "аппаратный ключ" (наподобие HASP), а может быть и какой иной механизм, важно, что внутри самой себя "защищённая программа" может выяснить есть "ключ" или нет. И в процессе своей инициализации она это обстоятельство выясняет. Алгоритм этого выяснения совершенно несущественен, важен только результат. Если "ключ - есть, ограничений - нет", то программа является полнофункциональной версией и совершенно свободно должна создавать экземплярыCOM-типов посредством методаIClassFactory::CreateInstance... Если же это не так, т.е. "ключа - нет, ограничения - есть", то вызовIClassFactory::CreateInstanceдолжен возвратить специализированный код ошибкиCLASS_E_NOTLICENSEDи не возвращать ссылку на экземпляр объекта.

Здесь должно быть понятно - поскольку IClassFactoryобслуживает не весь сервер, а только один данный статическийCOM-тип, то и ограничения на запуск относятся не ко всему серверу в целом, а только к тем статическим типам, которые программист решил "предоставлять только в лицензированной версии". Поэтому вполне допустимо, если какие-то статические типы сервера будут "защищены ключом", а какие-то - будут доступны свободно. "Свободным типам" незачем реализовывать интерфейсIClassFactory2.

Программа-клиент, получив  CLASS_E_NOTLICENSED, может обратиться к интерфейсуIClassFactory2(который, в таком случае, статическийCOM-тип реализовывать должен) чтобы попытаться получить ссылку на экземпляр класса посредством методаIClassFactory2::CreateInstanceLic. Если же и это не получается, то клиент может утешиться хотя бы тем, что получит отIClassFactory2"разъяснение своих прав". Т.е. сервер способен предоставить не только "двоичный" сервис "да/нет". Напротив,IClassFactory2может снабдить сервер верхом обходительности с клиентом!

Делается всё это интерфейсом IClassFactory2следующим образом. Под словами "ключ лицензии" понимается некоторая двоичная строка разумной длины, что-то наподобие "машинного пароля". И эта строка может играть роль "пароля на предъявителя". Предполагается, что если сервер выяснил, что он запускается в нелицензированной среде, то сервер всё равно "может быть столь любезен", чтобы дать клиенту возможность самому показать ему ключ лицензии. Этот "ключ" может не соотноситься с "ключом", который имеется у средств, которые контролируют "лицензированность" среды, т.е. этот "ключ" - "персональная отмычка" только для интерфейсаIClassFactory2. Получив именно еёIClassFactory2должен "провернуться" так, как если бы он работал на лицензированной машине.

Для "выяснения своих прав" интерфейс IClassFactory2предоставляет клиенту методGetLicInfo:

HRESULT GetLicInfo(LICINFO *pLicInfo);

Клиент вызывает этот метод, предоставляя серверу пустую структуру LICINFO, а сервер её заполняет:

typedef struct tagLICINFO

{

ULONG cbLicInfo;

BOOL fRuntimeKeyAvail;

BOOL fLicVerified;

} LICINFO;

В ней cbLicInfo- просто длина структуры, а значащими членами являютсяfRuntimeKeyAvailиfLicVerified. Оба принимают значение " == 0" (false) и " != 0"(true). ПараметрfLicVerifiedсообщает, каков ответ на вопрос "работает ли сервер в лицензированной среде?" и будут ли работать методыIClassFactory::CreateInstanceиIClassFactory2::RequestLicKey. Еслиtrue- лицензия обнаружена.

Параметр fRuntimeKeyAvailсообщает, позволяет ли сервер делегировать право создания экземпляров данного статического типа нелицензированной машине. Еслиtrue- позволяет, т.е. можно бежать к лицензированной машине, вызывать методIClassFactory2::RequestLicKeyна ней, получать ключ лицензии, тащить его к себе и вызывать у своего сервера методIClassFactory2::CreateInstanceLicс этим самым ключом. Если возвращается значениеfalse, то делать это бесполезно - методIClassFactory2::CreateInstanceLicвсё равно откажется работать.

Если интерфейс IClassFactory2реализован, то как минимум, методGetLicInfoу него должен работать - именно этот метод и сообщает клиенту работоспособны ли два других метода. И ещё важное замечание - совсем не так, как это принято в большинствеCOM-методов, пустую структуру для заполнения в этом методе должен предоставить клиент, а не сервер. Должен ли клиент перед вызовом этого метода заполнить поле длины структуры? Не знаю... Реализация этого метода - ваша, собственная. Вы можете проверять эту длину и, если она коротка - отказаться заполнять структуру. А можете и не обращать на это внимания, всегда предполагая, что длина - достаточна. В реализации этого метода вATLсервер ничего не проверяет, кроме того не равен ли указатель, который он получил от клиента,NULLи помещает в поле длины чрезвычайно "содержательную" информацию -sizeof(LICINFO).

Следующий метод - IClassFactory2::RequestLicKeyснабжает клиентапролицензированноймашины (на нелицензированной машине метод должен отказаться работать) ключом лицензии (видимо, для того, чтобы тот мог "поделиться с друзьями"):

HRESULT RequestLicKey(DWORD dwReserved, //Не используется, должен быть 0

BSTR *pbstrKey //Указатель на ключ лицензии

);

Здесь pbstrKey- предоставляемый клиентомуказатель, на строку типаBSTR, куда сервер поместит адрес размещенного "ключа лицензии". Но в этом методе, как и положено вCOM, память под собственно строку ключа - предоставляет сервер. И её после этого - надо вернуть системе. Поскольку реализация - ваша, то размещать эту память вы будете функциейSysAllocString, а клиент будет освобождать вызывая функциюSysFreeString. Но об именно размещении/освобождении строк - в нашем же изложении, только позже. Полученный ключ клиент может использовать в вызове методаIClassFactory2::CreateInstanceLic.

Нужно отметить, что RequestLicKeyотработает успешно только в том случае, если это "не запрещено творцом" реализации данногоCOM-класса, т.е. если методGetLicInfoвозвращает членLICINFO::fRuntimeKeyAvailсо значениемtrue. Если возвращаетсяLICINFO::fRuntimeKeyAvail==false, то и методRequestLicKeyдолжен вернуть кодE_NOTIMPL- метод не реализован.

Ну и, наконец, предел всем мытарствам с обретением ключа лицензии на запуск и созданием экземпляра кладёт метод CreateInstanceLic:

HRESULT CreateInstanceLic(IUnknown *pUnkOuter, //IUnknown агрегата

IUnknown *pUnkReserved, //Не используется. Должен быть NULL

REFIID riid, //IID запрашиваемого интерфейса

BSTR bstrKey, //ключ лицензии

void**ppvObject//адрес указателя под ссылку

);

Метод очень похож на метод CreateInstance, только появляются параметрыpUnkReservedиbstrKey.

При этом  CreateInstanceLicтоже "условно-работоспособный" метод. Он может принятьbstrKeyи создать экземпляр объекта на нелицензированной машине как на лицензированной. А может и не принять и не создать - это опять определяет творец реализации данногоCOM-класса. Если методуRequestLicKeyзапрещено возвращать ключ лицензии, то и методCreateInstanceLicдолжен всегда возвращать код возвратаE_NOTIMPL- создание объектов данного типа разрешено только на полностью пролицензированной машине. АделаетэтоIClassFactory::CreateInstance.

Поэтому неразобранным остаётся только один вопрос - а как применять-то? Да вот хотя бы и так - вы выпускаете в обращение "сервер с ключом". Понятно, что появляются его нелицензионные копии... Но вам это даже и на руку, т.к. "номальный" клиент, который делает всё, использует для создания экземпляров метод IClassFactory::CreateInstanceи который "без ключа" не работает, а в распространяемого вами "демонстрационного" клиента "забит" ключ лицензии на предъявителя и использует этот клиентIClassFactory2::CreateInstanceLic. И с этим клиентом работает всё - но в демонстрационном варианте... А сервер - один и тот же.

С методической точки зрения интерфейс IClassFactory2интересен с нескольких сторон. Во-первых, это живая иллюстрация принципа - опубликованный интерфейс никогда не изменяется. Если вам требуется модернизировать существующий интерфейс, то нельзя делать его версию. Нужно разработать совершенно новый (с другимIID) интерфейс и опубликовать его. Что мы и видим,IID_IClassFactory-{00000001-0000-0000-C000-000000000046}, аIID_IClassFactory2-{B196B28F-BAB4-101A-B69C-00AA00341D07}. Во-вторых, это иллюстрация, как именно можно и нужно расширять функциональность существующих (опубликованных, известных) интерфейсов на какой-то частный случай применения. Мы будем впоследствии рассматривать этот вопрос отдельно и подробно, но вCOMдействует принцип "лучше реализовывать много небольших интерфейсов, чем несколько - больших". В-третьих, это пример протокола, когда два разных интерфейса (пусть они в данном случае и могут быть реализованы в однойvtbl, но так бывает не всегда) работают в паре - еслиIClassFactory"проваливается", то клиенту должен предлагаться другой интерфейс. А оба они, скорее всего, реализованы внутри сервера так тесно, что их и не разделить. В-четвертых... я понимаю, что это очень "дискуссионабельный" вопрос -IClassFactory2есть ещё и пример как соединить байты и центы и при этом остаться вежливым к клиенту с самым разным "уровнем доходов". Мы часто наблюдаем, что программа, в которую встроены всякого рода "защиты по ключу" то просто виснет, то вообще не даёт на себя посмотреть без этого самого ключа. А ведь можно - и чуть-чуть по другому...

Ну и уж коль скоро мы затронули эту тему, нужно заметить, что вообще COMв смысле сохранения проприетарности сконструирован просто замечательно. Если вы рассматриваетеDLL, то как минимум, вседа сможете увидеть её таблицы импортируемых и экспортируемых функций и глядя на них вычислить точки входа и как-то соотнести мнемонику имён с функциями, которые они выполняют. А вот рассматриваяCOM-сервер... вы не выясните ничего. Вы не сможете выяснить у самого сервера (библиотеки типов не касаемся!) ни какие интерфейсы он поддерживает, ни состав этих интерфейсов, ни точки входа... Даже методом перебора это вряд-ли удастся установить в силу общего числа возможныхGUID. Эту информацию приходится публиковать в системном реестре, если у васlocalилиremoteсервер - без неё не работает системный слой поддержкиCOM, а вот дляinproc-сервера её публикация не обязательна - наш сервер из примера работал с тем минимумом опубликованной информации, чтобы система могла его отыскать для загрузки на исполнение. Поэтому, если в вашем проекте есть некая особенность, которую бы вы хотели подальше спрятать от любопытных глаз исследователя с отладчиком, так, чтобы он и догадаться-то не мог о том, что она есть и когда-то откуда-то вызывается - оформите еёinproc-сервером, который замаскируйте под обычнуюDLL. Ничто не запрещает, чтобыCOM-сервер был бы "COM-сервером" только наполовину и экспортировал бы какие-то другие функции как обычнаяDLL. Но можно сделать и ещё хитрее - и функциюDllGetClassObjectпереименовать, т.е. на принципах, на каких построенCOM-сервер, сделать свою реализацию "внутреннего маленькогоCOM" протокол связи с которым был бы известен только в рамках данного проекта.

ПредыдущаяОглавлениеСледующая