User-mode support for WMI relies on the facilities of COM. To summarize a very complicated situation, the Windows Management Service acts as the clearinghouse for information flowing between consumers and providers by implementing several COM interfaces. Providers register their existence with Windows Management via certain interfaces. Consumers indirectly communicate with providers via interfaces. All of these interfaces are documented in the Platform SDK, so I'm going to illustrate only the important method routines a consumer uses. I'll start, though, by explaining the basic mechanics of using COM for those readers who have little or no experience with COM.
As I said, this section is for readers who don't know the basics of using COM interfaces. I spent years deliberately avoiding COM because its unique terminology made me think it was too intricate to understand. I won't say that COM aficionados want it that way, but I will say that I was once roundly criticized for presenting the following simplified overview to a conference audience.
You'll encounter three crucial terms when you hear about COM. In COM, an object is a software entity that implements the methods belonging to an interface. (People in my generation will be imagining Bill Cosby saying, "R-i-g-h-t! What's an interface?" just about now.) The key element you deal with when acting as a COM client is a pointer to an interface, which you can dereference to invoke the method routines. You get an interface pointer either because someone gives it to you or because you call an API that returns it to you. From the perspective of a client program, some mysterious "them" takes care of creating and destroying objects.
Now let's go through these three concepts more slowly, starting with the last one. An interface is nothing more than a C++ class that has a bunch of virtual member functions but no data members and no nonvirtual member functions. You can implement or use a COM interface in many languages, not just C++. But because C++ gives us a common ground for understanding the concept, I'll forge ahead as if C++ were the only language you'd ever use. Here's the declaration of a simple interface as it might look before being translated into the language of COM:
class IUnknown { public: virtual long _ _stdcall QueryInterface(const GUID& riid, void** ppvObject); virtual unsigned long _ _stdcall AddRef(); virtual unsigned long _ _stdcall Release(); }; |
Instances of IUnknown (objects in COM) implement three public, virtual functions (methods) named QueryInterface, AddRef, and Release. AddRef and Release are part of the mechanism by which COM makes sure that objects persist long enough for clients to make use of them. QueryInterface is how client programs obtain pointers to additional interfaces that an object supports.
In the Interface Definition Language (IDL) of COM, this interface description would look like this:
interface IUnknown { HRESULT QueryInterface(REFIID riid, void** ppvObject); ULONG AddRef(); ULONG Release(); }; |
Apart from the syntactic differences, I think it's obvious how the IDL description of this interface relates to a C++ class declaration. An IDL compiler can be used that translates an interface declaration like this into syntax understandable by C and C++ compilers. Some programming languages understand the IDL syntax without a translation, even.
Just like C++ classes, interfaces can be derived from other interfaces. In COM, one doesn't declare interfaces with more than one base class. In addition, every COM interface derives ultimately from IUnknown—meaning that every COM object supports the QueryInterface, AddRef, and Release methods. Here's an example from the WMI world that we'll be using later on:
interface IWbemLocator : IUnknown { HRESULT ConnectServer(BSTR strNetworkResource, BSTR strUser, BSTR strPassword, BSTR strLocale, long lSecurityFlags, BSTR strAuthority, IWbemContext* pCtx, IWbemServices** ppNameSpace); }; |
So, an object that implements IWbemLocator has four method routines: QueryInterface, AddRef, Release, and ConnectServer.
Getting an interface pointer that you can use to talk to an object is possible in many ways. Calling CoCreateInstance is a common way:
IWbemLocator* locator; HRESULT hr = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (PVOID*) &locator); |
CoCreateInstance consults the registry to locate a server that can instantiate a CLSID_WbemLocator class of object. CLSID_WbemLocator is a 128-bit GUID of the same kind I mentioned in Chapter 2 ("Basic Structure of a WDM Driver") in connection with registered device interfaces. It's called a class identifier because it identifies a kind, or class, of COM object. The HKEY_CLASSES_ROOT branch of the registry contains a key named CLSID, the subkeys of which are the ASCII representations of all the class identifiers that COM knows anything about. In the example we're considering, CLSID_WbemLocator would be conventionally represented as {4590f811-1d3a-11d0-891f-00aa004b2e24}, and the CLSID key includes a subkey named exactly that in the registry. A subkey named InProcServer32 designates a DLL (named WBEMPROX.DLL, a part of the WMI core) as the server that implements this class of object.
Having located the class key in the registry, CoCreateInstance loads the designated server into your address space and uses magic we don't need to discuss here to instantiate a WbemLocator object and develop a pointer to the IWbemLocator interface that the object supports. (IID_IWbemLocator is another GUID declared in WBEMCLI.H, which you'll #include in your consumer project files.)
Following a successful call to CoCreateInstance, you'll have an interface pointer that you can use like any pointer to a C++ class to call the method functions associated with the interface. Somewhere in the world (maybe not even on the same computer) a concrete object exists that implements those method functions. The object occupies storage and the executable program whose instructions comprise the implementation also occupies storage. At some point in time, presumably, you'll be done using your interface pointer and will be prepared to destroy the object and, maybe, unload the program. The question is, when? That's where AddRef and Release come in.
Each COM object has a reference count. Whenever someone obtains a pointer to an interface on the object, the program that implements the object increments the reference count. So, CoCreateInstance will always return a referenced interface pointer, and you can be sure that the pointer will remain valid for the time being. You can increase the reference count on an object explicitly by calling AddRef. When you're done using an interface pointer, you call the Release method. The implementation of Release decrements the reference count. If the count drops to 0, the implementation deletes the object. When a server doesn't own any more objects, it can be unloaded.
Your job as a COM client is simply to release your reference to an interface when you no longer need the underlying object. The following stylized coding sequence is pretty typical:
IWbemLocator* locator; HRESULT hr = CoCreateInstance(...); if (SUCCEEDED(hr)) { ... locator->Release(); } |
When you want to access WMI facilities in user mode, you need to first establish a connection to a particular namespace. Within the context of the namespace, you can then find instances of WMI classes. You can query and set the data blocks associated with class instances, invoke their method routines, and monitor the events that they generate.
When you connect to a WMI namespace, you obtain a pointer to an IWbemServices interface that Windows Management implements. The following code—based on the TEST program in the WMI42 sample—shows how to do this:
1 2 3 4 5 6 |
HRESULT hr = CoInitializeEx(NULL, 0); if (!SUCCEEDED(hr)) return; hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, 0); if (!SUCCEEDED(hr)) { CoUninitialize(); return; } IWbemLocator* locator; hr = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (PVOID*) &locator); if (SUCCEEDED(hr)) { IWbemServices* services; BSTR pnamespace = SysAllocString(L"root\\CIMV2"); hr = locator->ConnectServer(pnamespace, NULL, NULL, 0, 0 &services); SysFreeString(pnamespace); if (SUCCEEDED(hr)) { IClientSecurity* security; hr = services->QueryInterface(IID_IClientSecurity, (PVOID*) &security); if (SUCCEEDED(hr)) { security->SetBlanket(services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); security->Release(); } // use the services interface services->Release(); } locator->Release(); } CoUninitialize(); |
Using an IWbemServices interface, you can enumerate all the instances of a particular WMI class. WMI42's test program, for example, enumerates all the WMI42 class instances with the following code:
1 2 |
IEnumWbemClassObject* enumerator = NULL; BSTR bs = SysAllocString(L"WMI42"); HRESULT hr = services->CreateInstanceEnum(bs, WBEM_FLAG_SHALLOW | WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &enumerator); SysFreeString(bs); if (SUCCEEDED(hr)) { while (TRUE) { ULONG junk; IWbemClassObject* cop = NULL; hr = Enumerator->Next(INFINITE, 1, &cop, &junk); if (hr == WBEM_S_FALSE) break; if (!SUCCEEDED(hr)) break; // Use IWbemClassObject interface cop->Release(); } enumerator->Release(); } |
The IWbemClassObject interface is the key that unlocks the WMI functionality of your driver. With a pointer to this interface, you can easily get or set the values of items in a data block:
IWbemClassObject* cop; VARIANT answer; BSTR propname = SysAllocString(L"TheAnswer"); cop->Get(propname, 0, &answer, NULL, NULL); VariantClear(&answer); answer.vt = VT_I4; answer.lVal = 6 * 9; // should be done in base 13! cop->Put(propname, 0, &answer, 0); VariantClear(&answer); SysFreeString(propname); |
In these fragments, we use a system string to name the property (that is, the item within our schema) we want to get or put, and we use an OLE VARIANT structure (which can hold any type of data) as the data value. Calling the Get method on this interface results in our driver getting a QUERY_ALL_DATA or QUERY_SINGLE_INSTANCE. Calling the Put method results in a CHANGE_SINGLE_INSTANCE or CHANGE_SINGLE_ITEM. You can observe for yourself what happens by loading the WMI42 sample driver and invoking the test program a time or two. You shouldn't try to predict exactly which type of IRP will be used to support a Get or Put call because the WMI provider is free to package data requests to drivers in any convenient way.
To receive notifications that WMI events have occurred, an application has to register interest in specific events. To register interest, you must formulate a query in the so-called WMI Query Language (WQL). WQL is a great deal like the Structured Query Language (SQL) one uses in the world of relational databases. For example, to sign up to receive WMIEXTRA_EVENT notifications, you could submit the following query:
IWbemServices* services; BSTR query = SysAllocString(L"select * from WMIEXTRA_EVENT"); BSTR language = SysAllocString(L"WQL"); IEnumWbemClassObject* enumerator = NULL; HRESULT hr = services->ExecNotificationQuery(language, query, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &enumerator); SysFreeString(language); SysFreeString(query); if (SUCCEEDED(hr)) { ... enumerator->Release(); } |
The flag arguments to ExecNotificationQuery must be specified exactly as shown, by the way.
Once you have the enumeration interface, you can call its Next method to poll for events. For example:
IWbemClassObject* cop = NULL; DWORD junk; hr = enumerator->Next(1000, 1, &cop, &junk); |
In this call, we specify that we will wait up to 1000 milliseconds to obtain one event. If an event is already pending or fires within this timeout period, Next will return us a (referenced) IWbemClassObject pointer. Recall from the previous discussion of how a driver fires an event that the event is represented by an instance of a WMI class. We can therefore call the object's Get method to interrogate properties of the event.
In a real-world application, you should use ExecNotificationQueryAsync instead of ExecNotificationQuery. The asynchronous form of the query allows you to provide an IWbemObjectSink interface that WMI can call when events occur. Please refer to the Platform SDK for additional information.
Invoking a method routine requires just a few deceptively simple statements, as shown in the following excerpt from WMIEXTRA's test program:
IWbemServices* services; // developed as shown earlier IWbemClassObject* result = NULL; BSTR pmethod = SysAllocString(L"AnswerMethod"); BSTR objpath; // more about this later IWbemClassObject* inarg; // ditto HRESULT hr = services->ExecMethod(objpath, pmethod, 0, NULL, inarg, &result, NULL); ... result->Release(); SysFreeString(pmethod); <more cleanup> |
Calling ExecMethod invokes the method routine. You supply values for the input arguments in the inarg object. The result of the call appears as the result object.
Invoking a method in this way would be almost trivial if it weren't for two complicating factors. First, you have to come up with the full pathname (that is, the objpath argument to ExecMethod) to the object you want to address. And you must construct and initialize a WMI object to contain the input arguments (if any) for the method call. I found the first of these tasks to be a gigantic pain in the neck, as shown by the following snippet from WMIEXTRA's test program:
IWbemServices* services; // someone gives me this BSTR pclass = SysAllocString(L"wmiextra_method"); BSTR objpath = NULL; HRESULT hr; IEnumWbemClassObject* enumerator = NULL; hr = services->CreateInstanceEnum(pclass, <etc.>); if (SUCCEEDED(hr)) { IWbemClassObject* instance = NULL; ULONG junk; hr = enumerator->Next(INFINITE, 1, &instance, &junk); if (SUCCEEDED(hr)) { VARIANT instname; BSTR propname = SysAllocString(L"InstanceName"); hr = instance->Get(propname, 0, &instname, NULL, NULL); SysFreeString(propname); if (SUCCEEDED(hr)) { WCHAR fullpath[256]; WCHAR escapedname[256]; <code to double backslashes in instname> swprintf(fullpath, L"%ws.InstanceName=\"%s\"", pclass, escapedname); objpath = SysAllocString(fullpath); VariantClear(&instname); } instance->Release(); } enumerator->Release(); } |
Ugh. Especially the part (which I omitted here in the text) that goes through the instance name and changes each backslash to two backslashes. In my opinion, there should be a method on the IWbemClassObject interface that you can call to get the full pathname of an object. Such a method would prevent our needing to discover the algorithm that some other system component has used to construct the instance name. But, as I frequently find to be the case, no one asked me for my opinion.
The Platform SDK documentation describes how to build the input arguments (that is, the inarg argument to ExecMethod). Here's how I did it for WMIEXTRA:
IWbemClassObject* cop = NULL; // the class, not an instance hr = services->GetObject(pclass, 0, NULL, &cop, NULL); if (SUCCEEDED(hr)) { IWbemClassObject* iop = NULL; // another class hr = cop->GetMethod(pmethod, 0, &iop, NULL); if (SUCCEEDED(hr)) { IWbemClassObject* inarg = NULL; // an instance of iop hr = iop->SpawnInstance(0, &inarg); if (SUCCEEDED(hr)) { BSTR argname = SysAllocString(L"TheAnswer"); VARIANT argval; argval.vt = VT_I4; argval.lVal = 41; hr = inarg->Put(argname, 0, &argval, 0); SysFreeString(argname); <the actual call to ExecMethod> inarg->Release(); } iop->Release(); } cop->Release(); } |
This code uses the data dictionary to obtain a description of the input argument class (the iop variable). It then creates and initializes an instance of the input argument class (the inarg variable) for use as an argument to the method routine.
I didn't check, but I assume that MFC provides a streamlined way to do all of this.