Wouldn’t the Recycle Bin sample program have been simpler without COM?


Steve Wolf suggests that the sample program would have been much simpler had the shell extension model been a flat Win32 interface.

Okay, let's try it.

Since this is an extension model, each extension needs to specify the callbacks for each namespace operation. Perhaps it could have been done like this:

HRESULT (CALLBACK *SHELLFOLDER_EXTENDHANDLER)(
    void *lpContext,
    OBJECTTYPE type, void **phObject);
HRESULT (CALLBACK *SHELLFOLDER_PARSEDISPLAYNAMEHANDLER)(
    void *lpContext,
    HWND hwnd, LPBINDCTX pbc, LPWSTR pszDisplayName,
    ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes);
HRESULT (CALLBACK *SHELLFOLDER_ENUMOBJECTSHANDLER)(
    void *lpContext,
    HWND hwnd, SHCONTF grfFlags, HENUMIDLIST *pheidl);
HRESULT (CALLBACK *SHELLFOLDER_BINDTOOBJECTHANDLER)(
    void *lpContext,
    PCUIDLIST_RELATIVE pidl, LPBINDCTX pbc,
    OBJECTTYPE type, void **phObject);
HRESULT (CALLBACK *SHELLFOLDER_BINDTOSTRAGEHANDLER)(
    void *lpContext,
    PCUIDLIST_RELATIVE pidl, LPBINDCTX pbc,
    OBJECTTYPE type, void **phObject);
HRESULT (CALLBACK *SHELLFOLDER_COMPAREIDSHANDLER)(
    void *lpContext,
    LPARAM lParam, PCUIDLIST_RELATIVE pidl1,
    PCUIDLIST_RELATIVE pidl2);
... (etc) ...

HFOLDER CreateShellFolderImplementation(
    SHELLFOLDER_EXTENDHANDLER pfnExtend,
    SHELLFOLDER_PARSEDISPLAYNAMEHANDLER pfnParseDisplayName,
    SHELLFOLDER_ENUMOBJECTSHANDLER pfnEnumObjects,
    SHELLFOLDER_BINDTOOBJECTHANDLER pfnBindToObject,
    SHELLFOLDER_BINDTOSTRAGEHANDLER pfnBindToStorage,
    SHELLFOLDER_COMPAREIDSHANDLER pfnCompareIDs,
    ... (etc) ...
    void *lpContext);

This would be the function that allows a third party to create a shell folder implementation. You pass it a bunch of flat callback functions, one for each operation that a shell folder supports, so that when the application tries to perform that operation on your custom folder, the operating system can ask your custom implementation to do that thing.

If additional shell folder operations are added in the future, the operating system needs to know how to ask your shell extension whether it knows how to do those extended things. That's what the Extend method is for. The operating system could ask to extend your object to one that supports HFOLDER2 operations.

Actually, if you look at it, these are exactly the same as COM methods. The first parameter says what object you are operating on ("this"), and the rest are the parameters.

Okay, so I'm setting up a straw man that looks just like COM. So let's do something that looks very different from COM. We could use the window procedure paradigm:

HRESULT (CALLBACK *SHELLFOLDER_INVOKE)(
    void *lpContext,
    FOLDERCOMMAND cmd, void *parameters);

HFOLDER CreateShellFolderImplementation(
    SHELLFOLDER_INVOKE pfnInvoke,
    void *lpContext);

Your invoke function receives a FOLDER­COMMAND enumeration which specifies what command the client is trying to perform, and then switches on the command to perform the command, or returns E_NOT­IMPL if you don't handle the command. Since each of the methods takes different parameters, we have to do some work to pack them up into a generic parameter block, and then unpack it on the receiving end. Let's assume some helper functions that do this packing and unpacking.

HRESULT UnpackParseDisplayName(
    void *parameters,
    HWND *phwnd,
    LPBINDCTX *ppbc,
    LPWSTR *ppszDisplayName,
    ULONG **ppchEaten,
    PIDLIST_RELATIVE **ppidl,
    ULONG **ppdwAttributes);
);

HRESULT UnpackEnumObjects(
    void *parameters,
    HWND *phwnd,
    SHCONTF *pgrfFlags,
    HENUMIDLIST **ppheidl);

HRESULT AwesomeShellFolderInvoke(
    void *lpContext,
    FOLDERCOMMAND cmd,
    void *parameters)
{
  HRESULT hr = E_NOTIMPL;
  CAwesome *self = reinterpret_cast<CAwesome*>(lpContext);

  switch (cmd) {
  case FOLDERCOMMAND_PARSEDISPLAYNAME:
    {
      HWND hwnd;
      LPBINDCTX pbc;
      LPWSTR pszDisplayName;
      ULONG *ppchEaten;
      PIDLIST_RELATIVE *pidl;
      ULONG *pdwAttributes;
      hr = UnpackParseDisplayName(parameters, &hwnd, &pbc,
              &pszDisplayName, &ppchEaten, &pidl,
              &pdwAttributes);
      if (SUCCEEDED(hr)) {
        hr = ... do the actual work ...
      }
    }
    break;

  case FOLDERCOMMAND_ENUMOBJECTS:
    {
      HWND hwnd;
      SHCONTF grfFlags;
      HENUMIDLIST *pheidl;
      hr = UnpackEnumObjects(parameters, &hwnd, &grfFlags,
              &pheidl);
      if (SUCCEEDED(hr)) {
        hr = ... do the actual work ...
      }
    }
    break;
    ... (etc) ...
  }
  return hr;
}

This could be made a lot simpler with the addition of some helper functions.

HRESULT DispatchParseDisplayName(
  HRESULT (CALLBACK *)(
    void *lpContext,
    HWND hwnd, LPBINDCTX pbc, LPWSTR pszDisplayName,
    ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes),
  void *lpContext,
  void *parameters);

HRSEULT DispatchEnumObjects(
  HRESULT (CALLBACK *)(
    void *lpContext,
    HWND hwnd, SHCONTF grfFlags, HENUMIDLIST *pheidl),
  void *lpContext,
  void *parameters);

The implementation would then go like this:

HRESULT AwesomeParseDisplayName(
    void *lpContext,
    HWND hwnd, LPBINDCTX pbc, LPWSTR pszDisplayName,
    ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes)
{
  CAwesome *self = reinterpret_cast<CAwesome*>(lpContext);
  HRESULT hr;
  ... do the actual work ...
  return hr;
}

HRESULT AwesomeEnumObjects(
    void *lpContext,
    HWND hwnd, SHCONTF grfFlags, HENUMIDLIST *pheidl),
{
  CAwesome *self = reinterpret_cast<CAwesome*>(lpContext);
  HRESULT hr;
  ... do the actual work ...
  return hr;
}

HRESULT AwesomeShellFolderInvoke(
    void *lpContext,
    FOLDERCOMMAND cmd,
    void *parameters)
{
  switch (cmd) {
  case FOLDERCOMMAND_PARSEDISPLAYNAME:
    return DispatchParseDisplayName(AwesomeParseDisplayName,
              lpContext, parameters);

  case FOLDERCOMMAND_ENUMOBJECTS:
    return DispatchEnumObjects(AwesomeEnumObjects,
              lpContext, parameters);
    ... (etc) ...
  }
  return E_NOTIMPL;
}

You might decide to make the parameter packing transparent instead of opaque, so that they are passed as, say, an array of generic types like VARIANTs. (Note that I'm abusing VARIANTs here. These are not valid VARIANTs, but it saves me from having to declare my own generic type. This is just a design discussion, not an actual implementation.)

HRESULT (CALLBACK *SHELLFOLDER_INVOKE)(
    void *lpContext,
    FOLDERCOMMAND cmd,
    VARIANT *rgvarArgs,
    UINT cArgs);

// error checking elided for expository purposes
// In real life, you would have to validate cArgs
// and the variant types.

HRESULT AwesomeShellFolderInvoke(
    void *lpContext,
    FOLDERCOMMAND cmd,
    VARIANT *rgvarArgs,
    UINT cArgs)
{
  CAwesome *self = reinterpret_cast<CAwesome*>(lpContext);

  switch (cmd) {
  case FOLDERCOMMAND_PARSEDISPLAYNAME:
    return self->ParseDisplayName(
      reinterpret_cast<HWND>(rgvarArgs[0]->byref),
      reinterpret_cast<LPBINDCTX>(rgvarArgs[1]->byref),
      reinterpret_cast<LPWSTR>(rgvarArgs[2]->byref),
      reinterpret_cast<ULONG*>(rgvarArgs[3]->byref),
      reinterpret_cast<PIDLIST_RELATIVE*>(rgvarArgs[4]->byref),
      reinterpret_cast<ULONG**>(rgvarArgs[5]->byref));

  case FOLDERCOMMAND_ENUMOBJECTS:
    return self->EnumObjects(
      reinterpret_cast<HWND>(rgvarArgs[0]->byref),
      reinterpret_cast<SHCONTF>(rgvarArgs[1]->lVal),
      reinterpret_cast<HENUMIDLIST *>(rgvarArgs[2]->byref));

    ... (etc) ...
  }
  return E_NOTIMPL;
}

(This is basically the plug-in model that some people have chosen to pursue. It is also basically the same as IDispatch::Invoke.)

Okay, that's how you implement the plug-in. Now how do you call it?

You would have to pack the parameters, then call through the Invoke method with your command ID. For example, a call to FOLDER­COMMAND_ENUM­OBJECTS would go like this:

// was: hr = psf->EnumObjects(hwnd, shcontf, &peidl);
// now:
HENUMIDLIST heidl;
VARIANT args[3];

args[0].vt = VT_BYREF;
args[0].byref = hwnd;

args[1].vt = VT_I4;
args[1].lVal = shcontf;

args[2].vt = VT_BYREF;
args[2].byref = &heidl;

hr = InvokeShellFolder(hsf, FOLDERCOMMAND_ENUMOBJECTS, args, 3);

Yuck.

Let's assume that the shell provides helper functions that do all this parameter packing for you. (This is more than certain plug-in models give you.)

HRESULT ShellFolder_ParseDisplayName(
    HSHELLFOLDER hsf,
    HWND hwnd, LPBINDCTX pbc, LPWSTR pszDisplayName,
    ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes)
{
  VARIANT args[6];

  args[0].vt = VT_BYREF;
  args[0].byref = hwnd;

  args[1].vt = VT_BYREF;
  args[1].byref = pbc;

  args[2].vt = VT_BYREF;
  args[2].byref = pszDisplayName;

  args[3].vt = VT_BYREF;
  args[3].byref = pchEaten;

  args[4].vt = VT_BYREF;
  args[4].byref = ppidl;

  args[5].vt = VT_BYREF;
  args[5].byref = pdwAttributes;

  return InvokeShellFolder(hsf, FOLDERCOMMAND_PARSEDISPLAYNAME,
                           args, 6);
}

HRESULT ShellFolder_EnumObjects(
    HSHELLFOLDER hsf,
    HWND hwnd, SHCONTF grfFlags, HENUMIDLIST *pheidl)
{
  VARIANT args[3];

  args[0].vt = VT_BYREF;
  args[0].byref = hwnd;

  args[1].vt = VT_I4;
  args[1].lVal = shcontf;

  args[2].vt = VT_BYREF;
  args[2].byref = &heidl;

  return InvokeShellFolder(hsf, FOLDERCOMMAND_ENUMOBJECTS, args, 3);
}
... (etc) ...

The naming convention above is kind of awkward, so let's give them a bit less clumsy names.

HRESULT ParseShellFolderDisplayName(
    HSHELLFOLDER hsf,
    HWND hwnd, LPBINDCTX pbc, LPWSTR pszDisplayName,
    ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes);
HRESULT EnumShellFolderObjects(
    HSHELLFOLDER hsf,
    HWND hwnd, SHCONTF grfFlags, HENUMIDLIST *pheidl);
... (etc) ...

Okay, now that we have a flat API, let's convert the original code. The first function now goes like this:

HRESULT BindToCsidl(int csidl,
    // REFIID riid, void **ppv
    HSHELLFOLDER *phsf)
{
 HRESULT hr;
 PIDLIST_ABSOLUTE pidl;
 hr = SHGetSpecialFolderLocation(NULL, csidl, &pidl);
 if (SUCCEEDED(hr)) {
  // IShellFolder *psfDesktop;
  HSHELLFOLDER hsfDesktop;
  hr = SHGetDesktopFolder(&hsfDesktop);
  if (SUCCEEDED(hr)) {
   if (pidl->mkid.cb) {
    // hr = psfDesktop->BindToObject(pidl, NULL, riid, ppv);
    hr = BindToShellFolderObject(hsfDesktop, pidl, NULL, phsf);
   } else {
    // hr = psfDesktop->QueryInterface(riid, ppv);
    *phsf = hsfDesktop;
    hsfDesktop = nullptr; // transfer to owner
    hr = S_OK;
   }
   // psfDesktop->Release();
   if (hsfDesktop) ShellFolder_Destroy(hsfDesktop);
  }
  CoTaskMemFree(pidl);
 }
 return hr;
}

What happened here? The IShell­Folder interface was replaced by a HSHELL­FOLDER flat handle. Flat APIs use handles to refer to objects instead of interface pointers.

A method call on an interface pointer becomes a flat API call. In general, pInterface->VerbNoun(args) gets flattened to VerbInterfaceNoun(h, args). But that's just renaming and doesn't change the underlying complexity of the issue.

I could've added reference counting to these flat objects, but then I would be accused of intentionally making it look like COM, so let's say that these flat objects are not reference-counted. Therefore, we have to be more careful about not destroying the object we plan on returning.

On to the next two functions:

void PrintDisplayName(
    // IShellFolder *psf,
    HSHELLFOLDER hsf,
    PCUITEMID_CHILD pidl, SHGDNF uFlags, PCTSTR pszLabel)
{
 STRRET sr;
 // HRESULT hr = psf->GetDisplayNameOf(pidl, uFlags, &sr);
 HRESULT hr = GetShellFolderDisplayNameOf(hsf, pidl, uFlags, &sr);
 if (SUCCEEDED(hr)) {
  PTSTR pszName;
  hr = StrRetToStr(&sr, pidl, &pszName);
  if (SUCCEEDED(hr)) {
   _tprintf(TEXT("%s = %s\n"), pszLabel, pszName);
   CoTaskMemFree(pszName);
  }
 }
}

void PrintDetail(
    // IShellFolder2 *psf,
    HSHELLFOLDER hsf,
    PCUITEMID_CHILD pidl,
    const SHCOLUMNID *pscid, PCTSTR pszLabel)
{
 VARIANT vt;
 // HRESULT hr = psf->GetDetailsEx(pidl, pscid, &vt);
 HRESULT hr = GetShellFolderDetailsEx(hsf, pidl, pscid, &vt);
 if (SUCCEEDED(hr)) {
  hr = VariantChangeType(&vt, &vt, 0, VT_BSTR);
  if (SUCCEEDED(hr)) {
   _tprintf(TEXT("%s: %ws\n"), pszLabel, V_BSTR(&vt));
  }
  VariantClear(&vt);
 }
}

Not really all that different. Last function:

int __cdecl _tmain(int argc, PTSTR *argv)
{
 HRESULT hr = CoInitialize(NULL);
 if (SUCCEEDED(hr)) {
  // IShellFolder2 *psfRecycleBin;
  HSHELLFOLDER hsfRecycleBin;
  hr = BindToCsidl(CSIDL_BITBUCKET, &hsfRecycleBin);
  if (SUCCEEDED(hr)) {
   // IEnumIDList *peidl;
   HENUMIDLIST heidl;
   // hr = psfRecycleBin->EnumObjects(NULL,
   hr = EnumShellFolderObjects(hsfRecycleBin, NULL,
     SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &heidl);
   if (hr == S_OK) {
    PITEMID_CHILD pidlItem;
    // while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
    while (EnumerateNextShellFolderObject(heidl, 1, &pidlItem, NULL) == S_OK) {
     _tprintf(TEXT("------------------\n"));

     PrintDisplayName(hsfRecycleBin, pidlItem,
                      SHGDN_INFOLDER, TEXT("InFolder"));
     PrintDisplayName(hsfRecycleBin, pidlItem,
                      SHGDN_NORMAL, TEXT("Normal"));
     PrintDisplayName(hsfRecycleBin, pidlItem,
                      SHGDN_FORPARSING, TEXT("ForParsing"));

     PrintDetail(hsfRecycleBin, pidlItem,
                 &SCID_OriginalLocation, TEXT("Original Location"));
     PrintDetail(hsfRecycleBin, pidlItem,
                 &SCID_DateDeleted, TEXT("Date deleted"));
     PrintDetail(hsfRecycleBin, pidlItem,
                 &PKEY_Size, TEXT("Size"));

     CoTaskMemFree(pidlItem);
    }
   }
   // psfRecycleBin->Release();
   DestroyShellFolder(hsfRecycleBin);
  }
  CoUninitialize();
 }
 return 0;
}

So we see that flattening the API didn't really change the code at all. You're still invoking methods on objects. Whether you use a flat API to do it or an object-based API is just changing the decorations. The underlying logic doesn't change.

One disadvantage of the flat version is that it requires everything to be mediated by the shell. Instead of invoking a method directly on the object, you have to call the flat function in the shell, which then packages up the call and dispatches it, and the recipient then needs to unpack the parameters (possibly with help from the shell) before finally getting around to doing the actual work.

It also means that any interface change requires an operating system upgrade, since the mediator (the shell) needs to understand the new interface.

But if this whole object-oriented syntax really annoys you and you want a flat API, then feel free to add the line

#define CINTERFACE

before including COM header files. If you do that, then you get the old flat C-style version of COM. Instead of the p->Method(args) new hotness, you can stick to the old trustworthy p->lpVtbl->Method(p, args) version, or use the InterfaceName_MethodName(p, args) helper macro.

Comments (17)
  1. Joshua says:

    Indeed implementing shell objects or altering their behavior requires infrastructure that is most of COM. But merely calling them could have been much simpler with a little ingenuity.

    COM without CINTERFACE has this weird artifact of depending on the C++ memory layout (which is not normally a platform constant).

    [I also showed how calling them could be done with a flat API. But it's just a difference in how you type the words. Either pObject->Verb(Args) or VerbObject(p, Args). Or did you have something more radical in mind? Why not share your idea with us? -Raymond]
  2. Anon says:

    "Show me the implementation"

    When a non-dev presents an alternative, they can be excused when they don't show the code.

    When a dev presents an alternative, they need to have code to show.

  3. Cracky says:

    For myself, working out how IAdvise (proper) coped with network failure, etc was vexing and then head banging in its simplicity.

  4. alegr1 says:

    "How about we make the drivers OO, but because we want to save memory, we put the methods into a single per-driver table". And this is why Windows drivers have to have the same dispatch routines for all device objects they create, which is so inconvenient for some types of drivers.

  5. Azarien says:

    @Joshua: which only means that there is only One True memory layout for C++ on Windows. Using something different in your compiler is asking for troubles.

  6. kantos says:

    @Joshua actually COM was deliberately designed to follow the windows implementation of the C++ memory layout, I believe Raymond actually mentioned this a post awhile back talking about why the shell class headers were exposed directly (because the internals are C due to when the shell was written, and they manually build the vTables).

    [I already covered this. That's why we have macros like BEGIN_INTERFACE. -Raymond]
  7. skSdnW says:

    And guess what, the shell already uses the message based extension model in some places.

    When implementing/extending the default IShellView (DefView) you even get to choose if you want a callback function (LPFNVIEWCALLBACK) or a COM object with a single method (IShellFolderViewCB). You also have IShellMenuCallback for menu bands? and IContextMenuCB when you need to provide a IContextMenu.

    In typical Microsoft fashion if you actually try to use this stuff you often hit a wall of poor documentation and lots of undocumented messages…

  8. Joshua says:

    [Not sure how you intend to do this in a flat C-like API while still supporting extensibility. This isn't C# where you can reflect on objects. -Raymond]

    The shell folder tree has an apparently small number of objects (really interfaces). The trick involved is to reduce the number of interfaces to 1. The only reflection still required would be the single bit "is this a kind of entity that could have children" which should by handled by the enumerate operation, and the custom menu verbs which aren't specific interfaces right now.

    [Now you're answering a question different from the one that was asked. The original question was "Wouldn't a flat C API be simpler?" -Raymond]
  9. Joshua says:

    [I also showed how calling them could be done with a flat API. But it's just a difference in how you type the words. Either pObject->Verb(Args) or VerbObject(p, Args). Or did you have something more radical in mind? Why not share your idea with us? -Raymond]

    You would have had to change the fundamental design so that almost all verbs are universal and objects (not just directories) can be accessed by descending a path through the shell namespace and single-op verbs do not require taking references on their own object.

    Potential verb list (this is really hard to make exhaustive one one pass):

    * New (descends like 95-XP — folder, shortcut, file from template)

    * GetBackingPath **

    * GetHandle

    * ReleaseHandle

    * Copy

    * Move

    * Delete

    * Rename

    * Enum-Menu-Verbs

    * Menu-Verb (calls whetever verbs returned by Enum-Menu-Verbs)

    * GetIconFromForm (works on invalid paths such as *.doc)

    * GetIcon

    * GetIconOverlay

    * OpenWith

    * Get-Properties

    * Set-Properties

    * Get-Extended-Properties (details in the UI)

    * Set-Extended-Properties (when they can be edited)

    ** GetBackingPath for a file is the full path. GetBackingPath for things like documents is SHGetSpecialFolder. GetBackingPath for Desktop returns the current user's desktop for the obvious reason.

    Most verbs would need a flag that says whether or not a UI is acceptable to show.

    For this design to be coherent, SHGetSpecialFolder must be moved to kernel32 where it should have been in the first place.

    [Not sure how you intend to do this in a flat C-like API while still supporting extensibility. This isn't C# where you can reflect on objects. -Raymond]
  10. smf says:

    I'm not sure about Extensibility either.

    Adding new verbs either means you need to ask the "object" about the verbs it supports and call through a pointer (like COM) or you're just jamming extra calls into whatever will pass as the vtable & then hopefully versioning it so you don't ever try to call something past the end of the vtable ( which is how AmigaOS worked circa 1985).

    I'd be more interested in how you'd implement something like COM in BCPL or Objective C :D

  11. Joshua says:

    [Now you're answering a question different from the one that was asked. The original question was "Wouldn't a flat C API be simpler?" -Raymond]

    I think I'm answering the question in the spirit of what was asked.

    [Okay, fine, then be specific. You haven't really made a concrete proposal so much as presented a list of goals. How exactly do you have universal verbs in a flat C API? Are you saying that it should be
    VARIANT args[2];
    args[0].vt = VT_BYREF;
    args[0].byref = pszNewName;
    args[1].vt = VT_I4;
    args[1].lVal = flags;
    InvokeUniversalVerbOnObjectWithArguments(hObject, ShellVerb_Rename, args, 2);

    ? This is basically the same as AwesomeShellFolderInvoke. And if the verb doesn't take a reference to the object, then there is no hObject parameter, so how does it know which object to operate on? -Raymond
    ]
  12. Joshua says:

    [The issues you have with COM are not actually issues with COM]

    Issue #1 with COM is X-apartment model resulting in incompatibility. E.g. OpenFileDialog doesn't work very will with multi-thread apartment. This results in certain sets of COM components being completely incompatible with each other.

    Issue #2 with COM is it's got this habit of loading DLLs at stupid times. Plug-in in this context means not implemented in shell32.dll.

    Issue #3 with COM is memory corruption due to buggy third-party shell extension.

    [What is the threading model for your alternative? Are all objects free-threaded, or do they have thread affinity? (Remember, whatever decision you make affects all implementations.) Also, defining a plug-in by what DLL implements them is very fragile. Components move around between DLLs a lot. (Control panel items in particular.) What would prevent your alternative from loading DLLs at stupid times? And buggy third party shell extensions don't go away when you switch from COM to a custom plug-in model, so it's not clear why you are blaming COM. And what is your extensibility model? What if a plug-in wants to add a new verb? ("Add to Playlist" or something.) Do they have to ask Microsoft to add an API for that new verb and then hope everybody upgrades to the next version of Windows? -Raymond]
  13. Joshua says:

    [What is the threading model for your alternative? Are all objects free-threaded, or do they have thread affinity?]

    This should be defined independently per interface rather than per memory space.

    [(Control panel items in particular.)]

    No case where I have to care needs to load control panel items or anything else that isn't a directory, drive, network share, computer, desktop, start menu, or special folder that behaves like desktop (think quick launch or send to). I don't have to care about recycle bin either but it is likely somebody else would.

    [What would prevent your alternative from loading DLLs at stupid times?]

    Either flag or sensible error handling when LoadLibrary returns EGOAWAY (i.e. don't stop enumeration if this object can't be started. Either pretend it's a file object or skip it).

    [And buggy third party shell extensions don't go away when you switch from COM to a custom plug-in model, so it's not clear why you are blaming COM]

    Because COM is defined in a way that COM interfaces can be hijacked by overriding registration. This should be flat-out impossible.

    [And what is your extensibility model?]

    I actually did think of the idea of making new shell folders via plugins. As I said before the actual implementation on the extender's side looks like COM with a few more variables floating.

    [("Add to Playlist" or something.)]

    Same way I did in Windows 95. Add a new verb in the file types dialog that spawns a program with specific options. This is what I call Menu-Verb above.

  14. alegr1 says:

    >Because COM is defined in a way that COM interfaces can be hijacked by overriding registration.

    ANY plug-in model can be hijacked by overriding registration.

  15. Joshua says:

    > ANY plug-in model can be hijacked by overriding registration.

    But a built-in component shouldn't be overridable that way. This limits plugins to overriding other plugins.

    [Whether a registration can be overridden doesn't have anything to do with whether the Recycle Bin sample program would have been simpler with a flat API model. -Raymond]
  16. Joshua says:

    What I call root here is more like current directory but we can have arbitrary N of them.

    NT Native API has the same concept.

    Operations that display pre-defined dialogs may accept a list of strings to populate the dialog.

    The most prominent of these is new shortcut. Menu-verb has the same artifact.

    The idea of XSTR is it's a string that's only reference counted if you return it. If passed one,

    don't assume you can keep it after returning. I'm assuming that auto_ptr hasn't been discovered yet.

    Since we now have exactly 1 interface, we may almost completely depend on strong typing (no need for VT_…) and declare C functions for them as so:

    /* declarations */

    #define SHFLAG_NOUI 1

    #define SHFLAG_DONTLOADPLUGINS 2 /* for processes that must function with special memory-model constraints */

    #define SHEFLAG_CANDESCEND 1

    #define SHEFLAG_NOBACKINGFILE 2

    #define SHEFLAG_NOSTATE 4 /* requires SHEFLAG_NOBACKINGFILE */

    /* these are the only entities that exist at the root of the shell namespace */

    /* All valid file paths are also valid SHNAME paths */

    /* These teplates exist at root level to keep people from deleting them */

    #define SHNAME_DESKTOP "\\!\desktop";

    #define SHNAME_TEMPLATE_EMPTY "NUL";

    #define SHNAME_TEMPLATE_FOLDER "\\!\{::newfolder}";

    #define SHNAME_TEMPLATE_SHORTCUT "\\!\{::newshortcut}";

    #define SHNAME_TEMPLATE_TEMPLATEDIR "\\!\{::templates};"

    #define SHNAME_MY_COMPUTER "\!desktopcomputer";

    /* Most of these exist ONLY to identify shell special folders independent of name */

    #define PROVIDER_FILESYSTEM MKGUID(…)

    #define PROVIDER_TEMPLATE MKGUID(…)

    #define PROVIDER_DESKTOP MKGUID(…)

    #define PROVIDER_RECYCLE_BIN MKGUID(…)

    typedef struct _xstr { ssize_t owner_refcnt; size_t sz, wchar_t *data; } XSTR;

    typedef struct _verb { DWORD flags, GUID providerid, DWORD nmargs /* most stock have 0 here */, XSTR verb; } SHVERB;

    typedef struct _entity { DWORD flags, GUID providerid, DWORD attributes, FILETIME moddatetime, QWORD inum, XSTR name); } SHENTITY

    inline XSTR StringToXSTR(wchar_t str) { XSTR x { -1 /* cannot be freed */, _wstrlen(str), str}; return x; }

    HRESULT SHGetBackingPath(HANDLE root, XSTR path, OUT *XSTR backing);

    HRESULT SHGetHandle(HANDLE root, XSTR path, OUT *HANDLE child);

    HRESULT SHReleaseHandle(HANDLE root);

    HRESULT SHNew(HANDLE root, DWORD flags, HWND owner, XSTR template, XSTR newname, size_t nargs, IN XSTR *args);

    HRESULT SHCopy(HANDLE root, DWORD flags, HWND owner, XSTR oldname, XSTR newname);

    HRESULT SHMove(HANDLE root, DWORD flags, HWND owner, XSTR oldname, XSTR newname);

    HRESULT SHRename(HANDLE root, DWORD flags, HWND owner, XSTR oldname, XSTR newname_nameonly);

    HRESULT SHDelete(HANDLE root, DWORD flags, HWND owner, XSTR name);

    HRESULT SHGetMenuVerbs(HANDLE root, XSTR path, OUT DWORD *verbs, OUT SHVERB *verbs);

    HRESULT SHFindOpen(HANLDE root, XSTR path, OUT *enumhandle);

    HRESULT SHFindNext(HANLDE enumerator, OUT SHENTITY *entity);

    HRESULT SHFindClose(HANLDE enumerator);

    /* … */

  17. Joshua says:

    [please shorten your comment to 3072 bytes or less] ok breaking

    So the rename recycle bin would look like:

    int main(int argc, char **argv)

    {

    wchar_t *newname = /* gimmie first arg as whar_t */

    HRESULT hr;

    SHENTITY e;

    HANDLE h;

    hr = SHFindOpen(NULL, SHNAME_DESKTOP, h);

    if (SUCCEEDED(hr)) {

    while (1) {

    SHFindNext(h, entity);

    if (e.name.sz == 0) break; /* EOL */

    if (e.providierid == PROVIDERID_RECYCLE_BIN) {

    XSTR rc = JoinXstrs(3, SHNAME_DESKTOP, "\", e.name);

    SHRename(rc, SHFLAG_NOUI, NULL, rc, StringToXSTR(newname));

    ReleaseXSTR(rc);

    }

    }

    SHFindClose(h);

    }

    }

    For an example of implementation disptach (this is a library function that would already be provided): SHRename

    HRESULT SHRename(HANDLE root, DWORD flags, HWND owner, XSTR oldname, XSTR newname_nameonly)

    {

    XSTR folder;

    XSTR file;

    HANDLE h;

    HRESULT hr;

    if (!XSTRGetFolderAndFile(oldname, &folder, &file)) /* if no folder component, returns "." */

    return EBADPATH;

    hr = SHGetHandle(root, folder, &hr);

    if (!SUCCEEDED(hr)) return hr;

    hr = ((struct internalentity *)h)->vtbl->rename(flags, owner, file, newname_nameonly);

    SHReleasehandle(h);

    return hr;

    }

    Key differences between this and COM such that no wrapper could ever be written:

    1. Does not impose any memory model or message pumping requirements if the _NOUI flag is passed.

    2. The SHFLAG_DONTLOADPLUGINS exists, which permits access to core shell functions without imposing any more memory model constraints than WIN32. This would allow things like SQL server to use this directly.

    [The issues you have with COM are not actually issues with COM. They are issues with the shell interface itself. For example, there is COM-imposed reason why there can't be a "DONTLOADPLUGINS" flag. That's a shell limitation. Internally, everything is a plug-in. Control Panel? That's a plug-in. Files on disk? That's a plug-in. It happens to be a plug-in provided with Windows, but from the shell namespace's standpoint, it's still a plug-in. So as far as I can tell, your complaints have nothing to do with COM. You merely wish the shell interfaces were designed some other way. -Raymond]

Comments are closed.

Skip to main content