Что такое "фабрика класса"? Это понятие приходит из ООП и обозначает механизм, который "производит объекты". Проблема, в которой оно возникает состоит в следующем - до запуска программы у нас есть полный набор статических типов. И нет ни одного их экземпляра. Нам требуется процедура времени исполнения, которая бы из статического типа делала экземпляр, т.е. выполняла бы преобразование "имя статического типа - адрес экземпляра". Эта процедура и называется "фабрика класса", она существует в любой реализации ООП. Но в каждой реализации она существует по-своему, сообразно тому "набору артефактов реализации", который используется в данном средстве. В 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):
Интерфейс состоит из двух "своих" методов - 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):
Видно, что интерфейс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" протокол связи с которым был бы известен только в рамках данного проекта.
Предыдущая Оглавление Следующая