История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21620063)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). Обратная совместимость интерфейсов DLL и COM — таблица: какие изменения ломают вызывающий код. KomuraSoft LLC. https://comcomponent.com/ru/blog/dll-com-interface-backward-compatibility/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21620063
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21620064
«Для этого исправления достаточно подменить DLL или нужно ещё пересобрать вызывающий код?» Если вы сопровождаете общую DLL или COM-компонент, на который ссылаются несколько приложений, этот вопрос возникает на каждом релизе. Ошибиться в ответе значит: старый EXE у заказчика перестанет запускаться — или, хуже, запустится как ни в чём не бывало, а результаты расчётов тихо изменятся.
Неприятность в том, что решение часто принимают по ощущению «вроде рискованно». На самом деле какие изменения ломают совместимость, можно определить почти механически. У нативных DLL есть правила экспорта и соглашений о вызовах; у COM — явно записанное правило «интерфейс неизменен»1; у .NET — перечень правил изменений для совместимости, которым сама Microsoft пользуется при разработке библиотек .NET2.
Основы COM мы разбирали в статье «Что такое COM / ActiveX / OCX», а замысел этой модели — в «Что такое COM — почему дизайн Windows COM до сих пор красив». Здесь для DLL, COM и сборок .NET сводим в таблицу, какие изменения ломают вызывающий код, и описываем, как действовать, когда совместимость всё же приходится нарушить.
Термины, которые используются в этой статье
Чтобы читать таблицу решений, нужны несколько понятий с уровня двоичного файла. Подробности — в соответствующих разделах; здесь достаточно одной строки на термин.
| Термин | Кратко |
|---|---|
| ABI (Application Binary Interface) | Договорённость между уже скомпилированными двоичными файлами: как передавать аргументы, как лежат структуры в памяти, как именуются символы — контракт на уровне машинного кода, а не исходников |
| Соглашение о вызовах (calling convention) | Часть ABI: аргументы идут через регистры или стек и в каком порядке; кто снимает аргументы со стека — вызывающий или вызываемый (__cdecl / __stdcall и т.д.) |
| vtable (таблица виртуальных функций) | Таблица указателей на функции в фиксированном порядке. Это и есть интерфейс COM: вызывающий код обращается к методу по позиции слота |
| Порядковый номер экспорта (ordinal) | Номер функции в таблице экспорта DLL. Импортировать можно не по имени, а по этому номеру |
| IID / CLSID / ProgID | По порядку: идентификатор интерфейса COM (контракта), идентификатор класса-реализации и человекочитаемый псевдоним CLSID (раздел 4.1) |
| Строгое имя (strong name) | Способ однозначно идентифицировать сборку .NET: имя + версия + культура + токен открытого ключа и подпись |
| Перенаправление привязки (binding redirect) | В .NET Framework: настройка в файле конфигурации, которая подменяет запрошенную вызывающим кодом версию сборки фактически загружаемой |
1. Сначала выводы
- Совместимость бывает трёх слоёв: бинарная (работает без пересборки), исходного кода (работает после пересборки) и поведенческая (поведение не меняется). «Пересборка не нужна» не значит «безопасно»: смотреть нужно и на поведение.3
- Для нативной DLL базовое правило: добавление экспорта безопасно; изменение или удаление существующего экспорта — критическое. Сигнатура функции, соглашение о вызовах и раскладка структуры — это и есть бинарный контракт.
- Интерфейс COM неизменен (immutable) после публикации. Добавлять, удалять или переставлять методы после публикации нельзя по спецификации. Изменение оформляют как новый интерфейс с новым IID (IFoo → IFoo2).41
- Клиенты VB6/VBA с ранним связыванием зашивают позиции в vtable при компиляции и от смены раскладки интерфейса ломаются чаще других.
- В .NET «что в общедоступном API считается критическим изменением» опубликовано как правила изменений для совместимости Microsoft: к критическим относят не только удаление метода и смену сигнатуры, но и добавление
virtualи даже переименование параметра.2 - Семантическое версионирование — соглашение «критическое изменение → повышай MAJOR», но оно работает только после того, как вы объявили, что считать критическим.5 Таблицу из этой статьи можно взять как такое определение.
- Если совместимость всё же приходится ломать, порядок такой: старое и новое рядом → период устаревания → инвентаризация вызывающего кода → снятие старого API. Сразу подменять нельзя.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 27, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Три слоя совместимости — кто и когда ломается
То, что коротко называют «обратной совместимостью», на деле распадается на три слоя. Даже официальная документация .NET классифицирует критические изменения по осям совместимости исходного кода, бинарной и поведенческой.3
| Слой | Значение | Что происходит при поломке | Кто страдает в первую очередь |
|---|---|---|---|
| Бинарная совместимость | Вызывающий код работает с новой DLL без пересборки | При запуске не находится точка входа, во время выполнения — MissingMethodException, падения |
Старые EXE у заказчика, сторонние приложения, которые нельзя пересобрать |
| Совместимость исходного кода | Вызывающий код работает, если его пересобрать | Ошибки компиляции при следующей сборке | Другая команда в компании, разработчики, у которых есть исходники |
| Поведенческая совместимость | Заданное спецификацией поведение не меняется | Результаты, время срабатывания или тип исключения меняются без какой-либо ошибки | Конечные пользователи (и все, кто потом разбирает инцидент) |
Эти три слоя удобнее мыслить вложенными.
[Поведенческая совместимость] поведение не меняется ← самый внешний слой
└─ [Совместимость исходного кода] работает после пересборки
└─ [Бинарная совместимость] работает без пересборки ← самый внутренний слой
Чем ближе слой к центру, тем жёстче условия; чем лучше соблюдён внутренний слой, тем меньше работы у вызывающего кода. Важно, что внутренний слой может быть цел, а внешний — уже сломан. Например, правка, которая меняет смысл возвращаемого значения существующей функции, сохраняет и бинарную совместимость, и совместимость исходного кода, ломая только поведенческую. Ошибки линковки и компиляции нет — это самая легко пропускаемая строка таблицы.
И наоборот: если у всех вызывающих есть исходники и их можно пересобрать одновременно (внутренняя система в одном репозитории), достаточно совместимости исходного кода и поведения — бинарную можно убрать из требований. Есть ли среди клиентов вашей DLL двоичный файл, который нельзя пересобрать — первая развилка при чтении таблицы.
3. Таблица решений для нативных DLL (C/C++)
Совместимость нативной DLL определяется таблицей экспорта, соглашениями о вызовах и раскладкой памяти. Как DLL ищут и загружают, разобрано в «Как работает разрешение имён DLL в Windows»; после успешной загрузки совместимость смотрят по таблице ниже.
| Изменение | Бинарная совместимость | Примечание |
|---|---|---|
| Добавление экспортируемой функции | Не ломает | Самый безопасный способ расширить API. Но если вы полагаетесь на неявные порядковые номера из .def-файла, добавление может перенумеровать уже существующие номера в зависимости от позиции; если хоть один клиент линкуется по номеру, явно зафиксируйте текущие номера и добавляйте новые в конец |
| Удаление или переименование экспортируемой функции | Ломает | Разрешение импорта не удаётся: ошибка при загрузке или из GetProcAddress |
| Смена сигнатуры существующей функции (добавление, удаление, смена типа аргументов, смена типа возвращаемого значения) | Ломает | Расходится передача через стек и регистры. С возвращаемым значением то же: смена целого (RAX) на число с плавающей точкой (XMM0) заставляет вызывающий код читать мусор по старому ABI. Ошибки может не быть — процесс просто начнёт вести себя непредсказуемо |
Смена соглашения о вызовах (__cdecl ↔ __stdcall) |
Ломает (32-бит) | На x86 меняется, кто чистит стек, и стек повреждается. У x64 соглашение одно, эти спецификаторы фактически игнорируются — строка относится только к 32-битным DLL |
| Смена порядкового номера экспорта (ordinal) | Ломает при условии | Клиент, который линкуется по номеру, начинает вызывать другую функцию. Если все линкуются только по имени, влияния нет |
| Добавление поля в структуру, которую выделяет вызывающий код | Ломает | Старый вызывающий код по-прежнему выделяет меньший буфер и передаёт его (смягчается соглашением cbSize, см. ниже) |
Смена упаковки и выравнивания общедоступной структуры (#pragma pack, /Zp, смена средств сборки) |
Ломает | Смещения существующих полей и общий размер меняются, даже если ни одно поле не трогали. cbSize от сдвинутой раскладки не спасает — упаковку явно фиксируют в общедоступном заголовке |
| Внутренние изменения структуры, которую выделяет и освобождает только сама DLL | Не ломает | Если наружу отдан только указатель (handle), внутреннее устройство можно менять свободно |
| Смена смысла возвращаемого значения или кода ошибки | Не ломает (но ломает поведенческую совместимость) | Линковка проходит, поведение меняется — паттерн, который находят позже всех |
| Добавление поля данных или виртуальной функции в класс C++, который экспортируют напрямую | Ломает | Меняется размер объекта или раскладка vtable. Добавление только невиртуальной функции-члена раскладку не меняет и существующих клиентов напрямую не ломает, но прямой экспорт класса C++ и так несовместим между компиляторами: если это решение приходится принимать каждый раз, ABI уже хрупкий |
Таблица отвечает на вопрос «что мы собираемся менять». На практике чаще смотрят в обратную сторону: «этот симптом у заказчика — какая строка его вызвала». Ниже — как строки проявляются.
| Симптом в поле | Какую строку подозревать |
|---|---|
При запуске диалог вроде «Не найдена точка входа процедуры…», приложение даже не стартует. NTSTATUS — 0xC0000139 (STATUS_ENTRYPOINT_NOT_FOUND, в оригинале “The procedure entry point %hs could not be located in the dynamic link library %hs.”)6 |
Удаление или переименование экспортируемой функции. В C++ смена сигнатуры меняет декорированное (mangled) имя, и получается тот же симптом «стало другим именем» |
GetProcAddress возвращает NULL, приложение показывает своё сообщение об ошибке |
То же. У клиентов с отложенной или динамической загрузкой проявляется именно так |
Сама DLL не находится, приложение не стартует. NTSTATUS — 0xC0000135 (STATUS_DLL_NOT_FOUND)6 |
Это не совместимость, а размещение и порядок поиска. Прежде чем смотреть таблицу, проверьте пути поиска DLL |
Падение сразу после возврата из функции. В отладочной сборке (/RTCs или /RTC1) проверка времени выполнения ловит это как порчу указателя стека |
Смена соглашения о вызовах (32-бит). Microsoft прямо пишет, что порча указателя стека может быть следствием несовпадения соглашений о вызовах7. В release-сборке это не ловят, и падение происходит совсем в другом месте |
| Ошибки нет, но отдельные поля структуры содержат мусор. Реже — выход за границу буфера | Добавление поля в структуру или смена упаковки и выравнивания. Без соглашения cbSize (раздел 3.1) это трудно отделить |
У вызывающего кода на .NET — MissingMethodException |
Удаление, переименование или смена сигнатуры члена на стороне .NET (раздел 5) |
| Ошибок нет, но цифры в отчётах и сводках изменились | Смена смысла возвращаемого значения или кода ошибки. Сломана только поведенческая совместимость; обнаруживают позже всех |
Проектный вывод из таблицы не менялся десятилетиями: границу держите на C ABI (функции extern "C" и простые структуры), а расширяйте API добавлением функций. То же относится к нативной DLL из C#: экспортируемую поверхность из «Как вызвать Native AOT DLL на C# из C/C++» ведут по этой же таблице.
3.1 Соглашение cbSize — как в Win32 делают структуры расширяемыми
Классический ответ на «добавление поля в структуру ломает совместимость» — соглашение Win32 класть поле размера в начало структуры. Вызывающий код записывает в cbSize размер структуры, известный ему на момент компиляции, и передаёт её; DLL по этому размеру определяет, какое поколение структуры знает данный клиент.
typedef struct KS_CONFIG {
DWORD cbSize; // вызывающий код задаёт sizeof(KS_CONFIG)
DWORD dwMode;
DWORD dwTimeout;
// новые поля всегда добавлять в конец
} KS_CONFIG;
// сторона DLL: по cbSize определяем поколение; для старого клиента — значение по умолчанию
if (pConfig->cbSize >= FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD)) {
timeout = pConfig->dwTimeout; // новый вызывающий код
} else {
timeout = DEFAULT_TIMEOUT; // старый вызывающий код
}
В условии стоит не sizeof(KS_CONFIG), а FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD), чтобы проверять «есть ли поколение вплоть до dwTimeout» по последнему известному полю. Если сравнивать с sizeof, то в момент, когда в конец добавят ещё одно поле, условие ужесточится, и клиенты, которые знают dwTimeout, но не знают нового поля, будут приняты за старое поколение. Проверка на каждое поле в таком виде избавляет от переписывания уже существующих условий при каждом расширении.
Именно так Windows API версионирует структуру NOTIFYICONDATA: официально задокументировано, что правильное значение cbSize сохраняет совместимость со старыми версиями Shell32.dll.8 Если положить cbSize в общедоступные структуры своей DLL с самой первой версии, последующие расширения уходят из «критического изменения» на безопасную сторону таблицы. При этом новые поля всегда добавляют в конец, а менять тип или порядок существующих полей по-прежнему нельзя. Ещё одно правило касается структур, которые DLL заполняет на выход: писать и инициализировать можно только в пределах полученного cbSize. Безусловная запись всего нового sizeof вылезает за меньший буфер старого клиента — и ту поломку, которую соглашение должно было предотвратить, устраивает уже сама DLL.
4. Неизменность интерфейса COM — после публикации менять нельзя
COM дал на эту задачу самый прямой ответ. По спецификации COM интерфейс подчиняется следующим правилам.
- У интерфейса есть уникальный IID (идентификатор интерфейса).1
- Интерфейс неизменен (immutable). После создания и публикации ни одну часть определения менять нельзя.1
- Добавление или удаление метода либо смена семантики — это не «новая версия старого интерфейса», а новый интерфейс с другим IID.4
Строгость здесь не эстетика: сущность интерфейса COM — vtable, таблица указателей на функции, то есть бинарная раскладка. Клиент на C++ или VB6 при компиляции зашивает факт «слот 3 — это GetName», то есть позицию. Вставьте метод после публикации — и старый клиент вызовет другой метод, без какой-либо ошибки. Поэтому COM убрал саму операцию «изменить интерфейс» из спецификации и вместо неё дал такой способ расширяться.
// v1: уже опубликован. Больше не менять
[object, uuid(1111....)]
interface ICalc : IUnknown {
HRESULT Add([in] long a, [in] long b, [out, retval] long* result);
};
// v2: новый интерфейс с новым IID. Расширяет ICalc наследованием
[object, uuid(2222....)]
interface ICalc2 : ICalc {
HRESULT AddChecked([in] long a, [in] long b, [out, retval] long* result);
};
В IDL выше uuid сокращён до 1111.... — это иллюстрация, так компилироваться не будет. На практике пишут полный GUID из средства создания GUID в Visual Studio (guidgen) или из команды uuidgen. Один и тот же GUID на двух интерфейсах лишает контракты различия, поэтому на каждый новый интерфейс GUID генерируют заново.
Класс-реализация (coclass) реализует и ICalc, и ICalc2; старые клиенты по-прежнему ходят в ICalc, новые запрашивают ICalc2 через QueryInterface. Официальная теория версионирования RPC/COM формулирует то же самое: новый интерфейс, унаследованный от старого, — аналог повышения минорной версии; смена существующего метода или типа требует совершенно нового интерфейса без наследования — аналог повышения MAJOR.9 Схема держится на том, что QueryInterface позволяет вызывающему коду во время выполнения безопасно узнать, что поддерживается. Замысел этого механизма разобран в «Что такое COM».
4.1 Роли CLSID, ProgID и IID
Думая о версиях COM, три вида идентификаторов лучше не смешивать.10
- IID идентифицирует интерфейс (контракт). Сменился контракт — всегда новый IID.
- CLSID идентифицирует класс-реализацию. Подменять реализацию, оставляя тот же CLSID, можно свободно, пока соблюдён контракт каждого опубликованного интерфейса.
- ProgID — человекочитаемый псевдоним (
KomuraSoft.Calc.1), по которому в реестре находят CLSID. Принято держать и версионированный ProgID, и не зависящий от версии ProgID (KomuraSoft.Calc), который всегда указывает на последнюю версию; второй сопоставляется с текущей черезCurVer.10
Иначе говоря, «повышение версии реализации» — мир CLSID и ProgID, «смена контракта» — мир IID; смешивать их нельзя. Если регистрацию в реестре хочется обойти, этот вариант разобран в «Что такое Reg-Free COM».
4.2 Почему клиенты VB6/VBA ломаются особенно легко
Когда VB6 или VBA используют COM-компонент через ссылку в проекте (раннее связывание), они при компиляции читают библиотеку типов и по ней разрешают вызовы. Раннее связывание — рекомендуемая форма: работают IntelliSense и проверка типов, выполнение быстрее11, — но плата за это — жёсткая привязка к раскладке библиотеки типов. Смена vtable интерфейса, разумеется, ломает вызовы; но даже смена одних только определений в библиотеке типов проявляется как «открыли проект — ссылка сломана» или как ошибки времени выполнения 430/438.
Поэтому в компонентах, которые вызывают VB6, VBA или макросы Excel, правило неизменности интерфейса нужно соблюдать максимально строго. У библиотеки типов тоже есть версия (major.minor); её повышают, когда контракт расширяют. Как генерировать библиотеку типов при публикации типизированного кода .NET для VBA, разобрано в «Как использовать DLL на .NET 8 из VBA с типизацией — публикация COM и TLB через dscom». Клиенты с поздним связыванием, которые ходят только через CreateObject, разрешают вызовы по имени и к смене раскладки устойчивее, но от смены смысла метода (поведенческой совместимости) страдают так же.
5. Совместимость сборок .NET — оценка по официальным правилам
В .NET опубликован набор «правил изменений для совместимости», которым сама Microsoft пользуется при разработке библиотек .NET: каждое изменение классифицируют как разрешённое (✔️), запрещённое (❌) или требующее оценки (❓).2 В документации прямо сказано, что этот набор можно взять как критерий для собственных библиотек, поэтому ниже — основные строки.
| Изменение общедоступного API | Вердикт | Примечание |
|---|---|---|
| Добавление метода, типа или члена | ✔️ В целом безопасно | Осторожно с добавлением, которое меняет разрешение существующих перегрузок. Добавление поля экземпляра в общедоступную структуру — исключение: меняются размер и раскладка, ломаются интероперабельность и потребители unsafe-кода |
| Удаление или переименование общедоступного типа или члена | ❌ Критическое | Ломается во время выполнения с MissingMethodException и подобными |
| Смена сигнатуры (добавление, удаление, порядок, тип аргументов, тип возвращаемого значения) | ❌ Критическое | Ломает и бинарную совместимость, и совместимость исходного кода |
| Переименование параметра | ❌ Критическое | Ломает именованные аргументы C# и позднее связывание VB. Легко пропустить |
Добавление virtual к члену |
❌ Критическое | Классическая ловушка: «это же просто добавление». Может разойтись IL вызова (call/callvirt) |
Снятие virtual, превращение виртуального члена в abstract |
❌ Критическое | Ломает переопределения в производных классах |
| Добавление абстрактного члена в незапечатанный общедоступный тип | ❌ Критическое | У существующих производных классов нет реализации |
Запечатывание (sealed) типа |
❌ Критическое | Существующие производные классы перестают компилироваться |
| Добавление члена в интерфейс | ❓ Нужна оценка | Реализация по умолчанию (DIM) позволяет не ломать существующие классы-реализации, но условий много (ниже) |
| Смена константы или значения enum, переименование или удаление члена enum | ❌ Критическое | Значение зашивается в вызывающий код при компиляции |
| Начать бросать более производное исключение | ✔️ Разрешено | Существующие catch продолжают срабатывать |
| Бросать исключение нового вида на существующем пути кода | ❌ Критическое | Бросать его только для нового значения параметра — можно |
Единственная строка ❓ (нужна оценка) — «добавление члена в интерфейс»; на практике именно она вызывает больше всего сомнений. Реализация по умолчанию (DIM: Default Interface Members) позволяет добавить член, не оставляя существующие классы-реализации без реализации, но официальные правила называют такие условия.2
- Минимальные требования у потребителей поднимаются до .NET Core 3.0 / C# 8.0. DIM появились в этих версиях: добавили реализацию по умолчанию — и все, кто сидит на более старой среде выполнения, остаются за бортом. .NET Framework сюда не входит, поэтому если в библиотеке остался хотя бы один клиент на .NET Framework, этим послаблением пользоваться нельзя.
- Есть языки без поддержки DIM. .NET вызывают из нескольких языков; если интерфейс реализован не на C#, на реализацию по умолчанию опираться нельзя.
- Среда выполнения не всегда может выбрать, какую реализацию по умолчанию вызвать. Когда в схеме участвует несколько интерфейсов, разрешение DIM может стать неоднозначным.
- Начиная с C# 13, добавление члена экземпляра по умолчанию в интерфейс, который реализует
ref struct, — критическое изменение исходного кода.ref structнельзя ни упаковать, ни привести к типу интерфейса, поэтому откатиться на реализацию по умолчанию нельзя: члены экземпляра нужно реализовывать явно.
При этом добавление статических неабстрактных и невиртуальных членов разрешено.2 Если «хочется добавить член, но у потребителей ещё есть .NET Framework», безопаснее не трогать интерфейс, а по той же логике, что в разделе 4 про COM, добавить новый интерфейс — или сначала проверить, не закрывают ли задачу методы расширения.
Это не так просто, как правило COM «интерфейс неизменен», но мысль та же. Общедоступный API — контракт: в контракт можно добавлять, существующий контракт менять нельзя. И именно то, что «на вид безопасные» правки вроде виртуализации метода или переименования параметра отнесены к критическим, и есть причина решать по таблице, а не по ощущению.
5.1 Строгое имя и три номера версии
У сборки .NET несколько номеров версии, и роли у них разные.12
- AssemblyVersion: единственная версия, по которой среда выполнения идентифицирует и загружает сборку. Для сборок со строгим именем CLR .NET Framework требует точного совпадения, поэтому каждое повышение заставляет вызывающий код добавлять перенаправление привязки (.NET / .NET Core, напротив, автоматически принимают более высокую версию). Чтобы сократить число перенаправлений, официальные рекомендации предлагают отражать в AssemblyVersion только основную версию.
- FileVersion (AssemblyFileVersion): виден только в свойствах файла в Проводнике и на поведение среды выполнения не влияет. Рекомендуемое место для номера сборки CI.
- InformationalVersion: произвольная строка для человека. Сюда пишут версию пакета в формате semver или хеш коммита исходников.
На практике удобна трёхслойная схема: «совместимость объявлять версией пакета или продукта (semver), в AssemblyVersion отражать только MAJOR, сборки сопровождать через FileVersion».
6. Как назначать номера версий — semver работает только вместе с определением
Суть семантического версионирования (semver) укладывается в три строки: повышайте MAJOR при несовместимом изменении, MINOR при обратно совместимом добавлении функциональности и PATCH при обратно совместимом исправлении ошибки.5
Легко пропустить, что первое требование спецификации semver — программное обеспечение, которое использует semver, обязано объявить общедоступный API.5 Пока не объявлено, что считается общедоступным API, критерия «несовместимого изменения» нет, и решение повышать MAJOR зависит от настроения конкретного человека. Там, где semver «не работает», чаще пропущено именно это объявление, а не способ нумерации.
Для DLL, которую разносят внутри компании, реалистичная схема такая.
- Объявите границы общедоступного API — для нативной DLL это экспортируемые функции и общедоступные заголовки, для COM — IDL и библиотека типов, для .NET — общедоступные типы и члены. Прямо напишите: «всё остальное — внутренняя реализация и может измениться без предупреждения».
- Примите определение критического изменения — положите таблицы из разделов 3 и 5 этой статьи и правила изменений .NET2 в репозиторий как «собственное определение».
- Автоматизируйте проверку — для .NET инструменты Package Validation / ApiCompat механически сверяют бинарную совместимость с предыдущей версией.13 На ревью пропадает «наверное, нормально».
- Заведите в заметках к релизу графу совместимости — каждый раз явно пишите одно из трёх: «пересборка не нужна / пересборка рекомендуется / есть критическое изменение». Это способ ответить на вопрос из начала статьи — «достаточно ли подменить?» — письменно, ещё до того, как его зададут.
7. Что делать, когда совместимость всё же приходится ломать
Если изменение, которое таблица пометила как критическое, всё равно необходимо, идите не через подмену, а через параллельную поставку.
- Поставляйте старое и новое рядом — для COM это добавление
IFoo2с сохранениемIFoo(раздел 4). Для нативной DLL — новая функция (FooEx) или соседство новой DLL с другим именем. Для .NET — выпуск как нового пакета с повышенным MAJOR, при этом старый MAJOR продолжает получать только исправления ошибок. - Задайте период устаревания — в .NET атрибут
[Obsolete]даёт предупреждение при компиляции. Для нативного кода и COM объявляйте это комментарием в заголовке и в заметках к релизу, с конкретной датой снятия. Суть — назначить дату, а не расплывчатое «когда-нибудь уберём». - Проведите инвентаризацию вызывающего кода — соберите список тех, кто ещё вызывает старый API: поиск по внутренним исходникам, записи о рассылке установщиков, для COM — ссылки в реестре. Если найдётся двоичный файл, который нельзя пересобрать (инструмент ушедшего сотрудника, стороннее приложение), либо продлите жизнь старого API именно для этого случая, либо перекиньте мост обёрткой.
- Удалите старый API — только после того, как инвентаризация подтвердила, что вызывающих не осталось, удалите его и повысьте MAJOR.
Эта процедура затратна. Поэтому, как ни парадоксально, самое сильное средство совместимости — с первого релиза проектировать небольшой API с оглядкой на таблицу (то, что вы не опубликовали, обязательств по совместимости не несёт).
8. Кратко
- Совместимость смотрят в трёх слоях: бинарном, исходного кода и поведенческом. Даже без пересборки поведение может измениться.3
- Для нативных DLL: добавление безопасно; смена существующего экспорта, сигнатуры или раскладки структуры — критическая. В структуры кладите
cbSize, чтобы оставить место для расширения.8 - Интерфейс COM неизменен после публикации. Изменения добавляйте как новый интерфейс с новым IID (IFoo2) и различайте их через
QueryInterface.149 При клиентах VB6/VBA с ранним связыванием соблюдайте это особенно строго. - .NET можно оценивать механически по официальным правилам изменений для совместимости. Следите за «на вид безопасными» правками — виртуализацией, переименованием параметров, запечатыванием типа, — которые отнесены к критическим.2
- Реалистична трёхслойная схема: в AssemblyVersion только MAJOR, сборки сопровождать FileVersion, совместимость объявлять semver.12
- Semver работает только после объявления общедоступного API и определения критического изменения.5 Возьмите таблицу как это определение и проверяйте её автоматически через Package Validation и аналоги.13
- Когда что-то приходится ломать: параллельная поставка → период устаревания → инвентаризация → удаление. Отказ от одномоментной подмены — вот что защищает старый EXE у заказчика.
Статьи по теме
- Что такое COM / ActiveX / OCX — различия и связь
- Что такое COM — почему дизайн Windows COM до сих пор красив
- Как использовать DLL на .NET 8 из VBA с типизацией — публикация COM и TLB через dscom
- Как работает разрешение имён DLL в Windows — порядок поиска и SxS
- Что такое Reg-Free COM: механизм использования COM без регистрации
- Как вызвать Native AOT DLL на C# из C/C++
Смежные направления консультаций
KomuraSoft LLC занимается проектированием совместимости DLL, COM-компонентов и библиотек .NET, на которые ссылаются другие системы: инвентаризацией общедоступного API и политикой версий, а также проектированием и реализацией расширений, которые не ломают существующих клиентов (схема IFoo2, параллельная поставка).
- Повторное использование и перенос существующих активов
- Доработка и сопровождение существующего ПО для Windows
- Технические консультации и ревью архитектуры
- Контакты
Справочные материалы
-
Microsoft Learn, Interface Design Rules. О том, что интерфейсы объекта COM должны иметь уникальный IID и что после создания и публикации ни одну часть определения менять нельзя (неизменность). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Change rules for compatibility (.NET). О том, что изменения API .NET классифицируют как разрешённые, запрещённые или требующие оценки; что удаление или переименование общедоступных типов и членов, смена сигнатуры, переименование параметров, добавление и снятие
virtual, запечатывание, смена значений констант и enum запрещены (критические); что добавление члена в интерфейс требует оценки; что авторы библиотек могут использовать эти правила как критерий для своей библиотеки. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, Breaking changes (.NET library guidance). О том, что критические изменения делят на ломающие исходный код, поведение и бинарную совместимость; при поломке бинарной совместимости сборка, скомпилированная против старой версии, падает во время выполнения с MissingMethodException и подобными. ↩ ↩2 ↩3
-
Microsoft Learn, Interface Pointers and Interfaces. О том, что интерфейс COM неизменен и что добавление или удаление метода либо смена семантики означают создание нового интерфейса, а не новой версии старого; IID однозначно задаёт контракт. ↩ ↩2 ↩3
-
semver.org, Semantic Versioning 2.0.0. О повышении MAJOR при несовместимых изменениях API, MINOR при обратно совместимом добавлении функциональности и PATCH при обратно совместимом исправлении ошибок; о том, что программное обеспечение, использующее semver, обязано объявить общедоступный API; о том, что обратно несовместимое изменение общедоступного API всегда требует повышения MAJOR. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MS-ERREF 2.3.1 NTSTATUS Values. О том, что
STATUS_ENTRYPOINT_NOT_FOUND(0xC0000139) соответствует тексту “The procedure entry point %hs could not be located in the dynamic link library %hs.”, аSTATUS_DLL_NOT_FOUND(0xC0000135) — “This application has failed to start because %hs was not found.” ↩ ↩2 -
Microsoft Learn, /RTC (Run-time error checks). О том, что
/RTCs(и/RTC1) проверяет указатель стека и обнаруживает его порчу; что порча может быть следствием несовпадения соглашений о вызовах (например, функцию, экспортированную как__stdcall, вызывают через указатель__cdecl); что/RTCнельзя использовать в release-сборке с оптимизацией. ↩ -
Microsoft Learn, NOTIFYICONDATAW structure (shellapi.h). Об установке размера структуры в члене cbSize, о том, что структура расширялась от поколения к поколению, и о том, что правильное значение cbSize позволяет сохранять совместимость со старыми версиями Shell32.dll. ↩ ↩2
-
Microsoft Learn, The Versioning Theory for RPC and COM. О том, что для расширения функциональности в COM лучше всего создавать новый интерфейс; новый интерфейс, унаследованный от старого, соответствует минорному повышению версии; смена существующих методов или типов требует совершенно нового интерфейса без наследования; QueryInterface позволяет вызывающему коду проверить, что поддерживается. ↩ ↩2
-
Microsoft Learn, COM Registry Keys. О том, что CLSID — GUID, идентифицирующий класс COM; ProgID — человекочитаемая строка, сопоставленная с CLSID без гарантии уникальности; не зависящий от версии ProgID сопоставляется с последней версией класса через CurVer; ключ Interface регистрирует IID. ↩ ↩2
-
Microsoft Learn, OLE programmatic identifiers, late binding, and early binding (Project). О том, что в VBA рекомендуется раннее связывание через ссылку в проекте; что позднее связывание (CreateObject/ProgID) не показывает члены при написании кода и работает медленнее; что для раннего связывания нужна ссылка на целевую библиотеку объектов. ↩
-
Microsoft Learn, Versioning (.NET library guidance). О том, что AssemblyVersion используется средой выполнения для загрузки и для сборок со строгим именем в .NET Framework требует точного совпадения; что в AssemblyVersion предлагается включать только основную версию; что FileVersion предназначен для отображения в Windows и не влияет на поведение во время выполнения; что InformationalVersion служит для записи дополнительной информации о версии; что для версий пакетов NuGet рекомендуется semver 2.0.0. ↩ ↩2
-
Microsoft Learn, NuGet package compatibility rules. О необходимости избегать изменений, ломающих бинарную совместимость; о том, что Package Validation и ApiCompat автоматически обнаруживают совместимость с базовой версией; о том, что AssemblyVersion нельзя понижать между релизами. ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как выбрать межпроцессное взаимодействие в Windows — таблица: именованные каналы, TCP, gRPC, разделяемая память, COM
Как выбрать способ связи между Windows-приложениями. В таблице решений разобраны сильные стороны и типичные ошибки именованных каналов, л...
Почему ломаются аргументы ── правила аргументов командной строки Windows
В Windows массива аргументов нет: в CreateProcess уходит одна строка, делит её принимающая сторона. Правила деления CommandLineToArgvW, C...
Версионирование схемы БД бизнес-приложения — практика миграций, чтобы у клиентов не оказалось «у каждого своя база»
Практическое руководство по версионированию схемы БД бизнес-приложения, которое стоит у множества клиентов. Разбираем PRAGMA user_version...
CI/CD для WinForms / WPF: сборка, подпись и распространение в GitHub Actions
Практическое руководство по CI/CD для WinForms / WPF в GitHub Actions. Минимальный YAML сборки и тестов на windows-latest, нумерация верс...
Как безопасно менять унаследованное бизнес-приложение без тестов — характеризационные тесты и рефакторинг на практике
На примерах C# разбираем, как безопасно менять бизнес-приложение без тестов: как зафиксировать текущее поведение характеризационным тесто...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Миграция ActiveX
Решения о сохранении, обёртке или замене компонентов COM / ActiveX / OCX.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Использование и перенос существующих активов
Помогаем использовать и переносить активы COM / ActiveX / OCX и зависимости 32/64 бит.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Если в DLL только добавить функцию, нужно ли пересобирать вызывающий код?
- Как правило, нет: если вы только добавляете экспортируемую функцию, существующий вызывающий код продолжает работать. Разрешение импорта по-прежнему проходит, пока не меняются имя, сигнатура, соглашение о вызовах и порядковый номер экспорта уже существующих функций. Но если добавить поле в структуру, которую выделяет и передаёт вызывающий код, или изменить смысл возвращаемого значения либо кода ошибки существующей функции, поведенческая совместимость может нарушиться даже без пересборки. Базовое правило: добавление функции безопасно, изменение существующей сигнатуры — критическое.
- Почему в интерфейс COM нельзя добавить метод после публикации?
- Потому что по спецификации COM опубликованный интерфейс неизменен (immutable). Интерфейс — это контракт бинарной раскладки: vtable, упорядоченный список указателей на функции. Вставка, удаление или перестановка методов приводят к тому, что старый двоичный файл вызывает другой метод по позиции слота, зашитой при компиляции. Добавление в конец не сдвигает существующие слоты, но создаёт другую аварию: новый клиент может взять старый компонент, решив, что «добавленный» метод уже есть, и вызвать слот, которого там нет. Поэтому добавлять методы, оставляя тот же IID, тоже нельзя. Нужна новая функциональность — добавляют новый интерфейс с новым IID (IFoo2) и оставляют существующий IFoo как есть. Вызывающий код через QueryInterface безопасно узнаёт, какой из интерфейсов — старый или новый — реализован.
- Как в .NET развести AssemblyVersion, FileVersion и InformationalVersion?
- AssemblyVersion — единственная версия, по которой среда выполнения идентифицирует и загружает сборку. Для сборок со строгим именем CLR .NET Framework требует точного совпадения, поэтому каждое повышение заставляет вызывающий код добавлять перенаправление привязки (binding redirect). Поэтому официальные рекомендации предлагают отражать в AssemblyVersion только основную (major) версию. FileVersion виден только в свойствах файла в Проводнике и на поведение среды выполнения не влияет: туда удобно писать номер сборки CI. InformationalVersion — произвольная строка для человека: версия в формате semver или хеш коммита.
- Решает ли семантическое версионирование (semver) проблемы совместимости само по себе?
- Нет. Semver — это соглашение повышать MAJOR, если изменение несовместимо с предыдущими клиентами, но оно требует заранее объявить, что считается общедоступным API и что — критическим изменением. Если проставлять номера без такого определения, решение каждый раз зависит от человека, и схема не работает. Semver обретает смысл, когда вы принимаете таблицу решений из этой статьи (для нативных DLL) или официальные правила совместимости Microsoft (для .NET) как собственное определение критического изменения и встраиваете его в процесс релиза.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.