JesInclAnalyzer
包含分析器(移除不必要的头文件包含)

引言
JesInclAnalyzer
是一个简单的应用程序,旨在过滤掉 C++ 源文件中不必要的头文件。我并不声称这是解决此问题的最佳方案。基本思路是逐个注释掉源文件中包含的头文件,然后编译每个源文件,并检查编译器输出以确定某个头文件是否必需。实现中使用的技术是简单的文件操作和文本处理。但我相信,它能给出不错的结果。
背景
随着项目经历不同的开发阶段,其规模通常会增加。当新工程师加入并更新源代码或添加新功能时,他们可能会添加传统上用于添加的所有头文件。实际上,并非所有这些头文件都是必需的。久而久之,此类模块的编译时间会显著增加,并耗费开发者大量 productive 时间,从而惹恼开发者。我想,即使等待 15 到 20 分钟来完成编译也不是一种愉快的体验。当被要求紧急修复一个 bug 时,情况会变得更糟。
我一直在寻找解决此问题的方法。我发现了一些商业工具,如 Gimpel、Pro-Factor IncludeManager 等。另一个信息来源是 AT&T Lab 工程师提交的一篇论文:“Incl: A tool to analyze include files”。这篇论文直接解决了这个问题,但仅限于“C”语言。然而,他们提出的算法可以用于实现。他们使用了 AT&T Labs 的一个程序数据库,称为 CIA(C Information Abstractor),它将为“incl”提供足够的信息。我开始分析 VC++ 6.0 编译器生成的程序数据库(PDB)是否能提供“incl”所需的相同信息。我需要一个 API 集来访问 PDB 文件,这可以通过 Visual Studio 2005 附带的 DIA(Debug Interface Access)SDK 来实现。我不得不注册“msdia80.dll”并修改该 SDK 的一些头文件,以便可以在我的开发机器(Windows 2000 和 VC++ 6.0 SP5)上使用它。我可以解析 PDB 并获取有关所有符号和源文件的信息。但我找不到一种方法可以从 PDB(我认为 PDB 是由后编译器阶段生成的)中获取有关头文件、全局常量等方面的信息。而且,预处理器将在其阶段展开所有头文件。
我找到了另一篇与 AT&T Labs 的 C++ 程序数据库相关的论文。CIA++ 是 CIA 的增强版;它是 C++ 的程序数据库。我也分析了这种可能性;即实现 CIA++ 算法并为“incl”准备输入。但这对我来说也行不通,原因可能是缺乏足够实现细节的文档,或者是我知识的不足。最终,我决定采用一种更直接的方法;即通过注释掉源文件中包含的每个头文件来迭代编译。
使用工具/代码
该工具(JesInclAnalyzer
)可以这样使用。该工具要求环境设置为“CL(VC++ 6.0 SP5 编译器)”,以便能够从命令行执行。浏览包含源文件的项目文件夹,并为“CL”选择命令文件。命令文件应包含编译所需的标志和其他编译器所需的参数。单击“Analyze”按钮。该工具将分析源文件,并创建带有后缀“_Copy”的更新文件的副本。处理状态将在主对话框中更新。请注意,该工具不会处理“只读”文件。对于“只读”文件,它会给出“Access denied”错误消息。
首先,该工具使用 JesCPPIterator
类列出给定路径下的源文件。方法 bool JesCPPIterator::PopulateFileList( const CString& csPath_i )
使用 CFileFind
创建源文件列表。源文件存储在 CStringArray
成员中,方法 bool JesCPPIterator::GetNextFile( CString& csSrcFile_o )
逐一返回文件名。我没有充分的理由说明为什么我没有从 CStringArray
派生此类,而是对其进行封装。我所做的可能不是一个好的设计。
JesFileProcessor
类负责创建源文件的副本并更新文件;例如,逐个注释掉头文件包含、恢复更改、删除不需要的副本等。
JesCompilerProxy
类负责与编译器通信。该类的构造函数准备了一个管道,作为编译器的标准输出和标准错误。方法 bool JesCompilerProxy::Compile()
在将标准输出和标准错误句柄设置为自定义管道句柄后创建编译器进程。这段代码可能有些价值,尽管它并不复杂或罕见。
JesCompilerProxy::JesCompilerProxy( const CString& csCompiland_i )
: m_bObjCreated( true ),
m_bOPFileNeeded( false )
{
m_hReadPipe = 0;
m_hWritePipe = 0;
m_csCompiland = csCompiland_i;
HANDLE hTmpReadPipe;
SECURITY_ATTRIBUTES stSecAttribs;
stSecAttribs.nLength = sizeof( SECURITY_ATTRIBUTES );
stSecAttribs.bInheritHandle = TRUE;
stSecAttribs.lpSecurityDescriptor = 0;
if( !CreatePipe( &hTmpReadPipe, &m_hWritePipe, &stSecAttribs, BUFF_SIZE ))
{
AfxMessageBox( _T( "JesCompilerProxy creation failed..." ));
CloseHandles();
m_bObjCreated = false;
return;
}
// Create duplicate Non-inheritable Read Pipe
if( !DuplicateHandle( GetCurrentProcess(), hTmpReadPipe,
GetCurrentProcess(), &m_hReadPipe,
0, FALSE, DUPLICATE_SAME_ACCESS ))
{
AfxMessageBox( _T( "JesCompilerProxy creation failed..." ));
CloseHandles();
m_bObjCreated = false;
return;
}
CloseHandle( hTmpReadPipe );
}
JesCompilerProxy::~JesCompilerProxy()
{
CloseHandles();
}
bool JesCompilerProxy::Compile()
{
if( !m_bObjCreated )
{
return false;
}
STARTUPINFO stStartupInfo;
::ZeroMemory( &stStartupInfo, sizeof( STARTUPINFO ));
stStartupInfo.cb = sizeof( STARTUPINFO );
stStartupInfo.dwFlags = STARTF_USESTDHANDLES;
stStartupInfo.hStdOutput = m_hWritePipe;
stStartupInfo.hStdError = m_hWritePipe;
PROCESS_INFORMATION stProcInfo;
::ZeroMemory( &stProcInfo, sizeof( PROCESS_INFORMATION ));
CString csCmdLine( "cl.exe /c " );
if( !m_csCLCmdFile.IsEmpty())
{
static const CString CMD_FILE_TAG = _T( "@" );
static const CString PARAMETER_DELIMITER = _T( " " );
csCmdLine += CMD_FILE_TAG + m_csCLCmdFile + PARAMETER_DELIMITER;
}
csCmdLine += m_csCompiland;
if( !CreateProcess( 0, csCmdLine.GetBuffer( csCmdLine.GetLength()),
0, 0, TRUE, CREATE_NO_WINDOW, 0, 0,
&stStartupInfo, &stProcInfo ))
{
AfxMessageBox( _T( "CreateProcess() failed..." ));
csCmdLine.ReleaseBuffer();
return false;
}
csCmdLine.ReleaseBuffer();
CString& csResult = ReadResultInPipe();
// Dump compiler output
if( m_bOPFileNeeded )
{
DumpResult( csResult );
}
static const CString csErrMsg( _T( " error " ));
static const CString csWarnMsg( _T( " warning " ));
if( ( -1 != csResult.Find( csErrMsg, 0 )))// ||
( -1 != csResult.Find( csWarnMsg, 0 )))
{
return false;
}
return true;
}
方法 CString JesCompilerProxy::ReadResultInPipe()
使用下面的代码从管道中读取并返回编译器的输出
CString JesCompilerProxy::ReadResultInPipe()
{
// Close Child's write pipe before reading
CloseHandle( m_hWritePipe );
m_hWritePipe = 0;
CString csResult;
// ReadFile problem in UNICODE build
#ifdef _UNICODE
char szBuff[ BUFF_SIZE ] = { 0 };
#else
TCHAR szBuff[ BUFF_SIZE ] = { 0 };
#endif
DWORD dwReadBytesCnt = 0;
while( true )
{
if( !ReadFile( m_hReadPipe, szBuff, BUFF_SIZE, &dwReadBytesCnt, 0 )
|| ( 0 == dwReadBytesCnt ))
{
return csResult;
}
else
{
// ReadFile problem in UNICODE build
#ifdef _UNICODE
WCHAR wszResult[ BUFF_SIZE ] = { 0 };
if( MultiByteToWideChar( CP_ACP, 0, szBuff, strlen( szBuff ) + 1,
wszResult, sizeof( wszResult ) / sizeof( wszResult[ 0 ])))
{
csResult += wszResult;
continue;
}
else
{
continue;
}
#endif
csResult += szBuff;
}
}
}
从项目设置中准备 CL(编译器)命令文件
从 Visual Studio 6.0 项目设置中准备编译器的命令文件可能并不难。请按照以下步骤操作:
- 复制“Project Settings”->“C/C++”选项卡->“Project Options:”到文本文件中。
- 删除预编译/PDBs/OBJs/IDE/Optimization 的标志/开关(例如:/Fo"Debug/" /Fd"Debug/" /FD)。
- 使用 /I(绝对路径)添加编译所需的包含文件路径。
- 有关详细信息,请参阅 MSDN 中的 CL-Command line options。
关注点
最初,Unicode 构建工作不正常。我发现 ReadFile()
API 没有正确填充 WCHAR
缓冲区。我找不到有关此问题的相关文档。最后,我进行了非常规的修复。请查看 CString JesCompilerProxy::ReadResultInPipe()
代码以了解此“野狐”修复。
历史
- 版本 1.0 - 2009 年 3 月 11 日