Introduction
Developing full-featured add-ins for Visual Studio 6.0 has never been a trivial task. The automation interface provided to the add-in developers is comparatively poor, and it's almost always necessary to utilize special tricks to implement the required functionality. Luckily, there are some sources of information available on the topic, for example, an excellent article Undocumented Visual C++ by Nick Hodapp. In fact, my add-in utilizes several ideas stated there.
Add-in description
SppMk add-in integrates a make-based build/unit testing system with VC IDE. The build system is based on a GNU make
utility. With make
, projects are built in a following way: a special file named Makefile
is used to configure different tools such as compiler and linker and to describe source-target dependencies, and a single call to make
executable does all the job of producing the output binary. Unit tests are run in a similar way - by invoking make
with specific parameters. Internally, the build system uses the CppUnit testing framework for executing tests.
Thus, building/unit testing a project by means of the build system is just a matter of calling make
with correct parameters in the project folder.
SppMk has a standard set of toolbar buttons launching make
for the current project. make
commands can also be issued from a context menu in the separate tab of the WorkspaceView
window, as shown on the figure below.
The output of make
is always sent to the "Build" tab of the VC Output window. By the way, this also allows for easy location of unit test failures if they are logged in an appropriate manner (i.e. file(line):text
).
Add-in implementation
SppMk is perhaps too specific to be readily used without any modifications. At least, it requires properly configured make
, and also some other Unix utilities to be present on the target machine. Nevertheless, I believe several SppMk features might be of interest to add-in developers. I will describe these features in detail shortly.
Running a process from within VC IDE
The first thing to do to integrate the build system with the Visual Studio IDE was to manage to run a process with appropriate make
command line and send its output to the VC "Build" tab. I also wanted to have the possibility to interrupt the running process. This is exactly what VC does when building a project. So, after playing for a while with different process-running VC commands, such as build itself or items available in the Tools menu, I realized that the simplest way to implement this functionality would be to hook the CreateProcess
call made by VC and substitute my own command line.
Hooking CreateProcess
(CreateProcessA
to be precise) which is exported by kernel32.dll
is performed by patching the importing module's import table. I used HookImportedFunctionsByName
routine written by John Robbins for that purpose. I've extracted the necessary code from his BugslayerUtil library, which can be found, for example, here. It was easy to locate the importing module by running VC under debugger and setting a breakpoint at the CreateProcess
address. The module found was the old famous DevShl.dll
. Till then, I was able to call my own function instead of "real" CreateProcess
API.
The second problem I had to deal with was to make VC call CreateProcess
when one of SppMk toolbar commands was activated. An add-in is able to issue IDE commands such as "BuildToggleBuild"
, "BuildRebuildAll"
, etc., which is done via calling IApplication::ExecuteCommand()
method. From the available commands, "BuildRebuildAll"
is the only one that is guaranteed to always call CreateProcess
, while "BuildToggleBuild"
might merely do nothing if current project is up-to-date. So, "BuildRebuildAll"
seems to be the only possible choice. But, this command tries to delete all output files for the current project, and that is definitely not acceptable for the case. So, I had to hook DeleteFile
also, this one imported by DevBld.pkg
. Actually, I've also tried to fool "BuildToggleBuild"
by hooking GetFileAttributesEx
API to provide newer dates for source files, but it did not help.
Now to the source. ProcessRunner
class which can be found in ProcessRunner.h
and ProcessRunner.cpp
files provides the required functionality. Its methods are called from within add-in interface implementation (Commands.h
and Commands.cpp
). ProcessRunner::Run
, shown below, starts a VC build.
void ProcessRunner::Run(const SetupParams &Params, IApplication *pApp)
{
LOG(Logger::cDebug, "ProcessRunner::Run(%p){", pApp);
TEST_BOOL(!GetRunning());
m_pOrigCreateProcess = NULL;
m_pOrigDeleteFile = NULL;
m_bFirst = true;
Hook(cCreateProcessA, true);
Hook(cDeleteFileA, true);
m_SetupParams = Params;
TEST_HR(pApp->ExecuteCommand(L"BuildRebuildAll"));
LOG(Logger::cDebug, "ProcessRunner::Run()}");
}
It's called from one of add-in's command handlers, the latter being responsible for creating a correct command line.
STDMETHODIMP CCommands::SppMkMakeMethod()
{
MK_TRY;
RunMake(SupportedCommands::cMake);
MK_MSGRETURN;
}
void CCommands::RunMake(SupportedCommands::CmdId cmd)
{
LOG(Logger::cDebug, "CCommands::RunMake(%d){", cmd);
do
{
if(ProcessRunner::GetInstance().GetRunning())
{
LOG(Logger::cDebug, "CCommands::RunMake(): already running");
break;
}
bool bMakeable = MkUtils(m_spApplication).IsProjectMakeable(
DSUtils(m_spApplication).GetActiveProjectName());
if(!bMakeable)
MK_THROW(SppMkError::eFileNotFound,
"CCommands::CheckCanBuild(): cannot start make: "
"check if project Makefile exists");
ProcessRunner::SetupParams Params(
MkUtils(m_spApplication).CreateMakeCommand(cmd), false);
ProcessRunner::GetInstance().Run(Params, m_spApplication);
}while(false);
LOG(Logger::cDebug, "CCommands::RunMake()}");
}
The hooked version of CreateProcess
just calls the original one substituting the new command line. It also sees whether it's the first CreateProcess
call made by VC, as some build configurations may execute it several times.
BOOL WINAPI ProcessRunner::CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
LOG(Logger::cDebug,
"ProcessRunner::CreateProcess(%s, %s, %p, %p, %d, %d, %p, %s, %p, %p){",
lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes,
bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory,
lpStartupInfo, lpProcessInformation);
MK_TRY;
GetInstance().Hook(cDeleteFileA, false);
long lEvent = 0;
char *p = strstr(lpCommandLine, "-e ");
if(NULL != p)
{
p += 3;
char *q = strstr(p, " ");
if(NULL != q)
{
char buf[100];
memset(buf, 0, sizeof buf);
memcpy(buf, p, q-p);
lEvent = atoi(buf);
}
}
_bstr_t sNewCommadLine = GetInstance().CreateCommandLine(lEvent);
BOOL bRet = GetInstance().m_pOrigCreateProcess(lpApplicationName,
static_cast<LPTSTR>(sNewCommadLine),
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
GetInstance().m_bFirst = false;
MK_CATCH;
::SetLastError(MK_VAR.GetErrorCode());
return FAILED(MK_VAR.GetErrorCode()) ? FALSE : TRUE;
}
All command lines created by VC to build projects invoke the same binary, vcspawn.exe
, that is. An event identifier is passed to it in order for IDE to be able to stop the build process upon user request. The code in the beginning of ProcessRunner::CreateProcess
function tries to extract the value of -e
parameter which stores the aforementioned event identifier.
The final step is unhooking CreateProcess
as we don't want to interfere with all of its invocations. The obvious place to do it is the handler for "BuildFinish"
event.
HRESULT CCommands::BuildFinish(long nNumErrors, long nNumWarnings)
{
MK_TRY;
LOG(Logger::cDebug, "CCommands::BuildFinish(%d, %d){", nNumErrors,
nNumWarnings);
ProcessRunner::GetInstance().TearDown();
ProcessRunner::GetInstance().SetRunning(false);
LOG(Logger::cDebug, "CCommands::BuildFinish()}");
MK_RETURN;
}
void ProcessRunner::TearDown()
{
LOG(Logger::cDebug, "ProcessRunner::TearDown(){");
if(m_SetupParams.bDeleteAfterRun)
{
LOG(Logger::cDebug, "ProcessRunner::TearDown(): deleting file '%S'",
static_cast<WCHAR*>(m_SetupParams.sCommandLine));
::DeleteFileW(m_SetupParams.sCommandLine);
}
m_SetupParams = SetupParams();
SetRunning(false);
Hook(cCreateProcessA, false);
LOG(Logger::cDebug, "ProcessRunner::TearDown()}");
}
The ProcessRunner::Hook
routine itself is fairly simple as it relies completely on John Robbins's code. The only thing left is specifying the appropriate importing modules.
Adding a tab to VC WorkspaceView
window
Being able to run make
for the current project was a great progress, but something more convenient and functional was required. Visual studio provides a way to build/clean projects from the context menu in the File tab of the WorkspaceView
window. My first idea was to try to add my own commands to that context menu. Unfortunately, I did not succeed. Besides, I also planned to have slightly different functionality for displaying a project list, say, hiding all unit tests in a workspace. All this led to the idea of adding my own tab to the VC WorkspaceView
window.
I immediately gave up an idea to reverse engineer VC code to determine the "correct" way of implementing a new tab. The problem is, WorkspaceView
is not a standard Windows tab control, but something rather different. So, I decided to take a simple "brute force" approach: create a standard tab control above the VC WorkspaceView
window. Of course, this way does not guarantee full UI compatibility, as standard tab looks and behaves differently in many ways. But, this approach worked (as you should see soon), and you can always owner draw the tab control if closer resemblance is required.
As you can probably guess, the implementation is again based on a hook, but window procedure hook (or window sub-classing), this time. The window under consideration is of course the WorkspaceView
window. I've developed a WsViewHook
class (WsViewHook.h
and WsViewHook.cpp
) encapsulating the details of locating the window and processing some of its messages. This class derives from the SubclassWnd
class written by Paul DiLascia (you can find it here).
I had to obtain a window handle of the WorkspaceView
window to be able to hook it. Locating the WorkspaceView
window is done via enumerating the child windows of the main VC window. The enumeration is performed from within add-in's OnConnection
method.
HRESULT CCommands::OnConnection(IApplication* pApp, VARIANT_BOOL bFirstTime,
long dwAddInID, VARIANT_BOOL* bOnConnection)
{
if(cfg.GetInstallTab())
{
LOG(Logger::cDebug,
"CCommands::OnConnection(): installing WsView hook");
static WsViewHook hook(m_spApplication);
::EnumChildWindows(AfxGetApp()->m_pMainWnd->m_hWnd,
WsViewHook::FindWorkspaceProc,
reinterpret_cast<LPARAM>(&hook));
}
}
BOOL CALLBACK WsViewHook::FindWorkspaceProc(HWND hwnd, LPARAM lParam)
{
WsViewHook* pThis = reinterpret_cast<WsViewHook*>(lParam);
CWnd* pWnd = CWnd::FromHandle(hwnd);
if (NULL != pWnd &&
"CWorkspaceView" == CString(pWnd->GetRuntimeClass()->m_lpszClassName))
{
pThis->HookWindow(pWnd);
LOG(Logger::cInfo,
"WsViewHook::FindWorkspaceProc(): hooked WorkspaceView, hwnd = 0x%x",
hwnd);
}
return TRUE;
}
WorkspaceView
window lives as long as VC itself, and it's not destroyed when the workspace is closed. So WorkspaceView
can be hooked only once and unhooked on destruction. OnConnection
is a convenient place to do it. Another good place for hooking the window might be an InitPackage
method, as it's called even earlier than OnConnection
. But, this requires an add-in to be also a Visaul Studio package, i.e., export the InitPackage
and ExitPackage
methods and be placed in a correct folder.
The code below shows the "window procedure" of the WsViewHook
class.
LRESULT WsViewHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
LRESULT ret = 0;
MK_TRY;
ret = CSubclassWnd::WindowProc(msg, wp, lp);
switch(msg)
{
case WM_SIZE:
m_Tab.PostMessage(WsTabRepl::WM_ADJUSTSIZE);
break;
case WM_PARENTNOTIFY:
{
WORD wEvent = LOWORD(wp);
switch(wEvent)
{
case WM_CREATE:
if(NULL == m_Tab.m_hWnd)
{
LOG(Logger::cDebug, "WsViewHook::WindowProc(): creating WsTabRepl");
m_Tab.Create(m_pWndHooked);
}
LOG(Logger::cDebug, "WsViewHook::WindowProc(): registering 0x%x", lp);
m_Tab.PostMessage(WsTabRepl::WM_REGISTERCHILD, lp);
break;
case WM_DESTROY:
LOG(Logger::cDebug, "WsViewHook::WindowProc(): deregistering 0x%x", lp);
m_Tab.SendMessage(WsTabRepl::WM_DEREGISTERCHILD, lp);
break;
}
break;
}
case WM_DESTROY:
m_Tab.DestroyWindow();
m_spApp = NULL;
break;
default:
break;
}
MK_CATCH;
return ret;
}
There're 3 important messages to be processed for the WorkspaceView
window, namely WM_SIZE
, WM_PARENTNOTIFY
and WM_DESTROY
.
WM_PARENTNOTIFY
helps to determine when VC creates and destroys the child windows of WorkspaceView
, such as "FileView"
and "ClassView"
. m_Tab
, of type WsTabRepl
, is the replacing tab control window. It is created with the first WM_PARENTNOTIFY
/WM_CREATE
message, and destroyed on WorkspaceView
destruction. All children created/destroyed are registered with the WsTabRepl
instance, which rebuilds its tabs to correspond to those displayed in the WorkspaceView
window.
The message handlers for WM_REGISTERCHILD
and WM_DEREGISTERCHILD
just call the RebuildTabs
method which takes care of synchronizing the tabs.
void WsTabRepl::RebuildTabs()
{
LOG(Logger::cDebug, "WsTabRepl::RebuildTabs(){");
DeleteAllItems();
const CPtrList *pList = GetInternalTabList();
TCITEM item;
memset(&item, 0, sizeof(item));
item.mask = TCIF_TEXT | TCIF_IMAGE;
int nItem = 0;
bool bSeenFileView = false;
for(POSITION pos = pList->GetHeadPosition(); NULL != pos; ++nItem)
{
CWnd *pTabWnd = reinterpret_cast<CWnd*>(pList->GetNext(pos));
TEST_BOOL(NULL != pTabWnd);
CString strTitle;
pTabWnd->GetWindowText(strTitle);
LOG(Logger::cInfo, "WsTabRepl::RebuildTabs(): adding %s",
static_cast<LPCTSTR>(strTitle));
item.pszText
= const_cast<LPTSTR>(static_cast<LPCTSTR>(strTitle));
item.iImage = ImageFromId(strTitle);
InsertItem(nItem, &item);
if(GetInternalCurSel() == nItem)
SetCurSel(nItem);
if("FileView" == strTitle)
bSeenFileView = true;
}
if(bSeenFileView && DSUtils(m_pApp).IsNormalDsw())
{
if(NULL == m_MkView.m_hWnd)
{
LOG(Logger::cInfo, "WsTabRepl::RebuildTabs(): creating MkView");
m_MkView.Create(GetParent());
m_MkView.SetWindowText("MkView");
}
LOG(Logger::cInfo, "WsTabRepl::RebuildTabs(): adding MkView as tab %d",
nItem);
item.pszText = "MkView";
item.iImage = ImageFromId("MkView");
InsertItem(nItem, &item);
}
else if(!bSeenFileView && NULL != m_MkView.m_hWnd)
{
LOG(Logger::cInfo, "WsTabRepl::RebuildTabs(): destroying MkView");
m_MkView.DestroyWindow();
}
PostMessage(WM_ADJUSTSIZE);
LOG(Logger::cDebug, "WsTabRepl::RebuildTabs()}");
}
My own "MkView" tab is created after the "FileView" tab was registered. Then its added to the tab control as the last tab. The IsNormalDsw()
routine checks whether the current workspace is an "normal" one, i.e. not the one created for debugging a process. IsNormalDsw()
just checks the workspace file extension to be "dsw"
.
To be able to synchronize my tabs with the VC ones I had to know the list of VC tabs and the currently active VC tab. After spending some time under debugger I've determined two members of the WorkspaceView
class responsible for that. They are (presumably) an int
at offset 0xA4
which stores the current tab index (-1
for none), and a CPtrList
at offset 0x88
which contains pointers to the tab windows. The following 2 methods extract this data from a WorkspaceView
instance.
int WsTabRepl::GetInternalCurSel()
{
LOG(Logger::cDebug, "WsTabRepl::GetInternalCurSel(){");
CWnd *pWsView = GetParent();
int nIndex = *reinterpret_cast<int*>(
reinterpret_cast<BYTE*>(pWsView)+0xA4);
LOG(Logger::cDebug, "WsTabRepl::GetInternalCurSel()}, nIndex = %d", nIndex);
return nIndex;
}
const CPtrList* WsTabRepl::GetInternalTabList()
{
LOG(Logger::cDebug, "WsTabRepl::GetInternalTabList(){");
CWnd *pWsView = GetParent();
CPtrList *pList = reinterpret_cast<CPtrList*>(
reinterpret_cast<BYTE*>(pWsView)+0x88);
TEST_BOOL(NULL != pList);
LOG(Logger::cDebug, "WsTabRepl::GetInternalTabList()}, pList = %p", pList);
return pList;
}
Switching tabs
As you may have noticed, the 2 methods just described provide read-only access to the VC internal data, so they won't allow to modify the current tab index. This is done intentially to preserve the integrity of the WorkspaceView
instance.
My first approach to tab switching was direct modification of the current index, manually displaying the corresponding child window and hiding the inactive one. But, merely changing the index is not enough, as more data gets changed when the tab is switched by VC itself. Changing only the index results in some deviations from the standard behaviour, for example, Ctrl-PgUp
/Ctrl-PgDown
shortcuts do not always switch tabs in the right order.
So I decided to make VC switch the tabs, by calling the SendInput
API to emulate the necessary keyboard events (Ctrl-PgUp
and Ctrl-PgDown
). The following code does the switching.
void WsTabRepl::SwitchInternalTab(int to)
{
LOG(Logger::cDebug, "WsTabRepl::SwitchInternalTab(%d){", to);
int from = GetInternalCurSel();
if(-1 != from && to != from)
{
bool bLeftToRight = from < to;
INPUT input[4];
memset(input, 0, sizeof(input));
input[0].type = input[1].type = input[2].type =
input[3].type = INPUT_KEYBOARD;
input[0].ki.wVk = input[2].ki.wVk = VK_CONTROL;
input[1].ki.wVk = input[3].ki.wVk = static_cast<WORD>(
bLeftToRight ? VK_NEXT : VK_PRIOR);
input[2].ki.dwFlags = input[3].ki.dwFlags = KEYEVENTF_KEYUP;
input[0].ki.time = input[1].ki.time =
input[2].ki.time = input[3].ki.time = GetTickCount();
LOG(Logger::cInfo, "WsTabRepl::SwitchInternalTab(): "
"switching from %d to %d", from , to);
for(int i = 0; i < abs(from - to); ++i)
{
GetParent()->SetFocus();
SendInput(4, input, sizeof(INPUT));
}
}
LOG(Logger::cDebug, "WsTabRepl::SwitchInternalTab()}");
}
void WsTabRepl::SwitchTab(int from, int to)
{
LOG(Logger::cDebug, "WsTabRepl::SwitchTab(%d, %d){", from, to);
CString sFrom = GetTabId(from);
CString sTo = GetTabId(to);
if("MkView" != sTo)
{
HideChild(&m_MkView);
SwitchInternalTab(to);
}
else
{
ResizeChild(&m_MkView, GetInternalCurTabWnd());
}
if("MkView" == sFrom || "MkView" == sTo)
DisplayChild(GetCurTabWnd());
LOG(Logger::cDebug, "WsTabRepl::SwitchTab()}");
}
void WsTabRepl::OnSelchange(NMHDR* pNMHDR, LRESULT* pResult)
{
LOG(Logger::cDebug, "WsTabRepl::OnSelchange(%p, %p){", pNMHDR, pResult);
SwitchTab(m_nPrevTab, GetCurSel());
*pResult = 0;
LOG(Logger::cDebug, "WsTabRepl::OnSelchange()}");
}
I've chosen tab's window title to serve as an unique tab identifier.
Window size and position
The size and position of the WsTabRepl
window is always the same as those of the WorkspaceView
window. The handler for WM_SIZE
message takes care of it. As for the child windows, I decided that the best way to determine their size would be let VC do the job as well. As one of the windows is always "active" from the VC point of view, VC will set its size/position to the correct value. So I track the id for the VC "active" tab and use its dimensions when
necessary.
Tracking internal tab index changes
Current tab can be changed by means other than using the WsTabRepl
tab control. To track these changes, a timer is used, which periodically checks for internal tab changes and switches WsTabRepl
tab if necessary.
Conclusion
There are still a lot of things to be done to further increase the level of SppMk's integration with Visual Studio IDE. To name a few, toolbar buttons should be disabled while build is in progress, tabs' display and behaviour differs a lot from that of VC native tabs, a way of
running a process via "BuildRebuildAll"
command is not perfect as it prints misleading messages to the "Build" tab (like "Deleting intermediate files and output files for project Project"
). It also makes VC think that current project is unbuilt if the process launched returns with an error, and this is usually the case when some of unit tests fail.
Nevertheless the two techniques, that is being able to run a process from within VC IDE and add a tab to the WorkspaceView
window, allowed to provide a very convenient user interface for a console based build/unit testing system.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.