Взаимодействие Revit API с нативным Revit-кодом
Всем привет! В сегодняшней статье попробую разобраться, как под капотом устроено подключение C#-плагина к Revit и какие полезные выводы из этого можно получить. Сегодня не будет много кода или чего-то полезного, но будет интересно. Поехали!
Введение
Наверняка вы слышали, что в основном Revit написан на C++. Это более низкоуровневый язык, чем наш любимый C#. Главной его особенностью является
ручное управление памятью. В C# за нас всё делает сборщик мусора (Garbage Collector), в C++ же мы должны всегда следить за тем,
чтобы память вовремя освобождалась. Да, есть исключения, что у нас могут быть unmanaged-ресурсы (низкоуровневые ресурсы внутри нашего класса),
которые надо или оборачивать в using, или вызывать Dispose(). Но это скорее исключение, а не правило, и вызвано оно как раз использованием неуправляемых объектов (возможно, написанных на C++).
Вызов плагина Ревитом
Тут всё пока несложно. У нас есть addin-файл. Ревит сканирует папки C:\Users\%username%\AppData\Roaming\Autodesk\Revit\Addins
и C:\ProgramData\Autodesk\Revit\Addins, заходит в папки с номером версии и парсит addin-файлы как обычные xml.
<AddIn Type="Application">
<Name>My App</Name>
<Assembly>My App/My App.dll</Assembly>
<FullClassName>My App.Application</FullClassName>
<ClientId>eA1838CAE-502F-4B15-AD93-2EE73424C191</ClientId>
</AddIn>
Далее всё просто. Мы видим что у нас тип надстройки — Application, значит в сборке по пути My App/My App.dll нужно найти класс с полным
именем My App.Application. Код ищет его через рефлексию, передавая только имя класса. Если такой класс найден, он реализует IExternalApplication,
то просто вызывается его метод OnStartup. Наша надстройка запущена.
Как устроена RevitApi.dll
Кто работал в Dynamo, наверняка помнит такие вещи как “элементы Revit”, “элементы Dynamo”, “UnwrapElement()” и так далее. Как ни странно, у нас тут есть много похожего. Начнём с класса Element — мы с ним встречаемся часто. По сути, элемент представлен у нас несколькими способами:
- собственно, запись в базе данных Ревита
- сущность в памяти на языке C++, которая собственно и отображается в пространстве модели
- класс
Elementв RevitApi.dll. Под капотом он содержит указатель на элемент из памяти, а так же предоставляет методы для работы через API.
Если мы раскроем класс Element через декомпилятор, то именно это и увидим:

То есть у нас есть указатель на какой-то неуправляемый ресурс (m_proxy), и мы освобождаем его при Dispose. Реализации свойств и методов из Revit API
там тоже есть:

Для других классов всё может быть устроено проще (например, я бы хранил XYZ просто как набор 3 чисел и правил математических действий над ними), но для
объектов, которые дают возможность взаимодействовать с базой данных Ревита, устроено так
Что это даёт нам
Более глубоко в C++ код я пока залезть, наверное, не смогу. То есть залезть смогу, но понять — вряд ли. Базово суть такая: внутри Revit API классов на C# хранятся указатели на C++ объекты и их свойства и методы на C#, реализация которых обращается к C++ объектам. Всё это скомпилировано в CIL (Common Intermediate Language) и собрано в dll.
Для нас важно то, что используемые нами объекты обращаются к неуправляемой памяти. А если у нас есть такие ресурсы, то мы должны всегда вызывать Dispose(). Или не должны?
Давайте рассмотрим такой код:
var element = doc.GetElement(id);
//some action
element.Dispose(); // ???
Нужно ли нам тут очищать ресурсы элемента? Давайте подумаем.
После того как мы поработали с элементом (с Revit API-обёрткой настоящего элемента), сборщик мусора очистил этот объект.
Но у нас остались неосвобождённые C++ ресурсы. Можем ли мы их освободить? В целом, нам никто не запрещает, но элемент то у нас не перестал существовать, в базе данных Ревита он есть.
И даже если мы его удалили, он как бы не совсем перестал существовать — мы можем его и вернуть, отменив транзакцию. Получается, в данном
случае вызов Dispose() не имеет смысла.
Зато он имеет смысл в других случаях — когда мы создаём IDisposable самостоятельно. Самый распространённый случай — транзакции.
Но не только они.
Например, это:
- FilteredElementCollector
- ElementFilter (все)
- GeometryObject
- Да в целом любой наследник класса APIObject
Обычно вызывать Dispose нужно у объектов которые мы сами и создаём. Но как понять, какие объекты могут вызвать утечки памяти, а какие не могут?
Давайте попробуем измерить. Я недавно писал про Unit-тесты для Ревит от Романа Карповича. У него есть ещё и бенчмарк-репозиторий для Revit
С его помощью можно измерять скорость и расход памяти у методов. Я запустил вот такие 2 метода на измерение:
[Benchmark]
public List<Element> ElementIntersectsElementFilterToList()
{
var filter = new ElementIntersectsElementFilter(_wall);
var result = new FilteredElementCollector(_document)
.WherePasses(filter)
.ToList();
return result;
}
[Benchmark]
public List<Element> ElementIntersectsElementFilterToListDisposed()
{
using var filter = new ElementIntersectsElementFilter(_wall);
var result = new FilteredElementCollector(_document)
.WherePasses(filter)
.ToList();
return result;
}
И получил вот такой результат:

Что ж, пока мы видим что вызов метода Dispose добавляет времени на выполнение, но на память сам по себе не влияет.
Но точно ли этого достаточно? Попробуем пойти по другому пути: запустим код из DockablePane внутри Revit. Он будет выполнять
вычисления, а за утечками памяти мы будем следить через панель мониторинга в Rider:
Код очень базовый, нагрузим Revit помощнее:
var document = RevitContext.ActiveDocument!;
for (var i = 0; i < 100; i++)
{
var walls = new FilteredElementCollector(document)
.OfClass(typeof(Wall))
.ToElements();
foreach (var wall in walls)
{
var filter = new ElementIntersectsElementFilter(wall);
var pipes = new FilteredElementCollector(document)
.OfCategory(BuiltInCategory.OST_PipeCurves)
.WherePasses(filter);
foreach (var pipe in pipes)
{
var a = pipe.Id.Value * i;
}
}
}
После таких расчётов панель мониторинга в Rider осталась стабильной:

Никаких особых изменений по памяти, никаких особо частых сборок мусора. Код выполняется, и всё. Если честно, я надеялся, что
тут будут какие-нибудь интересные результаты, типа “вот, если не написать using к ElementFilter, вы через 4 часа работы пользователя получите OutOfMemoryException”.
Но нет. Зато теперь мы не просто предполагаем что “ну вот эти объекты обычно не диспозят, мы их и не трогаем”, а прямо знаем: да, мы сами создаём эти объекты,
да, они являются IDisposable, но к утечке памяти это всё не приведёт.
И чуть больше понимаем, как устроена RevitAPI.dll, и знаем что в плане работы с памятью она сделана вполне неплохо.
Ну а на этом на сегодня всё! Мой телеграм-канал. Подписывайтесь, задавайте вопросы в комментариях и до новых встреч!