Старые приложения никогда не умирают, они просто продолжают существовать, как ветхий отель, готовый оставаться открытым до тех пор, пока здание не рухнет или туалеты, наконец, не перестанут смываться.

Однако, в отличие от отелей, некоторые приложения устарели настолько, что программные эквиваленты архитекторов, сантехников, плотников, электриков, каменщиков и т. д. больше не существуют, оставляя персонал отеля на произвол судьбы, когда внезапно гаснет свет и гости злятся. . Это было со мной несколько лет назад, когда приложение, написанное на самом «ужасном языке программирования» (3 года работы), внезапно и необъяснимо перестало работать.

Ниже приводится просто журнал моих усилий по обнаружению и устранению проблемы в таком приложении, основанный на инструментах, сообщениях в блогах и документации прошлых лет.

Проблема и предыстория

«Приложение перестает работать при установке .NET 4.7 на рабочих станциях клиентов. Во многих случаях .NET 4.7 автоматически устанавливалась как часть обновления Windows».

Ошибка проявляется как:

Run-time error '430':
Class does not support Automation or does not support expected interface

Это объем информации, которую я имел при рассмотрении проблемы. Рассматриваемое приложение использует COM Interop для связи с библиотеками C# .NET.

Начальное устранение неполадок

Мои первоначальные наблюдения. Я буду использовать слово Foo вместо настоящего имени dll:

  • Откат обновления .NET 4.7 устраняет неисправную среду.
  • Ошибки Fusion Log были одинаковыми на сломанной и рабочей среде
  • Были некоторые ошибки, указывающие на то, что Foo.resources.dll не был найден, но это было одинаково в рабочей и сломанной средах.
  • Были некоторые ошибки, указывающие на то, что Foo.Serializor.dll не найден, но это было одинаково в рабочей и сломанной средах.
  • В Procmon.exe ошибки RegQueryKey NAME NOT FOUND были одинаковыми в рабочей и сломанной средах.
  • Были некоторые ошибки, указывающие на то, что HKLM\SOFTWARE\Microsoft\Fusion\PublisherPolicy\Default\v4.0_policy.9.12.Foo.resources_en-US_c618714dcc4dcf7c не найдено (и подобные записи), но они были одинаковыми в обеих средах.
  • Файл Foo.dll не изменился в рабочей и сломанной средах.
  • Файл Foo.tlb не изменился в рабочей и сломанной средах.
  • Записи DebugView не выявили никакой полезной информации, кроме указания нам на строки:
Dim Bar As Foo.MyForm

Set Bar = New Foo.MyForm
  • Элемент управления Foo.MyObject представляет собой элемент управления .NET System.Windows.Forms.Form.
  • API System.Windows.Forms изменен в .NET 4.7
namespace System.Windows.Forms {
	public class Control : Component, IArrangedElement, IBindableComponent, IComponent, IDisposable, IDropTarget, ISynchronizeInvoke, IWin32Window {
  public int DeviceDpi { get; }
  public int LogicalToDeviceUnits(int value);
  protected virtual void OnDpiChangedAfterParent(EventArgs e);
  protected virtual void OnDpiChangedBeforeParent(EventArgs e);
  protected virtual void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew);
  public void ScaleBitmapLogicalToDevice(ref Bitmap logicalBitmap);
  public event EventHandler DpiChangedAfterParent;
  public event EventHandler DpiChangedBeforeParent;
}
  • Создание приложения, использующего Foo.dll, на компьютере с .NET 4.7 решает проблему на компьютере с 4.7.

Воссоздание проблемы

Мне удалось воспроизвести проблему в небольшом изолированном примере проекта со следующими артефактами.

  • TestCOMInterop.dll — содержит одну оконную форму .NET с именем TestForm.
  • TestCOMvb.exe — исполняемый файл VB6, который создает экземпляр TestCOMInterop.TestForm и вызывает его метод show.

ПРОЦЕСС СОЗДАНИЯ

На машине .Net 4.6.2

  • Сборка TestCOMInterop.dll
  • Зарегистрируйте его в GAC и для COM
gacutil.exe /i TestCOMInterop.dll
C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe TestCOMInterop.dll /tlb
rem C:\Windows\Microsoft.NET\Framework\v4.0.30319\regtlibv12.exe TestCOMInterop.tlb
  • Откройте файл TestCOMvb.vbp.
  • Добавьте ссылку на TestCOMInterop.tlb (от GAC, а не от файловой системы).
  • Запустите команду Make из VB6 IDE, чтобы создать TestCOMvb.exe.
  • Запустите TestCOMvb.exe и убедитесь, что он успешно создает и отображает TestCOMInterop.TestForm.

ТЕСТИРОВАНИЕ

  1. Скопируйте файлы TestCOMInterop.dll и TestCOMvb.exe на компьютер .NET 4.7. Запустите команды регистрации для TestCOMInterop.dll (* из приведенного выше процесса сборки).
  • Запустите TestCOMvb.exe. Обратите внимание, что вы получаете ошибку.

Run-time error '430':

Class does not support Automation or does not support expected interface

  • Повторите описанные выше шаги на компьютере .NET 4.6.2 и обратите внимание, что вы не получаете сообщение об ошибке и все работает нормально.
  • Скопируйте все исходные артефакты на компьютер .NET 4.7. Повторите все шаги из раздела Процесс сборки. Исполняемый файл, созданный в этой среде, будет корректно работать в среде .NET 4.7.

Вторичное устранение неполадок

  • Теперь мои подозрения обратились к файлу TestCOMInterop.tlb, сгенерированному regasm.exe.
  • С помощью oleview.exe мы можем увидеть COM-интерфейсы, сгенерированные в TestCOMInterop.tlb.
  • Просмотр выходных данных TestCOMInterop.tlb из сред .NET 4.7 и .NET 4.6.2 выявил следующие различия.
  • uuid для interface _TestForm : IDispatch был другим
  • Были дополнительные методы интерфейса из среды .NET 4.7
HRESULT DeviceDpi([out, retval] long* pRetVal);
HRESULT LogicalToDeviceUnits(
[in] long value,
[out, retval] long* pRetVal);
HRESULT ScaleBitmapLogicalToDevice([in, out] _Bitmap** logicalBitmap);
  • Последовательность идентификаторов методов ([id(0x###)]) была изменена из-за добавления новых методов.

.NET 4.6.2

[id(0x60020122)]
HRESULT Invalidate([in] IUnknown* Region);

.NET 4.7

[id(0x60020127)]
HRESULT Invalidate([in] IUnknown* Region);

Объяснение проблемы

Используя различия в файлах TLB, мои подозрения теперь указывали на то, что причиной несовместимости являются изменения интерфейса.

  • uuid в файлах TLB соответствуют идентификаторам интерфейса .NET (IID). Эти идентификаторы используются при создании VTBL
    . VTBL представляет собой виртуальную таблицу с адресом функции, используемую COM-клиентами для запросов с использованием IUnknown::QueryInterface и для вызова этих функций. источник
  • Когда .Net создает IID:
    — IID генерируются на основе хэша полного имени интерфейса и подписей всех членов интерфейса, предоставляемых через Interop. источник
  • Это означает, что любые изменения в интерфейсе вызывают создание нового IID -> uuid.
  • VB6 использует раннее связывание VTBL для создания сопоставлений между собой и компонентами COM во время во время компиляции.
    . Этот подход, называемый связыванием VTBL, не использует интерфейс IDispatch. Клиент получает информацию о типах из библиотеки типов во время компиляции, а затем напрямую вызывает методы и функции. Привязка VTBL выполняется быстрее, чем привязка идентификатора и поздняя привязка, поскольку доступ осуществляется напрямую, и вызовы через IDispatch не выполняются. источник
  • Поскольку наше приложение VB6 было построено в среде .NET 4.6.2 с использованием .tlb, который генерировал идентификатор интерфейса (uuid) для этой среды, мы получили несоответствие uuid → iid при выполнении нашей .dll.

Причина и решение

Основная проблема заключалась в том, что наш класс был украшен [ClassInterface(ClassInterfaceType.AutoDual)]

[ClassInterface(ClassInterfaceType.AutoDual)]
[Guid("48ACC03F-853F-4FDE-9CA3-18A2D4648118")]
[ComVisible(true)]
public partial class TestForm : Form

Это указывает на то, что этот класс должен автоматически генерировать свой собственный COM-интерфейс (tlb). Microsoft прямо предостерегает от использования автоматически сгенерированных интерфейсов.

  • Всегда атрибутируйте COM-видимые классы и интерфейс с GuidAttribute, используя уникальные значения.
  • Никогда не используйте автоматически сгенерированные интерфейсы классов; всегда явно определяйте свои интерфейсы.
  • Никогда не меняйте опубликованную реализацию интерфейса; получить новый интерфейс как новый интерфейс класса или другой интерфейс для реализации. источник

Он также предоставляет предупреждение о совместимости CA1408.

Типы, использующие двойной интерфейс, позволяют клиентам привязываться к определенному макету интерфейса. Любые изменения в будущей версии макета типа или любых базовых типов нарушат COM-клиенты, которые привязываются к интерфейсу. По умолчанию, если атрибут ClassInterfaceAttribute не указан, используется интерфейс только для отправки.

Если не указано иное, все общедоступные неуниверсальные типы видны для COM; все непубличные и универсальные типы невидимы для COM.

Переключившись на [ClassInterface(ClassInterfaceType.None)] и добавив явный интерфейс ITestForm:

namespace TestCOMInterop
{
       [ClassInterface(ClassInterfaceType.None)]
       [Guid("48ACC03F-853F-4FDE-9CA3-18A2D4648118")]
       [ComVisible(true)]
       public partial class TestForm : Form, ITestForm
       {
              public TestForm()
	      {
	             InitializeComponent();
	      }
	      public new void Show()
	      {
		     base.Show();
	      }
       }
       [Guid("F9BCF165-62DA-4DF0-95E6-6630B2437BB2")]
       [ComVisible(true)]
       public interface ITestForm
       {
              void Show();
       }
}

Мы можем явно управлять интерфейсом, представленным в файле TestCOMInterop.tlb, в результате чего исполняемый файл не зависит от изменений в .Net.

[
uuid(F9BCF165-62DA-4DF0-95E6-6630B2437BB2),
version(1.0),
dual,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "TestCOMInterop.ITestForm")
]
dispinterface ITestForm {
properties:
methods:
	[id(0x60020000)]
	void Show();
};

Полученный файл TestCOMvb.exe теперь работает везде.

Параметры изменений.

  • Вариант 1. Все объекты [ClassInterface(ClassInterfaceType.AutoDual)], которые предоставляют методы/свойства/поля, которые не находятся под нашим контролем, должны быть переключены на [ClassInterface(ClassInterfaceType.None)] с явно определенными интерфейсами, если компонент используется COM.
  • Вариант 2. Все COM-использования этих компонентов должны быть преобразованы для использования подхода позднего связывания. Преимущество в том, что нужно будет изменить только компоненты VB6. Компромисс заключается в снижении производительности (которое может не иметь значения или может быть незаметным в зависимости от контекста), и вы теряете интеллектуальное восприятие объекта в VB6 IDE. Вы также получите сбой во время выполнения, если метод интерфейса, который вы вызываете, изменился (его имя, сигнатура).

В качестве альтернативы мы можем использовать позднее связывание вместо раннего связывания VTBL. источник

Раннее связывание

Dim xlApp1 As Excel.Application
Set xlApp1 = New Excel.Application

Поздняя привязка

Dim xlApp2 As Object
Set xlApp2 = CreateObject("Excel.Application")

Сценарии автоматизации MS Office (Word, Excel) (макросы) часто используют позднее связывание, чтобы отделить установленную версию Word от запущенного сценария. источник, источник2

Вот еще более глубокий взгляд на позднее и раннее связывание в COM.

Эта статья была написана в 2017 году