|
A Simple COM Object Interface
We begin our code development by defining a simple COM interface named ISimpleCOMObject. Using Visual Studio .NET, we create an ATL project named SimpleCOMObject.sln (complete source codes for this project is included in the downloadable sample codes).
This SimpleCOMObject.sln project will not contain any useful implementation codes. Its purpose is simply to allow us to automate the creation and maintenance of the ISimpleCOMObject interface. This is accomplished via the use of the ATL Wizards.
The project file folder also serves as the central point for the storage and generation of various .NET related resources (e.g. Primary Interop Assembly, Strong Name Key Files, etc). More on these later.
Listed below is a fragment of code taken from SimpleCOMObject.idl showing the ISimpleCOMObject interface :
[
object,
uuid(9EB07DC7-6807-4104-95FE-AD7672A87BD7),
dual,
nonextensible,
helpstring("ISimpleCOMObject Interface"),
pointer_default(unique)
]
interface ISimpleCOMObject : IDispatch{
[propget, id(1), helpstring("property LongProperty")] HRESULT LongProperty([out, retval] LONG* pVal);
[propput, id(1), helpstring("property LongProperty")] HRESULT LongProperty([in] LONG newVal);
[id(2), helpstring("method Method01")] HRESULT Method01([in] BSTR strMessage);
};
ISimpleCOMObject contains one property (LongProperty) and one method (Method01()). We stipulate that ISimpleCOMObject is meant to be implemented by an object that takes a long value (LongProperty) and then displays its value when we call Method01(). The BSTR parameter "strMessage" is meant to be a short message which is to be displayed with the LongProperty value when Method01() is called.
We defined ISimpleCOMObject as a dual interface. Hence a client application can call its methods by using a vtable interface pointer or by using an IDispatch interface pointer. Although it is possible to define ISimpleCOMObject as deriving from IUnknown, I have chosen to derive it from IDispatch in order to ensure that parameters and return values for its methods be strictly automation-compatible (i.e. the types that can be stored in a VARIANT structure). This is done to keep things simple.
I will assume that the reader is already well-versed in the implementation of a COM interface using an unmanaged language like C++. It is the implementation in a .NET language like C# that I intend to explain in detail in this article. The use of automation-compatible types will help to keep this process as simple as possible.
Although SimpleCOMObject.sln contains no useful implementation for ISimpleCOMObject, we will nevertheless need to compile it. Once this project is compiled successfully, two important files will be produced :
1. A Type Library (SimpleCOMObject.tlb).
2. A DLL (SimpleCOMObject.dll).
These will be created in the Debug directory of the project's containing folder. If you have downloaded my sample codes, and have not modified anything so far, these files will be stored in the following path :
<main folder>\SimpleCOMObject\interfaces\SimpleCOMObject\Debug
where is wherever you have copied the source codes zip file to.
The SimpleCOMObject.tlb will be used to produce something known as a Primary Interop Assembly. More on this in the next section.
The SimpleCOMObject.dll itself will contain the SimpleCOMObject.tlb (embedded as a binary resource). It is important as it will be registered (by the Visual Studio .NET IDE) as the Type Library for the coclasses and interfaces defined in SimpleCOMObject.idl.
This Type Library registration is done so that COM will know where to look for type information when it needs to perform marshalling for the interfaces defined in SimpleCOMObject.idl. This process is also known as Type Library Marshaling.
-- modified at 19:55 Tuesday 18th October, 2005
|
|
|
|
|
The CreateAndRegisterPrimaryInteropAssembly.bat Batch File
After compiling the SimpleCOMObject.sln
|
|
|
|
|
The following is extracted from :
" HOWTO: Use structs in Automation-compatible interfaces "
<a href="http://www.mvps.org/vcfaq/com/4.htm" rel="nofollow">http://www.mvps.org/vcfaq/com/4.htm</a>[<a href="http://www.mvps.org/vcfaq/com/4.htm" target="_blank" rel="nofollow" title="New Window">^</a>]
Neither IDL nor Automation define byte alignment for a struct. VB assumes 4-byte alignment, while #import in VC assumes 8-byte alignment. For most Automation structs this isn't an issue. However, if you use 8-byte types like double, this may make a difference:
[uuid(...), helpstring(...)]
struct BadAlign
{
long nLongValue;
double dblDoubleValue;
};
For VB, the double field starts at the fourth byte of the struct, while for VC's #import it starts at the eight byte. This poses a significant problem. It can be solved by adding an inject_statement clause to #import:
#import "My.tlb" inject_statement("#pragma pack(4)")
|
|
|
|
|
When setting the property of a COM object via IDispatch::Invoke() using DISPATCH_PROPERTYPUT, it is important to set the following :
* DISPPARAMS.rgdispidNamedArgs must point to a DISPID whose value is
DISPID_PROPERTYPUT (-3).
* DISPPARAMS.cNamedArgs must be equal to 1.
If this is not done, IDispatch::Invoke() will return DISP_E_PARAMNOTFOUND. The following is a sample IDispatch::Invoke() call used to set a long property to value 202:
void InvokeProperty(IDispatch* dispatch)
{
DISPPARAMS dp;
long nError = 0;
long nWarning = 0;
DISPID dPutID;
memset(&dp, 0, sizeof(DISPPARAMS));
dp.cArgs = 1; // Number of arguments to Invoke() remains 1.
// This is important.
dPutID = DISPID_PROPERTYPUT;
dp.rgdispidNamedArgs = &dPutID;
dp.cNamedArgs = 1;
VARIANTARG* pArg = new VARIANTARG[dp.cArgs];
dp.rgvarg = pArg;
memset(pArg, 0, (sizeof(VARIANT))*(dp.cArgs));
V_VT(&(dp.rgvarg[0])) = VT_I4;
V_I4(&(dp.rgvarg[0])) = 202;
VARIANTARG vr;
VariantInit(&vr);
//Finally making the call
UINT nErrArg;
HRESULT hr = S_OK;
hr = dispatch -> Invoke(0x03, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dp, &vr, 0, &nErrArg);
return;
}
By the way, I also checked up MFC's implementation codes for
COleDispatchDriver. It does the same thing, i.e. set
DISPPARAMS.rgdispidNamedArgs to DISPID_PROPERTYPUT and DISPPARAMS.cNamedArgs
to 1 before calling Invoke().
|
|
|
|
|
Sample code can be found in Develope\COM\DispatchAPI\kenB.
|
|
|
|
|
The following information is gotten from mariabl's blog (http://blogs.msdn.com/mariabl/archive/2005/07/21/441664.aspx[^]) :
At various times I've been annoyed with the way MIDL will mangle enum names. For example, if you have this in an IDL file:
typedef enum
{
value1,
value2
} MyEnum;
...if you run MIDL on it, in your .h file you'll get something like:
typedef
enum __MIDL___MIDL_itf_test_0000_0001
{
value1 = 0,
value2 = value1 + 1
} MyEnum;
The part that bugs me is the part in red. (I've run across various inconveniences with it when using .NET interop and even in C++.)
All you have to do is specify that tag (name) yourself!
So if I instead put in my IDL file:
typedef enum MyEnum
{
value1,
value2
} MyEnum;
I get in my .h file:
typedef
enum MyEnum
{
value1 = 0,
value2 = value1 + 1
}
MyEnum;
It seems that in some cases (going through the process of TLBs, .NET interop, etc.) the tag is used and sometimes the typedef name is used, and sometimes you get messed up if you have different tags because you've imported the same IDL file in two different places. So if you just use the same name for both you won't have weird problems later.
I guess everyone except me already knew this, but I just thought I'd pass it on in case someone didn't!
|
|
|
|
|
Recently, a CP member named LongIsland wrote the following post in the ATL forum :
--- START ---
Hello. I am new to ATL and had a serious problem, see I want to use interface A and interface B, both from MS. The bad thing is they all have method named "GetWindowHandle", what should I do ???!!!
I tried to modify one of them's header to just make it compile, but it just sucks, I think there must be a decent solution.
Thanks a lot ahead.
--- END ---
It was replied by Jörgen Sigvardsson. See next message.
|
|
|
|
|
Jörgen Sigvardsson's response :
--- START ---
Assuming that you want to return different things for each GetWindowHandle, you can do this:
template <typename T>
class InterfaceAShim : public InterfaceA
{ STDMETHOD(GetWindowHandle)(HWND* phwnd) { return static_cast<T*>(this)->InterfaceA_GetWindowHandle(phwnd); }};
class YourClass : public InterfaceAShim<YourClass>, public InterfaceB
{
...
HRESULT InterfaceA_GetWindowHandle(HWND* phwnd)
{ // do your stuff here for InterfaceA::GetWindowHandle() }
STDMETHOD(GetWindowHandle)(HWND* phwnd)
{ // do your stuff here for InterfaceB::GetWindowHandle() }
};
If you are going to return the same handle in both methods, you don't have to do anything.
--- END ---
|
|
|
|
|
LongIsland further responded with :
--- START ---
Hi, Jörgen:
Thank you so much, just 3 small questions:
1. Then the best bet in my code is Not use a method name same as common interface method, like GetWindowHandle, IsDirty, and so on. Right? Otherwise, it is tedious to write this kind of wrapper. (Is it the only solution? I think so)
2. why do you use static_cast, since static_cast do not do run time check, there is no difference with reinterpret_cast here.
3. Here:
template
class InterfaceAShim : public InterfaceA {
STDMETHOD(GetWindowHandle)(HWND* phwnd) { return static_cast(this)->InterfaceA_GetWindowHandle(phwnd); }
};
I think inside InterfaceAShim, I need only declare one duplicate method name OR I have to declare all methods from InterfaceA, just want to confirm.
Thanks again, you are ATL guru.
--- END ---
|
|
|
|
|
Jörgen Sigvardsson further responded :
--- START ---
LongIsland wrote:
1. Then the best bet in my code is Not use a method name same as common interface method, like GetWindowHandle, IsDirty, and so on. Right? Otherwise, it is tedious to write this kind of wrapper. (Is it the only solution? I think so)
AFAIK, there is no automated process for this. But I wouldn't say it's the best approach either. I mean, if your method names become odd looking and non descriptive, it's not good.
LongIsland wrote:
why do you use static_cast, since static_cast do not do run time check, there is no difference with reinterpret_cast here.
Because I'm doing an "upcast". T is a subclass of the template. Draw a picture! Thus I don't have to do any casts, because the pointer types are related.
LongIsland wrote:
I think inside InterfaceAShim, I need only declare one duplicate method name OR I have to declare all methods from InterfaceA, just want to confirm.
You only need to declare the duplicate names. Unbound names are deferred to the next subclass, which is your COM class.
--- END ---
|
|
|
|
|
When you work on an ATL ActiveX Control, if you were to add a new property to the property map, e.g. :
BEGIN_PROP_MAP(CFPCaptureControl)
...
...
...
PROP_ENTRY_EX("Plugin", 8, CLSID_NULL, IID_IFingerprintCaptureDevice)
END_PROP_MAP()
You may have to delete away the control from the window of a client app's development environment (e.g. VB) and then re-inserting the control.
This is because previously saved stream data for the project (this is usually in the form of a blob) for the control did not have any information on the new property.
After re-insertion and subsequent saving, the full property set of the control (including the new property) will be saved into the stream for the project.
|
|
|
|
|
If you use CComVariant, be sure to refer to the MS SDK oleauto.h header file.
This is because the CComVariant class uses the VarCmp() function which takes 3 parameters. This 3 param version VarCmp() has been superceded by a 4 parameter version.
In the MS SDK oleauto.h header file, the following is defined :
// dwFlags passed to CompareString if a string compare
STDAPI VarCmp(LPVARIANT pvarLeft, LPVARIANT pvarRight, LCID lcid, ULONG dwFlags);
#ifdef __cplusplus
extern "C++" {
// Add wrapper for old ATL headers to call
__inline
HRESULT
STDAPICALLTYPE
VarCmp(LPVARIANT pvarLeft, LPVARIANT pvarRight, LCID lcid) {
return VarCmp(pvarLeft, pvarRight, lcid, 0);
}
} // extern "C++"
#endif
|
|
|
|
|
If you intend to transfer in-memory data (structs, arrays, etc) via a method call (using VARIANT), e.g. :
STDMETHOD(get_Buffer)(VARIANT* pVal);
STDMETHOD(put_Buffer)(VARIANT pVal);
then always do so with a SAFEARRAY instead of directly using memory pointers (via VT_BYREF).
Using VT_BYREF
---------------
If you use VT_BYREF, you would transfer memory data in get_Buffer() this way :
V_VT(pVal) = VT_BYREF;
VT_BYREF(pVal) = &some_struct;
In put_Buffer(), you would do it this way :
if (V_VT(&pVal) == VT_BYREF)
{
memcpy (&some_struct, V_BYREF(&pVal), sizeof(some_struct));
}
The above techniques works most of the time provided :
1. The object which exposes get_Buffer() and put_Buffer() is housed in an in-proc server (i.e. a COM DLL), AND ...
2. The get_Buffer() and put_Buffer() calls are made from the same Apartment. Once cross-apartment calls are made (via proxies), you will get an error code 0x80020008 (DISP_E_BADVARTYPE).
Using SAFEARRAY
----------------
If you use SAFEARRAYs to transfer memory objects, you would do it this way in get_Buffer() :
SAFEARRAY* pSAFEARRAY = NULL;
VariantInit(pVal);
VariantClear(pVal);
CreateSafeArrayFromBytes
(
(LPBYTE)&some_struct,
(ULONG)sizeof(some_struct),
(SAFEARRAY**)&pSAFEARRAY
);
V_VT(pVal) = VT_UI1 | VT_ARRAY;
V_ARRAY(pVal) = pSAFEARRAY;
The way to code put_Buffer() would be :
if (V_VT(pVal) == (VT_UI1 | VT_ARRAY))
{
LPBYTE lpbyByteArray = NULL;
ULONG ulSize = 0;
GetByteArrayFromSafeArray
(
V_ARRAY(&pVal),
&lpbyByteArray,
&ulSize
);
if (lpbyByteArray)
{
memcpy (&some_struct, lpbyByteArray, sizeof(some_struct));
free (lpbyByteArray);
lpbyByteArray = NULL;
}
SafeArrayDestroy(V_ARRAY(&pVal));
}
The above techniques is guarenteed to work all the time whether the calls are made in the same apartment or across apartments using proxies/stubs.
However, there will be performance and overhead penalties.
|
|
|
|
|
Member data of a class which is of reference type must be instantiated during the construction phase of the class, e.g. :
class CMyData
{
int m_i;
};
class CMyDataFactory
{
public :
static CMyData& Create()
{
static CMyData myData;
return myData;
}
};
class CMyClass
{
public :
CMyClass() :
m_MyData(CMyDataFactory::Create())
{
}
~CMyClass()
{
}
CMyData& m_MyData;
};
1. CMyClass contains a member data "m_MyData" which is a reference to a CMyData class.
2. CMyClass::CMyData must be instantiated in the constructor of CMyClass :
CMyClass() :
m_MyData(CMyDataFactory::Create())
{
}
3. Note that m_MyData can be instantiated by a static function call :
CMyDataFactory::Create()
|
|
|
|
|
The registry location of Windows Services and Device Drivers is :
HKLM\System\CurrentControlSet\Services\<driver or service name>
|
|
|
|
|
The time now in Singapore is 12:23 PM Jan 1st 2005. Wonder if it's new year now @ CodeProject ?
|
|
|
|
|
Lim Bio Liong wrote: Wonder if it's new year now @ CodeProject ?
Just Ping you to say Happy New Year 2006 Sir!
"Opinions are neither right nor wrong. I cannot change your opinion. I can, however, change what influences your opinion." - David Crow
cheers,
Alok Gupta
VC Forum Q&A :- I/ IV
|
|
|
|
|
Original Question from "bluetrutle" :
Hi,
In encounter the following problem when using COM objects in a multi -
threaded environment.
I have 2 objects ("A" and "B") that behave differently.
the scenario:
thread 1 - CoInitialize(NULL)
thread 1 is remained alive all the time.
thread 2 - create object
thread 3 - use object ( thru IDispatch )
thread 4 - release object
each thread N (where N > 1), exits after executing the task.
the object is not used concurrently by threads.
there is always only one thread trying to use it.
for object "A" this will work, but for object "B", it wont work.
when I will try to use it, I will get error : RPC_E_SERVER_DIED
in order it to work for object "B", I need to:
thread 2 - create object in thread 1 and marshal it
thread 3,4.. unmarshall before use etc.
so using marshalling cause "B" to work.
But, I dont understand why objects "A" and "B", behave differently.
I wrote object "A" by myself using ATL ( CComSingleThreadModel ) and it
is marked in registry as "ThreadingModel=Apartment"
I dont have the code \ documentation for object "B", but it registers
itself also as "ThreadingModel=Apartment",
and I know it was written in VB.
I thought I need to marshal anyway, but when I use object "A" without
marshalling it works.
I try to understand \ explain this behavior.
Any help is appreciated.
Thanks,
Dave.
|
|
|
|
|
An answer from Alexander Nickolov
This is what's called random behavior. Did you
expect it to always work when you break the rules?
|
|
|
|
|
Reply from "blueturtle"
Hi,
I expected, that without marshalling, it will _never_ work.
So how come it works (on some objects) ?
I try to understand it.
What is the piece of code in the COM server that cause the "random"
behavior ?
Or maybe I'm doing something wrong somewhere.
( Forgot to mention - each thread N has CoInitialize() )
Thanks,
Dave.
|
|
|
|
|
Answer from Bio :
Hello blueturtle,
Here are my thoughts concerning why "A" worked while "B" did not :
1. The fact that "A" and "B" are both marked in the registry as
"ThreadingModel=Apartment" indicates that they are both STA (Single-Threaded
Apartment) objects.
2. Now, for both "A" and "B", the whole idea behind making them STA objects
is to ensure their thread safety without having to use additional
thread-synchronization code. However, as you have explained, you have already
personally ensured that no two threads access them at any time.
3. Therefore, as far as "A" is concerned, passing a direct pointer to it to
other threads and then calling its methods from those threads, while
technically unsound, did not pose any danger. You -have- broken the rules.
But the rules were not needed anyway.
4. If you had marshalled "A" across to thread 3 and thread 4, thread 3 and
thread 4 will not receive a direct pointer to "A". They will instead each
receive a proxy to "A". Method calls to "A" will have to go through the
marshalling process and a private message will be sent to the message loop of
thread 2 (the thread which created "A") which will eventually result in the
execution of the method.
5. Point 4 entails some performance penalties as you can imagine. In fact,
since you have ensured that no two threads will ever touch "A" at anytime,
"A" is technically a Free-Threaded Object.
6. Now, on to "B". You mentioned that "B" is written in VB. This can be
significant. Because STA objects have the advantage of thread-affinity, VB
uses thread-local storage (TLS) to store various state information on
VB-generated COM objects. The thread whose local storage is used is the
thread of the STA in which the COM object lives in and is also the one which
created it.
7. Using these VB-generated COM objects from an external thread without
marshalling is thus fatal. They will not be able to access information stored
inside their TLS.
Hope the above helps.
Best Regards,
Bio.
PS : just curious : Did you transport the interface pointer from thread to
thread via thread parameters ?
|
|
|
|
|
Reply from "blueturtle" :
Hi,
Thanks for your help.
The information about VB's implementation using the TLS was important
for me.
I had to verify in which cases I can avoid the marshalling (none left
in my case...).
Regarding your question:
The information is not actually passed between the threads.
In the case where I had to use the marshalling, it is a kind of an
application server
which accept requests from web browsers, and each request is executed
in a new thread ( to minimize amount of running threads )
So when a session starts, the web browser can ask to load object X,
in return I send back an identifier that represents the marshalled
object.
In the next request, a new thread will start to execute it, unmarshal,
IDispatch::Invoke, marshal, etc.
What I like about it, is that it should work with any COM object (
there is no set of predefined of objects ),
so it is very interesting to make it work with all the scenarios,
parameter types, etc.
What I dont like about it, is that it should work with _every_ COM
object...
So I have to solve many internal problems of customers,
E.g: TLS (yes again, but nothing to do with marshalling this time )
I encountered a COM object that was using TlsAlloc (inside ACE
library),
and that collided (a nice random crash) with some other code which used
TlsAlloc in that thread,
because the ACE library was compiled with the wrong value for maximum
indices for that OS.
( from MSDN:
Windows 2000/XP: There is a limit of 1088 TLS indexes per process.
Windows 98/Me: There is a limit of 80 TLS indexes per process. )
Thanks again for your help,
Dave.
|
|
|
|
|
It's now 2:15 pm 31st Dec 2004 in Singapore.
Happy New Year to all CodeProject Members !
- Bio.
|
|
|
|
|
A coclass is COM's way of defining a class (class in the object-oriented sense) which is meant to be language independent.
An IDL file is what COM provides that allows developers to define such language independent object-oriented classes.
An IDL file is compiled by the MIDL compiler into a Type Library (.TLB file) which is a binary form of an IDL file meant to be processed by various language compilers (e.g. VB, VC++, Delphi, etc).
The end result of such .TLB processing is that the specific language compiler produces the language-specific constructs (VB classes for VB, C++ classes, various structs, macros and typedefs for VC++) that represent the coclass defined in the .TLB (and ultimately that which was defined in the originating .IDL).
When you look at an example coclass definition in an IDL :
coclass MyObject
{
[default] interface IMyObject;
[default, source] dispinterface _IMyObjectEvents;
};
it is declaring a COM class named MyObject which must implement an interface named IMyObject and which supports (not implement) the event interface _IMyObjectEvents.
This is conceptually equivalent to defining a C++ class like this :
class CSomeObject : public ISomeInterface
{
...
...
...
};
where ISomeInterface is a C++ virtual class.
It is up to the individual language compiler to produce whatever code (in the specific compiler's language) is necessary for its user to implement and ultimately produce a binary which can be deemed by COM to be of coclass MyObject.
Now, in languages like C++, we can use CoCreateInstance() in which we can specify the CLSID of the coclass and select the interface from that coclass that we want to use to interact with that coclass. Calling CoCreateInstance() like this :
STDAPI CoCreateInstance(
CLSID_MyObject,
NULL,
CLSCTX_INPROC_SERVER,
IID_IMyObject,
(void**)&m_pIMyObject
);
is conceptually equivalent to :
ISomeInterface* pISomeInterface = NULL;
pISomeInterface = new CSomeObject();
In the first case, we are saying to the COM sub-system that we want to obtain a pointer to an object that implements the IMyObject interface and we want coclass CLSID_MyObject's particular implementation of this interface.
In the second case, we are saying that we want to create an instance of a C++ class that implements the interface ISomeInterface and we are using CSomeObject as that C++ class.
Do you see the equivalence ? A coclass, then, is an object-oriented class in COM's own language.
|
|
|
|
|
Note that if a thread (in a COM Exe Server) is initialized with ::CoInitializeEx(NULL, COINIT_MULTITHREADED) and this thread is used to register a class object :
DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
LPDWORD lpdwCookieReceiver = (LPDWORD)lpvParameter;
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
RegisterClassObject_CTestServer02(lpdwCookieReceiver);
::CoUninitialize();
return 0;
}
void RegisterClassObject_CTestServer02(LPDWORD lpdwCookieReceiver)
{
HRESULT hr = S_OK;
CTestServer02_Factory* pCTestServer02_Factory = new CTestServer02_Factory();
if (pCTestServer02_Factory)
{
IUnknown* pIUnknown = NULL;
pCTestServer02_Factory -> QueryInterface(IID_IUnknown, (void**)&pIUnknown);
hr = ::CoRegisterClassObject
(
CLSID_TestServer02,
pIUnknown,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
lpdwCookieReceiver
);
pIUnknown -> Release();
pIUnknown = NULL;
pCTestServer02_Factory -> Release();
pCTestServer02_Factory = NULL;
}
}
This thread need not stay alive for the registered class object. In fact, as you can see, the thread simply exits after performing the registration.
The pointer to the class factory remains alive inside the COM engine somewhere.
However, this will not work if the thread is initialized with ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED).
|
|
|
|
|