<FONT color=#008000>N = New, <FONT color=#ff9900>U = Updated
Project Line Counter Overview
The Project Line Counter add-in reports statistics about files in your Visual Studio .NET (2002, 2003) and Visual C++ (v5, v6) projects.
- Supports all versions of Visual Studio going back to Visual C++ 5!:
Read below to see how that was done.
- Visual Studio .NET 2003 NEW!
- Visual Studio .NET 2002
- Developer Studio 6
- Developer Studio 5
- Automatically scans your workspace and project files.
- Varied statistical data about your source code, including: lines of code and comment lines.
- Includes parsers for: C/C++, VB, INI and other file types.
- Can filter statistics based on workspace or project files and/or custom wildcards.
- Reporting & Exporting:
- Export as CSV file for processing by Microsoft Excel (includes sample worksheet with statistical analysis).
- Export as XML for use with the report stylesheets. IMPROVED!
- Online help.
- Full source code.
Click here for a full list of new features and bug fixes.
Overview Of This Article
Generally, Line Counter is a simple program to figure out: It comes with an installer and there is extensive online help. Therefore, the focus of this article is mainly about the internals of Line Counter. If you want to write an addin for Visual Studio .NET, you will find this article particularly useful, as it discusses important details about creating these addins. New to this revision of the article is discussion about addins in the latest Visual Studio, VS.NET 2003.
The article also contains other topics of general interest, such as discussion of memory maps, and my discussion of the use of the MSXML library in Line Counter.
Working With Line Counter
Statistics and Code Metrics
It is important to understand that Line Counter is not a code metrics analysis tool. Line Counter counts physical lines, not statements. So writing:
will cause Line Counter to report two extra lines.
For this reason, Line Counter is most useful for getting an overall sense of code size. Also, if you have a particular coding style, you should be able to compare results from various projects you have. Line Counter is also useful in assessing your code to comments ratio.
If you are looking for a code metrics program, I suggest the wonderful Source Monitor application, which was presented in a March 2000 DDJ article by James F. Wanner. You can download Source Monitor here.
Reporting With XSLT Stylesheets
Line Counter was my first attempt to do something useful with XSLT. XSLT (XSL Transformations) are used to transform XML data into new documents. In the context of Line Counter, XSLT stylesheets are used to transform statistical data (exported as XML) into HTML reports. The combination of XML and XSLT is a powerful one, allowing countless customized reports to be created from the same XML data.
An XSLT programming tutorial is beyond the scope of this article, but I invite you to peek into the stylesheet source code. This will help you see the kinds of things you can do with XSLT. A list of all available stylesheets (with sample HTML output) can be found at the Stylesheet Gallery. Starting with Line Counter v2.10, you can also see the contents of the stylesheet gallery from the Help|Stylesheet Gallery menu item. It is only fitting that the stylesheet dialog is itself powered by XML and XSLT. This is discussed below.
In order to use the stylesheets, you will need a stylesheet processor. The simplest one to use is MSXML, which is built into IE 5.0+. However, users of IE 5.x will have to upgrade their MSXML (the IE 5.x version supports only outdated XSLT syntax). Instructions for upgrading MSXML can be found here. With MSXML installed, all you need to do is to open your XML file in IE in order to see the report.
If MSXML doesn't fit your bill, feel free to use any other XSLT engine. I've included a precompiled version of the Apache Foundation's Xalan XSLT processor on the downloads page for your convenience. One advantage of a standalone processor is that you can easily script the transformations.
The Source Code
Writing A Dual VC6/VS.NET Addin
Design Goals for Line Counter
I am proud to introduce Line Counter as the world's first (and probably only ever) dual VC6/VS.NET addin! Why so proud, you ask? Well, with VS.NET, Microsoft (finally) dumped the old VS5/6 extensibility model and implemented a much more robust model based on IDTExtensibility2. The only downside is that all our favorite VS6 addins won't work anymore.
Mostly as an exercise, instead of simply porting Line Counter to .NET, I decided to get it to work under both .NET and VS6. In a VS6 only system, I wanted the new Line Counter to behave like older versions (this is important, see below). On a system with .NET, Line Counter would look like any other .NET addin.
Starting With the Wizard Code
I started out the same way anyone would - I built myself a little .NET addin stub with the .NET addin wizard. A little warning to all of you out there: the .NET addin wizard generates horrible, horrible code. The code is locale specific, uses tons of goto's, has code and classes in
stdafx.h, and a ton of other things. I don't want to vent too much about the wizard here, so I'll try concentrate on the issues specific to this article.
One thing the wizard does that is not evil in general but was a problem in my case was the use of both ATL7 and VC7 specific things in the code it produces. Why a problem you ask? Earlier, I mentioned that I wanted Line Counter to work without a hitch on a system without VS.NET. If I compiled Line Counter with the standard wizard generated code, it would have dependencies on MFC7. Since most VC6 programmers don't have MFC7 on their systems, I would have to ship MFC7 or statically link with it. That would grow the distribution size by at least 300%. This is unacceptable.
So I went around and ripped out all V7 specific code and primitives. Some of these were (and I'm sure I'm forgetting some): #import statements based on GUIDs, the use of
CAddInModule, and some other ATL7 tricks. Instead of the GUID imports, I had VS.NET create the .tlh files once, and I use those to build with VC6.
Really, all my problems would have gone away if I didn't use MFC. I could have just compiled the whole thing with VS.NET and I wouldn't have had to go through all this hoopla. Unfortunately, MFC is deeply rooted and taking it out was not an option. And as I said, this had to be a VC6 build.
Luckily, after cleaning up most of the Wizard code, it was simply a matter of including all the MFC headers and MFC was in for both the VS.NET and the VC6 addin parts.
Two Addin Interfaces
The resulting code exposes two interfaces. The first is the VC6 addin interface, the second is the VS.NET addin interface. An interesting tidbit to note is that when VC6 loads an addin through the Tools|Customize|Addins dialog, it "monitors" all interfaces that are added to the registry and assumes that they are all for VC6 addins. Since this is not true in our case, this will cause VS6 to generate corrupt entries in its addin list.. To circumvent this problem, I check to see if at the time of
DllRegisterServer the addin is loaded inside of VC6 or eVC. In that case I don't register the VS.NET interfaces:
if (GetModuleHandle("MSDEV.EXE") != NULL ||
GetModuleHandle("EVC.EXE") != NULL)
OK, you got me. Things are not as simple as dropping a new external interface. The reason is that while an addin exposes an interface to VC, it also uses the interfaces available from VC. Since the VS.NET interfaces don't even resemble the VS6 ones, more work is in order. Actually, it is possible to write a VS.NET to VC6 interface emulation library (anybody have the time?) as the VS.NET interface is a superset of the VC6 one. This would allow VC6 addins to be ported with minimal effort. If Microsoft had any brains they would have provided this library already, but they didn't...
Line Counter itself doesn't use many VC6 interfaces, as it relies on the wonderful Workspace Whiz! Interface library to get all the information it wants. However, for VS.NET I wanted to use the new native interfaces to get all the project information I needed. To do this, I created some abstraction classes which hide the details of retrieving project and workspace (arr... solution) information. When loaded under VC6, the addin uses objects that talk to the Workspace Whiz! Interface. Under VS.NET, the objects talk directly to the VS.NET interfaces. See the file
WorkspaceInfo.cpp for more information.
Writing VS.NET Addins
I know I said I wouldn't vent too much about the addin wizard generated code, but I can't help it. You'll have to bear with me... To Microsoft's defense, they were very open minded when I gave them my list of complaints and hopefully all this stuff will be fixed by the next release. [Time lapse update: I was disappointed to see that the wizard wasn't updated in VS.NET 2003 and it still generates the exact same horrible code as it did in VS.NET 2002.]
Registering Your Addin
When the Addin Wizard generates your project, it will generate a setup project which will take care of registering your addin. Unfortunately, this assumes that you want to ship with an MSI installer, which I don't. I've had very bad experiences with MSI, which you can read about in my WndTabs article.
For self registration, there is an ATL registrar file in the generated project. Unfortunately, this file is is incomplete, and I had to add two sections to it (see
LineCountVC7.rgs in the source code for the complete listing):
val FriendlyName = s 'Project Line Counter'
val AboutBoxDetails = s 'Project Line Counter Addin
Copyright (c) 1999-2002 by Oz Solomonovich
val AboutBoxIcon = b ... <long data cut>
val CommandLineSafe = d 0
val CommandPreload = d 1
val Descrption = s 'Counts lines of code
'in your projects'
val LoadBehavior = d 1
val SatelliteDllName = s '%MODULE%'
val LineCount = d 1
I will explain the most important additions. Pay attention, this is very important:
HKCU\Software\Microsoft\VisualStudio\7.0\PreloadAddinState (the last part in the .rgs file) is the magic key that will instruct VS.NET to place your addin in the
AddInDesignerObjects::ext_cm_UISetup connection mode after registration. Without this key, your addin will never get a chance to add its toolbars, etc. As an added bonus, you can just reregister your addin in order to force the toolbars again (as opposed to running "
devenv /setup" which resets all addins).
HKLM\Software\Microsoft\VisualStudio\7.0\Addins\<YourAddin>\AboutBoxIcon is where you put your custom icon. Use the "unsupported tool" GenerateIcoData.exe to generate this information from an .ico file (do a Google search to find the tool on MSDN).
HKLM\Software\Microsoft\VisualStudio\7.0\Addins\<YourAddin>\AboutBoxDetails is the text information for the VS.NET about box. Notice that I've split this into multiple lines in the .rgs file so I can get multiple lines in the about box. I mention this because I've heard this question asked before.
HKLM\Software\Microsoft\VisualStudio\7.0\Addins\<YourAddin>\SatelliteDllName is a must if you want to load toolbar bitmaps from your .dll. Tip: the "transparent" color for command buttons is (0, 254, 0).
Supporting both VS.NET 2002 and VS.NET 2003
You must noticed that all the registry entries mentioned above include '7.0' in their path. 7.0 is the internal version number of Visual Studio .NET 2002. Similarly, 7.1 is the internal number for VS.NET 2003. This means that if you want to target both of these VS.NET versions, you will need to create two sets of registry entries (one set with 7.0, and one with 7.1).
Although VS.NET 2002 and VS.NET 2003 basically share the same interfaces, there are subtle differences (some intentional, some just bugs in the interfaces) that make your addin behave differently. So don't forget to test test test! You might also want to check out the Visual Studio .NET Add-ins discussion group where people tell their compatibility war stories.
Adding a Command
It is not obvious from the VS.NET documentation why menu commands/toolbars/etc. have to be added only at the
AddInDesignerObjects::ext_cm_UISetup stage. The idea is that the user should be able to move any command around within the menus and toolbars. If you keep adding your command every startup, these customizations will be gone.
Another thing you should probably do (which the wizard code doesn't) is check if your command is already registered before adding it. If so, delete it. This will reset the location of the command button, which is desirable, but (again) only during
ext_cm_UISetup. Have a look at
ConnectVC7.cpp for a simple implementation of this (Line Counter only has one command so I didn't need anything fancy).
Project Item: File or Folder???
Sometimes I think MS goes out of its way to make a grandiose API and then forgets to give us the simple things. And sometimes I just find out they gave us the simple things, but they're so deeply buried in MSDN that you just can't find them.
One such case involves item enumeration of VS.NET project items. After playing around with it for way to long, I was finally able to get to all the items in a proper way. However, I couldn't for the life of me find a way to determine if an item was a file! They can be whole bunch of other things (a physical folder, a virtual folder, etc.). So I wrote the following little function to try to deduce the information through indirect means. The frustrating part is that VC projects behave differently from VB.NET and C# projects, requiring multiple checks. Oh well...
typedef CComPtr<EnvDTE::Project> DTEProject;
bool IsAFile(DTEProjectItem& pItem)
ASSERT(pItem != NULL);
if (!SUCCEEDED(pItem->get_FileCount(&cFiles)) || cFiles != 1)
CComBSTR bStrName, bStrFileName;
if (!SUCCEEDED(pItem->get_Name(&bStrName)) ||
!SUCCEEDED(pItem->get_FileNames(1, &bStrFileName)) ||
wcscmp(bStrName, bStrFileName) == 0)
if (CHK_FLAG(GetFileAttributes(sPath), FILE_ATTRIBUTE_DIRECTORY))
General Coding Topics (Not Addin Specific)
Memory Mapped Files
I personally believe that memory mapped files are one of the unsung heroes of the Win32 API. OK, maybe that's a bit overstating it, but one thing is for sure: Memory mapped files are definitely an underutilized gem.
Memory mapped files are ideal in situations where you want to access the entire contents of an on-disk file's. Instead of dealing with the headaches of reading and buffering, you essentially delegate all these responsibilities to the operating system. The OS makes it appear as though the entire file has been loaded into memory (hence the term mapping), when if fact it only reads and caches sections as you access them.
Starting with version 2.10 of Line Counter, memory mapped files replaced the C++ IO-stream library for reading and parsing files. This made code more straight forward. However, I had to consider one important fact: For small files, the overhead of setting up the memory map makes it less optimal than really reading the whole file into memory. Since many of the files Line Counter reads are small, I wanted to ensure I won't get a performance hit.
To get around the performance hit with small files, I borrowed an idea from Visual Studio.NET. VS.NET sets a 64KB threshold before setting up a memory map: Files that are smaller than the threshold get read into a standard buffer, and don't use memory maps. To encapsulate this functionality, I wrote the
CReadOnlyMemMappedFile class. Below, you can see the constructor for this class, which takes care of either reading in the file or setting up the memory map:
CReadOnlyMemMappedFile::CReadOnlyMemMappedFile(LPCTSTR pszFileName) :
const int threshold = 64 * 1024 - 1;
m_hFile = CreateFile(pszFileName, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (m_hFile == INVALID_HANDLE_VALUE)
throw "Could not open file";
DWORD dwSizeLow, dwSizeHigh;
dwSizeLow = ::GetFileSize(m_hFile, &dwSizeHigh);
ASSERT(dwSizeHigh == 0);
m_iFileSize = dwSizeLow;
if (dwSizeLow > threshold)
m_hMap = CreateFileMapping(m_hFile, NULL, PAGE_READONLY,
0, 0, NULL);
ASSERT(m_hMap != INVALID_HANDLE_VALUE);
m_pBuf = (BYTE *)MapViewOfFileEx(m_hMap, FILE_MAP_READ, 0, 0, 0, 0);
m_pBuf = new BYTE[m_iFileSize];
ReadFile(m_hFile, m_pBuf, m_iFileSize, &dwRead, NULL);
ASSERT(dwRead == m_iFileSize);
m_hFile = INVALID_HANDLE_VALUE;
m_hMap = INVALID_HANDLE_VALUE;
MSXML Visited, XSLT Revisited
In order to comply with the CodeProject June 2003 'Projects' competition, I added a nice little dialog that displays a preview of the contents of the online reports stylesheet gallery (see image above). The competition rules called for XML data to be download from an external source, in this case an XML describing all the available stylesheets. I decided that since the stylesheet gallery is a bunch of XSL templates (XSLT) that it would only be fitting to have XSLT power the stylesheet dialog. This was easily achieved with the Microsoft XML library (MSXML).
Before I explain the relevant code, I have to admit that I hate COM. It's convoluted and makes life just too complicated for C++ users, even with the wonderful ATL. I've made every attempt over the years to avoid learning COM (there's I've admitted it), and even though I've written countless addins and other components that use COM, I've been able to learn just enough to get buy. I figured that if I'd wait long enough it would go away (and as far as I'm concerned it mostly has).
Even with my (un)love for COM, I decided to make an exception to use the extremely powerful Microsoft XML library. This library not only performed the XSL transformations on the downloaded XML, it also took care of actually downloading the XML! The HTML resulting from the transform was then placed in an embedded browser control, and voila! The following is a pseudo code equivalent of the code that downloads and formats the XML data (the code itself is too long, and can be found in
XML_Data = Load XML from
XSLT_Text = GetResourceAsString(..., MAKEINTRESOURCE(IDR_XSL_GALLERY), ...)
XSLT_Doc = Load XML from XSLT_Text
HTML = XML_Data->transformNode(XSLT_Doc)
The actual code is a bit longer (error checking, etc), but it's just that simple! By performing the download and transformation in a separate thread, the dialog remains responsive at all times. All in all, the whole dialog implementation, including the XML downloading and processing, is under 250 lines.
The Line Counter source code makes use of many reusable components that you may find useful, even for non-addin projects. Here is a short list:
- A pluggable HTML Help support module for MFC (
HHSupp.cpp/.h and others) - Using this module, you gain instant support for HTML Help. The module can be used by both dialog and full MFC applications.
- Automatic persistent configuration (
Config.cpp/.h and CfgVars.h) - Lets you define global variables which are automatically stored/restored from the registry.
- Automatic Property Sheets (
AutoPropPage.cpp/.h) - Property sheets that automatically manage configuration variables.
The Line Counter source code is an excellent tutorial for anyone wanting to write a VC6 or VS.NET addin. And if addin source code isn't your interest, heck, it's simply a useful tool! (Just remember that it's not a code metrics analyzer...)
If you have any suggestions for improvement, feel free to drop me a line!
I'd like to thank Nick Hodapp of Microsoft for his support with the port to VS.NET!