Как использовать DLL на .NET 8 из VBA с типизацией — публикация COM и TLB через dscom

· · C#, .NET 8, VBA, COM, Office, dscom

Ситуации, когда из VBA нужно вызвать код на .NET 8, до сих пор встречаются вполне обычно. Особенно когда хочется сохранить существующие наработки в Excel или Access как есть, но вынести в C# только тяжёлые вычисления, обработку строк, HTTP, криптографию или бизнес-логику.

Однако если полагаться на позднее связывание через CreateObject, код на стороне VBA заполняется типом Object. IntelliSense почти перестаёт работать, опечатки в именах методов обнаруживаются только во время выполнения, и постепенно вы увязаете в трясине, построенной на строках.

Поэтому в этот раз мы сосредоточимся на том, как опубликовать DLL на .NET 8 как COM-компонент, сгенерировать библиотеку типов (TLB) с помощью dscom и использовать её из VBA с типизацией через раннее связывание.

Старую историю про .NET Framework + RegAsm, ручное написание IDL и сборку через MIDL, а также тему Reg-Free COM в этот раз оставим в стороне. Здесь мы рассматриваем только один путь: .NET 8 / COM host / dscom / раннее связывание VBA.

Кроме того, весь код, встречающийся в этой статье, опубликован на GitHub в виде полного набора примеров, которые можно собрать и проверить (библиотека, публикуемая как COM-компонент, скрипты генерации и регистрации TLB, модули VBA, модульные тесты).

dotnet8-dll-typed-vba-com-dscom-tlb - komurasoft-blog-samples (GitHub)

1. Сначала — вывод

Если сразу изложить вывод, последовательность действий такая.

  • Собрать библиотеку классов .NET 8 с EnableComHosting=true
  • Создать явный интерфейс и класс, которые будут видны COM
  • Установить для класса ClassInterfaceType.None и не полагаться на AutoDual
  • Для интерфейса, используемого из VBA, установить InterfaceIsDual
  • Из полученного после сборки *.dll сгенерировать *.tlb командой dscom tlbexport
  • Зарегистрировать *.comhost.dll через regsvr32
  • Зарегистрировать *.tlb через dscom tlbregister
  • Добавить ссылку в VBA и использовать библиотеку с типизацией, например Dim x As ИмяБиблиотеки.IYourInterface

Иными словами, конфигурация такая: точка входа для COM — это *.comhost.dll, создаваемый .NET SDK, информация о типах — это *.tlb, создаваемый dscom, а VBA использует этот TLB для раннего связывания.

2. Общая картина этой конфигурации

Сначала посмотрим на одной схеме, кто за что отвечает.

Получает информацию о типах из подключённого TLBВызовы COMVBA / Excel / AccessVbaTypedComSample.tlbVbaTypedComSample.comhost.dllVbaTypedComSample.dll (.NET 8).NET 8 Runtime

Роли распределяются так.

Файл Роль
VbaTypedComSample.dll Сама реализация на .NET 8
VbaTypedComSample.comhost.dll Точка входа, вызываемая из COM
VbaTypedComSample.tlb Информация о типах, которую видит VBA
VbaTypedComSample.deps.json Информация для разрешения зависимостей
VbaTypedComSample.runtimeconfig.json Информация для запуска среды выполнения .NET

Здесь важно то, что для того, чтобы VBA узнавал типы, нужен TLB, а для того, чтобы COM мог активировать компонент, нужен comhost.

То, что нельзя просто передать один .dll файл и на этом закончить, — одна из не самых очевидных особенностей мира COM.

3. Что решить в первую очередь — согласовать разрядность 32-бит / 64-бит

Если упустить этот момент, с большой вероятностью всё скатится к ошибке Компонент ActiveX не может создать объект.

Разрядность Office/VBA и COM-сервера должны совпадать.

Сторона использования Ориентир для .NET Генерация TLB Команда регистрации
64-разрядный Office x64 / win-x64 dscom C:\Windows\System32\regsvr32.exe
32-разрядный Office (на 64-разрядной Windows) x86 / win-x86 dscom32.exe C:\Windows\SysWOW64\regsvr32.exe

В COM host начиная с .NET 5+, если оставить проект как AnyCPU, *.comhost.dll склонен собираться в сторону 64-разрядной версии и может не совпасть с 32-разрядным Office. Поэтому безопаснее явно указывать x86 / x64 в соответствии с установленным Office.

Код в этой статье рассматривается на примере 64-разрядного Office. Для 32-разрядного Office далее по тексту читайте x64 как x86, а win-x64 как win-x86.

4. Создаём часть на .NET 8

Здесь мы делаем минимальный пример, в котором из VBA можно вызвать Add, Divide и Hello.

4.1 .csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <EnableComHosting>true</EnableComHosting>
    <PlatformTarget>x64</PlatformTarget>
    <NETCoreSdkRuntimeIdentifier>win-x64</NETCoreSdkRuntimeIdentifier>
  </PropertyGroup>
</Project>

Ключевой момент здесь — EnableComHosting. Если его указать, при сборке будет сгенерирован VbaTypedComSample.comhost.dll.

4.2 По умолчанию делаем всю сборку невидимой для COM

Поскольку видимыми для COM должны быть только те типы, которые мы явно помечаем ComVisible(true), проще всего установить для всей сборки значение false.

using System.Runtime.InteropServices;

[assembly: ComVisible(false)]

4.3 Пишем публикуемые интерфейс и класс

using System.Runtime.InteropServices;

namespace VbaTypedComSample;

[ComVisible(true)]
[Guid("2A1BBEDE-DE6E-4C34-AD60-2E9E0E33E999")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICalculator
{
    [DispId(1)]
    int Add(int x, int y);

    [DispId(2)]
    double Divide(double x, double y);

    [DispId(3)]
    string Hello(string name);
}

[ComVisible(true)]
[Guid("FAD1C752-0BB6-4DDD-889F-FE446350847A")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICalculator))]
public class Calculator : ICalculator
{
    public Calculator()
    {
    }

    public int Add(int x, int y) => checked(x + y);

    public double Divide(double x, double y)
    {
        if (y == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(y), "Деление на 0 невозможно.");
        }

        return x / y;
    }

    public string Hello(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return "Hello";
        }

        return $"Hello, {name}";
    }
}

В этом коде стоит обратить внимание на следующее.

  • Guid назначается отдельно интерфейсу и классу
  • Устанавливаем ClassInterfaceType.None, чтобы не зависеть от автоматически сгенерированного интерфейса класса
  • Для удобства работы из VBA устанавливаем InterfaceIsDual
  • Назначение DispId заранее снижает риск сбоев при последующем изменении порядка методов
  • Поскольку COM создаёт объект через New, нужен публичный конструктор без параметров

5. Собираем проект

Выполняем сборку Release.

dotnet build -c Release

После сборки в выходной папке появляется как минимум следующий набор файлов.

bin/
  Release/
    net8.0-windows/
      VbaTypedComSample.dll
      VbaTypedComSample.comhost.dll
      VbaTypedComSample.deps.json
      VbaTypedComSample.runtimeconfig.json

Именно эта папка используется для распространения и регистрации. Если позже изменить место размещения, регистрацию придётся выполнять заново.

6. Генерируем TLB с помощью dscom

6.1 Для 64-бит

Сначала устанавливаем dscom.

dotnet tool install --global dscom

Затем генерируем TLB из собранной сборки.

dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

6.2 Для 32-разрядного Office

Здесь есть небольшая ловушка. Для генерации TLB под 32-разрядный Office безопаснее использовать dscom32.exe.

.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

7. Регистрируем COM host и TLB

Выполняйте это в командной строке / PowerShell, запущенной от имени администратора.

7.1 Для 64-разрядного Office / 64-разрядного COM

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"

7.2 Для 32-разрядного Office (на 64-разрядной Windows)

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\SysWOW64\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
.\tools\dscom32.exe tlbregister "$out\VbaTypedComSample.tlb"

Здесь выполняются два действия.

  • regsvr32 регистрирует *.comhost.dll как COM-сервер
  • tlbregister регистрирует *.tlb как библиотеку типов

8. Добавляем ссылку в VBA и используем библиотеку с типизацией

  1. Открыть Excel или Access
  2. Открыть редактор VBA
  3. Сервис -> Ссылки (Tools -> References)
  4. Если библиотека уже есть в списке, отметить её флажком
  5. Если в списке её нет, выбрать VbaTypedComSample.tlb через Обзор...
Option Explicit

Public Sub UseCalculator()
    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator

    Debug.Print calc.Add(10, 20)
    Debug.Print calc.Divide(10, 4)
    Debug.Print calc.Hello("VBA")
End Sub

Благодаря этому на стороне VBA появляются следующие преимущества.

  • работает IntelliSense;
  • опечатки в именах методов легче заметить ещё до выполнения;
  • в Object Browser можно посмотреть публичный API;
  • код читается лучше, чем при работе напрямую с Object.

8.1 Исключения на стороне VBA превращаются в ошибки COM

Например, если на стороне .NET выбрасывается исключение, как в случае Divide(10, 0), на стороне VBA оно проявляется как ошибка COM.

Option Explicit

Public Sub UseCalculatorWithErrorHandling()
    On Error GoTo EH

    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator

    Debug.Print calc.Divide(10, 0)
    Exit Sub

EH:
    Debug.Print Err.Number
    Debug.Print Err.Description
End Sub

9. Как подойти к распространению

При распространении важно передавать не отдельную DLL, а весь комплект выходных файлов.

VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(при необходимости — весь набор зависимых DLL)

Кроме того, на клиентском ПК должна быть установлена соответствующая среда выполнения .NET 8. COM host не рассчитан на self-contained-развёртывание — по сути, эксплуатация возможна только в режиме framework-dependent.

10. Типичные подводные камни

10.1 Не оставляйте проект в AnyCPU

Если разрядность VBA/Office и разрядность COM host расходятся, сбои получаются весьма неприятными.

  • Для 64-разрядного Office: x64 / win-x64
  • Для 32-разрядного Office: x86 / win-x86

10.2 Не используйте ClassInterfaceType.AutoDual

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

Если нужно стабильно использовать библиотеку из VBA с типизацией, стандартная практика — определить явные интерфейсы и установить для класса ClassInterfaceType.None.

10.3 Не перегенерируйте GUID необдуманно

В COM GUID — это сам контракт. Беспечная замена IID или CLSID после публикации ломает существующие ссылки и регистрации VBA.

10.4 Не ломайте уже опубликованный интерфейс

В COM даже «просто добавить один метод потом» не всегда проходит бесследно.

  • Оставляйте ICalculator как есть
  • Если изменения существенные, создавайте новый интерфейс, например ICalculator2
  • Класс может реализовывать оба интерфейса

10.5 Держитесь простых типов

На границе, которую видит VBA, безопаснее не мудрить.

Хорошо сочетаются, для начала, следующие типы:

  • int
  • double
  • bool
  • string
  • DateTime
  • decimal
  • enum

10.6 Не обновляйте DLL, пока открыт Office

Excel или Access может удерживать DLL занятой, что создаёт проблемы при сборке или повторной регистрации.

  • Закрыть Office
  • При необходимости отменить регистрацию
  • Пересобрать проект
  • Зарегистрировать заново

11. Итог

Тема «использовать DLL на .NET 8 из VBA с типизацией» перестаёт быть страшной процедурой, если свести её к публикации COM-компонента + генерации TLB через dscom. На стороне .NET 8 нужно установить EnableComHosting=true и подготовить явные интерфейсы (класс — с ClassInterfaceType.None, интерфейс для VBA — с InterfaceIsDual), сгенерировать TLB командой dscom tlbexport, зарегистрировать *.comhost.dll через regsvr32, а *.tlb — через dscom tlbregister. После этого остаётся лишь добавить ссылку в VBA и использовать раннее связывание.

Если возникают сомнения, помогает приём — рассматривать COM host и TLB раздельно.

  • Точка входа — *.comhost.dll
  • Информация о типах — *.tlb
  • Сама реализация — *.dll

12. Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Разработка приложений для Windows

Проектирование интеграционного слоя, охватывающего VBA, COM, Office, .NET 8 и генерацию библиотек типов, тесно связано с разработкой Windows-приложений, поэтому эта тема хорошо сочетается с услугой разработки Windows-приложений.

Технические консультации и ревью дизайна

Если нужно упорядочить проектирование границы между существующими VBA-активами и .NET 8, включая разрядность (bitness), регистрацию, генерацию TLB и способ распространения, эту задачу удобно вести как техническую консультацию и ревью архитектуры.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что нужно, чтобы использовать DLL на .NET 8 из VBA с типизацией (ранним связыванием)?
Нужно собрать библиотеку классов .NET 8 с EnableComHosting=true, чтобы получить *.comhost.dll, а затем сгенерировать *.tlb командой dscom tlbexport. Далее *.comhost.dll регистрируется через regsvr32, а *.tlb — через dscom tlbregister; после добавления этого TLB в ссылки VBA можно использовать библиотеку типизированно, в виде Dim x As ИмяБиблиотеки.IYourInterface. Распределение ролей такое: точка входа для COM — *.comhost.dll, информация о типах, которую видит VBA, — *.tlb, а сама реализация — *.dll.
В чём причина ошибки «Компонент ActiveX не может создать объект»?
Типичная причина — несовпадение разрядности (bitness) между Office/VBA и COM-сервером. Для 64-разрядного Office нужно собирать проект как x64/win-x64 и регистрировать через regsvr32 из System32, а для 32-разрядного Office (на 64-разрядной Windows) — как x86/win-x86, регистрировать через regsvr32 из SysWOW64, а TLB генерировать через dscom32.exe. В COM host на .NET 5+ проект, оставленный как AnyCPU, склонен собираться в сторону 64-разрядной версии и может не совпасть с 32-разрядным Office, поэтому безопаснее явно указывать x86 или x64 в соответствии с установленным Office.
Можно ли использовать ClassInterfaceType.AutoDual?
На первый взгляд это удобно, но такой подход стоит избегать: после публикации легко всё сломать, изменив порядок членов или состав класса. Чтобы стабильно использовать библиотеку из VBA с типизацией, стандартная практика — определить явные интерфейсы, установить для класса ClassInterfaceType.None, а для интерфейса, используемого из VBA, — InterfaceIsDual. Назначение DispId заранее снижает риск сбоев при изменении порядка методов. Кроме того, в COM GUID — это сам контракт, поэтому беспечная перегенерация IID или CLSID после публикации ломает существующие ссылки и регистрации VBA.
Достаточно ли при распространении передать только DLL?
Нет, одной DLL недостаточно. Нужно разместить вместе основную реализацию *.dll, *.comhost.dll, *.deps.json, *.runtimeconfig.json, *.tlb и при необходимости весь набор зависимых DLL. Кроме того, на клиентском ПК должна быть установлена соответствующая среда выполнения .NET 8: COM host рассчитан на framework-dependent развёртывание, а не на self-contained. Стоит также учитывать, что при последующем изменении места размещения регистрацию придётся выполнять заново.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог