Ситуации, когда из 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. Общая картина этой конфигурации
Сначала посмотрим на одной схеме, кто за что отвечает.
flowchart LR
VBA["VBA / Excel / Access"] -->|Получает информацию о типах из подключённого TLB| TLB["VbaTypedComSample.tlb"]
VBA -->|Вызовы COM| COMHOST["VbaTypedComSample.comhost.dll"]
COMHOST --> DOTNET["VbaTypedComSample.dll (.NET 8)"]
DOTNET --> RUNTIME[".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 и используем библиотеку с типизацией
- Открыть Excel или Access
- Открыть редактор VBA
Сервис->Ссылки(Tools -> References)- Если библиотека уже есть в списке, отметить её флажком
- Если в списке её нет, выбрать
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, безопаснее не мудрить.
Хорошо сочетаются, для начала, следующие типы:
intdoubleboolstringDateTimedecimalenum
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. Справочные материалы
- Полный набор примеров кода к этой статье (библиотека, публикуемая как COM-компонент, скрипты, VBA, тесты) - komurasoft-blog-samples (GitHub)
- Expose .NET components to COM - Microsoft Learn
- Квалификация типов .NET для взаимодействия с COM - Microsoft Learn
- Перечисление ComInterfaceType - Microsoft Learn
- Перечисление ClassInterfaceType - Microsoft Learn
- COM-совместимая обёртка (COM Callable Wrapper) - Microsoft Learn
- Класс DispIdAttribute - Microsoft Learn
- dscom - NuGet Gallery
- How to use the Regsvr32 tool and troubleshoot Regsvr32 error messages - Microsoft Support
- .NET 8 downloads
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Почему процесс EXCEL.EXE остаётся висеть после работы с Excel через COM в C# — паттерны освобождения ссылок и когда стоит отказаться от COM
Разбираем, почему при автоматизации Excel из C# через Microsoft.Office.Interop.Excel остаётся висеть процесс EXCEL.EXE, опираясь на подсч...
Перенос макросов Excel VBA на Power Automate — что можно заменить Office Scripts, а что оставить как VBA
Разбираем, можно ли перенести макросы Excel VBA на Power Automate: что заменяется Office Scripts, что умеет только VBA, ограничения конне...
Обратная совместимость интерфейсов DLL и COM — таблица решений: какие изменения ломают вызывающую сторону
Какие изменения DLL или COM-компонента на самом деле ломают вызывающую сторону? Разбираем три слоя совместимости — бинарную, исходную и п...
Работают ли бизнес-приложения на Windows на Arm — реальность x64-эмуляции (Prism) и нативных DLL/COM
Отвечаем разработчикам и ИТ-специалистам на вопрос «заработает ли наше бизнес-приложение на Windows на Arm». Разбираем принцип работы x64...
Как выбрать межпроцессное взаимодействие в Windows — таблица решений: именованные каналы / TCP / gRPC / разделяемая память / COM
Разбираем, как выбрать способ взаимодействия Windows-приложений друг с другом: сильные стороны и ловушки именованных каналов, локального ...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Миграция ActiveX
Решения о сохранении, обёртке или замене компонентов COM / ActiveX / OCX.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки