Сущность технологии COM

       

Двунаправленные интерфейсные контракты


Как было показано в главе 5, объекты, постоянно находящиеся в различных апартаментах, могут использовать сервисные программы друг друга вне зависимости от того, резидентом какого апартамента является другой объект. Поскольку удаленный доступ в СОМ основан на концепции апартаментов, разработчикам необходимо рассматривать процессы не как клиенты или серверы в чистом виде, а скорее как набор из одного или нескольких апартаментов, которые способны одновременно экспортировать и импортировать интерфейсы.

Как два объекта договариваются о том, чьи интерфейсы будут использоваться для взаимного сотрудничества, в значительной степени является спецификой области применения. Для примера рассмотрим следующий интерфейс, моделирующий программиста:

[uuid(75DA6457-DD0F-11d0-8C58-0080C73925BA),object] interface IProgrammer : IUnknown { HRESULT StartHacking(void); HRESULT IsProductDone([out, retval] BOOL *pbIsDone); }

Клиент будет использовать такой интерфейс следующим образом:

HRESULT ShipSoftware(void) { IProgrammer *pp = 0; HRESULT hr = CoGetObject(OLESTR("programmer:Bob"), 0, IID_IProgrammer, (void**)&pp); if (SUCCEEDED(hr)) { hr = pp->StartHacking(); BOOL bIsDone = FALSE; while (!bIsDone && SUCCEEDED(hr)) { Sleep(15000); // wait 15 seconds // ожидаем 15 секунд hr = pp->IsProductDone(&bIsDone); // check status // проверяем состояние } pp->Release(); } }

Очевидно, что этот код весьма неэффективен, поскольку клиент каждые 15 секунд опрашивает состояние объекта. Более эффективным для клиента был бы следующий подход: подготовить второй объект, которому объект-программист (programmer object) мог бы сообщить, когда данный объект придет в нужное состояние. Этот подготовленный клиентом объект должен экспортировать интерфейс, предоставляющий контекст, через который мог бы работать объект-программист:

[uuid(75DA6458-DD9F-11d0-8C58-0080C73925BA),object] interface ISoftwareConsumer : IUnknown { HRESULT OnProductIsDone(void); HRESULT OnProductWillBeLate([in] hyper nMonths); }


При таком парном определении интерфейса должен существовать некий механизм для информирования объекта-программиста о том, что у клиента имеется реализация ISoftwareConsumer, с помощью которой он может получать уведомления от объекта-программиста об изменениях состояния. Одной из распространенных методик является определение IProgrammer таким образом, чтобы он имел явные методы, через которые клиенты могли бы связываться со своими объектами-потребителями (consumer object). Канонической формой этой идиомы является включение метода Advise:

interface IProgrammer : IUnknown { HRESULT Advise ([in] ISoftwareConsumer *psc, [out] DWORD *pdwCookie); : : :

посредством которого клиент подготавливает парный объект-потребитель, а программист возвращает DWORD для подтверждения связи. Затем этот DWORD можно было бы использовать в соответствующем методе Unadvise:

interface IProgrammer : IUnknown { : : : HRESULT Unadvise([in] DWORD dwCookie); }

для того, чтобы сообщить объекту-программисту о прерывании связи. При использовании уникальных DWORD для представления связи программист-потребитель дизайн интерфейса позволяет произвольному числу потребителей независимо друг от друга соединяться с объектом и отсоединяться от него.

Если эти два метода имеются в интерфейсе IProgrammer, то реализация программиста может быть соединена с объектом-потребителем с помощью метода Advise

STDMETHODIMP Programmer::Advise(ISoftwareConsumer *pcs, DWORD *pdwCookie) { assert(pcs); if (m_pConsumer != 0) // is there already a consumer? // уже есть потребитель? return E_UNEXPECTED; (m_pConsumer = pcs)->AddRef(); // hold onto new consumer // соединяемся с новым потребителем *pdwCookie = DWORD(pcs); // make up a reasonable cookie // готовим подходящий маркер return S_OK; }

Соответствующая реализация метода Unadvise выглядела бы примерно так:

STDMETHODIMP Programmer::Unadvise(DWORD dwCookie) { // does the cookie correspond to the current consumer? // соответствует ли маркер данному потребителю? if (DWORD(m_pConsumer) != dwCookie) return E_UNEXPECTED; (m_pConsumer)->Release(); // release current consumer // освобождаем текущего потребителя m_pConsumer = 0; return S_OK; }



Взаимоотношения между программистом и потребителем показаны на рис. 7.7. Хотя в данной реализации в каждый момент предусмотрен только один потребитель, возможно, что более опытный программист смог бы оперировать несколькими потребителями одновременно, управляя динамическим массивом интерфейсных указателей из ISoftwareConsumer.



Имея приведенную выше реализацию, метод программиста StartHacking может теперь использовать потребителя для индикации готовности результата:

STDMETHODIMP Programmer::StartHacking (void) { assert(m_pConsumer); // preemptively notify of lateness // приоритетно сообщаем о задержке HRESULT hr = m_Consumer->OnProductWillBeLate(3); if (FAILED(hr)) return PROGRAMMER_E_UNREALISTICCONSUMER; // generate some code // генерируем некоторый код extern char *g_rgpszTopFiftyStatements[]; for (int n = 0; n < 100000; n++) printf(g_rgpszTopFiftyStatements[rand() % 50]); // inform consumer of done-ness // извещаем потребителя о выполнении hr = m_pConsumer->OnProductIsDone(); return S_OK; }

To обстоятельство, что реализация ISoftwareConsumer может принадлежать к другому апартаменту, чем объект-программист, не является существенным. На самом деле метод StartHacking может быть вызван из того апартамента, который содержит объект-потребитель, и в этом случае будет осуществлено повторное вхождение в апартамент вызывающей программы, что, в сущности, является синхронным обратным вызовом. В то время как эта реализация делает вложенные вызовы на объект-потребитель, объект-программист может также в будущем производить вызовы методов объекта-потребителя в любое время. Эта привилегия остается до тех пор, пока не последует вызов метода Unadvise, разрывающий соединение.

Поскольку интерфейсы IProgrammer и ISoftwareConsumer, вероятно, были созданы в тандеме для совместной работы, использование явного метода интерфейса IProgrammer для установления связи становится частью протокола при работе с объектами-программистами и является вполне целесообразным.


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

[uuid(75DA645D-DD0F-11d0-8C58-0080C73925BA),object ] interface IShutdownNotify : IUnknown { HRESULT OnObjectDestroyed([in] IUnknown *pUnk); }

В этом интерфейсе предполагается, что разработчик IShutdownNotify заинтересован в получении сообщений о прекращении работы от других объектов. В данном определении, однако, не приведен механизм, с помощью которого эти заинтересованные стороны могли бы сообщить объектам, что они хотели бы быть уведомлены об уничтожении этого объекта. Как показано на рис. 7.8, одна из возможных стратегий осуществления этого состоит в определении второго (парного) интерфейса, который объекты могли бы реализовать:

[uuid(75DA645E-DD0F-11d0-8C58-0080C73925BA), object] interface IShutdownSource : IUnknown { HRESULT Advise([in] IShutdownNotify *psn, [out] DWORD *pdwCookie); HRESULT Unadvise([in] DWORD dwCookie); }



Данный интерфейс существует, однако, только для того, чтобы дать наблюдателям (observers) возможность соединить свои интерфейсы IShutdownNotify с объектом. Если имеется большое число типов интерфейсов обратного вызова, то необходимо определить столь же большое число соответствующих интерфейсов только для управления соединением. Ясно, что должен существовать более общий механизм: вхождение в точках стыковки.

Точки стыковки являются идиомой СОМ, предназначенной для регистрации связи интерфейсов обратного вызова с объектом и ее отмены. Точки стыковки не являются необходимыми для создания сетей из объектов с большим количеством соединений. К тому же точки стыковки не обеспечивают двунаправленных соединений.


Вместо этого идиома точек стыковки выражает общую концепцию регистрации экспортируемых интерфейсов как небольшого числа интерфейсов стандартной инфраструктуры. Наиболее фундаментальным из этих интерфейсов является IConnectionPoint:

[object, uuid(B196B286-BAB4-101A-B69C-00AA00341D07)] interface IConnectionPoint : IUnknown { // which type of interface can be connected // какой тип интерфейса можно присоединить HRESULT GetConnectionInterface( [out] IID * pIID); // get a pointer to identity of "real" object // получаем указатель на копию "реального" объекта HRESULT GetConnectionPointContainer([out] IConnectionPointContainer ** ppCPC); // hold and use pUnkSink until notified otherwise // сохраняем и используем pUnkSink, пока не объявлено другое HRESULT Advise([in] IUnknown * pUnkSink, [out] DWORD * pdwCookie); // stop holding/using the pointer associated with dwCookle // прекращаем хранение/использование указателя, связанного с dwCookie HRESULT Unadvise([in] DWORD dwCookie); // get information about currently held pointers // получаем информацию об имеющихся в данный момент указателях HRESULT EnumConnections([out] IEnumConnections ** ppEnum); }

Как показано на рис. 7.9, объекты представляют отдельную реализацию этого интерфейса каждому типу интерфейса, который может быть использован объектом в качестве интерфейса обратного вызова. Ввиду того, что IConnectionPoint не выставлен как часть единицы идентификации объекта, он не может быть обнаружен посредством QueryInterface. Вместо этого в СОМ предусмотрен второй интерфейс, который выставлен как часть единицы идентификации объекта, которая позволяет клиентам запрашивать реализацию IConnectionPoint, соответствующую отдельному типу интерфейса обратного вызова:

[object,uuid(B196B284-BAB4-101A-B69C-00AA00341D07)] interface IConnectionPointContainer : IUnknown { // get all possible IConnectionPoint implementations // получаем все возможные реализации IConnectionPoint HRESULT EnumConnectionPoints([out] IEnumConnectionPoints ** ppEnum); // get the IConnectionPoint implementation for riid // получаем реализацию IConnectionPoint для riid HRESULT FindConnectionPoint([in] REFIID riid, [out] IConnectionPoint ** ppCP); }





Как показано на рис. 7.9, каждая реализация IConnectionPoint выставляется из отдельной СОМ-единицы идентификации.

С учетом вышеупомянутых определений интерфейса клиент мог бы связать свою реализацию IShutdownNotify с объектом следующим образом:

HRESULT HookupShutdownCallback(IUnknown *pUnkObject, IShutdownNotify *pShutdownNotify, DWORD &rdwCookie) { IConnectionPointContainer *pcpc = 0; HRESULT hr = pUnkObject->QueryInterface(IID_IConnectionPointContainer, (void**)&pcpc); if (SUCCEEDED(hr)) { IConnectionPoint *pcp = 0; hr =pcpc->FindConnectionPoint(IID_IShutdownNotify,&pcp); if (SUCCEEDED(hr)) { hr = pcp->Advise(pShutdownNotify, &rdwCookie); pcp->Release(); } pcpc->Release(); } }

Соответствующий код для разрыва связи выглядит так:

HRESULT TeardownShutdownCallback(IUnknown *pUnkObject, DWORD dwCookie) { IConnectionPointContainer *pcpc = 0; HRESULT hr = pUnkObject->QueryInterface(IID_IConnectionPointContainer, (void**)&pcpc); if (SUCCEEDED(hr)) { IConnectionPoint *pcp = 0; hr =pcpc->FindConnectionPoint(IID_IShutdownNotify,&pcp); if (SUCCEEDED(hr)) { hr = pcp->Unadvise(dwCookie); pcp->Release(); } pcpc->Release(); } }

Отметим, что в обоих примерах клиент использует метод IConnectionPointContainer::FindConnectionPoint для вызова из объекта его IShutdownNotify-реализации IConnectionPoint. Если объект отклоняет вызов FindConnectionPoint, это говорит о том, что он не понимает семантику интерфейса IShutdownNotify. Это оберегает пользователя от прикрепления произвольных интерфейсов обратного вызова к объекту без полного согласия на это разработчика объекта.

Как и в случае с IUnknown, реализации IConnectionPointContainer и IConnectionPoint в значительной степени типичны. Объекту C++ требуется отдельная единица идентификации СОМ для каждого типа экспортируемого интерфейса, который он предполагает поддерживать. Одна из методик реализации ConnectionPoint состоит в использовании того варианта методики вложения класса/композиции, которая учитывает различия в отношениях тождественности:



class Surfboard : public ISurfboard, public IHazardousDevice, public ISharkBait, public IConnectionPointContainer { LONG m_cRef; // СОM reference count // счетчик ссылок СОМ // Surfboards don't support multiple outbound interfaces // of a given type, so it simply declares single pointers // of each possible type of callback interface // Surfboard не поддерживает несколько экспортируемых // интерфейсов заданного типа, поэтому он просто // объявляет одиночные указатели каждого возможного // типа интерфейса обратного вызова IShutdownNotify *m_pShutdownNotify; ISurfboardUser *m_pSurfer;

// to deal with identity relationship of IConnectionPoint, // define an IShutdownNotify-specific nested class + member // для работы с отношением тождественности // IConnectionPoint, определяем специфический для // IShutdownNotify вложенный класс+член

class XCPShutdownNotify : public IConnectionPoint { Surfboard *This(void); // use fixed offset // испопьзуем постоянное смещение

// IUnknown methods... // методы IUnknown...

// IConnectionPoint methods... // методы IConnectionPoint... } m_xcpShutdownNotify;

// define an ISurfboardUser-specific nested class + member // определяем специфический для IShutdownNotify вложенный класс+член

class XCPSurfboardUser : public IConnectionPoint { Surfboard *This(void); // use fixed offset // используем постоянное смещение

// IUnknown methods... // методы IUnknown...

// IConnectionPoint methods... // методы IConnectionPoint... } m_xcpSurfboardUser;

// IUnknown methods... // методы IUnknown...

// ISurfboard methods... // методы ISurfboard...

// IHazardousDevice methods... // методы IHazardousDevice...

// ISharkBait methods... // методы ISharkBait...

// IConnectionPointContainer methods... // методы IConnectionPointContainer... };

Следует указать, что экземпляры класса Surfboard будут иметь две отдельные реализации IConnectionPoint, одна из которых используется для присоединения интерфейсов обратного вызова IShutdownNotify, а вторая — для присоединения интерфейсов ISurfboardUser.


Эти две реализации разделены на отдельные классы C++, что позволяет каждой реализации IConnectionPoint иметь свои собственные уникальные реализации IUnknown и IConnectionPoint. В частности, может иметься три отдельных реализации QueryInterface со своими собственными наборами интерфейсных указателей, которые могут быть выделены для создания трех отдельных СОМ-копий.

Из приведенного выше определения класса следует такая QueryInterface-peaлизация основного класса Surfboard:

STDMETHODIMP Surfboard::QueryInterface(REFIID riid, void**ppv) { if (riid == IID_IUnknown riid == IID_ISurfboard) *ppv = static_cast<ISurfboard*>(this); else if (riid == IID_IHazardousDevice) *ppv = static_cast< IHazardousDevice *>(this); else if (riid == IID_ISharkBait) *ppv = static_cast<ISharkBait *>(this); else if (riid == IID_IConnectionPointContainer) *ppv = static_cast<IConnectionPointContainer *>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

Отметим, что доступ к интерфейсу IConnectionPoint не может быть осуществлен через эту главную реализацию QueryInterface. Каждый из методов QueryInterface вложенного класса будет выглядеть примерно так:

STDMETHODIMP Surfboard::XCPShutdownNotify::QueryInterface(REFIID riid, void**ppv) { if (riid == IID_IUnknown riid == IID_IConnectionPoint) *ppv = static_cast<IConnectionPoint *>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

Эту же реализацию можно было бы применить и к классу XCPSurfboardUser. Между объектом Surfboard и двумя подобъектами, которые реализуют интерфейс IConnectionPoint не существует идентичности.

Для того чтобы объект Surfboard не уничтожил себя раньше времени, подобъекты администратора соединений просто делегируют вызовы своих методов AddRef и Release в содержащий их объект surfboard:

STDMETHODIMP_(ULONG) Surfboard::XCPShutdownNotify::AddRef(void) { return This()->AddRef(); /* AddRef containing object */ /* AddRef объекта-контейнера */ }



STDMETHODIMP_(ULONG) Surfboard::XCPShutdownNotify::Release(void) { return This()->Release(); /* Release containing object */ /* Release объекта-контейнера */ }

В приведенных выше методах предполагается, что метод This возвращает указатель на объект-контейнер Surfboard, используя вычисление некоторого постоянного смещения.

Клиенты находят интерфейсы объекта IConnectionPoint посредством вызова метода объекта FindConnectionPoint, который для класса Surfboard мог бы выглядеть примерно так:

STDMETHODIMP Surfboard::FindConnectionPoint(REFIID riid, IConnectionPoint **ppcp) { if (riid == IID_IShutdownNotify) *ppcp = IID_IShutdownNotify; else if (riid == IID_ISurfboardUser) *ppcp = &m_xcpSurfboardUser; else return (*ppcp = 0), CONNECT_E_NOCONNECTION; (*ppcp)->AddRef(); return S_OK; }

Отметим, что объект выдает интерфейсные указатели IConnectionPoint только при запросе тех интерфейсов, на которые он сможет сделать обратный запрос. Необходимо указать также на поразительное сходство с большинством реализации QueryInterface. Основное различие состоит в том, что QueryInterface имеет дело с импортируемыми (inbound) интерфейсами, в то время как FindConnectionPoint — с экспортируемыми (outbound) интерфейсами.

Поскольку метод IConnectionPoint::Advise принимает только интерфейс IUnknown, статически типизированный как интерфейсный указатель обратного вызова, то реализации Advise должны использовать QueryInterface для того, чтобы привязать указатель обратного вызова к соответствующему типу интерфейса:

STDMETHODIMP Surfboard::XCPShutdownNotify::Advise(IUnknown *pUnk, DWORD *pdwCookie) { assert (pdwCookie && pUnk); *pdwCookie = 0; if (This()->m_pShutdownNotify) // already have one // уже имеем один return CONNECT_E_ADVISELIMIT; // QueryInterface for correct callback type // QueryInterface для корректирования типа обратного вызова HRESULT hr = pUnk->QueryInterface(IID_IShutdownNotify, (void**)&(This()->m_pShutdownNotify)); if (hr == E_NOINTERFACE) hr = CONNECT_E_NOCONNECTION; if (SUCCEEDED(hr)) // make up a meaningful cookie // готовим значимый маркер *pdwCookie = DWORD(This()->m_pShutdownNotify); return hr; }



Напомним, что QueryInterface неявно вызывает AddRef, что означает следующее: объект Surfboard теперь хранит ссылку обратного вызова, причем она остается легальной за пределами области действия метода Advise. Отметим также, что если объект обратного вызова не реализует соответствующий интерфейс, то результирующий HRESULT преобразуется в CONNECT_E_NOCONNECTION. Если же сбой QueryInterface последовал по какой-либо иной причине, то HRESULT от QueryInterface передается вызывающей программе.

Основанный на приведенной выше реализации Advise соответствующий метод Unadvise имеет следующий вид:

STDMETHODIMP Surfboard::XCPShutdownNotify::Unadvise(DWORD dwCookie) { // ensure that the cookie corresponds to a valid connection // убеждаемся, что маркер соответствует допустимому соединению if (DWORD (This()->m_pShutdownNotify) != dwCookie) return CONNECT_E_NOCONNECTION;

// release the connection // освобождаем соединение This()->m_pShutdownNotify->Release(); This()->m_pShutdownNotify = 0; return S_OK; }

В интерфейсе IConnectionPoint имеется три дополнительных вспомогательных метода, два из которых реализуются тривиально:

STDMETHODIMP Surfboard::XCPShutdownNotify::GetConnectionInterface( IID *piid) { assert (piid); // return IID of the interface managed by this subobject // возвращаем IID интерфейса, управляемого этим подобъектом *piid = IID_IShutdownNofify; return S_OK; }

STDMETHODIMP Surfboard::XCPShutdownNotify::GetConnectionPointContainer( IConnectionPointContainer **ppcpc) { assert(ppcpc); (*ppcpc = This())->AddRef(); // return containing object // возвращаем объект-контейнер return S_OK; }

Последний из этих трех методов, EnumConnections, позволяет вызывающим программам перенумеровывать соединенные интерфейсы. Данный метод является дополнительным, так что реализации могут законно возвращать E_NOTIMPL.

Для объявления о том, какие из экспортируемых интерфейсов класс реализации поддерживает, в IDL предусмотрен атрибут [source]:

[uuid(315BC280-DEA7-11d0-8C5E-0080C73925BA) ] coclass Surfboard { [default] interface ISurfboard; interface IHazardousDevice; interface ISharkBait; [source] interface IShutdownNotify; [source, default] interface ISurfboardUser; }



Кроме этого, в СОМ предусмотрено два интерфейса, которые позволяют средам этапа выполнения запрашивать объект самостоятельно (introspectively) возвращать информацию об импортируемых в него и экспортируемых им типах интерфейсов:

[object,uuid(B196B283-BAB4-101A-B69C-00AA00341D07) ] interface IProvideClassInfo : Unknown { // return description of object's coclass // возвращаем описание кокласса объекта HRESULT GetClassInfo([out] ITypeInfo ** ppTI); }

[object, uuid(A6BC3AC0-DBAA-11CE-9DE3-00M004BB851) ] interface IProvideClassInfo2 : IProvideClassInfo { typedef enum tagGUIDKIND { GUIDKIND_DEFAULT_SOURCE_DISP_IID = 1 } GUIDKIND; // return IID of default outbound dispinterface // возвращаем IID принятого по умолчанию экспортируемого диспинтерфейса HRESULT GetGUID([in] DWORD dwGuidKind, [out] GUID * pGUID); }

Оба этих интерфейса весьма просты для реализации:

STDMETHODIMP Surfboard::GetClassInfo(ITypeInfo **ppti) { assert(ppti != 0); ITypeLib *ptl = 0; HRESULT hr = LoadRegTypeLib(LIBID_BeachLib, 1, 0, 0, &ptl); if (SUCCEEDED(hr)) { hr = ptl->GetTypeInfoOfGuid(CLSID_Surfboard, ppti); ptl->Release(); } return hr; }

STDMETHODIMP Surfboard::GetGUID (DWORD dwKind, GUID *pguid) { if (dwKind != GUIDKIND_DEFAULT_SOURCE_DISP_IID !pguid) return E_INVALIDARG; // ISurfboardUser must be defined as a dispinterface // ISurfboardUser должен быть определен как диспинтерфейс *pguid = IID_ISurfboardUser; return S_OK; }

Хотя экспортируемые интерфейсы не должны быть обязательно диспетчерскими интерфейсами (диспинтерфейсами), но ряд сред сценариев требуют этого, чтобы осуществлять естественное преобразование обратных вызовов в текст сценария.

Предположим, что интерфейс ISurfboardUser определен как диспинтерфейс следующим образом:

[uuid(315BC28A-DEA7-11d0-8C5E-0080C73925BA)] dispinterface ISurfboardUser { methods: [id(1)] void OnTiltingForward( [in] long nAmount); [id(2)] void OnTiltingSideways( [in] long nAmount); }

При программировании на Visual Basic можно объявить переменные, понимающие тип интерфейса обратного вызова, принятый по умолчанию, таким образом:



Dim WithEvents sb as Surfboard

Наличие такого описания переменной дает программистам на Visual Basic возможность писать обработчики событий. Обработчики событий — это функции или подпрограммы, использующие соглашение VariableName_EventName. Например, для обработки события обратного вызова ОпТiltingForward на определенную выше переменную sb программисту Visual Basic пришлось бы написать следующий код:

Sub sb_OnTiltingForward(ByVal nAmount as Long) MsgBox "The surfboard just tilted forward" End Sub

Виртуальная машина Visual Basic будет действительно на лету обрабатывать реализацию ISurfboardUser, преобразуя поступающие вызовы методов в соответствующие определенные пользователем подпрограммы.

1

В этом заключается один из известных дефектов в схеме точек стыковки. Другой хорошо известный дефект состоит в том, для каждого типа интерфейса обратного вызова требуется явный вызов FindConnectionPoint. Оба этих дефекта отрицательно сказываются на производительности с связи в увеличением числа полных обходов, которые вносит каждый из этих дефектов. Влияние использования точек стыковки на производительность служит напоминанием, что интерфейсы следует разрабатывать, имея в виду возможный межапартаментиыи доступ.

2

Распространенная ошибка, которая может привести к отказу в доступе, состоит в том, что объект пытается вступить в контакт с процессом объекта обратного вызова, так как контроль доступа вызывающей программы не разрешает вызовы из принципала зашиты объекта.


Содержание раздела