Click here to Skip to main content
15,885,365 members
Articles / Programming Languages / C++

Writing a shell extension in plain C++

Rate me:
Please Sign up or sign in to vote.
4.53/5 (8 votes)
28 Sep 2013CPOL3 min read 46.1K   1.8K   31   8
This article describe how to write a shell extension in plain C++ without ATL

Introduction

A shell extension is just a COM component with specific interfaces of shell. There are many articles on how to write shell extensions with ATL on codeproject. Though ATL is a really nice tool to build a COM object, we have to do it without ATL or MFC, just plain C++. In this way, we can understand how a Shell Extension works in more depth and even build it without commerical libraries from Microsoft.

In this article, the simple shell extension example The Complete Idiot's Guide to Writing Shell Extensions - Part I  will be rewritten without ATL. Its name is HelloExtNoATL.   

With ATL, most the processes are be done by project or class wizard. We don't need to write even one line to create the skeleton of Shell Extension project. Only methods for the COM object need be implemented by ourself. 

Without ATL, most the code for the COM object is the same, but IClassFactory and registration have to be done in our own code.  We can create it in follow steps:  

  1. Add a GUID
  2. Implement IClassFactory
  3. Implement CShellExt 
  4. Implement registration functions

Step 1: add a GUID for the COM object

Globally Universal Identifier (GUID) is mandatory for all COM objects. It's done by class wizard in Visual Studio for ATL. We can use guidgen.exe from Visual Studio or some online guid generator to create one for HelloExtNoATL without the help of Class Wizard.  

C++
// 
// guid.h
//
// {CBF88FC2-F150-4F29-BC80-CE30EFD1B62C}
DEFINE_GUID(CLSID_HelloExtNoAtl, 0xcbf88fc2, 0xf150, 0x4f29, 0xbc, 0x80, 0xce, 0x30, 0xef, 0xd1, 0xb6, 0x2c);
 
  

Step 2: Implement IClassFactory   

It's one of major differences of COM objects between plain C++ and ATL. We have to implement our own ClassFactory. In ATL, it can be created automatically by an IDL file.

In CClassFactory, interface IUnknow and IClassFactory need be implemented. So the COM object interface methods can be queried and called. Its instance can be created with method CreateInstance and its ID.

 In constract, ATL has implemented IUnknown by default.  No more code for that. 

C++
// 
// ClassFactory.h: interface for the CClassFactory class.
//

#if !defined(AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_)
#define AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

extern HINSTANCE  g_hInst;
extern UINT       g_DllRefCount;

class CClassFactory : public IClassFactory  
{
protected:
	DWORD m_ObjRefCount;
public:
	CClassFactory();
	virtual ~CClassFactory();

   //IUnknown methods
   STDMETHODIMP QueryInterface(REFIID, LPVOID*);
   STDMETHODIMP_(DWORD) AddRef();
   STDMETHODIMP_(DWORD) Release();

   //IClassFactory methods
   STDMETHODIMP CreateInstance(LPUNKNOWN, REFIID, LPVOID*);
   STDMETHODIMP LockServer(BOOL) { return E_NOTIMPL; };
};

#endif // !defined(AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_) 

 
Most methods is plain and the same for all COM object classes. Method QueryInterface and  CreateInstance should be implemented accordingly for HelloExtNoATL.

In CClassFactory, this pointer is converted to IClassFactory * when the ID equals to IID_IClassFactory.

C++
//
// CClassFactory::QueryInterface
//
STDMETHODIMP CClassFactory::QueryInterface( REFIID riid, LPVOID *ppReturn )
{
	*ppReturn = NULL;

	if( IsEqualIID(riid, IID_IUnknown) )
	    *ppReturn = this;
	else 
	    if( IsEqualIID(riid, IID_IClassFactory) )
		*ppReturn = (IClassFactory*)this;


	if( *ppReturn )
	{
		LPUNKNOWN pUnk = (LPUNKNOWN)(*ppReturn);
		pUnk->AddRef();
		return S_OK;
	}

	return E_NOINTERFACE;
} 

In CClassFactory::CreateInstance, the COM object CShellExt is created,  then its Interface pointer is returned by its ID,.
C++
//
// CClassFactory::CreateInstance
//
STDMETHODIMP CClassFactory::CreateInstance( LPUNKNOWN pUnknown, REFIID riid, LPVOID *ppObject )
{
	*ppObject = NULL;
	if( pUnknown != NULL )
		return CLASS_E_NOAGGREGATION;

	// creates the namespace's main class
	CShellExt *pShellExt = new CShellExt();
	if( NULL==pShellExt ) 
		return E_OUTOFMEMORY;

	// query interface for the return value
	HRESULT hResult = pShellExt->QueryInterface(riid, ppObject);
	pShellExt->Release();
	return hResult;
}  

Step 3: Implement the COM class

HelloExtNoAtl just extends the context menu of Windows explorer. IShellExtInit and IContextMenu should be implemented in the COM class.  Since in pure plain C++, CShellExt inherits IShellExtInit and IContextMenu directly. Most code can be copied from the ATL sample directly.

 It also implements IUknown which is common for all COM objects.  In contract, ATL has implemented IUnknown by default. We have to code it ourself for class CShellExt. 

C++
// 
// ShellExt.h: interface for the CShellExt class.
//

#if !defined(AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_)
#define AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class CShellExt : public IShellExtInit, IContextMenu
{
protected:
	DWORD m_ObjRefCount;
public:
	CShellExt();
	virtual ~CShellExt();

	// IUnknown methods
	STDMETHOD (QueryInterface) (REFIID riid, LPVOID * ppvObj);
	STDMETHOD_ (ULONG, AddRef) (void);
	STDMETHOD_ (ULONG, Release) (void);
	

    // IShellExtInit
    STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

    // IContextMenu
    STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT);
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
protected:
    TCHAR m_szFile [MAX_PATH];
};

#endif // !defined(AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_)

Step 4: Registration

In ATL, a registration script is included in the project. It can be used to register and unregister a shell extension. But with plain C++, we have to implement all the registry operations manually.

Except adding registry entries for common COM objects, extra entries for shell extension also need be created. For HelloExtNoAtl, HKEY_CLASSES_ROOT\Folder\ShellEx\ContextMenuHandlers\HelloExtNoAtl is created. Because use it will extend the context menu functions of explorer. As the entry indicates, a menu entry will be appended when the context menu of a folder is popped up. 

C++
// 
// HelloExtNoAtl.cpp: register and unregister
//
#include <windows.h>
#include <shlobj.h>

#include "HelloExtNoAtl.h"

#include "ClassFactory.h"
#include "Utility.h"

#include <olectl.h>

// data
HINSTANCE   g_hInst;
UINT        g_DllRefCount;

#pragma data_seg(".text")
#define INITGUID
#include <initguid.h>
#include <shlguid.h>
#include "Guid.h"
#pragma data_seg()

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
    switch (ul_reason_for_call)
	{
		case DLL_PROCESS_ATTACH:
			g_hInst = (HINSTANCE)hModule;
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
    }
    return TRUE;
}

/*---------------------------------------------------------------*/
// DllCanUnloadNow()
/*---------------------------------------------------------------*/
STDAPI DllCanUnloadNow( VOID )
{
	return (g_DllRefCount ? S_FALSE : S_OK);
}


/*---------------------------------------------------------------*/
// DllGetClassObject()
/*---------------------------------------------------------------*/
STDAPI DllGetClassObject( REFCLSID rclsid, REFIID riid, LPVOID *ppReturn )
{
	*ppReturn = NULL;

	// do we support the CLSID?
	if( !IsEqualCLSID(rclsid, CLSID_HelloExtNoAtl) )
	   return CLASS_E_CLASSNOTAVAILABLE;
   
	// call the factory
	CClassFactory *pClassFactory = new CClassFactory();
	if( pClassFactory==NULL )
	   return E_OUTOFMEMORY;
   
	// query interface for the a pointer
	HRESULT hResult = pClassFactory->QueryInterface(riid, ppReturn);
	pClassFactory->Release();
	return hResult;
}


/*---------------------------------------------------------------*/
// DllGetRegisterServer()
/*---------------------------------------------------------------*/

typedef struct{
   HKEY  hRootKey;
   LPTSTR lpszSubKey;
   LPTSTR lpszValueName;
   LPTSTR lpszData;
}REGSTRUCT, *LPREGSTRUCT;

STDAPI DllRegisterServer( VOID )
{
	INT i;
	HKEY hKey;
	LRESULT lResult;
	DWORD dwDisp;
	TCHAR szSubKey[MAX_PATH];
	TCHAR szCLSID[MAX_PATH];
	TCHAR szModule[MAX_PATH];
	LPWSTR pwsz;

	// get the CLSID in string form
	StringFromIID( CLSID_HelloExtNoAtl, &pwsz );

	if( pwsz )
	{
		WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
        LPMALLOC pMalloc;
        CoGetMalloc(1, &pMalloc);
		if( pMalloc )
		{
	      pMalloc->Free(pwsz);
		  pMalloc->Release();
		}
    }

	// get this DLL's path and file name
	GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );

	// CLSID entries
	REGSTRUCT ClsidEntries[] = {  
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  NULL,                   TEXT("HelloExtNoAtl"),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  TEXT("InfoTip"),        TEXT("HelloExtNoAtl."),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  NULL,                   TEXT("%s"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  TEXT("ThreadingModel"), TEXT("Apartment"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\DefaultIcon"),     NULL,                   TEXT("%s,0"),
        NULL,              NULL,                               NULL,                   NULL};

	for( i=0; ClsidEntries[i].hRootKey; i++ )
    {
		// create the sub key string.
		wsprintf( szSubKey, ClsidEntries[i].lpszSubKey, szCLSID );
        lResult = RegCreateKeyEx(  ClsidEntries[i].hRootKey,
                              szSubKey,
                              0,
                              NULL,
                              REG_OPTION_NON_VOLATILE,
                              KEY_WRITE,
                              NULL,
                              &hKey,
                              &dwDisp );
   
		if( lResult==NOERROR )
		{
			 TCHAR szData[MAX_PATH];
			 wsprintf(szData, ClsidEntries[i].lpszData, szModule);
			 lResult = RegSetValueEx( hKey,
                            ClsidEntries[i].lpszValueName,
                            0,
                            REG_SZ,
                            (LPBYTE)szData,
                            lstrlen(szData) + 1);
      
			 RegCloseKey(hKey);
		}
		else
			return SELFREG_E_CLASS;
    }

	// Context Menu
	lstrcpy( szSubKey, TEXT("Folder\\ShellEx\\ContextMenuHandlers\\HelloExtNoAtl"));
	lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT,
		szSubKey,
		0,
		NULL,
		REG_OPTION_NON_VOLATILE,
		KEY_WRITE,
		NULL,
		&hKey,
		&dwDisp);

	if( lResult==NOERROR )
	{
		TCHAR szData[MAX_PATH];
		lstrcpy(szData, szCLSID);
		lResult = RegSetValueEx( hKey,
			NULL,
			0,
			REG_SZ,
			(LPBYTE)szData,
			lstrlen(szData) + 1);

		RegCloseKey(hKey);
	}
	else
		return SELFREG_E_CLASS;

   // register the extension as approved by NT
   OSVERSIONINFO  osvi;
   osvi.dwOSVersionInfoSize = sizeof(osvi);
   GetVersionEx( &osvi );

   if( VER_PLATFORM_WIN32_NT == osvi.dwPlatformId )
   {
	   lstrcpy( szSubKey, TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"));
	   lResult = RegCreateKeyEx(  HKEY_LOCAL_MACHINE,
                              szSubKey,
                              0,
                              NULL,
                              REG_OPTION_NON_VOLATILE,
                              KEY_WRITE,
                              NULL,
                              &hKey,
                              &dwDisp);

       if( lResult==NOERROR )
       {
		   TCHAR szData[MAX_PATH];
	       lstrcpy(szData, TEXT("HelloExtNoAtl"));
           lResult = RegSetValueEx( hKey,
                                 szCLSID,
                                 0,
                                 REG_SZ,
                                 (LPBYTE)szData,
                                 lstrlen(szData) + 1);
      
	      RegCloseKey(hKey);
      }
	  else
		  return SELFREG_E_CLASS;
   }

   return S_OK;
}

/*---------------------------------------------------------------*/
// DllUnregisterServer()
/*---------------------------------------------------------------*/
STDAPI DllUnregisterServer( VOID )
{
	INT i;
	//HKEY hKey;
	LRESULT lResult;
	//DWORD dwDisp;
	TCHAR szSubKey[MAX_PATH];
	TCHAR szCLSID[MAX_PATH];
	TCHAR szModule[MAX_PATH];
	LPWSTR pwsz;

	// get the CLSID in string form
	StringFromIID( CLSID_HelloExtNoAtl, &pwsz );

	if( pwsz )
	{
		WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
        LPMALLOC pMalloc;
        CoGetMalloc(1, &pMalloc);
		if( pMalloc )
		{
	      pMalloc->Free(pwsz);
		  pMalloc->Release();
		}
    }

	// get this DLL's path and file name
	GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );

	// CLSID entries
	REGSTRUCT ClsidEntries[] = {  
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  NULL,                   TEXT("HelloExtNoAtl"),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  TEXT("InfoTip"),        TEXT("HelloExtNoAtl."),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  NULL,                   TEXT("%s"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  TEXT("ThreadingModel"), TEXT("Apartment"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\DefaultIcon"),     NULL,                   TEXT("%s,0"),
        NULL,              NULL,                               NULL,                   NULL};

	for( i=0; ClsidEntries[i].hRootKey; i++ )
        {
		// create the sub key string.
		wsprintf( szSubKey, ClsidEntries[i].lpszSubKey, szCLSID );
		lResult = RegDeleteKey(ClsidEntries[i].hRootKey, szSubKey);
        }


	// Context Menu
	lstrcpy( szSubKey, TEXT("Folder\\ShellEx\\ContextMenuHandlers\\HelloExtNoAtl"));
	lResult = RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);


   // register the extension as approved by NT
   OSVERSIONINFO  osvi;
   osvi.dwOSVersionInfoSize = sizeof(osvi);
   GetVersionEx( &osvi );

   if( VER_PLATFORM_WIN32_NT == osvi.dwPlatformId )
   {
	   lResult = RegDeleteKey(HKEY_LOCAL_MACHINE, szSubKey);
   }

	return S_OK;
}   

 

Installation

The installation step is the same for all COM objects whose standard register/unregister methods are implemented. Just use regsvr32.exe to install or uninstall it.  

What Does It All Look Like?  

 

 Image 1

 

Image 2

Conclusion

After reading this article and trying the code, we can find the major difference of building a Shell Extension between ATL and plain C++. You can also read articles about creating COM objects in plain C or C++ on CodeProject.

 

References

COM-in-plain-C 

The Complete Idiot's Guide to Writing Shell Extensions - Part I

History

  October 28, 2013: Article first published.


License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUpdate Pin
Member 139772616-Mar-24 17:43
Member 139772616-Mar-24 17:43 
QuestionThank you! All working =) Pin
mildok9-Dec-20 20:53
mildok9-Dec-20 20:53 
Questionrefcount is uninitialized Pin
kjhsdfksdhfjkds20-Dec-19 11:49
kjhsdfksdhfjkds20-Dec-19 11:49 
QuestionExtension not working Pin
Derek Johnson15-Aug-15 23:49
Derek Johnson15-Aug-15 23:49 
AnswerRe: Extension not working Pin
Member 1270477426-Aug-16 1:38
Member 1270477426-Aug-16 1:38 
SuggestionPlease continue this work! Pin
Michaela Joy16-Feb-15 5:16
Michaela Joy16-Feb-15 5:16 
BugNot about shell extensions, and not really working. Pin
Axel Rietschin28-Sep-13 14:25
professionalAxel Rietschin28-Sep-13 14:25 
The bulk of your article is not about shell extensions but more about writing COM components "by hand", something that you really, REALLY should not do given the risk of error that you will introduce and that will cause whathever you are building to malfunction in ways that will be next to impossible to troubleshoot later-on.

You are just rewriting ATL's tried and true boilerplate code for no real benefit, only to introduce your own errors. For example, your IUnknown implementation has a very naive bug (the refcount is not initialized) that basically guarantee your object will be leaked. And if you add a line to initialize m_ObjRefCount to zero to fix that bug, as you should as new-ed objects are not yet referenced thus their refcount must be zero, your class factory will now start to return freed memory and most likely Explorer will begin to crash.

AnswerRe: Not about shell extensions, and not really working. Pin
huys0328-Sep-13 15:42
huys0328-Sep-13 15:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.