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

Однако, в отличие от отелей, некоторые приложения устарели настолько, что программные эквиваленты архитекторов, сантехников, плотников, электриков, каменщиков и т. д. больше не существуют, оставляя персонал отеля на произвол судьбы, когда внезапно гаснет свет и гости злятся. . Это было со мной несколько лет назад, когда приложение, написанное на самом «ужасном языке программирования» (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.
ТЕСТИРОВАНИЕ
- Скопируйте файлы 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 году