История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619716)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Как использовать DLL на .NET 8 из VBA с типизацией — публикация COM и TLB через dscom. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619716 https://comcomponent.com/ru/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
- DOI (последняя версия)
- 10.5281/zenodo.21619716
- DOI (эта версия)
- 10.5281/zenodo.21619717
Ситуации, когда из VBA нужно вызвать код на .NET 8, до сих пор встречаются вполне обычно. Особенно когда существующие наработки в Excel или Access хочется оставить как есть, а в C# унести только тяжёлые вычисления, обработку строк, HTTP, криптографию или бизнес-логику.
Но если опереться на позднее связывание через CreateObject, на стороне VBA всё становится Object. IntelliSense слабеет, опечатки в именах методов находятся только во время выполнения, и со временем вы оказываетесь в хрупком коде, который живёт строковыми именами.
flowchart TB
accTitle: Хрупкость позднего связывания
accDescr: Если опереться на позднее связывание через CreateObject, на стороне VBA всё становится Object, IntelliSense слабеет, и опечатки в именах методов находятся только во время выполнения.
lb1["Позднее связывание через CreateObject"] --> lb2["На стороне VBA сплошной Object"]
lb2 --> lb3["IntelliSense слабеет"]
lb2 --> lb4["Опечатка всплывает только в runtime"]
Рис. 1: Чем сильнее опираетесь на позднее связывание, тем хрупче становится код из-за строковых имён.
Поэтому в этот раз сузим тему до одного пути: опубликовать DLL на .NET 8 как COM, сгенерировать библиотеку типов (TLB) через dscom и вызывать её из VBA типизированно, ранним связыванием.
Старую историю про .NET Framework + RegAsm, ручное написание IDL и сборку через MIDL, а также Reg-Free COM в этот раз оставляем в стороне. Здесь только одна дорога: .NET 8 / COM host / dscom / VBA early binding.
Код из этой статьи опубликован на GitHub как полный набор примеров, которые можно собрать и проверить (библиотека, публикуемая как COM, скрипты генерации и регистрации TLB, модули VBA, модульные тесты).
dotnet8-dll-typed-vba-com-dscom-tlb - komurasoft-blog-samples (GitHub)
Необходимая среда
| Что | Что нужно |
|---|---|
| ОС | Windows. Идёт регистрация COM, поэтому regsvr32 должен запускаться с правами администратора |
| .NET SDK | .NET 8 SDK. EnableComHosting — возможность .NET 5 и новее |
| Office | Excel или Access. Сначала выясните, 32-разрядный это выпуск или 64-разрядный (глава 3) |
| Средство генерации TLB | dscom. Для 64-бит и 32-бит способы получения разные (глава 6) |
| Клиентский ПК | Среда выполнения .NET 8 той же разрядности, что и Office (глава 9) |
Перед тем как идти по шагам, настоятельно советую записать версии своей среды. Когда позже окажется «те же шаги, а не работает», сравнивать будет не с чем, кроме этих записей.
# Список .NET SDK и runtime (видно, стоят ли x64 и x86)
dotnet --info
# Номер сборки Windows
winver
Разрядность и выпуск Office можно посмотреть в Excel: Файл > Учётная запись > Сведения о Excel. В конце строки заголовка диалога будет 32-разрядная или 64-разрядная.
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 и идёт в раннее связывание.
flowchart TB
accTitle: Одна дорога до типизированного вызова
accDescr: Собираем с EnableComHosting, делаем TLB через dscom tlbexport, регистрируем comhost через regsvr32 и TLB через dscom tlbregister, затем добавляем ссылку в VBA и вызываем типизированно.
st1["Сборка с EnableComHosting"] --> st2["TLB через dscom tlbexport"]
st2 --> st3["Регистрация comhost через regsvr32"]
st3 --> st4["Регистрация TLB через dscom tlbregister"]
st4 --> st5["Ссылка в VBA и типизированный вызов"]
Рис. 2: Если идти в порядке сборка → генерация TLB → две регистрации → ссылка, вызов получается типизированным.
Карта знаний этой статьи
Чтобы типизированно использовать библиотеку классов .NET 8 из VBA, необходима библиотека типов COM; её генерирует и регистрирует инструмент dscom — преемник tlbexp.exe и RegAsm.exe, которые в .NET Framework уже недоступны. На стороне .NET точкой входа COM служит COM host, собранный с EnableComHosting; предпосылки — регистрация через regsvr32 и совпадение разрядности с Office. Сторона VBA загружает библиотеку типов в ссылках проекта и получает раннее связывание — это типобезопаснее, чем позднее связывание через CreateObject. Совместимость после публикации зависит от обращения с IID и CLSID и от выбора ClassInterfaceType; сочетание ClassInterfaceType.None, InterfaceIsDual и DispId — установившаяся практика, которая не даёт сломать ссылки VBA.
flowchart LR
accTitle: Карта знаний: типизированное использование DLL .NET 8 из VBA
accDescr: Схема, которая показывает, что для типизированного раннего связывания COM из VBA нужна библиотека типов; что dscom отвечает за её генерацию и регистрацию; что сторона .NET 8 публикуется как COM host; и как ClassInterfaceType и обращение с IID и CLSID влияют на совместимость ссылок VBA
vba["VBA (Visual Basic for Applications)"]
dscom["dscom"]
type_library["библиотека типов (TLB)"]
com_early_binding["раннее связывание (VBA)"]
com_late_binding["позднее связывание (CreateObject)"]
tlbexp_regasm["tlbexp.exe / RegAsm.exe"]
comhost["COM host (*.comhost.dll)"]
regsvr32["regsvr32"]
dotnet[".NET (начиная с Core)"]
com["COM (Component Object Model)"]
iid["IID (идентификатор интерфейса)"]
clsid["CLSID (Class ID)"]
vba_reference_break["повреждение ссылок и регистрации VBA"]
classinterfacetype_autodual["ClassInterfaceType.AutoDual"]
classinterfacetype_none["ClassInterfaceType.None"]
dispid_attribute["DispIdAttribute"]
interface_is_dual["InterfaceIsDual (dual interface)"]
hresult["HRESULT"]
dotnet_exception["исключение .NET"]
com_visible_attribute["ComVisibleAttribute"]
bitness_match_requirement["требование совпадения разрядности"]
vba -.->|"требует"| type_library
com_early_binding -->|"требует"| type_library
vba -->|"использует"| com_early_binding
vba -->|"использует"| com_late_binding
com_late_binding -.->|"не рекомендуется"| vba
dscom -->|"реализует"| type_library
dscom -->|"преемник"| tlbexp_regasm
type_library -.->|"настраивается"| dscom
comhost -->|"настраивается"| regsvr32
comhost -->|"требует"| dotnet
comhost -->|"реализует"| com
vba -.->|"использует"| com
com -->|"требует"| iid
com -->|"требует"| clsid
iid -.->|"может вызвать"| vba_reference_break
clsid -.->|"может вызвать"| vba_reference_break
classinterfacetype_autodual -.->|"может вызвать"| vba_reference_break
classinterfacetype_autodual -->|"не рекомендуется"| vba
classinterfacetype_none -->|"рекомендуется для"| vba
dispid_attribute -->|"снижает"| vba_reference_break
interface_is_dual -->|"рекомендуется для"| vba
com -->|"использует"| hresult
dotnet_exception -.->|"проверяется"| hresult
dotnet -->|"настраивается"| com_visible_attribute
comhost -->|"требует"| bitness_match_requirement
dscom -.->|"требует"| bitness_match_requirement
regsvr32 -->|"требует"| bitness_match_requirement
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 27, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
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"]
Рис. 3: VBA берёт информацию о типах из TLB и вызывает реализацию на .NET 8 через comhost.
Роли распределяются так.
| Файл | Роль |
|---|---|
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.
flowchart TB
accTitle: Несовпадение, к которому ведёт AnyCPU
accDescr: Если оставить AnyCPU, comhost склонен уезжать в 64-бит и может не совпасть с 32-разрядным Office, поэтому безопаснее явно указать x86 или x64 под Office.
b1["Оставить AnyCPU"] --> b2["comhost склонен уехать в 64-бит"]
b2 --> b3["Не совпадает с 32-разрядным Office"]
b3 -.-> b4["Явно указать битность под Office"]
Рис. 4: Расхождение bitness — короткий путь к «невозможно создать объект».
Код в этой статье разбираем на примере 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, поэтому нужен открытый конструктор без параметров
flowchart TB
accTitle: Как собрать публикуемые типы
accDescr: Явный интерфейс ICalculator получает InterfaceIsDual и DispId, класс Calculator реализует его с ClassInterfaceType.None, Guid назначается интерфейсу и классу отдельно.
if1["ICalculator (явный интерфейс)"] --> d1["InterfaceIsDual и DispId"]
cl1["Calculator (класс)"] -->|"реализует"| if1
cl1 --> d2["ClassInterfaceType.None"]
if1 -.-> g1["Guid — каждому свой"]
cl1 -.-> g1
Рис. 5: Типизированная публикация держится на паре «явный интерфейс + класс с None».
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 Что такое dscom
dscom — это открытый инструмент командной строки, который генерирует и регистрирует COM-библиотеку типов (TLB) из сборки .NET. Его публикует компания dSPACE, лицензия — Apache-2.0.
Зачем он нужен: начиная с .NET 5 упразднены tlbexp.exe и RegAsm.exe. В эпоху .NET Framework этими двумя средствами генерировали TLB и регистрировали сборку, а в .NET 5+ преемника в поставке нет. dscom как раз закрывает эту дыру.
flowchart TB
accTitle: Какую дыру закрывает dscom
accDescr: В эпоху .NET Framework TLB генерировали и сборку регистрировали tlbexp.exe и RegAsm.exe; начиная с .NET 5 оба упразднены, преемника в поставке нет, и эту дыру закрывает dscom.
old1["Эпоха .NET Framework"] --> old2["tlbexp.exe и RegAsm.exe"]
new1[".NET 5 и новее"] --> new2["Оба упразднены, преемника нет"]
new2 --> ds1["Дыру закрывает dscom"]
Рис. 6: Начиная с .NET 5 инструмент генерации TLB сменился на dscom.
Из подкоманд достаточно запомнить эти.
| Подкоманда | Роль |
|---|---|
tlbexport |
Выгрузить TLB из сборки |
tlbregister |
Зарегистрировать TLB в системе |
tlbunregister |
Снять регистрацию TLB |
tlbdump |
Вывести содержимое TLB и проверить его |
tlbembed |
Встроить TLB в файл |
tlbdump удобен тем, что ещё до открытия VBA можно проверить, попали ли в сгенерированный TLB задуманные типы.
6.2 Для 64-бит
Если нужна только 64-битная TLB, достаточно dotnet tool.
dotnet tool install --global dscom
Затем генерируем TLB из собранной сборки.
dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
6.3 Для 32-разрядного Office — откуда брать dscom32.exe
Именно здесь 32-разрядный Office чаще всего стопорится.
dscom, который ставится через dotnet tool install, умеет работать только со сборками AnyCPU или 64-бит и генерирует только 64-битную TLB. Чтобы сделать 32-битную TLB, нужен отдельный исполняемый файл dscom32.exe, и его берут не из NuGet, а скачивают со страницы релизов на GitHub.
- Откуда брать: https://github.com/dspace-group/dscom/releases
dscom.exe… 64-битная TLB из сборки AnyCPU или 64-битdscom32.exe… 32-битная TLB из сборки AnyCPU или 32-бит
Скачанный dscom32.exe в примере этой статьи лежит в папке tools в корне проекта. Куда класть — на ваше усмотрение, но не распространяйте его вместе с выходными файлами сборки. Это инструмент разработки, в runtime он не нужен.
Есть ещё одна предпосылка, которую легко пропустить. Чтобы запустить dscom32.exe, нужна x86-сборка среды выполнения .NET. Это из-за того, как dscom загружает hostfxr.dll: среда, где стоит только x64, его не поднимет. В выводе dotnet --info проверьте, есть ли x86 runtime.
flowchart TB
accTitle: Подготовка к 32-битной TLB
accDescr: dscom из dotnet tool делает только 64-битную TLB, поэтому 32-битную TLB делают dscom32.exe со страницы релизов на GitHub, а для запуска нужна x86-сборка среды выполнения .NET.
p1["Нужна 32-битная TLB"] --> p2["Взять dscom32.exe со страницы релизов"]
p2 --> p3["Проверить, есть ли x86 runtime"]
p3 --> p4["tlbexport через dscom32.exe"]
p1 -.-> p5["Вариант из dotnet tool — только 64-битная TLB"]
Рис. 7: Для 32-бит уже путь получения инструмента другой, поэтому здесь легко застрять.
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
В документации dscom тоже сказано: если оставить AnyCPU, *.comhost.dll собирается как 64-бит, поэтому для 32-битного использования саму сборку рекомендуют компилировать как 32-бит. Это тот же вывод, что и в главе 3.
Если не хочется каждый раз запускать это руками после сборки, пакет dSPACE.Runtime.InteropServices.BuildTasks умеет генерировать 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как библиотеку типов
flowchart TB
accTitle: Регистрация идёт в две линии
accDescr: С правами администратора regsvr32 регистрирует comhost как COM-сервер, а dscom tlbregister регистрирует TLB как библиотеку типов — две отдельные регистрации.
adm["Запуск с правами администратора"] --> r1["regsvr32 регистрирует comhost"]
adm --> r2["tlbregister регистрирует TLB"]
r1 -.-> m1["Регистрация точки входа COM"]
r2 -.-> m2["Регистрация типов, которые видит VBA"]
Рис. 8: Точка входа и информация о типах — разные вещи, поэтому и регистрации две, комплектом.
8. Добавляем ссылку в VBA и вызываем типизированно
- Открыть Excel или Access
- Открыть редактор VBA (VBE) сочетанием
Alt+F11. Из ленты это вкладкаРазработчик>Visual Basic. Если вкладкиРазработчикнет, включите её:Файл>Параметры>Настройка ленты, флажокРазработчик - В меню VBE:
Сервис>Ссылки - Список
Доступные ссылкиидёт по алфавиту. Если регистрация прошла, в нём будет имя библиотеки (по умолчанию то же, что у сборки:VbaTypedComSample) — поставьте флажок слева иОК - Если в списке её нет, кнопкой
Обзор...выберитеVbaTypedComSample.tlbнапрямую
Если библиотека в списке не появляется, причина почти всегда одна из двух: несовпадение bitness из главы 3 или не прошедший tlbregister из главы 7. Из 32-разрядного Office 64-битная зарегистрированная TLB не видна.
flowchart TB
accTitle: Как разобрать отсутствие в списке ссылок
accDescr: Если библиотеки нет в списке ссылок, причина почти всегда либо несовпадение bitness, либо не прошедший tlbregister; из 32-разрядного Office 64-битная TLB не видна.
q1["Библиотеки нет в списке"] --> a1["Подозревать bitness из главы 3"]
q1 --> a2["Подозревать сбой tlbregister из главы 7"]
a1 -.-> nt1["Из 32-битного Office 64-битная TLB не видна"]
Рис. 9: Если библиотеки нет в списке, почти всегда это либо bitness, либо регистрация.
Проверить, что ссылка встала, можно так: Вид > Просмотр объектов (F2), в выборе библиотеки слева сверху должен находиться VbaTypedComSample. Если видны ICalculator и Calculator, а также Add / Divide / Hello, 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
Поставьте курсор на эту процедуру, выполните её через F5 и откройте окно Immediate сочетанием Ctrl + G: появятся три строки в соответствии с реализацией главы 4.
30
2.5
Hello, VBA
Если здесь всё как ожидалось, пройден весь путь: ссылка, регистрация COM, запуск runtime, маршалинг аргументов и возвращаемых значений. Если значения не те — смотрите реализацию на стороне .NET; если выполнить вообще нельзя — подозревайте главы 3 и 7.
flowchart TB
accTitle: Разбор по результату выполнения
accDescr: Если образец даёт ожидаемый результат, путь от ссылки до маршалинга цел; если значения не те — смотреть реализацию .NET; если выполнить нельзя — bitness из главы 3 и регистрацию из главы 7.
r1["Выполнить образец VBA"] --> q1{"Какой результат?"}
q1 -->|"как ожидалось"| ok1["Весь путь цел"]
q1 -->|"значения не те"| ng1["Смотреть реализацию .NET"]
q1 -->|"выполнить нельзя"| ng2["Смотреть bitness и регистрацию"]
Рис. 10: По трём строкам вывода уже понятно, какой слой подозревать.
После этого на стороне VBA появляются такие преимущества.
- работает IntelliSense
- опечатки в именах методов легче поймать ещё до выполнения
- публичный API можно посмотреть в Object Browser
- читать легче, чем сплошной
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 Hex$(Err.Number)
Debug.Print Err.Description
End Sub
Если сразу понять, как читать эти значения, разбор идёт быстрее.
| Поле | Что туда попадает |
|---|---|
Err.Number |
HRESULT, соответствующий исключению .NET, в виде знакового Long. В десятичном виде читать неудобно, поэтому переводите в шестнадцатеричный через Hex$(Err.Number) |
Err.Description |
Через COM IErrorInfo приходит сообщение исключения .NET как есть. Для кода выше это строка, содержащая Деление на 0 невозможно. |
Значение HRESULT зафиксировано для каждого типа исключения. Для ArgumentOutOfRangeException это COR_E_ARGUMENTOUTOFRANGE, значение 0x80131502. То есть если Hex$(Err.Number) даёт 80131502, дошла именно ожидаемая ArgumentOutOfRangeException со стороны .NET.
Основные соответствия такие.
| Исключение .NET | Константа HRESULT | Значение |
|---|---|---|
ArgumentException |
COR_E_ARGUMENT |
0x80070057 |
ArgumentOutOfRangeException |
COR_E_ARGUMENTOUTOFRANGE |
0x80131502 |
InvalidOperationException |
COR_E_INVALIDOPERATION |
0x80131509 |
NotSupportedException |
COR_E_NOTSUPPORTED |
0x80131515 |
| Прочие обычные исключения | COR_E_EXCEPTION |
0x80131500 |
Если на стороне VBA нужно ветвить обработку по типу исключения, ветвление пойдёт по этому HRESULT. Но схема «ветвить по HRESULT» хрупка к смене типа исключения на стороне .NET, поэтому деловые отказы на границе стабильнее возвращать не исключением, а возвращаемым значением или кодом ошибки.
flowchart TB
accTitle: Как исключение .NET доходит до VBA
accDescr: Исключение, выброшенное на стороне .NET, на границе COM превращается в HRESULT: в Err.Number оно попадает как знаковый Long, в Err.Description — сообщение через IErrorInfo.
x1["На стороне .NET throw исключения"] --> x2["На границе COM — в HRESULT"]
x2 --> x3["В Err.Number как знаковое"]
x2 --> x4["В Err.Description — сообщение"]
x3 -.-> h1["Читать в hex через Hex$"]
Рис. 11: На границе COM исключение меняет вид на HRESULT и доходит до Err в VBA.
9. Как думать о распространении
При распространении важно класть не одну DLL, а весь комплект выходных файлов.
VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(при необходимости — весь набор зависимых DLL)
Кроме того, на клиентском ПК нужна соответствующая среда выполнения .NET 8. COM host рассчитан не на self-contained-развёртывание: по сути эксплуатация идёт в режиме framework-dependent.
Конкретно ставить нужно следующее.
- Страница загрузки: .NET 8 downloads
- Нужен не SDK, а runtime. Образец в этой статье — библиотека классов без UI, поэтому хватает
.NET Runtime. Если используются типы WPF или Windows Forms, нужен.NET Desktop Runtime - Разрядность должна совпадать с Office. Для 64-разрядного Office — x64, для 32-разрядного — x86. По той же причине, по которой в главе 3 явно указали
x64/x86: если здесь не совпадёт, запуск не пойдёт - Проверить, стоит ли уже, можно на клиентском ПК командой
dotnet --list-runtimes: должна быть строкаMicrosoft.NETCore.App 8.x
В сопроводительных материалах к поставке обязательно напишите «какой runtime, какая версия, какая разрядность». Если это выпадет из инструкции, на месте увидят Компонент ActiveX не может создать объект. и начнут подозревать bitness, сжигая время.
flowchart TB
accTitle: Что нужно собрать при распространении
accDescr: Распространение — это не одна DLL, а комплект выходных файлов; на клиентский ПК ставят среду выполнения .NET 8 той же разрядности, что у Office, и в материалах поставки указывают тип, версию и разрядность runtime.
h1["Разместить комплект выходных файлов"] --> u1["Работает на клиенте"]
h2[".NET 8 runtime той же разрядности"] --> u1
h3["В материалах поставки указать runtime"] -.-> u1
Рис. 12: Одна DLL не работает; всё встаёт на место, только когда комплект и runtime собраны вместе.
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 - Класс может реализовывать оба
flowchart TB
accTitle: Как беречь уже опубликованный интерфейс
accDescr: Уже опубликованный ICalculator оставляют как есть; если изменения большие, заводят ICalculator2, и класс может реализовывать оба — так берегут совместимость.
k1["Уже опубликованный ICalculator"] --> k2["оставить как есть"]
k3["Хотим большое изменение"] --> k4["завести ICalculator2"]
k2 --> k5["Класс может реализовывать оба"]
k4 --> k5
Рис. 13: По-COM’овски существующий контракт оставляют, а новый кладут рядом.
10.5 Типы держите простыми
На границе, которую видит VBA, безопаснее не мудрить.
Сначала хорошо сочетаются такие типы:
intdoubleboolstringDateTimedecimalenum
10.6 Не обновляйте DLL, пока открыт Office
Excel или Access может держать DLL занятой, и тогда сборка или повторная регистрация начинают мешать.
- Закрыть Office
- При необходимости снять регистрацию
- Пересобрать
- Зарегистрировать снова
Снимать регистрацию нужно в обратном порядке относительно регистрации и той же разрядности командой, которой регистрировали. Права администратора нужны так же, как при регистрации.
# 64-разрядный Office / 64-разрядный COM
$out = Resolve-Path .\bin\Release\net8.0-windows
dscom tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\System32\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
# 32-разрядный Office (на 64-разрядной Windows)
$out = Resolve-Path .\bin\Release\net8.0-windows
.\tools\dscom32.exe tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\SysWOW64\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
У regsvr32 снятие регистрации — это /u. Другим regsvr32, не тем, которым регистрировали, снять нельзя (то, что зарегистрировали 64-битным, regsvr32 из SysWOW64 не снимет). Если папку снести или перенести, не сняв регистрацию, в реестре останется запись с несуществующим путём.
flowchart TB
accTitle: Как свернуть регистрацию до и после обновления
accDescr: При обновлении закрывают Office, при необходимости снимают регистрацию в обратном порядке той же разрядности командой, затем пересобирают и регистрируют снова.
u1["Закрыть Office"] --> u2["При необходимости снять в обратном порядке"]
u2 --> u3["Пересобрать"]
u3 --> u4["Зарегистрировать снова"]
u2 -.-> u5["Командой той же разрядности, что при регистрации"]
Рис. 14: Чтобы не остаться с занятой DLL и хвостом в реестре, снятие и повторную регистрацию делают парой.
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
- dspace-group/dscom - GitHub (сам dscom: список подкоманд и описание поддержки 32-бит)
- Страница релизов dscom (откуда брать
dscom32.exe) - Как сопоставить HRESULT и исключения - Microsoft Learn
- How to use the Regsvr32 tool and troubleshoot Regsvr32 error messages - Microsoft Support
- .NET 8 downloads
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Почему после работы с Excel из C# остаётся EXCEL.EXE — освобождение COM-ссылок и решение о замене
Разбираем, почему при автоматизации Excel из C# через COM остаётся процесс EXCEL.EXE: счётчик ссылок COM, RCW, ловушка «правила двух точе...
Перенос макросов Excel VBA в Power Automate — что заменить Office Scripts, а что оставить в VBA
Разбираем, можно ли перенести макросы Excel VBA в Power Automate: что заменяется Office Scripts, что умеет только VBA, ограничения коннек...
Обратная совместимость интерфейсов DLL и COM — таблица: какие изменения ломают вызывающий код
Какие изменения DLL или COM-компонента ломают вызывающий код. Разбираем три слоя совместимости — бинарную, исходного кода и поведенческую...
Запустятся ли бизнес-приложения на Windows on Arm — эмуляция x64 (Prism) и реальность нативных DLL и COM
Разработчикам и ИТ-службам: запустится ли бизнес-приложение на Windows on Arm. Разбираем, как устроена эмуляция x64 (Prism), какие слои о...
Как выбрать межпроцессное взаимодействие в 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, склонен собирать *.comhost.dll в 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 рассчитан не на self-contained, а в основном на framework-dependent развёртывание. Если позже смените каталог размещения, регистрацию придётся делать заново.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.