Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

SppMk - a unit testing and GNU make support add-in for Visual Studio 6.0

0.00/5 (No votes)
11 Jul 2002 1  
A DevStudio add-in described provides two interesting IDE integration features: adding a new tab to VC WorkspaceView window and running an arbitrary process under IDE with output sent to "Build" tab of VC Output window.

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,                 // name of executable module

    LPTSTR lpCommandLine,                      // command line string

    LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD

    LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD

    BOOL bInheritHandles,                      // handle inheritance option

    DWORD dwCreationFlags,                     // creation flags

    LPVOID lpEnvironment,                      // new environment block

    LPCTSTR lpCurrentDirectory,                // current directory name

    LPSTARTUPINFO lpStartupInfo,               // startup information

    LPPROCESS_INFORMATION lpProcessInformation // process information

)
{
    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(
                  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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here