Проектная модель Visual C++




Эта статья устарела. Обновленную версию этой статьи вы можете прочитать здесь.

Аннотация

В данном разделе будет рассмотрена структура проектной модели Visual C++ (VCProject) из объектной модели автоматизации Visual Studio. Приведены примеры использования проектной модели для получения списков проектных элементов и их компиляционных свойств через соответствующие конфигурации.

Введение

Проектная модель Visual C++ представляет собой группу интерфейсов, описывающих функционал компилятора, линковщика и других сборочных инструментов, а также структуру MSVS-совместимых проектов. Она связана с объектной моделью автоматизации Visual Studio через late-bound свойство VCProjects. Проектная модель Visual C++ является расширением стандартной проектной модели Visual Studio, обеспечивая возможность доступа к специфичному для Visual C++ (vcrpoj/vcxproj) проектов функционалу. Проектная модель Visual C++ является самостоятельным COM компонентом, доступным через файл VCProjectEngine.dll, который также может быть использован независимо вне среды Visual Studio.

Структура проектной модели VCProject

Visual Studio предоставляет расширяемую проектно-независимую объектную модель, в которой представлены решения, проекты, объекты кода, документы и т.п. Каждый тип MSVS проектов представлен соответствующим ему интерфейсом автоматизации. Каждый инструмент в среде, имеющий сопоставленные с ним проекты, также сопоставлен и с объектом типа Project. Модель Visual C++ также следует данной общей схеме проектной автоматизации:

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)

Интерфейс Projects предоставляет совокупность абстрактных проектов типа Project. Интерфейс Project описывает абстрактный проект, т.е. может указывать на проект любой проектной модели, придерживающейся стандартной схемы. При этом все уникальные свойства конкретной модели должны быть описаны через специальный, уникальный для данной модели, интерфейс. Ссылку на такой объект можно получить через поле Project.Object. Например, для проектной модели Visual C++ такой уникальный объект будет иметь тип VCProject:

VCProject vcproj = proj.Object as VCProject;

Совокупность Projects можно получить для всех загруженных в IDE проектов solution файла через поле dte.Solution.Projects или только для одной проектной модели через метод DTE.GetObject:

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

Интерфейс ProjectItems представляет совокупность абстрактных элементов дерева проекта типа ProjectItem. По аналогии с Project, ProjectItem может описывать элемент любого типа, содержащий в том числе и такую же вложенную совокупность ProjectItems (через поле ProjectItem.ProjectItems) или быть проектом Project. Объект уникальный для конкретной проектной модели можно по аналогии получить через ProjectItem.Object. Например, у проектной модели Visual C++ файл с исходным кодом представлен типом VCFile:

VCFile file = projectItem.Object as VCFile;

Аналогично выглядит и получение вложенного проекта:

Project proj = projectItem.Object as Project;

Рекурсивный обход всех элементов ветви Solution дерева

Для обхода ветви Solution дерева можно воспользоваться интерфейсом управления иерархиями IVsHierarchy. Интерфейс предоставляет доступ к абстрактным узлам дерева, каждый из которых может являться листом, контейнером элементов или ссылкой на другую иерархию. Каждый узел дерева уникально идентифицируется через DWORD идентификатор VSITEMID. Такие идентификаторы уникальны в рамках одной иерархии и имеют в ней ограниченный срок существования.

Объект иерархии можно получить для ветки отдельного проекта с помощью метода VsShellUtilities.GetHierarchy:

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);
}

Здесь иерархия была получена для проекта по его GUID идентификатору. Приведём пример метода GetProjectGuid, позволяющего получить такой идентификатор проекта:

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;
}

Интерфейс IEnumHierarchies позволяет сразу получить совокупность таких иерархий для проектов определённого, заданного типа, через метод solution. GetProjectEnum. Пример получения иерархий для всех Visual C++ проектов Solution дерева:

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

Здесь метод GetProjectEnum позволяет получить иерархии проектов указанного через GUID идентификатор типа. GUID идентификаторы для стандартных типов проектов Visual Studio/MSBuild можно посмотреть по этой ссылке. Метод penum.Next() позволит нам обойти все полученные таким образом проектные иерархии (массив rgelt). Стоит помнить, что пользовательские проектные модели, определяющие новый тип проекта, также могут иметь свой уникальный идентификатор.

Практика разработки IDE плагина PVS-Studio, однако, показывает, что возможна и обратная ситуация, т.е. пользовательский тип проекта, использующий GUID одного из стандартных проектных типов, причём обычно тот, от которого он наследовался. В частности, мы столкнулись с типом проектов VCProject, расширенным для разработки под платформу Android. Как результат, данное расширение проектной модели приводило к падению нашего модуля, т.к. оно не предоставляло через API интерфейсы некоторые из присутствующих в VCProject полей (например, поддержка OpenMP). Сложность данной ситуации в том, что такой расширенный проектный тип невозможно дифференцировать от обычных проектов, и соответственно, корректно обработать его. Поэтому при расширении проектной модели через пользовательские типы, для избегания конфликтов с другими (в том числе и пользовательскими) компонентами IDE, стоит помнить о необходимости обеспечения возможности для её уникальной идентификации.

Имея объект иерархии IVsHierarchy проекта, мы можем осуществить рекурсивный обход всех элементов данной ветки Solution дерева с помощью метода hierarchy.GetProperty, позволяющего получить заданные свойства каждого узла иерархии:

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;
  }

Полученный в данном примере объект типа ProjectItem для каждого узла дерева позволит нам получить соответствующий ему объект проектной модели Visual С++ через его поле Object, как было описано выше.

Обход всех проектов Solution дерева

Для обхода всех проектов дерева можно воспользоваться интерфейсом DTE.Solution.Projects:

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);
      } 
    }
  }

Помимо непосредственно проектов, Solution дерево также может содержать узлы-папки (Solution Folders). Их нужно учитывать при обходе каждого Project элемента:

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);
    }
  }
} 

Отдельную проверку стоит добавить для проектов, исключённых из сборки, т.к. их элементы не будут доступны через модель автоматизации после их выгрузки:

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;
}

Обход выделенных элементов

Для обхода элементов, выделенных пользователем в интерфейсе окна Solution Explorer, можно воспользоваться интерфейсом DTE.SelectedItems.

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);
            }
    }
}

Конфигурации и свойства проектов и файлов

Visual C++ хранит сборочные параметры (параметры компиляции, линковки, пред и после-сборочные шаги, параметры запуска сторонних утилит и т.п.) С/С++ файлов исходного кода в своих проектных xml файлах (vcproj/vcxproj). Данные параметры доступны пользователю Visual Studio через интерфейс диалоговых окон страниц свойств (Property Pages).

Наборы свойств определены для каждого сочетания сборочной конфигурации проекта (например, Debug и Release) и сборочной платформы (Win32, x64, IA64 и т.п.). При этом такие наборы свойств определены на уровне всего проекта, а отдельные свойства могут быть переопределены для каждого конкретного файла (по умолчанию свойства файла наследуются от проекта). Какие именно свойства могут быть переопределены зависит от типа файла, например для заголовочных файлов доступно переопределение только свойства ExcludedFromBuild, тогда как для cpp файла возможно переопределение любого свойства компиляции.

Получение конфигураций

В проектной модели Visual C++ страницы свойств представлены через интерфейсы VCConfiguration (для проекта) и VCFileConfiguration (для файла). Для получения данных объектов будем отталкиваться от объекта ProjectItem, представляющего собой абстрактный элемент Solution дерева среды.

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);
        

В данном примере мы получили файловую конфигурацию для объекта VCFile (С/C++ заголовочный или исходный файл) с помощью метода Item(), передав в него pattern (имя конфигурации и имя платформы) нужной нам конфигурации. Pattern сборочной конфигурации определён на уровне проекта. Приведём пример получения активной (выбранной в интерфейсе IDE) конфигурации проекта.

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

Свойство ActiveConfiguration следует использовать с осторожностью, т.к. достаточно часто мы сталкивались с исключениями при частом обращении к нему из IDE модуля-расширения PVS-Studio. В частности, данное поле оказывается недоступным через объектную модель автоматизации в моменты, когда пользователь Visual Studio осуществляет действия по сборке проектов либо просто взаимодействует с интерфейсом среды. Так как невозможно однозначно предсказать подобные действия со стороны пользователя, рекомендуется дополнительно обрабатывать подобные ситуации в механизмах доступа к свойствам объектов модели автоматизации. Заметим, что данная конкретная ситуация не является подвидом COM исключений, обработка которых рассматривалась в разделе, посвящённом EnvDTE интерфейсам, и скорее всего связана с недоработками в самой модели автоматизации.

Получим теперь конфигурацию проекта, содержащего данный файл:

VCConfiguration cfg=(VCConfiguration)fileconfig.ProjectConfiguration;

Непосредственно сами интерфейсы конфигураций содержат только общие свойства вкладки General, а свойства каждого конкретного сборочного инструмента определены в объектах, ссылки на которые доступны через поле VCConfiguration.Tools или VCFileConfiguration.Tool (одному файлу соответствует только один сборочный инструмент).

Рассмотрим, например, интерфейс, описывающий параметры C++ компилятора VCCLCompilerTool:

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

Получим для примера содержимое поля AdditionalOptions настроек компилятора, используя для вычисления значений макросов в этом поле метод Evaluate.

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

Property Sheets

Файлы свойств (property sheets) представляют собой XML файлы с расширением props, позволяющие независимо определять сборочные свойства проекта (т.е. параметры запуска различных сборочных инструментов, таких, как компилятор или линковщик). Property sheets поддерживают наследование и могут быть использованы для определения сборочных конфигураций в нескольких проектах одновременно, т.е. конфигурация, определённая в файле проекта (vcproj/vcxproj), может наследовать часть своих свойств из одного или нескольких props файлов.

Для работы с файлами свойств (Property sheets) проектная модель Visual C++ предоставляет интерфейс VCPropertySheet. Получить доступ к совокупности объектов VCPropertySheet проекта можно через поле VCConfiguration. PropertySheets:

IVCCollection PSheets_all = fileconfig.PropertySheets;

Аналогично поле PropertySheets интерфейса VCPropertySheet позволит получить ссылку на все дочерние файлы настроек для заданного объекта. Рассмотрим пример рекурсивного обхода всех файлов настроек проекта:

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);
      }
    }
}

В приведённом примере мы получаем объект типа VCCLCompilerTool (свойства компилятора) для PropertySheet каждого уровня. Таким образом, мы сможем собрать все параметры компиляции, определённые во всех файлах свойств проекта, в том числе и во вложенных.

Интерфейс VCPropertySheet не содержит методов для вычисления макросов в своих полях, поэтому для этого приходится использовать тот же метод Evaluate конфигурации проекта. Такая практика, однако, может приводить к неправильному поведению в случае, если значение вычисляемого макроса связано непосредственно с props файлом. Например, ряд макросов MSBuild, появившихся в 4 версии, могут также быть использованы внутри новых проектов vcxproj из Visual Studio 2010. Макрос MSBuildThisFileDirectory, к примеру, раскрывается в путь до папки текущего файла, и поэтому вычисление его через cfg.Evaluate раскроет его до пути к vcxproj файла, а не к props файлу, в котором он используется.

Все страницы свойств проекта Visual C++ можно разделить на пользовательские и системные файлы. При этом под пользовательскими мы подразумеваем props файлы, непосредственно созданные и включённые в проект самим пользователем. Однако как можно заметить, даже пустой MSVC проект очень часто включает несколько props страниц по умолчанию. Такие системные props файлы используются средой для фактического определения ряда компиляционных параметров, задаваемых в странице настроек самого проекта. Например, задание параметра CharacterSet через юникод приведёт к появлению в списке Property Sheets данного проекта системного props файла, определяющего несколько символов препроцессора (Unicode, _Unicode). Поэтому, при обработке свойств, хранящихся в props файлах, следует помнить, что заданные в системных файлах символы компиляции также определены и через соответствующее им свойство в конфигурации уже самого проекта, доступной через API модели автоматизации. Очевидно, что получение настроек через эти два механизма приведёт к дублированию таких аргументов.

Рекомендуемые ссылки

Другие статьи этого цикла



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


Найденные ошибки

Проверено проектов
355
Собрано ошибок
13 303

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;