Walter Oliver
What Is a Resource Dispenser?
Development Environment
Defining the Resource Dispenser Class
Initializing the Resource Dispenser
Writing a COM Interface or API for Your RM Proxy
Talking to DispMan Through IHolder
Implementing the IDispenserDriver
Releasing the Resource Dispenser
Implement the DLL EntryPoint and ExportsAppendix A: Threading and Marshalling Issues
Appendix B: Registry Entries
A resource dispenser (RD) provides the following services:
Build an RD if your requirements include:
Writing a resource dispenser to pool application component objects is not recommended. Future versions of MTS will use the IobjectControl interface, which can be implemented by application components to achieve object pooling and recycling. Moreover, in those cases where there are a few component objects that can be shared globally, setting up a shared property for each object and using them through the Shared Property Manager may provide the desired result.
When running under MTS, the Dispenser Manager can automate transactions and resource reclamation. When operating with MTS, your RD interacts with:
These relationships are:
When running independently from MTS, your RD interacts with all but two of the MTS components: Dispenser Manager and Holder. This implies that the RD will continue to use MS DTC to provide transaction propagation but will not provide MTS pooling (the RD may provide its own pooling mechanism).
There are many ways of setting up your development environment for writing a resource dispenser. Here is a set of tools and libraries that either are required or can simplify the job:
Define your class simply and easily by using the ATL Object Wizard in VC ++ and choosing the "simple object" option. The following class definition serves as a model for defining your resource dispenser class:
/////////////////////////////////////////////////////////////////////////////
// CResourceDispenser
class ATL_NO_VTABLE CRDisp :
public IDispenserDriver,
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CRDisp, &CLSID_ ResourceDispenser >,
public IDispatchImpl<IRDisp, &IID_IResourceDispenser, &LIBID_RESDISPLib>
{
private:
//
// Member variables needed to make a resource dispenser
//
IGlobalInterfaceTable * m_pGIT;
DWORD m_dwRmPtrCookie;
IHolder * m_pHolder;
IDispenserManager * m_pDispMan;
// A map of Resource handles to export objects
map<DWORD, ITransactionExport *> m_mapExport;
public:
//
// The resource dispenser must be a singleton object so that
// there is a unique IHolder which maintains the list of
// resources to be pooled.
//
DECLARE_CLASSFACTORY_SINGLETON(CFileRmPxy);
DECLARE_PROTECT_FINAL_CONSTRUCT();
// Set pointers to NULL within the constructor.
CResourceDispenser ();
~ CResourceDispenser ();
DECLARE_REGISTRY_RESOURCEID(IDR_RDISP)
DECLARE_GET_CONTROLLING_UNKNOWN()
BEGIN_COM_MAP(CResourceDispenser)
COM_INTERFACE_ENTRY(IResourceDispenser)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IDispenserDriver)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
END_COM_MAP()
// Do initialization work within this method.
HRESULT FinalConstruct();
// Do clean up work within this method.
void FinalRelease();
CComPtr<IUnknown> m_pUnkMarshaler;
//
// IResourceDispenser. This is the custom interface that the application
// components call.
//
// Define your custom interface here. The following methods are given as
//examples only, you can choose any names and/or signatures as you deem
// appropriate.
//
STDMETHOD(Connect)(long *hConnection);
STDMETHOD(Disconnect)(long hConnection);
STDMETHOD(SomeMethod_1)(long hConnection, DWORD dwQty );
STDMETHOD(SomeMethod_2)(long hConnection, BYTE* pData );
//
// IDispenserDriver
//
// This interface is required to run under MTS.
STDMETHOD(CreateResource)( /*[in]*/ const RESTYPID ResTypId,
/*[out]*/ RESID* pResId,
/*[out]*/ TIMEINSECS* SecsFreeBeforeDestroy );
STDMETHOD(RateResource)( /*[in]*/ const RESTYPID ResTypId,
/*[in]*/ const RESID ResId,
/*[in]*/ const BOOL fRequiresTransactionEnlistment,
/*[out]*/ RESOURCERATING* pRating );
STDMETHOD(EnlistResource)(/*[in]*/ const RESID ResId,/*[in]*/ const TRANSID TransId);
STDMETHOD(ResetResource)(/*[in]*/ const RESID ResId);
// Numeric resource ID
STDMETHOD(DestroyResource)(/*[in]*/ const RESID ResId);
// String resource ID
STDMETHOD(DestroyResourceS)(/*[in]*/ constSRESID ResId);
};
If you decide to use the ATL library to implement the resource dispenser, the RD class should be derived from CComObjectRootEx. This ATL template provides the FinalContruct method that you should use when initializing. Doing initialization work within this method will allow more flexibility when handling errors since they could be propagated all the way back to the user. This is because the ATL framework has fully created the object by the time it calls FinalContruct.
When initializing the resource dispenser you need to perform the following steps:
Initialize (to NULL) all member variables that will hold the various interface pointers. This step will help you make some basic decisions down the road, such as finding out whether a pointer should be released or whether the RD object is running under MTS. Perform this task within the default constructor of your class.
Call the GetDispenserManager function to get a pointer to the IDisperserManager interface. You must call this function to obtain a successful return code that indicates if the RD object is running under MTS. In addition, the GetDispenserManager function is the only way to obtain a pointer to the IDispenserManager interface. You should store the pointer received in a private member variable so that you can check it to verify if the RD is running under MTS. For example, you can use the following call to check if the RD is running under MTS:
hr = GetDispenserManager(&m_pDispMan);
If the previous call succeeded, then call the IDispenserManager::RegisterDispenser method to tell the DispMan that the RD object has started and wants to be connected. This function takes two input parameters and one output parameter. The input parameters are a pointer to the RD's IDispenserDriver interface and a WCHAR pointer containing the friendly name of your resource dispenser. The output parameter is a pointer to the pointer of the IHolder interface. Make sure you store this pointer in a private member variable. You will need it when implementing the resource allocation and freeing functionality (see the section "Talking to DispMan through IHolder"). For information on IDispenserDriver see the section "Implementing the IDispenserDriver".
Note The RegisterDispenser method does not add a reference count for the IDispenserDriver interface. This means that before you can call this method you need to obtain a pointer to the IDispenserDriver interface though the IUnknown::QueryInterface method so that the reference is properly added. This issue should be fixed in future releases of MTS.
For example, the following code demonstrates how to obtain a pointer to the IdispenserDriver interface through the IUnknown::QueryInterface method.
...hr = GetDispenserManager(&m_pDispMan);
...if (SUCCEEDED(hr))
...{
......// Call QueryInterface to get the right reference count.
......IDispenserDriver * pDriver;
......hr = GetUnknown()->QueryInterface(IID_IDispenserDriver, (void **)&pDriver);
......_ASSERTE(hr == S_OK);
......// Register with DispMan
......hr = m_pDispMan -> RegisterDispenser(pDriver, L"MyDispenser", &m_pHolder);......
......_ASSERTE(hr == S_OK);......
...}
Call the particular Resource Manager interface method (or API function) to establish a connection. This is not a resource connection, but the connection your resource dispenser needs for future requests. In other words, you should get back a handle or pointer to the RM, that is, a handle to a name pipe or a pointer to a COM Interface that will enable future calls to create and free resources or to propagate transactions. If the RM provides a COM Interface, call the CoCreateInstance function and pass the RM's CLSID to get a pointer to it.
The following code demonstrates how to make this call:
hr = CoCreateInstance(...CLSID_CoResourceManager,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IResourceManager ,
(void **)&m_pRm);...
Once you acquire a COM interface pointer to the Resource Manager, you should register it with the Global Interface Table (GIT), which allows the RD object to use the RM pointer within the right context even when the application component resides in a different apartment from the one the RM interface was created. In other words, the GIT ensures that the RM pointer will be valid within the particular thread in which it is needed. See Appendix A for more information on the GIT.
The following code demonstrates how to create an instance of the GIT:
hr = CoCreateInstance(...CLSID_StdGlobalInterfaceTable,
...NULL,
...CLSCTX_INPROC_SERVER,
... IID_IGlobalInterfaceTable,
...(void **)&m_pGIT);
After calling CoCreateInstance, call the IGlobalInterfaceTable::RegisterInterfaceInGlobal method and pass the RM pointer you got earlier along with its GUID. This method returns an out parameter that contains a cookie. From this point on you should always use this cookie when asking for RM pointer by calling IGlobalInterfaceTable::GetInterfaceFromGlobal. You do not need to have a member variable for the RM interface pointer; instead, you must have a member variable for the cookie.
It is strongly recommended that you create a small private method to make the call and check the return value. For example:
IFileRm * CFileRmPxy::GetResourceManagerPointer()
{
...IResourceManager * pRm =NULL;
...HRESULT hr;
...hr = m_pGIT->GetInterfaceFromGlobal(...m_dwRmPtrCookie,
IID_ IResourceManager,
(void **)&pRm);...
..._ASSERTE(pRm);
...return pRm;
}
As a final step to your initialization work, call CoCreateFreeThreadedMarshaler. Since your resource dispenser has a threading model value of "Both" (see Appendix B: Registry Entries), calling this function provides efficient inter-thread marshaling of the RD interface pointer within the same process. This function creates a free-threaded marshaler object and aggregates it to the RD object (refer to Appendix A for more information on marshalling issues.) The following code demonstrates how this function call is used:
hr = CoCreateFreeThreadedMarshaler(GetUnknown(), &m_pFreeThreadedMarshaler);
Notice that the first parameter corresponds to the IUknown interface pointer of the resource dispenser object.
One of the main requirements of your resource dispenser is to act as the Resource Manager's proxy or client-side interface. It is strongly recommended that you implement your RM proxy interface as a COM interface if possible. However, it is not mandatory for this interface to be COM compliant. An example of a resource dispenser that does not provide a COM interface is the Microsoft SQL Server™ ODBC driver. The interface it provides to clients is the standard ODBC API. For an example of a resource dispenser that implements a COM interface see the sample provided in the MTS 2.x SDK.
By using COM you will be able to group areas of functionality in simple interfaces as opposed to one big API library. For example, if the RM you are working with implements a nonintuitive interface, API, IPC layer, or requires several steps to accomplish one single operational unit, you may want to encapsulate all of the complexity behind one or more well though-out COM interfaces.
Independent of the interface implementation as a COM interface or API, the RD must accomplish two goals when working as the RM proxy:
The Dispenser Manager provides each resource dispenser (or RM proxy) with a Holder object that implements the IHolder interface. This Holder object works along side the RD to create and keep track of resources. In other words, the Holder object and the RD are part of the mechanism used by MTS to maintain an inventory of the resources provided by the Resource Manager.
When implementing your RM proxy's interface, you need to use the methods provided in IHolder to allocate and free resources. You should get a pointer to IHolder during initialization of your RM Proxy object by calling IDispenserManager::RegisterDispenser method, as described above.
Select among the methods of your interface those that will call the various methods of the IHolder interface. Of IHolder methods you will normally use only two: IHolder::AllocResource and IHolder::FreeResource. The other two IHolder::TrackResource and IHolder::UntrackResource are there for future functionality.
Whatever RD methods you select make sure there is a balance between AllocResource and FreeResource calls. For every call to AllocResource there should be a call to FreeResource. Failing to follow this rule will cause resources to hang around and not be returned to inventory until the object using them terminates or crashes. This behavior could seriously impair the entire system.
AllocResource should be called when the client requests a resource. In other words, the method that returns the pointer or handle that identifies the resource to the client should call this method to ask the corresponding Holder to allocate a resource.
Notice that the method that calls AllocResource is not the one that "explicitly" connects to the RM to create the resource. The section "Implementing the IDispenserDriver" has more detail on which method explicitly connects to the RM for resource creation.
The AllocResource method takes two parameters: a RESTYPID and a RESID*. You need to decide what these two parameters contain.
RESTYID is defined as a DWORD. Its purpose is to identify a type of resource not the resource itself. What you store in it is up to you. It could be a constant or a pointer to an object in the RD memory that contains a full description of the type of resource. DispMan does not care about its content. DispMan only uses RESTYPID to refer to a resource type within the resource dispenser.
RESID is also defined as a DWORD. Its purpose is to identify a particular instance of a resource. You would normally want to store in it a pointer to the resource itself. The section "Implementing the IDispenserDriver" has more detail on this topic.
// Running under MTS?
if (m_pDispMan)
{
*hConnection = NULL;
hr = m_pHolder -> AllocResource((RESID)1, (RESID *)hConnection);
if (FAILED(hr))
{
AtlTrace(_T("AllocResource failed! Error code %x\n"), hr);
}
}
Notice that hConnenction could be an output parameter declared as "long *hConnection" which would allow the client to get a handle to the resource. The client would then treat this handle as an opaque handle. For a more detailed example see the sample provided with the MTS 2.x SDK.
As part of the execution of the AllocResource method, DispMan does the following steps to produce a resource:
If the caller does not have a current transaction, then the enlistment is skipped. Or if the resource dispenser rejects the enlistment (meaning the resource is not transaction capable), then the enlistment is skipped.
The IHolder::FreeResource method is called when the client application component "frees" a resource previously allocated by AllocResource. For example, in the case of ODBC, FreeResource would be called during the execution of SQLDisconnect API function.
When calling this function you need to provide the RESID that identifies the resource. This implies that the method that calls FreeResource must have access to it. Normally you would let the client provide you with the RESID in the form of an opaque handle.
HRESULT hr;
if (m_pDispMan)
{
hr = m_pHolder -> FreeResource(hConnection);
if (FAILED(hr))
{
AtlTrace(_T("FreeResource failed! Error code %x\n"), hr);
}
}
In order to let MTS (in particular DispMan/Holder) communicate with the RD, you need to implement the IDisperserDriver interface. This is required if you want the RD to run under MTS. During the initialization process, you called IDispenserManager::RegisterDispenser and passed a pointer to the RD's IDispenserDriver interface (using the first parameter). The Holder object, which DispMan assigned to the RD instance, uses this interface pointer to communicate with the RD.
This interface provides the means by which MTS can create and maintain inventory of the resources provided by the RD. The Holder object calls this interface's methods for creating, rating, transaction enlisting, resetting, and destroying resources. The RD implements this interface as any other COM interface (such as the custom interface provided to clients for the RM Proxy functionality; see the section, "Writing a COM Interface or API for Your RM Proxy").
When it is time to create a new resource, the Holder asks the resource dispenser to create a resource by calling the IDisperserDriver::CreateResource method. In other words, when the RD's RM Proxy custom interface calls IHolder::AllocResource and not resources are available, the Holder object calls this method.
Follow these steps to implement this method:
IResourceManager * pRm = GetResourceManagerPointer();
if (!pRm)
{
return E_UNEXPECTED;
}
CComBSTR sRDName = L"SomeResourceDispenser";
hr = pRm -> Connect(sRDName.m_str, (long *)&lHandle);
if (FAILED(hr))
{
pRm-Release();
return hr;
}
*pResId = (RESID)lHandle;
//
// Set a 120-second time out.
//
*pSecsFreeBeforeDestroy = 120;
Release the RM interface pointer.
if (pRm)
{
pRm -> Release();
pRm = NULL;
}
As part of the process of allocating a resource, the Holder object calls the IDisperserDriver::RateResource method. The Holder object generates a list of candidates among the already created and sometimes enlisted resources. For each of these candidates, the Holder object calls the RateResource method to obtain a value rate (on a scale of 0 to 100) that will determine the "fitness" of the candidate with respect to the RESTYPID and the transaction itself.
The resource dispenser can terminate the rating loop early by assigning the candidate a resource rating of 100 (a perfect fit). A rating of 100 would normally be reserved for candidate resources that match the RESTYID and are already properly enlisted, unless the resource dispenser concludes that enlistment is an inexpensive operation. If all candidate resources (if any) are rated 0 (unuseable) then a new resource will be created by calling IDispenserDriver::CreateResource (see the section "Implement IDispenserDriver::CreateResource").
The steps to implement this method are:
...if (fRequiresTransactionEnlistment == FALSE)
...{
......*pRating = 100;.........
...}
...else
...{
......// not enlisted
......*pRating = 50;
...}
As part of the process of allocating a resource the Holder object may call the IDispenserDriver::EnlistResource method. There are two cases in which this method gets called:
To implement this method, you need to use two of the OLE Transactions objects: the Transaction and Export objects. The Transaction object represents the MS DTC transaction and the Export object represents the connection between an RM proxy (or resource dispenser) and Resource Manager. The Export object contains the name and the location of the Resource Manager's Transaction Manager and is used to propagate transactions between processes or systems.
Follow these steps to implement this method:
...ITransaction * pTransaction = (ITransaction*)TransId;
...pExport = m_mapExport[ResId];
...if (pExport == NULL)
...{
// Create an Export object
hr = GetExportObject(ResId, pTransaction, &pExport);
......if (FAILED(hr))
......{
.........pRm->Release();
.........return hr;
......}
// Create a map entry between pExport and ResId.
......m_mapExport[ResId] = pExport;
...}
...ULONG... cbTransactionCookie = 0;
...hr = pExport->Export (pTransaction, &cbTransactionCookie);
...rgbTransactionCookie = (BYTE *) CoTaskMemAlloc (cbTransactionCookie);
...if (0 == rgbTransactionCookie)
...{
......pRm->Release();
......return E_FAIL;
...}
...hr = pExport->GetTransactionCookie (...pTransaction,
..................bTransactionCookie,
..................rgbTransactionCookie,
..................&cbUsed...);
...hr = pRm->ExportTx (ResId, cbUsed, rgbTransactionCookie);
...CoTaskMemFree (rgbTransactionCookie);
...pRm->Release();
If a resource must be enlisted in a transaction and no Export object pointer (ITransactionExport*) is available for that resource, you must create an Export object.
Follow these steps to create an Export object:
...hr = pRm->GetWhereabouts (ResId, &rgbWhereabouts, &cbWhereabouts);
...hr = pTransaction->QueryInterface (IID_IGetDispenser, (LPVOID *) &pIDispenser);
...hr = pIDispenser->GetDispenser (IID_ITransactionExportFactory, (LPVOID *)&pTxExpFac );
...hr = pTxExpFac->Create (cbWhereabouts, rgbWhereabouts, ppExport);
...CoTaskMemFree (rgbWhereabouts);
At this point the RD is ready to continue with the enlistment process as depicted in the previous section.
The Holder object calls this method when it is time to put the resource back into general or enlisted inventory. This method prepares the resource before it goes into inventory.
Follow these steps to implement the method:
...hr = pRm -> ResetConnection((long)ResId);
The Holder object calls this method when it is time to destroy the resource.
The steps to implement this method are:
...hr = pRm -> Disconnect((long )ResId);
ITransactionExport *pExport;
pExport = m_mapExport[ResId];
int nElements = m_mapExport.erase(ResId);
If you are using ATL, you can use the FinalRelease method to undo the "things" did during initialization in the FinalConstruct method and throughout the execution of the resource dispenser.
To release an instance of the resource dispenser, follow these steps:
...hr =m_pGIT->RevokeInterfaceFromGlobal(m_dwRmPtrCookie);
There is one EntryPoint function called DllMain and four DLL export functions: DllCanUnloadNow, DllGetClassObject, DllRegisterServer, and DllUnregisterServer. The resource dispenser should implement them all. Normally you want to use the implementation generated by the ATL Wizard in Visual C++ 5.x.
The ATL Application Wizard generates the following code:
// ResDisp.cpp : Implementation of DLL EntryPoint and Exports.
#include "stdafx.h"
#include "resource.h"
#include "initguid.h"
#include "ResDisp.h"
#include "dlldatax.h"
#include "ResDisp_i.c"
#ifdef _MERGE_PROXYSTUB
extern "C" HINSTANCE hProxyDll;
#endif
CComModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
/////////////////////////////////////////////////////////////////////////////
// DLL EntryPoint
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
lpReserved;
#ifdef _MERGE_PROXYSTUB
if (!PrxDllMain(hInstance, dwReason, lpReserved))
return FALSE;
#endif
if (dwReason == DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap, hInstance);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE; // Okay
}
/////////////////////////////////////////////////////////////////////////////
// Used to determine whether the DLL can be unloaded by OLE
STDAPI DllCanUnloadNow(void)
{
#ifdef _MERGE_PROXYSTUB
if (PrxDllCanUnloadNow() != S_OK)
return S_FALSE;
#endif
return (_Module.GetLockCount()==0) ? S_OK : S_FALSE;
}
/////////////////////////////////////////////////////////////////////////////
// Returns a class factory to create an object of the requested type
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
#ifdef _MERGE_PROXYSTUB
if (PrxDllGetClassObject(rclsid, riid, ppv) == S_OK)
return S_OK;
#endif
return _Module.GetClassObject(rclsid, riid, ppv);
}
/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - adds entries to the system registry.
STDAPI DllRegisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
HRESULT hRes = PrxDllRegisterServer();
if (FAILED(hRes))
return hRes;
#endif
// registers object, typelib and all interfaces in typelib
return _Module.RegisterServer(TRUE);
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - removes entries from the system registry.
STDAPI DllUnregisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
PrxDllUnregisterServer();
#endif
_Module.UnregisterServer();
return S_OK;
}
Since the Holder object assigned to the resource dispenser object is the one that maintains the pool of resources, you must have only one instance of the resource dispenser. Otherwise, many instances of the RD would imply more than one Holder object and, consequently, more than one pool of resources. Therefore, the RD must be a singleton object. This means that all client threads will use the same RD object.
Care should be taken when implementing your RD class. The RD class has to be fully reentrant and thread-safe to be able to provide one single instance of the RD. Moreover, if you plan to provide the same Class factory instance for each calling thread, make sure that thread synchronization code is added to the implementation of the DllGetClassObject export function. Also your class factory itself needs to be carefully written so that it will return the same RD pointer for all its clients. For example, the reference count must be thread safe.
Fortunately, ATL takes care of these issues with: the CComObjectRootEx template, the DECLARE_CLASSFACTORY_SINGLETON macro, and the CComModule class provided when you run the ATL COM Application Wizard and the ATL Object Wizard.
Supporting both threading models (STA and MTA) implies that by using standard marshalling to accomplish inter-thread marshalling would result in a performance hit that could be easily avoided. When marshalling the RD's interface pointer between different threads in the same process, the client thread should have access to the same address space where the pointer resides. Therefore, the client can call the interface directly. This is a gain in performance as opposed to calling the interface through a proxy, which would be the case with standard marshalling. However, when marshalling across processes, you should use standard marshalling.
To easily accomplish this switch in marshalling, your RD should call the CoCreateFreeThreadedMarsharler function. This function will aggregate a free-threaded marshaller object to your RD's object. This object will perform either marshalling depending on the context in which the call is made.
There is one problem when aggregating the free-threaded marshaller. Normally your RD object holds, in its member variables, interface pointers to other objects that reside in other processes or are not free-threaded. The problem becomes apparent when the RD makes any reference to these objects from a thread different to the one where these objects' pointers were stored. Thus, any such call will result in the error RPC_E_WRONG_THREAD or some incorrect result. Clearly, this will be a common scenario for resource dispensers since they hold pointers to at least their Resource Managers and probably other objects.
To solve this problem, you should use a Global Interface Table object. It allows any apartment (STA or MTA) in a process to get access to an interface implemented on an object in any other apartment in the process. Thus, by using both the free-threaded marshaller and the GIT, your RD will have a better performance than using standard marshalling as the inter-thread marshalling mechanism.
The following code is an example of the registry entries needed for a resource dispenser. They are no different from those of any other COM component. If you were planning to have data stored in the registry, this would be the place to make the initial entries with their default values.
HKCR
{
ResDisp.ResDisp.1 = s 'ResDisp Class'
{
CLSID = s '{8A7339E4-5397-11D0-B151-00AA00BA3258}'
}
ResDisp. ResDisp = s ' ResDisp Class'
{
CurVer = s ' ResDisp. ResDisp.1'
}
NoRemove CLSID
{
ForceRemove {8A7339E4-5397-11D0-B151-00AA00BA3258} = s ' ResDisp Class'
{
ProgID = s ' ResDisp. ResDisp.1'
VersionIndependentProgID = s ' ResDisp. ResDisp '
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Both'
}
}
}
}