Using COM to explore the namespace

Windows 2007. 2. 8. 11:23 posted by deneb

출처 : http://netez.com/2xExplorer/shellFAQ/bas_xplore2.html

There are no two ways about it, using low-level COM to enumerate folders is quirky. Note that I didn't say "difficult", it just takes some time getting used to it. With COM you have to manage objects yourself: create the right one, ask for a specific interface for the functionality you are after, then use it and finally take care to release it. In contrast, in regular API-based programming the operating system is doing the management for you; you just call functions directly, oblivious about objects and things behind the scenes.

To enumerate a folder via COM, you need an IShellFolder pointer to the folder object you're after. We've seen a sketch of how to obtain this interface by parsing a path name in the previous section. The next step is to ask the object to enumerate itself using its EnumObjects method, which creates a new object which exposes an IEnumIDList interface. This enumerator object has the contents we are after, as a collection of local PIDLs, one for each item in the folder.

The best way to illustrate all this is with an example. Let's try to create the COM-equivalent of the EnumerateFolderFS() sample presented earlier, which read the contents of a filesystem folder given a full path to it. The following EnumerateFolder() sample produces the same results in a completely different approach.

#include <shlobj.h>

void EnumerateFolder(LPCTSTR path)
{
   HRESULT hr; // COM result, you'd better examine it in your code!
   hr = CoInitialize(NULL); // initialize COM
   // NOTE: usually COM would be initialized just once in your main()

   LPMALLOC pMalloc = NULL; // memory manager, for freeing up PIDLs
   hr = SHGetMalloc(&pMalloc);

   LPSHELLFOLDER psfDesktop = NULL; // namespace root for parsing the path
   hr = SHGetDesktopFolder(&psfDesktop);

   // IShellFolder::ParseDisplayName requires the path name in Unicode.
   OLECHAR olePath[MAX_PATH]; // wide-char version of path name
   MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, path, -1, olePath, MAX_PATH);

   // parse path for absolute PIDL, and connect to target folder
   LPITEMIDLIST pidl = NULL; // general purpose
   hr = psfDesktop->ParseDisplayName(NULL, NULL, olePath, NULL, &pidl, NULL);
   LPSHELLFOLDER psfFolder = NULL;
   hr = psfDesktop->BindToObject(pidl, NULL, IID_IShellFolder, 
                                 (void**)&psfFolder);
   psfDesktop->Release(); // no longer required
   pMalloc->Free(pidl);

   LPENUMIDLIST penumIDL = NULL; // IEnumIDList interface for reading contents
   hr = psfFolder->EnumObjects(NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, 
                               &penumIDL);
   while(1) {
      // retrieve a copy of next local item ID list
      hr = penumIDL->Next(1, &pidl, NULL);
      if(hr == NOERROR) {
         WIN32_FIND_DATA ffd; // let's cheat a bit :)
         hr = SHGetDataFromIDList(psfFolder, pidl, SHGDFIL_FINDDATA, &ffd, 
                                  sizeof(WIN32_FIND_DATA));

         cout << "Name = " << ffd.cFileName << endl;
         cout << "Type = " << ( (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                                ? "dir\n" : "file\n" );
         cout << "Size = " << ffd.nFileSizeLow << endl;
         
         pMalloc->Free(pidl);
      }
      // the expected "error" is S_FALSE, when the list is finished
      else break;
   }

   // release all remaining interface pointers
   penumIDL->Release();
   psfFolder->Release();
   pMalloc->Release();

   CoUninitialize(); // shut down COM
}

Oh dear, that sure was hard work. 56 lines of code to do exactly the same thing EnumerateFolderFS() managed in just 23 lines — less than half. But hey, that's C++ baby, either take it or leave it and join the VB club <g>. Since this is the first real COM example I presented, I'll try to be gentle and explain it thoroughly. Many typical issues in COM programming appear in this code:

  • COM initialization. Before you use any of the COM services and objects you need to call CoInitialize. Conversely, after you're done with COM you must call CoUninitialize. Typically an application that relies heavily on COM will initialize the subsystem just once when it starts, not inside each function that utilizes COM. Still it's no harm calling CoInitialize more than once, as long as it is balanced by a CoUninitialize.
    ADVANCED. If your program is multi-threaded, then COM must be initialized in each thread that uses its services. Initializing it once in the main thread won't be enough.
  • Object management. The code sample used four distinct COM objects to read the folder contents. Using desktop's IShellFolder the path was parsed, generating a pidl to the target folder. This second object was created using desktop's BindToObject. The enumerator (third object, the one we were after all along) was created by EnumObjects. Finally the shell memory manager was created by SHGetMalloc to free the various PIDLs that were allocated for us. All COM objects are dynamically created and must be released to free up the associated memory and resources via the object's Release method, inherited from IUnknown. Note that PIDLs are not objects, only pointers to allocated memory. That's why they are Free'd and not Release'd, something that took me some time to realise in the early dayz.
  • UNICODE strings. In case you haven't heard about them, UNICODE strings have wide characters, i.e. 2-bytes each. Apparently the orientals are responsible for this major upset <g>. COM has adopted the UNICODE system whole-heartedly, and most system (and shell) objects expect strings to be wide. The problem is that windows 95/98 (dunno about Me) work with multibyte strings, which are basically regular single-byte strings, despite what their name suggests. If you want your proggy to work in all windows platforms then you'd have to stick with single-byte strings and do manual conversions whenever a wide string is required. That is exactly what MultiByteToWideChar function does above. If you use MFC, you can take advantage the CString class, which simplifies string management and conversions. The drawback is the performance penalty involved for all these conversions; unfortunately there's no way around it, except if you are willing to distribute separate ANSI and UNICODE versions of your programs.
  • Enumerators. These fancy-name objects are frequent denizens of the COM landscape. Any VB people in the audience will be familiar with the term collections. Enumerators are simple objects that hold collections of similar items. The internal organization is transparent; enumerators expose an IEnumxxx interface, as IEnumIDList. Once initialized, items can be accessed sequentially by calling the Next method repeatedly, until some error indicates that the list is exhausted. Personally I feel it's a grand omission that all those objects don't have a method like Count to give the total number of elements in the list. An important detail is that enumerators usually return duplicates of their listed items which you are responsible to cleanup. In the code sample above note that each pidl is Free'd after use.

You can now appreciate why people say that COM is a technology with a steep learning curve. One has to be familiar with dozens of details before even the simplest "Hello COM" program can be build. And you'd better hurry in that climbing while learning, because mikro$oft have already prepared the COM+ mountain for you to climb next, slippery slopes an'all. Will this torment ever end? <g>

Of course, these issues are all for starters. The main dish is learning about all the COM objects supplied by the framework, finding out what they can do for you, and how to work with them, what interfaces they support, etc. The online documentation is guaranteed to be the most complete and up-to-date source of reference information. As they say, if you don't like reading, probably software development is not your ideal vocation. What you read in that good book published in 1998 may already be obsolete, plus there will be dozens of new objects introduced since then.

ADVANCED: Fending for your address space
You may have noticed in the sample code the abundance of NULL arguments to interface methods, most of the time implying a default action. Although this is easy to do, there's a possible risk here, if the NULL is a pointer meant to receive some result from the method. Take for example pdwAttributes parameter of ParseDisplayName. The docs state clearly that by passing NULL you specify that are not interested to receive any attributes at this time. In an ideal world you would be safe, but in this world there are many cowboys out there developing shell and namespace extensions, who wouldn't think twice before attempting to write on a NULL pointer without doing the proper checks first. Since that offending amateur object runs in the same address space as your app, it will drag you down in its demise. The workaround is to provide dummy variables for all such potential trouble-makers; for our example this would mean defining a "DWORD dummyAttrs = 0;" and passing it on to ParseDisplayName, even if you don't have any intention of using it in the end.

Ok, but what about that namespace exploring?

Sorry folks, I got a bit carried away there. So, let's get back to the subject, folder contents enumeration. The EnumerateFolder() sample will produce almost the same results as a the filesystem version EnumerateFolderFS(). Even the order of the items is the same, which hints that EnumObjects down deep must be using FindFirstFile et al for doing the actual folder reading. But almost the same implies there are some differences:

  • We no longer read the '.' and '..' pseudo-items. That's progress since I consider these two plain nuisances. <g>
  • We don't get automatic filename filtering; the best we can do is specify whether we want files, folders or both, via the SHCONTF_FOLDERS and SHCONTF_NONFOLDERS flags that EnumObjects understands.

The most important difference however, is the file date/time information. SHGetDataFromIDList doesn't fill in the file details in WIN32_FIND_DATA as thoroughly as an equivalent FindFirstFile would. Only the modification date is filled in, and even that is rounded to the nearest even second. That's the reason why I mentioned that you need both COM and traditional API to obtain complete information for filesystem folders.

ADVANCED: Stale PIDLs
SHGetDataFromIDList gets all the file data directly from the PIDL, without accessing the disc at all. Microsoft's implementation of filesystem PIDLs stores quite a large amount of data in each SHITEMID (cf. my earlier suggestion), except for those unfortunate creation etc dates of course. This is an advantage since the cached information can be accessed quickly, but it has to be interpreted carefully. A PIDL you obtained yesterday won't necessarily contain accurate information, if for example the file was modified in the meantime. Still, even a stale PIDL is good enough to uniquely identify a file. To convince yourselves, take two PIDLs to the same file, obtained at different times, and see what CompareIDs will return.


Shell을 이용하여 디렉토리 정보등을 알아 올 때 알아야 할 것들을 알차게 설명해 놓았다.