Visual C++ project model

29.10.2012 Paul Eremeev
This article is obsolete. You can read the new version of this article here.

Abstract

This article covers the structure of Visual C++ project model (VCProject). Also included are the cases of using the project model for enumeration of project elements and obtaining their compilation properties through the corresponding configurations.

Introduction

Visual C++ project model is a collection of interfaces describing the properties of a compiler, linker and other build tools, as well as the structure of MSVS-compatible projects themselves, and it is connected with the Visual Studio Automation Object Model through the VCProjects late-bound properties. Visual C++ project model extends the standard Visual Studio project model, providing access to the specific functionality of Visual C++ (vcproj/vcxproj) project types. Visual C++ project model is a stand-alone COM component available through the VCProjectEngine.dll assembly, which could also be used independently outside of Visual Studio development environment.

VCProject model structure

Visual Studio provides an extendable project-neutral object model that represents solutions, projects, code objects, documents, etc. Every MSVS project type has a corresponding project automation interface. Every tool in the environment that has a project also has an object of the 'Project' type associated with it. Visual C++ project model also complies with this general automation project model scheme:

Projects
  |- Project -- Object(unique for the project type)
      |- ProjectItems (a collection of ProjectItem)
          |- ProjectItem (single object) -- ProjectItems (another
                                                          collection)
              |- Object(unique for the project type)

The 'Projects' interface provides an ensemble of abstract objects of the 'Project' type. The 'Project' interface defines an abstract project, i.e. it can reference an object from any project model that complies with the standard scheme. Any peculiar properties of a specific model should be defined through a special interface which is unique only to this model alone. A reference for such an object could be acquired through the Project.Object property. For instance, specific properties of Visual C++ project could be obtained through the VCProject interface:

VCProject vcproj = proj.Object as VCProject;

It is possible to obtain a list of all projects loaded in IDE and belonging to any project model type through the dte.Solution.Projects field; projects belonging to a particular model can be acquired through the DTE.GetObject method (see the example below for Visual C++ model):

Projects vcprojs = m_dte.GetObject("VCProjects") as Projects;

The ProjectItems interface represents an ensemble of abstract solution tree elements of 'ProjectItem' type. Similar to the 'Project' interface, the 'ProjectItem' can define any kind of element; it can even contain the same 'ProjectItems' collection inside itself (accessible through the ProjectItem.ProjectItems) or it can be a Project altogether. An object unique for a specific project model can be obtained through the ProjectItem.Object field. For instance, a Visual C++ source code file is represented by a VCFile type:

VCFile file = projectItem.Object as VCFile;

An embedded project can be obtained in a similar manner:

Project proj = projectItem.Object as Project;

Recursively walking all elements of a Solution tree's branch

The interface for controlling hierarchies IVsHierarchy can be used to perform a passing of Solution tree's branch. This interface provides an access to abstract nodes of a tree, each one of which in turn could be a leaf, a container of elements or a link to another hierarchy. Each tree node is uniquely identified through the DWORD identifier VSITEMID. Such identifiers are unique within the scope of a single hierarchy and possess a limited lifetime within it.

A hierarchy object can be obtained for a tree branch of a single project through the VsShellUtilities.GetHierarchy method:

public static IVsHierarchy ToHierarchy(EnvDTE.Project project)
{
  System.IServiceProvider serviceProvider = 
    new ServiceProvider(project.DTE as
  Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
  Guid guid = GetProjectGuid(serviceProvider, project);
  if (guid == Guid.Empty)
    return null;
  return VsShellUtilities.GetHierarchy(serviceProvider, guid);
}

In the example above, the hierarchy was obtained for a project through its GUID identifier. Consider the example of obtaining this GUID identifier for a project:

private static Guid GetProjectGuid(System.IServiceProvider 
  serviceProvider, Project project)
{
  if (ProjectUnloaded(project))
    return Guid.Empty;

  IVsSolution solution = 
   (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)) as
     IVsSolution;
  IVsHierarchy hierarchy;
  solution.GetProjectOfUniqueName(project.FullName, out hierarchy);
  if (hierarchy != null)
  {
    Guid projectGuid;

    ErrorHandler.ThrowOnFailure(
      hierarchy.GetGuidProperty(
      VSConstants.VSITEMID_ROOT,
    (int)__VSHPROPID.VSHPROPID_ProjectIDGuid,
      out projectGuid));

    if (projectGuid != null)
    {
      return projectGuid;
    }
  }

  return Guid.Empty;
}

The IEnumHierarchies interface permits obtaining all of the hierarchies for projects of a particular type through the solution. GetProjectEnum method. Here is an example of obtaining the hierarchies for every Visual C++ project in a solution tree:

IVsSolution solution = PVSStudio._IVsSolution;
if (null != solution)
{
  IEnumHierarchies penum;
  Guid nullGuid = Guid.Empty;
  Guid vsppProjectGuid = 
    new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942");

  //You can ask the solution to enumerate projects based on the
  //__VSENUMPROJFLAGS flags passed in. For
  //example if you want to only enumerate C# projects use
  //EPF_MATCHTYPE and pass C# project guid. See
  //Common\IDL\vsshell.idl for more details.
  int hr = solution.GetProjectEnum(
    (uint)(__VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION |   
    __VSENUMPROJFLAGS.EPF_MATCHTYPE),
    ref vsppProjectGuid, out penum);
  ErrorHandler.ThrowOnFailure(hr);
  if ((VSConstants.S_OK == hr) && (penum != null))
  {
    uint fetched;
    IVsHierarchy[] rgelt = new IVsHierarchy[1];
    PatternsForActiveConfigurations.Clear();
    while (penum.Next(1, rgelt, out fetched) == 0 && fetched == 1)
    {
      ...
    }
  }
}

As evident by the example above, the GetProjectEnum method provides hierarchies for projects based on a project kind specified by the GUID identifier. GUID identifiers for regular Visual Studio/MSBuild project types can be obtained here. The penum.Next() method allows us to enumerate all project hierarchies we've acquired (the rgelt array). It should be remembered that user-created project models could possess their own unique identifiers in case they define a new project type for themselves.

But our own experience in developing PVS-Studio IDE plug-in demonstrates that an opposite situation is quite possible as well, that is, when a user-created project type uses a GUID from one of the stock project types, usually the one from which it was derived. In particular, we've encountered a VCProject type that was extended to provide development for Android platform. As a result, this project model extension had caused crashes in our plug-in because it did not provide several properties which are otherwise present in VCProject model (OpenMP for example) through the automation API. An intricacy of this situation is that such an extended project model type cannot be differentiated from a regular one, and thus, it is quite hard to correctly process it as well. Therefore, when you are extending a project model through your custom types, to avoid such conflicts with various IDE components (including other third-party extensions as well), it is always important to remember the necessity of providing means to uniquely identify your types.

Possessing an IVsHierarchy for the project, we are able to recursively enumerate all the elements of such solution tree branch through the hierarchy.GetProperty method, which in turn provides us with the specified properties for each one of the hierarchy nodes:

EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, MyProjectHierarchy, 
0, true);
  ...
public void EnumHierarchyItemsFlat(uint itemid, IVsHierarchy 
  hierarchy, int recursionLevel, bool visibleNodesOnly)
{
  if (hierarchy == null)
  return;
  int hr; object pVar;

  hr = hierarchy.GetProperty(itemid, 
    (int)__VSHPROPID.VSHPROPID_ExtObject, out pVar);

  ProjectItem projectItem = pVar as ProjectItem;
  if (projectItem != null)
  {
    ...
  }

  recursionLevel++;
  //Get the first child node of the current hierarchy being walked
  hr = hierarchy.GetProperty(itemid,
    (visibleNodesOnly ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild 
    :(int)__VSHPROPID.VSHPROPID_FirstChild),
    out pVar);
  Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
  if (VSConstants.S_OK == hr)
    {
      //We are using Depth first search so at each level we recurse
      //to check if the node has any children
      // and then look for siblings.
      uint childId = GetItemId(pVar);
      while (childId != VSConstants.VSITEMID_NIL)
      {
        EnumHierarchyItemsFlat(childId, hierarchy, recursionLevel, 
        visibleNodesOnly);
        hr = hierarchy.GetProperty(childId,
          (visibleNodesOnly ?
          (int)__VSHPROPID.VSHPROPID_NextVisibleSibling :
          (int)__VSHPROPID.VSHPROPID_NextSibling),
          out pVar);
        if (VSConstants.S_OK == hr)
        {
          childId = GetItemId(pVar);
        }
        else
        {
          Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
          break;
        }
      }
    }
  }

  private uint GetItemId(object pvar)
  {
    if (pvar == null) return VSConstants.VSITEMID_NIL;
    if (pvar is int) return (uint)(int)pvar;
    if (pvar is uint) return (uint)pvar;
    if (pvar is short) return (uint)(short)pvar;
    if (pvar is ushort) return (uint)(ushort)pvar;
    if (pvar is long) return (uint)(long)pvar;
    return VSConstants.VSITEMID_NIL;
  }

A ProjectItem object that we've acquired for each one of the tree's nodes will allow us to obtain its corresponding Visual C++ object through the 'Object' filed, as was described earlier.

Enumerating all projects in solution tree

DTE.Solution.Projects interface can be used to enumerate all projects in the solution:

if (m_DTE.Solution.Projects != null)
  {
  try
    {
      foreach (object prj in m_DTE.Solution.Projects)
      {
        EnvDTE.Project proj = prj as EnvDTE.Project;
        if (proj != null)
          WalkSolutionFolders(proj);
      } 
    }
  }

Besides projects, Solution tree can also contain folder nodes (Solution Folders). They should also be taken into account while processing each Project element:

public void WalkSolutionFolders(Project prj)
{
  VCProject vcprj = prj.Object as VCProject;
  if (vcprj != null && prj.Kind.Equals(VCCProjectTypeGUID))
  {
    if (!ProjectExcludedFromBuild(prj))
    {
      IVsHierarchy projectHierarchy = ToHierarchy(prj);
      EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
      projectHierarchy, 0, false);
    }
  }
  else if (prj.ProjectItems != null)
  {
    foreach (ProjectItem item in prj.ProjectItems)
    {
      Project nextlevelprj = item.Object as Project;
      if (nextlevelprj != null && !ProjectUnloaded(nextlevelprj))
      WalkSolutionFolders(nextlevelprj);
    }
  }
} 

Projects that are excluded from the build should be inspected separately, as they are not accessible through the automation model after being unloaded from the IDE:

public bool ProjectExcludedFromBuild(Project project)
{
  if (project.UniqueName.Equals("<MiscFiles>", 
    StringComparison.InvariantCultureIgnoreCase))
  return true;
  Solution2 solution = m_DTE.Solution as Solution2;
  SolutionBuild2 solutionBuild = 
    (SolutionBuild2)solution.SolutionBuild;
    SolutionContexts projectContexts = 
    solutionBuild.ActiveConfiguration.SolutionContexts;
    //Skip this  project if it is excluded from build.
    bool shouldbuild = 
      projectContexts.Item(project.UniqueName).ShouldBuild;
    return !shouldbuild;
}

Enumerating selected elements

The DTE.SelectedItems interface can be used to enumerate solution elements which are selected in the Solution Explorer window.

foreach (SelectedItem item in items)
{
  VCProject vcproj = null;
  if (item.Project != null)
  {
    vcproj = item.Project.Object as VCProject;

    if (vcproj != null && item.Project.Kind.Equals("{" + 
      VSProjectTypes.VCpp + "}"))
      {
        IVsHierarchy projectHierarchy = ToHierarchy(item.Project);
        PatternsForActiveConfigurations.Clear();
        EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
        projectHierarchy, 0, false, files, showProgressDialog);
      }
      else if (item.Project.ProjectItems != null)
      {
        //solution folder
        if (!ProjectUnloaded(item.Project))
          WalkSolutionFolders(item.Project);
      }
    }
    else if (item.ProjectItem != null)
    {
      //walking files
      ...
      else if (item.ProjectItem.ProjectItems != null)
      if (item.ProjectItem.ProjectItems.Count > 0)
        WalkProjectItemTree(item.ProjectItem);
    }
  }
  
private void WalkProjectItemTree(object CurrentItem)
{
  Project CurProject = null;
  CurProject = CurrentItem as Project;
  if (CurProject != null)
  {
    IVsHierarchy projectHierarchy = ToHierarchy(CurProject);
    PatternsForActiveConfigurations.Clear();
    EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
      projectHierarchy, 0, false);

    return;
  }
    ProjectItem item = null;
    item = CurrentItem as ProjectItem;
    if (item != null)
    {
        ...
        if (item.ProjectItems != null)
            if (item.ProjectItems.Count > 0)
            {
                foreach (object NextItem in item.ProjectItems)
                    WalkProjectItemTree(NextItem);
            }
    }
}

Configurations and properties of projects and files

Visual C++ stores build configurations (compilation and linking parameters, pre-build and post-build steps, external tool command lines etc.) for C/C++ source files inside its XML-based project files (vcproj/vcxproj). These settings are available to Visual Studio users through property page dialogs.

Each combination of project's build configuration (Debug, Release, etc.) and a platform (Win32, IA64, x64, etc.) is associated with a separate collection of settings. Although majority of the settings are defined at the project level, it is possible to redefine separate properties for each individual file (file properties are inherited from its project by default). The list of properties which can be redefined at the file level is dependent upon the type of a file in question. For example, only the ExcludedFromBuild property can be redefined for header files, but cpp source files permit the redefinition for any of its compilation properties.

Obtaining configurations

Visual C++ project model presents property pages through the VCConfiguration (for a project) and VCFileConfiguration (for a file) interfaces. To obtain these objects we will start from a ProjectItem object which represents an abstract Solution tree element.

ProjectItem item;
VCFile vcfile = item.Object as VCFile;
Project project = item.ContainingProject;
String pattern = "Release|x64";
if (String.IsNullOrEmpty(pattern))
  return null;

VCFileConfiguration fileconfig = null;
IVCCollection fileCfgs = (IVCCollection)vcfile.FileConfigurations;
fileconfig = fileCfgs.Item(pattern) as VCFileConfiguration;
if (fileconfig == null)
  if (fileCfgs.Count == 1)
    fileconfig = (VCFileConfiguration)fileCfgs.Item(0);
        

In the example above we've acquired a file configuration for VCFile object (which represents a C/C++ header or a source file) by passing a configuration pattern (configuration's name and platform) to the Item() method. Build configuration pattern is defined on the project level. The following example demonstrates the acquisition of active configuration (the one that is selected in IDE) of a project.

ConfigurationManager cm = project.ConfigurationManager;
Configuration conf = cm.ActiveConfiguration;
String platformName = conf.PlatformName;
String configName = conf.ConfigurationName;
String pattern = configName + "|" + platformName;
return pattern;

The ActiveConfiguration interface should be handled with care. Quite often we've encountered exceptions when calling it from our PVS-Studio IDE extension package. In particular, this field sometimes becomes inaccessible through the automation object model when a user is building a project, or in the presence of any other heavy user interaction with Visual Studio UI. As there is no assured way of predicting such user actions, it is advised to provide additional error handlers for such 'bottlenecks' when accessing settings with automation model. It should be noted that this particular situation is not related to COM exception handling that was described in the previous article dedicated to EnvDTE interfaces, and it is probably related to some internal issues within the automation model itself.

Next, let's acquire the configuration for a project that contains the file in question:

VCConfiguration cfg=(VCConfiguration)fileconfig.ProjectConfiguration;

While the interfaces representing configurations themselves contain settings only from the 'General' tab of the property pages, references for individual build tools can be acquired through the VCConfiguration.Tools and VCFileConfiguration.Tool interfaces (note that a single file contains settings respectively for only one build tool). Let's examine the VCCLCompilerTool interface representing the C++ compiler:

ct = ((IVCCollection)cfg.Tools).Item("VCCLCompilerTool") as 
  VCCLCompilerTool;
ctf = fileconfig.Tool as VCCLCompilerTool;

Now let's acquire the contents of, for example, the AdditionalOptions field belonging to the compiler tool, using the 'Evaluate' method to process any macros that we can encounter within its value:

String ct_add = fileconfig.Evaluate(ct.AdditionalOptions);
String ctf_add = fileconfig.Evaluate(ctf.AdditionalOptions);

Property Sheets

Property sheets are XML files with a props extension. They allow an independent definition of project's build properties, i.e. the command line parameters for various building tools, such as a compiler or a linker. Property sheets also support inheritance and can be used for specifying build configurations for several projects at once, i.e. the configuration defined inside the project file itself (vcproj/vcxproj) could inherit some of its properties from single or multiple props files.

To handle property sheets, Visual C++ project model provides the VCPropertySheet interface. A collection of VCPropertySheet objects can be obtained through the VCConfiguration. PropertySheets field:

IVCCollection PSheets_all = fileconfig.PropertySheets;

Similarly, the PropertySheets filed of the VCPropertySheet interface provides a reference to a collection of child property sheet files for this object. Let's examine the recursive enumeration of all of the project's property sheets:

private void ProcessAllPropertySheets(VCConfiguration cfg, IVCCollection PSheets)
{
  foreach (VCPropertySheet propertySheet in PSheets)
  {
    VCCLCompilerTool ctPS = 
      (VCCLCompilerTool)((IVCCollection)propertySheet.Tools).Item(
      "VCCLCompilerTool");

  if (ctPS != null)
  {
    ...
            
    IVCCollection InherPSS = propertySheet.PropertySheets;
    if (InherPSS != null)
      if (InherPSS.Count != 0)
        ProcessAllPropertySheets(cfg, InherPSS);
      }
    }
}

In the example above we've obtained an object of VCCLCompilerTool type (that is compilation settings) for PropertySheet on every level. In this way we could gather all compilation parameters defined in every property sheet, including the embedded ones.

The VCPropertySheet interface does not contain means to evaluate macros within its fields, so as a work-around, the Evaluate method from the project's configuration can be used instead. But, such approach could also lead to the incorrect behavior in case the value of the macro being evaluated is related to the props file itself. For instance, several MSBuild macros which were introduced in the MSBuild version 4 could also be utilized inside vcxproj projects from Visual Studio 2010. Let's take the MSBuildThisFileDirectory macro that evaluates as a path to the directory containing file in which it is used. Now, evaluating this macro through the cfg.Evaluate will result in a path to the vcxproj file, and not to props file, which actually does contains this macro.

All of the property sheets in Visual C++ project can be divided between user and system files. By user files we understand the props files which were created and added to the project by a user himself. But even an empty template-generated MSVC project often includes several property sheets by default. These system props files are utilized by the environment to specify various compilation parameters which were set inside the project's property page interface by the user. For example, setting up the CharacterSet property to use Unicode manually in the Property Page interface will result in the appearance of a special property sheet in the 'Property Sheets' window which will define several preprocessor symbols (Unicode, _Unicode), and this properties subsequently will be inherited by the project. Therefore when processing properties from inside a Property sheet, one should always remember that compilation symbols defined in system props files are also returned by their corresponding property in the project's configuration through the automation API. Evidently, processing these two simultaneously while gathering compilation arguments can result in a duplication of such arguments.

References

Other articles in this series