| Lesson 9 |
Aggregation guidelines: IUnknown information exchange |
| Objective |
List the guidelines for inner and outer IUnknown information exchange. |
IUnknown Information Exchange in COM Aggregation
COM aggregation enables composing complex objects from simpler components by making an outer object expose interfaces implemented by inner objects as if those interfaces were its own. This composition technique requires careful coordination between inner and outer objects through specific IUnknown pointer exchange protocols that maintain COM's fundamental reference counting and interface navigation contracts. Understanding these exchange rules proves essential for implementing aggregation correctly—violations lead to memory leaks, premature object destruction, or broken QueryInterface semantics. This lesson details the precise guidelines governing how inner and outer objects exchange IUnknown pointers, why these rules exist, and how to implement them correctly in both object factories and object constructors.
The Fundamental Aggregation Guideline
The central rule governing COM aggregation states that inner and outer objects must exchange IUnknown pointers during object creation. Specifically, the outer object provides its IUnknown pointer to the inner object, and the inner object provides its nondelegating IUnknown pointer back to the outer object. This exchange happens during inner object instantiation, establishing the connection that enables the outer object to control the composite object's lifetime and interface navigation. Without this bidirectional pointer exchange, aggregation cannot function—the inner object cannot delegate reference counting to the outer object, and the outer object cannot manage the inner object's lifetime or navigate its interfaces.
// Conceptual aggregation structure
class outer_object : public IUnknown {
private:
ULONG ref_count;
IUnknown* inner_nondelegating; // Inner's nondelegating IUnknown
public:
// During creation, outer receives inner's nondelegating IUnknown
HRESULT init() {
// Pass THIS object's IUnknown to inner
// Receive inner's nondelegating IUnknown
HRESULT hr = CoCreateInstance(
CLSID_InnerObject,
static_cast<IUnknown*>(this), // Outer's IUnknown
CLSCTX_INPROC_SERVER,
IID_IUnknown, // MUST ask for IUnknown
(void**)&inner_nondelegating
);
return hr;
}
};
class inner_object {
private:
IUnknown* outer_unknown; // Outer's IUnknown for delegation
public:
// Constructor receives outer's IUnknown
inner_object(IUnknown* outer) : outer_unknown(outer) {
// If outer is NULL, use own IUnknown (not aggregated)
if (!outer_unknown) {
outer_unknown = &nondelegating_iunknown;
}
}
};
Why Pointer Exchange is Necessary
The pointer exchange serves two critical purposes. First, the inner object needs the outer object's IUnknown to delegate AddRef, Release, and QueryInterface calls, ensuring clients see the composite as a single logical object with unified reference counting and interface navigation. Second, the outer object needs the inner object's nondelegating IUnknown to directly control the inner object's lifetime and navigate its interfaces without delegation loops. Without the outer's IUnknown, the inner object cannot properly delegate. Without the inner's nondelegating IUnknown, the outer object cannot manage the inner object independently of the delegation chain.
CoCreateInstance and Aggregation Parameters
CoCreateInstance provides explicit support for aggregation through its pUnkOuter parameter. When this parameter is NULL, CoCreateInstance creates a standalone object. When non-NULL, it signals aggregation: the outer object passes its IUnknown pointer, and CoCreateInstance must request IID_IUnknown from the inner object's class factory. This requirement ensures the outer receives the inner's nondelegating IUnknown rather than a delegating interface that would create a circular dependency. The combination of non-NULL pUnkOuter and IID_IUnknown tells the class factory "create this object as an inner object in an aggregation relationship."
// CoCreateInstance signature highlighting aggregation support
STDAPI CoCreateInstance(
REFCLSID rclsid, // CLSID of object to create
IUnknown* pUnkOuter, // Outer's IUnknown (NULL if not aggregating)
DWORD dwClsContext, // CLSCTX_INPROC_SERVER, etc.
REFIID riid, // Must be IID_IUnknown if pUnkOuter != NULL
void** ppv // Receives interface pointer
);
// Outer object creating aggregated inner object
class aggregating_outer : public IUnknown {
private:
IUnknown* inner_nondelegate;
public:
HRESULT create_inner() {
// Correct aggregation call
HRESULT hr = CoCreateInstance(
CLSID_InnerObject,
static_cast<IUnknown*>(this), // Pass outer's IUnknown
CLSCTX_INPROC_SERVER,
IID_IUnknown, // MUST be IUnknown for aggregation
(void**)&inner_nondelegate
);
if (FAILED(hr)) {
return hr;
}
// Now have inner's nondelegating IUnknown
// Can navigate inner's interfaces directly
return S_OK;
}
~aggregating_outer() {
// Release inner when outer destroyed
if (inner_nondelegate) {
inner_nondelegate->Release();
}
}
};
// WRONG: Requesting interface other than IUnknown during aggregation
HRESULT bad_aggregation_attempt() {
IPhoneBook* inner_phone;
// ERROR: Can't ask for specific interface when aggregating
HRESULT hr = CoCreateInstance(
CLSID_PhoneBook,
static_cast<IUnknown*>(this), // Aggregating
CLSCTX_INPROC_SERVER,
IID_IPhoneBook, // WRONG: Must be IID_IUnknown
(void**)&inner_phone
);
// This call will fail with E_NOINTERFACE
return hr;
}
IClassFactory::CreateInstance Aggregation Protocol
IClassFactory::CreateInstance mirrors CoCreateInstance's aggregation support through its pUnkOuter parameter. A class factory implementation must check whether pUnkOuter is NULL to determine if aggregation is requested. When pUnkOuter is non-NULL, the factory must verify that riid equals IID_IUnknown—requesting any other interface during aggregation violates COM rules and must return E_NOINTERFACE. This validation ensures outer objects receive the nondelegating IUnknown they need while preventing misconfigured aggregation attempts that would create invalid object structures.
// IClassFactory::CreateInstance signature
HRESULT IClassFactory::CreateInstance(
IUnknown* pUnkOuter, // Outer's IUnknown if aggregating
REFIID riid, // Must be IID_IUnknown if pUnkOuter != NULL
void** ppv // Receives interface pointer
);
// Class factory implementation for aggregable object
class inner_class_factory : public IClassFactory {
public:
STDMETHODIMP CreateInstance(
IUnknown* pUnkOuter,
REFIID riid,
void** ppv
) {
*ppv = nullptr;
// Check if being aggregated
if (pUnkOuter != nullptr) {
// Aggregation requested - MUST ask for IUnknown
if (riid != IID_IUnknown) {
return E_NOINTERFACE; // Violates aggregation rules
}
}
// Create inner object, passing outer's IUnknown
// (pUnkOuter is NULL if not aggregating)
inner_com_object* obj = new inner_com_object(pUnkOuter);
if (!obj) {
return E_OUTOFMEMORY;
}
// Get requested interface (IUnknown if aggregating)
HRESULT hr = obj->QueryInterface(riid, ppv);
if (FAILED(hr)) {
delete obj;
}
return hr;
}
// Other IClassFactory methods...
STDMETHODIMP LockServer(BOOL lock) { return S_OK; }
// IUnknown methods...
STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
};
Inner Object Constructor Pattern
The standard pattern for inner objects involves accepting the outer object's IUnknown pointer in the constructor. The inner object stores this pointer and uses it for all delegating IUnknown methods (AddRef, Release, QueryInterface). When pUnkOuter is NULL, the object is not being aggregated and should use its own nondelegating IUnknown for these operations. This conditional initialization in the constructor ensures the inner object works correctly both as an aggregated inner object and as a standalone object, maximizing code reuse and flexibility.
// Inner COM object implementation supporting aggregation
class inner_com_object {
private:
ULONG ref_count;
IUnknown* outer_unknown; // For delegation
// Nested class: nondelegating IUnknown
class nondelegating_iunknown_impl : public IUnknown {
private:
inner_com_object* parent;
public:
nondelegating_iunknown_impl(inner_com_object* p) : parent(p) {}
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
if (riid == IID_IUnknown) {
*ppv = static_cast<IUnknown*>(this);
AddRef();
return S_OK;
}
// Check other interfaces...
*ppv = nullptr;
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef() {
return InterlockedIncrement(&parent->ref_count);
}
STDMETHODIMP_(ULONG) Release() {
ULONG count = InterlockedDecrement(&parent->ref_count);
if (count == 0) {
delete parent;
}
return count;
}
} nondelegating_unknown;
public:
// Constructor: accept outer's IUnknown
inner_com_object(IUnknown* pUnkOuter)
: ref_count(1)
, nondelegating_unknown(this)
, outer_unknown(pUnkOuter)
{
// If not aggregating, delegate to self
if (!outer_unknown) {
outer_unknown = &nondelegating_unknown;
}
}
// Delegating IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
return outer_unknown->QueryInterface(riid, ppv);
}
STDMETHODIMP_(ULONG) AddRef() {
return outer_unknown->AddRef();
}
STDMETHODIMP_(ULONG) Release() {
return outer_unknown->Release();
}
// Get nondelegating IUnknown for outer object
IUnknown* get_nondelegating_unknown() {
return &nondelegating_unknown;
}
};
Preventing Aggregation in Non-Aggregable Objects
Some COM objects should not support aggregation due to design constraints or implementation complexity. Making an object non-aggregatable involves checking the pUnkOuter parameter in CreateInstance and returning CLASS_E_NOAGGREGATION when it's non-NULL. This explicit rejection prevents outer objects from attempting to aggregate objects that don't support the necessary nondelegating IUnknown infrastructure. Common reasons for disallowing aggregation include objects that maintain critical internal state requiring direct control, objects with threading requirements incompatible with delegation, or objects where the additional complexity of supporting aggregation provides no benefit.
// Class factory for non-aggregatable object
class nonaggregatable_factory : public IClassFactory {
public:
STDMETHODIMP CreateInstance(
IUnknown* pUnkOuter,
REFIID riid,
void** ppv
) {
*ppv = nullptr;
// Reject aggregation attempts immediately
if (pUnkOuter != nullptr) {
return CLASS_E_NOAGGREGATION;
}
// Create standalone object
nonaggregatable_object* obj = new nonaggregatable_object();
if (!obj) {
return E_OUTOFMEMORY;
}
HRESULT hr = obj->QueryInterface(riid, ppv);
if (FAILED(hr)) {
delete obj;
}
return hr;
}
// Other methods...
};
// Attempting to aggregate non-aggregatable object
void attempt_invalid_aggregation() {
IUnknown* inner = nullptr;
HRESULT hr = CoCreateInstance(
CLSID_NonAggregatable,
static_cast<IUnknown*>(this), // Try to aggregate
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(void**)&inner
);
// hr will be CLASS_E_NOAGGREGATION
assert(hr == CLASS_E_NOAGGREGATION);
}
Lifetime Synchronization
Proper aggregation requires synchronizing inner and outer object lifetimes. The outer object creates inner objects during its initialization (often in a custom Init method called by its class factory). The outer object destroys inner objects during its destruction, typically in its destructor, by calling Release on the inner object's nondelegating IUnknown. This synchronization ensures the composite object appears and behaves as a single unit—when clients release the last reference to the outer object, both outer and inner objects are destroyed together. Failure to synchronize lifetimes causes memory leaks when inner objects outlive the outer, or crashes when the outer releases after the inner has been destroyed.
// Complete outer object with lifetime synchronization
class complete_outer : public IUnknown {
private:
ULONG ref_count;
IUnknown* inner_nondelegate;
public:
complete_outer() : ref_count(1), inner_nondelegate(nullptr) {}
// Init method creates aggregated inner object
HRESULT init() {
// Create inner during outer initialization
HRESULT hr = CoCreateInstance(
CLSID_InnerObject,
static_cast<IUnknown*>(this),
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(void**)&inner_nondelegate
);
return hr;
}
// Destructor releases inner object
~complete_outer() {
// Destroy inner during outer destruction
if (inner_nondelegate) {
inner_nondelegate->Release();
inner_nondelegate = nullptr;
}
}
// IUnknown implementation
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
if (riid == IID_IUnknown) {
*ppv = static_cast<IUnknown*>(this);
AddRef();
return S_OK;
}
// Delegate to inner for its interfaces
if (inner_nondelegate) {
return inner_nondelegate->QueryInterface(riid, ppv);
}
*ppv = nullptr;
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef() {
return InterlockedIncrement(&ref_count);
}
STDMETHODIMP_(ULONG) Release() {
ULONG count = InterlockedDecrement(&ref_count);
if (count == 0) {
delete this; // Destructor releases inner
}
return count;
}
};
// Class factory for outer object
class outer_factory : public IClassFactory {
public:
STDMETHODIMP CreateInstance(
IUnknown* pUnkOuter,
REFIID riid,
void** ppv
) {
*ppv = nullptr;
// Outer objects typically don't support being aggregated
if (pUnkOuter != nullptr) {
return CLASS_E_NOAGGREGATION;
}
complete_outer* outer = new complete_outer();
if (!outer) {
return E_OUTOFMEMORY;
}
// Initialize outer (creates inner)
HRESULT hr = outer->init();
if (FAILED(hr)) {
delete outer;
return hr;
}
// Get requested interface
hr = outer->QueryInterface(riid, ppv);
if (FAILED(hr)) {
delete outer;
}
return hr;
}
};
Modern Context: COM Aggregation in .NET Interop
While COM itself is legacy technology, COM aggregation patterns appear in .NET interop scenarios where managed code exposes COM objects or consumes legacy COM components. Runtime Callable Wrappers (RCW) that wrap COM objects for .NET consumption may encounter aggregated COM objects and must handle their lifetime correctly through COM Callable Wrappers (CCW) providing COM interfaces for .NET objects can participate in aggregation when legacy COM code requires it. Understanding COM aggregation rules enables debugging interop issues and maintaining hybrid applications. Modern distributed systems use different composition patterns—dependency injection, microservices, and API gateways—but the core principle remains: composing complex functionality from simpler components while maintaining clear interface contracts and lifetime management.
Summary of Exchange Guidelines
The guidelines for inner and outer IUnknown information exchange form a precise protocol. First, inner and outer objects must exchange IUnknown pointers during inner object creation. Second, the outer object passes its IUnknown to the inner through CoCreateInstance or IClassFactory::CreateInstance's pUnkOuter parameter. Third, when pUnkOuter is non-NULL, the caller must request IID_IUnknown specifically—no other interface is valid during aggregation. Fourth, the inner object receives the outer's IUnknown and uses it for delegating AddRef, Release, and QueryInterface. Fifth, the inner object provides its nondelegating IUnknown to the outer object for direct lifetime management and interface navigation. Sixth, the outer object synchronizes inner object creation with its initialization and inner object destruction with its destruction. Seventh, objects that don't support aggregation return CLASS_E_NOAGGREGATION when pUnkOuter is non-NULL. Following these guidelines ensures aggregation works correctly, maintaining COM's reference counting and interface navigation contracts across the composite object boundary.
Conclusion
IUnknown information exchange in COM aggregation represents a carefully designed protocol enabling object composition while preserving COM's fundamental contracts. The bidirectional pointer exchange—outer providing its IUnknown to inner, inner providing its nondelegating IUnknown to outer—establishes the foundation for delegation and lifetime management that makes aggregation work. Understanding why these rules exist, not just what they are, enables implementing aggregation correctly and debugging problems when they arise. While modern development has moved beyond COM to simpler component models with garbage collection and different composition patterns, the principles underlying COM aggregation—clear interface contracts, explicit lifetime management, and controlled delegation—remain relevant for understanding component-based architecture. Whether maintaining legacy COM code, debugging .NET interop issues, or designing modern component systems, the lessons learned from COM aggregation's precise pointer exchange protocol inform better software architecture decisions.
Analyze Inner COM Object - Exercise
