Архив рубрики: .net

tlbimp.exe и целевая версия .NET Framework

ObjectARX SDK содержит некоторый набор TLB файлов. Для некоторых из них приходится генерировать управляемую обёртку "вручную" (причина указана ниже), посредством утилиты tlbimp.exe (если нужная обёртка изначально отсутствует). Обозначенная утилита содержит некоторый набор ключей для конфигурирования конечного результата. Однако одной, очень важной настройки, всё же не хватает...

На моём рабочем компьютере установлены MS Visual Studio 2005-2012 и .NET Framework 2.0 - 4.5. В первый раз я столкнулся с необходимостью прибегать к "ручному" использованию утилиты tlbimp.exe, когда в MS Visual Studio 2012 был создан проект плагина AutoCAD 2010 (целевая платформа .NET 3.5, разрядность x64), но при этом мне не далось подключить ссылки на acax18ENU.tlb и axdb18enu.tlb. (на самом деле именно эти tlb файлы нет никакой нужды подключать, т.к. вместо них нужно подключать библиотеки Interop, но в данном случае на их примере я показываю проблему, которая может возникнуть при использовании утилиты tlbimp.exe). "За кулисами", при подключении TLB файлов IDE обычно для них автоматически генерирует управляемые обёртки, но в данном случае этого не произошло:


Насколько мне известно, причиной подобной ошибки может быть некорректная регистрация COM в реестре, либо вообще отсутствие в реестре этой информации.

Та же самая ситуация для AutoCAD 2010 наблюдалась и в MS Visual Studio 2005-2010. В виду этого приходится генерировать DLL обёртку при помощи "ручного" запуска утилиты tlbimp.exe. Однако и тут не всё так красиво, как хотелось бы: к сожалению, в утилите tlbimp.exe нет возможности указать целевую версию платформы .NET, для которой следует генерировать обёртку.
    Я пытался сгенерировать обёртку для нужных мне TLB файлов из консолей разных версий MS Visual Studio следующим образом:

    tlbimp "C:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"C:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64

    tlbimp "C:SDKAutodeskAutoCADObjectARX 2010inc-x64axdb18enu.tlb" /out:"C:SDKAutodeskAutoCADObjectARX 2010inc-x64axdb18enu.Interop.dll" /namespace:AXDBLib /machine:x64

    Примечание:
    При подключении этих оболочек к проекту, необходимо их свойство Copy Local устанавливать в TRUE, поскольку они сгенерированы нами и будут отсутствовать в каталоге AutoCAD.

    Если использовать консоль VS 2010 или VS 2012, то обёртка будет создана для .NET 4.0. Однако в AutoCAD 2010 использует .NET 3.5, поэтому данный результат мне не подходит...

    Получить нужный результат мне удалось лишь воспользовавшись консолью VS 2008.

    На моей машинке, в составе установленных SDK

    утилиту tlbimp.exe мне удалось найти в каталогах:
    • C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools
    • C:Program Files (x86)Microsoft SDKsWindowsv8.0AbinNETFX 4.0 Tools
    • C:Program Files (x86)Microsoft SDKsWindowsv7.0ABin
    Кроме того, утилита была найдена и в каталоге
    • C:Program Files (x86)Microsoft Visual Studio 8SDKv2.0Bin
    В обозначенных выше первых трёх каталогах, я пробовал создавать конфигурационный файл tlbimp.exe.config следующего содержимого:

    <?xml version="1.0" encoding="utf-8"?>
       <startup>
       <supportedRuntime version="v2.0.50727"/>
    </startup>


    Затем из cmd.exe, перейдя в каталог указанной утилиты, запустил такую команду:

    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>tlbimp
     "D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64
    Не удалось запустить приложение, поскольку его параллельная конфигурация неправильна. Дополнительные сведения содержатся в журнале событий приложений или используйте программу командной строки sxstrace.exe для получения дополнительных сведений.


    Обозначенный выше результат наблюдался для версий v8.1A и v8.0A. Для версии v7.0A в перенастройке не было необходимости, т.к. на выходе и так получается библиотека для .Net 2.0.
    Если удалить созданный мною конфигурационный файл, то для v8.1A и v8.0A операция так же проходит успешно, но на выходе, конечно же, получаем библиотеку для .Net 4.0:

    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>
    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>tlbimp
     "D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64
    Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.33440
    Copyright (C) Microsoft Corporation.  All rights reserved.

    TlbImp : warning TI3019 : Interface 'IAcadShadowDisplay' is marked as [dual], but does not derive from IDispatch. It will be converted as an IUnknown-derived interface.
    TlbImp : Type library imported to D:SDKAutodeskAutoCADObjectARX 2010inc-x64
    acax18ENU.Interop.dll


    Вывод:
    Если необходимо сгенерировать управляемые DLL обёртки на основе файлов TLB, то для разных целевых версий .NET следует использовать утилиту tlbimp.exe из разных Windows SDK:
    • SDK v7.0A - для .NET 2.0, 3.0, 3.5 SP1
    • SDK v8.1A или v8.0A - для .NET 4.0, 4.5, 4.5.1

    Об управляемых "заглушках" из ObjectARX SDK и целевой платформе (x86x64)

    Как известно, управляемые "заглушки" находятся в ObjectARX SDK в подкаталогах inc, inc-win32 и inc-x64. В идеальном варианте, хотелось бы всегда компилировать код как AnyCPU, однако не во всех случаях это возможно и порой приходится создавать сборки отдельно для x86 и отдельно для x64.

    В ObjectARX SDK 2009 каталог inc не содержит управляемых "заглушек" - они там появились лишь начиная с версии 2011. До этого же их версии находились по подкаталогам inc-win32 и inc-x64. Изначально, базовый набор "заглушек", подключаемых в управляемом плагине AutoCAD состоял из файлов AcDbMgd.dll и AcMgd.dll. Позднее, начиная с версии 2013, к этому перечню добавился файл AcCoreMgd.dll.

    Согласно заявлению сотрудников Autodesk - в том случае, если в управляемом плагине подключается "заглушка" не из каталога inc, то такой плагин следует компилировать не как AnyCPU, но отдельно под x86 и отдельно под x64при этом переподключая "заглушки" из соответствующего каталога - для каждой платформы свои "заглушки". Т.е. если "заглушки" подключаются из inc-win32, то и компилировать такой код следует только для x86. А ежели нужно компилировать для x64, то в подключенных ссылках нужно переуказывать заглушки из каталога inc-x64, вместо заглушек из inc-win32. В том же случае, если в плагине используются лишь заглушки каталога inc, то его можно компилировать как AnyCPU.

    Порой нарушение этого правила может и сойти с рук: например проект, в котором подключены "заглушки" из inc-x64, может быть откомпилирован как x86, после чего успешно работать без каких либо проблем. Или же, как вариант: разработчиком могут быть подключены заглушки не из каталога inc, с последующей компиляцией сборки как AnyCPU. Однако компания Autodesk (в лице Stephen Preston) предупреждает, что в ряде случаев (какие именно - не уточнялось), при подобном подходе могут возникать проблемы.

    AutoCAD .NET API & IDisposable

    В данной заметке речь пойдёт об использовании IDisposable в AutoCAD .NET API. Если не освобождать ресурсы, то будет происходить утечка памяти, которая может привести к серьёзным проблемам. Дабы этого не происходило, нужно понимать, для каких объектов и когда следует вызывать Dispose(), а так же для каких объектов этого делать категорически нельзя.

    Для начала несколько ссылок, откуда черпалась информация:
      1. Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET
      2. Calling Dispose() on AutoCAD objects
      3. Examples of calling Dispose() on AutoCAD objects
      В AutoCAD класс DBObject реализовывает интерфейс IDisposable. Соответственно, указанный интерфейс реализуется и всеми классами, унаследованными от DBObject. Этот же интерфейс реализован и классами Document и Database, однако для Document  вызывать Dispose() никогда не следует (это делает сам AutoCAD), а так же нельзя вызывать Dispose() для объекта, хранящегося в свойстве Document.Database.

      "Вручную" вызывать Dispose() следует в следующих случаях:
      1. Для временных объектов, которые никогда не будут добавлены в базу данных чертежа. Например, это могут быть нерезидентные объекты, предоставленные через Autodesk.AutoCAD.Geometry.
      2. Для временных объектов, которые потенциально могут быть добавлены в базу данных чертежа, но тем не менее не были в неё добавлены.
      3. Если экземпляр класса Database был вами создан способом "new Database(...)" без открытия документа. При работе с созданным подобным образом объектом Database, необходимо не забывать предварительно инициализировать им свойство HostApplicationServices.WorkingDatabase, иначе можно получить исключение eWrongDatabase. По завершению работы с объектом, следует возвращать свойству HostApplicationServices.WorkingDatabase предыдущее значение.
      Внимание!
      Некоторые ресурсы, используемые экземплярами различных классов, могут быть разделяемыми, т.е. одним и тем же ресурсом пользуется сразу несколько разных объектов. К сожалению, в AutoCAD .NET API подобные ситуации могут привести к Fatal Error, если будет произведено освобождение такого общего ресурса в то время, как им продолжают пользоваться другие объекты. 

      Примечание 1
      Скорее всего, возникновение обозначенной выше проблемы с освобождением совместно используемых ресурсов обусловлено плохо написанным кодом разработчиков Autodesk: по-нормальному, для каждого совместно используемого ресурса должен вестись свой счётчик ссылок. В коде метода Dispose() значение счётчика должно сначала декрементироваться (т.е. уменьшаться на 1) и затем проверяться на равенство 0. Если равенство не выполняется, значит ресурс используется кем-то ещё - в этом случае общий ресурс не освобождается. Но если текущее значение счётчика равно 0, то ресурс можно смело освобождать. 

      Учитывая возможные проблемы при освобождении совместно используемых ресурсов, в подобных ситуациях компания Autodesk рекомендует пользоваться "методом научного тыка"  (см. вторую из обозначенных выше ссылок): если вдруг, добавив очередной вызов Dispose() вы обнаруживаете возникновение Fatal Error, то попробуйте закомментировать этот вызов. Если ошибка исчезла, значит в данном случае вызов Dispose() выполнять не следует (логично). Всех мест в API, где наблюдается проблема освобождения совместно используемых ресурсов, не знает даже Autodesk. Поэтому данные места вы порой будете определять экспериментально, методом проб и ошибок (к сожалению).

      Ресурсы всех объектов, полученных при помощи объекта Transaction, посредством метода GetObject(), а так же ресурсы новых объектов, добавленных в базу данных чертежа посредством метода AddNewlyCreatedDBObject(), будут освобождены автоматически в коде метода Dispose() этого объекта транзакции. Т.е. для них не нужно вызывать Dispose() персонально.

      Настоятельно не рекомендую инициализировать объект Transaction следующим образом:

         1:  // db - экземпляр Database
         2:  Transaction tr = db.TransactionManager.StartTransaction();
         3:  ...
         4:  tr.Commit();
         5:  tr.Dispose();

      При такой форме записи, если между инициализацией экземпляра транзакции и вызовом его метода Dispose() произойдёт необработанное исключение, то его Dispose() вызван не будет. Соответственно не будут освобождены ресурсы, связанные с этим объектом, а так же не будут вызваны методы Dispose() для всех объектов, полученных из базы данных при помощи этого экземпляра транзакции, а так же методы Dispose() новых объектов, с его помощью добавленных в базу данных чертежа.

      Дабы избежать обозначенной выше опасности, следует инициализацию объекта Transaction выполнять так:

         1:  // db - экземпляр Database
         2:  using(Transaction tr = db.TransactionManager.StartTransaction()){
         3:      ...
         4:      tr.Commit();
         5:  }

      При таком способе, Dispose() гарантированно будет вызван при выходе из блока using, как для объекта транзакции, так и для всех объектов, работающих в её контексте, даже если произойдёт необработанное исключение

      Примечание 2
      Использовать блок using следует и для инициализации объектов, созданных вне контекста транзакции, поскольку для них метод Dispose() вызывать необходимо вручную.