DLL・COMインターフェースの後方互換性 ── どの変更が呼び出し側を壊すのかの判断表
· 小村 豪 · COM, DLL, .NET, C#, C++, 後方互換性, バージョニング, レガシー技術, 既存資産活用, 判断表
「この修正、DLLを差し替えるだけでいいですか?それとも呼び出し側も再ビルドですか?」──複数のアプリから参照される共通DLLやCOMコンポーネントを保守していると、リリースのたびにこの質問に答えることになります。答えを間違えると、客先で動いている古いEXEが起動しなくなったり、もっと悪いことに、起動はするのに計算結果が静かに変わったりします。
厄介なのは、この判断が「なんとなく危なそう」という感覚で行われがちなことです。実際には、どの変更が互換性を壊すかはほぼ機械的に判定できます。ネイティブDLLにはエクスポートと呼び出し規約のルールがあり、COMには「インターフェースは不変」という明文化された鉄則があり1、.NETにはMicrosoft自身が.NETライブラリの開発で使っている互換性変更ルールの一覧があります2。
当ブログでは「COM / ActiveX / OCX とは何か」でCOMの基本を、「COM とは何か - Windows COM の設計が今でも美しい理由」でその設計思想を解説しました。この記事では、DLL・COM・.NETアセンブリのそれぞれについて「どの変更が呼び出し側を壊すのか」を判断表の形で整理し、壊さざるを得ないときの手順までまとめます。
1. まず結論
- 互換性にはバイナリ互換(再ビルドなしで動く)・ソース互換(再ビルドすれば動く)・動作互換(振る舞いが変わらない)の3層があります。「再ビルド不要=安全」ではなく、動作互換まで含めて判断する必要があります。3
- ネイティブDLLの基本線は、「エクスポートの追加は安全、既存エクスポートの変更・削除は破壊的」です。関数シグネチャ・呼び出し規約・構造体レイアウトはバイナリ契約そのものです。
- COMのインターフェースは公開したら不変(immutable)です。公開後のメソッド追加・削除・並べ替えは仕様違反であり、変更は新しいIIDを持つ新インターフェース(IFoo→IFoo2)として追加します。41
- VB6/VBAクライアントは事前バインディングでvtable上の位置を焼き込むため、インターフェースのレイアウト変更で最も壊れやすい呼び出し側です。
- .NETでは「public APIの何が破壊的か」がMicrosoftの互換性変更ルールとして公開されており、メソッド削除やシグネチャ変更だけでなく、仮想化(virtual追加)やパラメータ名の変更まで破壊的変更に分類されています。2
- セマンティックバージョニングは「破壊的変更ならメジャーを上げる」という規約ですが、何が破壊的変更かの定義を宣言して初めて機能します。5 本記事の判断表をその定義として使えます。
- 互換性を壊さざるを得ないときは、新旧並行提供→非推奨期間→呼び出し側の棚卸し→廃止の順で進めます。いきなり差し替えない、が原則です。
2. 互換性の3層 ── 誰が、いつ壊れるのか
「後方互換性」と一言で言われるものは、実際には3つの層に分かれます。.NETの公式ドキュメントでも、破壊的変更はソース互換・バイナリ互換・動作互換の観点で分類されています。3
| 層 | 意味 | 壊れると起きること | 主に困る人 |
|---|---|---|---|
| バイナリ互換 | 呼び出し側を再ビルドせずに新しいDLLで動く | 起動時のエントリポイント未検出、実行時のMissingMethodException、クラッシュ |
客先で動いている古いEXE、再ビルドできない他社製アプリ |
| ソース互換 | 呼び出し側を再ビルドすれば動く | 次回ビルド時のコンパイルエラー | 社内の別チーム、ソースを持っている開発者 |
| 動作互換 | 仕様としての振る舞いが変わらない | エラーにならないまま結果・タイミング・例外の種類が変わる | エンドユーザー(そして障害調査をする全員) |
この3層で重要なのは、内側の層が無事でも外側の層は壊れうることです。たとえば既存関数の戻り値の意味を変える修正は、バイナリ互換もソース互換も保たれたまま、動作互換だけを壊します。この種の変更はリンクエラーもコンパイルエラーも出ないため、判断表の中で最も見落とされやすい行です。
逆に、呼び出し側が全員ソースを持っていて同時に再ビルドできる環境(単一リポジトリの社内システムなど)であれば、守るべきはソース互換と動作互換だけで、バイナリ互換は要件から外せます。「自分のDLLの呼び出し側に、再ビルドできないバイナリがいるか」が、判断表を読むうえでの最初の分岐です。
3. ネイティブDLL(C/C++)の互換性判断表
ネイティブDLLの互換性は、エクスポートテーブルと呼び出し規約、そしてメモリレイアウトで決まります。DLLがどう探されロードされるかは「Windows DLL名前解決の仕組み」で解説した通りですが、ロードに成功した後の互換性は次の表で判断できます。
| 変更内容 | バイナリ互換 | 備考 |
|---|---|---|
| エクスポート関数の追加 | 壊れない | 最も安全な拡張手段。ただし.defファイルの暗黙序数に頼っていると追加位置次第で既存の序数が振り直されるため、序数リンクのクライアントがいるなら既存序数を明示固定して末尾に足す |
| エクスポート関数の削除・改名 | 壊れる | インポート解決に失敗し、ロード時またはGetProcAddressでエラー |
| 既存関数のシグネチャ変更(引数の追加・削除・型変更、戻り値の型変更) | 壊れる | スタック・レジスタの受け渡しが食い違う。戻り値も同様で、整数(RAX)から浮動小数点(XMM0)に変えれば呼び出し側は古いABIでゴミを読む。エラーにならず暴走することもある |
呼び出し規約の変更(__cdecl↔__stdcall) |
壊れる(32bit) | x86ではスタック後始末の責任が入れ替わり、スタック破壊につながる。x64は呼び出し規約が単一でこれらの指定は実質無視されるため、この行は32bit DLLの話 |
| エクスポート序数(ordinal)の変更 | 条件付きで壊れる | 序数リンクしている呼び出し側が別関数を呼ぶ。名前リンクのみなら影響なし |
| 呼び出し側が確保する構造体へのメンバ追加 | 壊れる | 古い呼び出し側は小さいまま確保して渡してくる(後述のcbSize慣習で緩和可能) |
公開構造体のパッキング・アラインメント変更(#pragma pack、/Zp、ツールチェーン変更) |
壊れる | メンバを1つも触らなくても既存メンバのオフセットと全体サイズが変わる。cbSizeでも位置ずれは救えないので、公開ヘッダーでパッキングを明示固定しておく |
| DLL側だけが確保・解放する構造体の内部変更 | 壊れない | ポインタ(ハンドル)だけを外に見せる設計なら内部は自由に変えられる |
| 戻り値・エラーコードの意味変更 | 壊れない(が動作互換が壊れる) | リンクは成功するのに挙動が変わる、最も発見が遅れるパターン |
| C++クラスを直接エクスポートしてのデータメンバ・仮想関数の追加 | 壊れる | オブジェクトサイズやvtableレイアウトが変わる。非仮想メンバ関数の追加だけならレイアウトは変わらず既存クライアントを直接は壊さないが、そもそもC++クラスの直接エクスポートはコンパイラ間互換がなく、この判断を毎回迫られる時点でABIとして脆い |
この表から導かれる設計指針は昔から変わりません。境界はC ABI(extern “C”の関数と単純な構造体)に限定し、拡張は関数追加で行うことです。C#からネイティブDLLを作る場合でも同じで、「C# Native AOT DLLを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; // 古い呼び出し側
}
実際、Windows APIのNOTIFYICONDATA構造体はまさにこの方式で世代管理されており、cbSizeに設定する値によって古いShell32.dllとの互換性を保てることが公式に文書化されています。6 自作DLLの公開構造体にも、最初のバージョンからcbSizeを入れておくと、後の拡張が「破壊的変更」から「判断表の安全側」に移ります。ただしメンバの追加は必ず末尾、既存メンバの型・順序の変更は依然として禁止です。もう1つ、出力に使う構造体ではDLL側の責務が増えます。書き込み・初期化は必ず受け取ったcbSizeの範囲に収めること。新しいsizeofぶんを無条件に書き込むと、古い呼び出し側が確保した小さいバッファをはみ出し、この慣習が防ぐはずだった破壊をDLL側が起こすことになります。
4. COMインターフェースの鉄則 ── 公開したら変更禁止
COMはこの問題に、最も明快な答えを出した技術です。COMの仕様では、インターフェースは次のルールに従います。
- インターフェースは一意なIID(インターフェースID)を持つ。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);
};
実装クラス(coclass)はICalcとICalc2の両方を実装し、古いクライアントは今まで通りICalcで、新しいクライアントはQueryInterfaceでICalc2を要求して使います。RPC/COMの公式なバージョニング理論でも、「旧インターフェースを継承した新インターフェースがマイナーバージョンアップ相当、既存メソッドや型を変えるなら継承しない完全に新しいインターフェース(メジャーバージョンアップ相当)」と整理されています。7 QueryInterfaceによって呼び出し側が対応状況を実行時に安全に確認できることが、この方式を成立させています。この仕組みの設計上の美しさは「COM とは何か」で掘り下げた通りです。
4.1 CLSID・ProgID・IIDの役割分担
COMのバージョニングを考えるときは、3種類の識別子の役割を分けて考えます。8
- IID はインターフェース(契約)の識別子。契約が変われば必ず新しいIIDになります。
- CLSID は実装クラスの識別子。同じCLSIDのまま実装を差し替えることは、公開済みインターフェースの契約を守る限り自由です。
- ProgID は人間可読な別名(
KomuraSoft.Calc.1)で、CLSIDへの対応をレジストリで引くためのものです。バージョン番号付きProgIDと、常に最新版を指すバージョン非依存ProgID(KomuraSoft.Calc)を併記する慣習があり、後者はCurVerで最新版に対応付けられます。8
つまり「実装のバージョンアップ」はCLSIDとProgIDの世界の話、「契約の変更」はIIDの世界の話で、混ぜてはいけません。レジストリ登録そのものを避けたい場合の選択肢は「Reg-Free COMとは」で扱っています。
4.2 VB6/VBAクライアントが特に壊れやすい理由
VB6やVBAから参照設定(事前バインディング)でCOMコンポーネントを使っている場合、コンパイル時にタイプライブラリを読んで呼び出しを解決します。事前バインディングはIntelliSenseと型チェックが効き、実行も速い推奨形態ですが9、その代償としてタイプライブラリのレイアウトに強く結合します。インターフェースのvtableが変わればもちろん、タイプライブラリ上の定義が変わっただけでも「プロジェクトを開いたら参照が壊れていた」「実行時にエラー430/438が出る」といった形で表面化します。
このため、VB6/VBA/Excelマクロが呼び出し側にいるコンポーネントでは、インターフェース不変の鉄則を最も厳格に守る必要があります。タイプライブラリにもバージョン(major.minor)があり、契約を増やしたら上げて管理します。.NET側からVBAへ型付きで公開する場合のタイプライブラリ生成は「.NET 8のDLLをVBAから型付きで呼ぶ ── dscomとTLB」で解説しています。一方、CreateObjectだけで使う遅延バインディングのクライアントは名前で解決するためレイアウト変更には強いものの、メソッドの意味変更(動作互換)の影響は同じように受けます。
5. .NETアセンブリの互換性 ── 公式ルールで機械的に判定する
.NETには、Microsoftが.NETライブラリ自身の開発で使っている「互換性のための変更ルール」が公開されており、変更が許可(✔️)・禁止(❌)・要判断(❓)に分類されています。2 自社ライブラリの判断基準としてそのまま採用できると明記されているので、主要な行を抜粋します。
| public APIへの変更 | 判定 | 補足 |
|---|---|---|
| メソッド・型・メンバの追加 | ✔️ 原則安全 | ただし既存オーバーロードの解決を変える追加は要注意。公開structへのインスタンスフィールド追加は例外で、サイズ・レイアウトが変わるため相互運用やunsafeな利用者を壊す |
| public型・メンバの削除・改名 | ❌ 破壊的 | MissingMethodException等で実行時に壊れる |
| シグネチャ変更(引数の追加・削除・順序・型、戻り値の型) | ❌ 破壊的 | バイナリ・ソース両方を壊す |
| パラメータ名の変更 | ❌ 破壊的 | C#の名前付き引数とVBの遅延バインディングを壊す。見落としやすい |
| メンバへのvirtual追加 | ❌ 破壊的 | 「追加だから安全」に見える代表的な落とし穴。呼び出しIL(call/callvirt)の食い違いが起きうる |
| virtualの削除、仮想メンバのabstract化 | ❌ 破壊的 | 派生クラスのオーバーライドが壊れる |
| 非sealedな公開型への抽象メンバ追加 | ❌ 破壊的 | 既存の派生クラスが実装を持たない |
| 型のsealed化 | ❌ 破壊的 | 既存の派生クラスがコンパイル不能に |
| インターフェースへのメンバ追加 | ❓ 要判断 | 既定実装(DIM)を付ければ緩和できるが言語・ランタイム条件あり |
| 定数・列挙値の値の変更、列挙メンバの改名・削除 | ❌ 破壊的 | 値はコンパイル時に呼び出し側へ埋め込まれる |
| より派生した例外を投げるよう変える | ✔️ 許可 | 既存のcatchが引き続き機能するため |
| 既存コードパスで新種の例外を投げる | ❌ 破壊的 | 新しいパラメータ値でのみ投げるのは可 |
COMの「インターフェース不変」ほど単純ではありませんが、思想は同じです。公開APIは契約であり、契約への追加は許されるが、既存の契約の変更は許されない。そして仮想メソッドやパラメータ名のような「一見安全な変更」が破壊的側に分類されている点こそ、感覚ではなく表で判断すべき理由です。
5.1 強い名前と3つのバージョン番号
.NETアセンブリには複数のバージョン番号があり、役割が異なります。10
- AssemblyVersion: ランタイムがアセンブリの識別・ロードに使う唯一のバージョン。強い名前付きアセンブリでは、.NET FrameworkのCLRは厳密一致を要求するため、上げるたびに呼び出し側のバインディングリダイレクトが必要になります(.NET/.NET Coreは上位バージョンを自動で受け入れます)。公式ガイダンスは、リダイレクトを減らすためにメジャーバージョンのみをAssemblyVersionに反映させることを提案しています。
- FileVersion(AssemblyFileVersion): エクスプローラーのプロパティで見えるだけで、ランタイムの動作には影響しません。CIのビルド番号を埋める場所として推奨されています。
- InformationalVersion: 人間向けの自由な文字列。semver形式のパッケージバージョンやソースのコミットハッシュを記録します。
つまり実務では、「互換性の宣言はパッケージ/製品バージョン(semver)で行い、AssemblyVersionはメジャーのみ、FileVersionでビルドを追跡する」という三層構成が扱いやすい形です。
6. バージョン番号の付け方 ── semverは「定義」があって初めて機能する
セマンティックバージョニング(semver)の要点は3行で書けます。互換性のない変更をしたらMAJOR、後方互換のある機能追加はMINOR、後方互換のあるバグ修正はPATCHを上げる、です。5
見落とされがちなのは、semver仕様の最初の要求が「semverを使うソフトウェアは公開APIを宣言しなければならない」であることです。5 何が公開APIかを宣言していなければ、「互換性のない変更」の判定基準が存在せず、メジャーを上げるべきかどうかは担当者の気分で決まります。semverが機能していない現場の多くは、番号の付け方ではなくこの宣言を省略しています。
社内配布DLLでの現実的な運用は次の形になります。
- 公開APIの範囲を宣言する ── ネイティブDLLならエクスポート関数と公開ヘッダ、COMならIDL/タイプライブラリ、.NETならpublic型・メンバ。「これ以外は内部実装で予告なく変わる」と明記します。
- 破壊的変更の定義を採用する ── 本記事の3章・5章の判断表、および.NETの変更ルール2を「自社の定義」としてリポジトリに置きます。
- 判定を自動化する ── .NETならPackage Validation / ApiCompatツールで、前バージョンとのバイナリ互換を機械チェックできます。11 レビューでの「たぶん大丈夫」を排除できます。
- リリースノートに互換性欄を設ける ── 「再ビルド不要/再ビルド推奨/破壊的変更あり」の3値を毎回明記します。冒頭の「差し替えだけでいいですか?」への答えを、聞かれる前に文書で出す仕組みです。
7. 互換性を壊さざるを得ないときの手順
判断表で「破壊的」と判定された変更がどうしても必要になったときは、差し替えではなく並行提供で進めます。
- 新旧を並行提供する ── COMなら
IFoo2を追加してIFooを残す(4章)。ネイティブDLLなら新関数(FooEx)の追加か、別名の新DLLを併存させる。.NETならメジャーバージョンを上げた新パッケージとして出し、旧メジャーはバグ修正のみ続けます。 - 非推奨期間を設ける ── .NETなら
[Obsolete]属性でコンパイル時に警告を出せます。ネイティブ/COMではヘッダのコメントとリリースノートで宣言し、廃止予定日を明記します。「いつか消す」ではなく日付を切るのが要点です。 - 呼び出し側を棚卸しする ── 社内のソース検索、インストーラーの配布記録、COMならレジストリの参照状況から「誰がまだ旧APIを呼んでいるか」を一覧化します。ここで再ビルドできないバイナリ(退職者のツール、他社製アプリ)が見つかったら、その分だけ旧APIの寿命を延ばすか、ラッパーで橋を架けます。
- 旧APIを削除する ── 棚卸しで呼び出し側ゼロを確認してから削除し、メジャーバージョンを上げます。
この手順はコストがかかります。だからこそ逆説的に、最初の公開時に判断表を意識してAPIを小さく設計する(公開しなければ互換性の義務は生じない)ことが、最大の互換性対策になります。
8. まとめ
- 互換性はバイナリ・ソース・動作の3層で考えます。再ビルド不要でも動作互換は壊れえます。3
- ネイティブDLLは「追加は安全、既存エクスポート・シグネチャ・構造体レイアウトの変更は破壊的」。構造体には
cbSizeを持たせて拡張余地を作ります。6 - COMインターフェースは公開したら不変。変更は新IIDの新インターフェース(IFoo2)として追加し、
QueryInterfaceで判別させます。147 事前バインディングのVB6/VBAクライアントがいる場合は特に厳格に守ります。 - .NETは公式の互換性変更ルールで機械的に判定できます。仮想化・パラメータ名変更・sealed化など「一見安全な変更」が破壊的に分類されている点に注意します。2
- AssemblyVersionはメジャーのみ・FileVersionでビルド追跡・semverで互換性宣言という三層構成が現実的です。10
- semverは公開APIと破壊的変更の定義を宣言して初めて機能します。5 判断表を定義として採用し、Package Validation等で自動チェックします。11
- 壊すときは並行提供→非推奨期間→棚卸し→削除。いきなり差し替えないことが、客先の古いEXEを守ります。
関連記事
- COM / ActiveX / OCX とは何か - 違いと関係をまとめて解説
- COM とは何か - Windows COM の設計が今でも美しい理由
- .NET 8のDLLをVBAから型付きで呼ぶ - dscomとTLB
- Windows DLL名前解決の仕組み - 検索順序とSxS
- Reg-Free COMとは - 登録不要でCOMを使う仕組み
- C# Native AOT DLLをC/C++から呼び出す方法
関連する相談領域
合同会社小村ソフトでは、他システムから参照されるDLL・COMコンポーネント・.NETライブラリの互換性設計、公開APIの棚卸しとバージョニング方針の整備、既存クライアントを壊さない拡張(IFoo2方式・並行提供)の設計・実装を扱っています。
参考リンク
-
Microsoft Learn, Interface Design Rules. COMオブジェクトが実装するインターフェースは一意なIIDを持たねばならないこと、作成・公開後は定義のいかなる部分も変更してはならない(不変である)ことについて。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Change rules for compatibility (.NET). .NETのAPI変更が許可・禁止・要判断に分類されていること、public型・メンバの削除や改名、シグネチャ変更、パラメータ名の変更、virtualの追加・削除、sealed化、定数・列挙値の値変更などが禁止(破壊的)であること、インターフェースへのメンバ追加が要判断であること、ライブラリ開発者が自身のライブラリの評価基準として使えることについて。 ↩ ↩2 ↩3 ↩4 ↩5
-
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. 互換性のないAPI変更でMAJOR、後方互換のある機能追加でMINOR、後方互換のあるバグ修正でPATCHを上げること、semverを使うソフトウェアは公開APIを宣言しなければならないこと、公開APIへの後方互換性のない変更ではMAJORバージョンを必ず上げなければならないことについて。 ↩ ↩2 ↩3 ↩4
-
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がCOMクラスを識別するGUIDであること、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
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
テストのないレガシー業務アプリに安全に手を入れる ── 特性化テストとリファクタリングの実践
テストのない業務アプリを「触ると壊れそうで直せない」と感じている方向けに、現在の挙動を固定する特性化テスト(ゴールデンマスター法)の具体手順、テストを差し込む継ぎ目(seam)の作り方、どこまで整備するかの判断表、リファクタリングと機能追加を混ぜない運用ルールをC#の例とと...
VB6アプリはいつまで動くのか ── ランタイムのサポート状況と現実的な.NET移行の進め方
VB6アプリケーションはいつまで動くのか。VB6ランタイムのサポートポリシー(Windows 11でも動作対象)と、IDEサポート終了済みという非対称な現状を整理し、全面リライト・自動変換・段階移行の判断表、移行前の棚卸し、VB6と.NETの非互換、C#とVB.NETの選択...
Windowsのプロセス間通信をどう選ぶか ── 名前付きパイプ / TCP / gRPC / 共有メモリ / COM 判断表
Windowsアプリ同士の連携手段をどう選ぶか。名前付きパイプ、ローカルTCP、gRPC、共有メモリ、ファイル連携、COMの得意分野と落とし穴を判断表で整理し、UI+サービス分離・32bit/64bitブリッジ・権限境界などの定番構成と、名前付きパイプの実装例まで実務目線で...
業務アプリのDBスキーマをバージョン管理する ── 「客先ごとにDBが違う」を防ぐマイグレーションの実践
客先ごとに分散する業務アプリのDBスキーマをバージョン管理する実践ガイド。PRAGMA user_versionと前進マイグレーションのC#実装、EF Core Migrations・DbUp・自前実装の判断表、expand-contractの2段階リリース、適用前バックア...
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
既存資産活用・移行支援
COM / ActiveX / OCX、32bit / 64bit 制約を抱える既存資産の活用と移行を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- DLLに関数を追加するだけなら、呼び出し側の再ビルドは不要ですか?
- エクスポート関数を追加するだけなら、既存の呼び出し側はそのまま動くのが原則です。既存関数の名前・シグネチャ・呼び出し規約・エクスポート序数を変えない限り、インポート解決は今まで通り成立するためです。ただし、呼び出し側が確保して渡す構造体にメンバを追加した場合や、既存関数の戻り値・エラーコードの意味を変えた場合は、再ビルドなしで動いても動作互換が壊れることがあります。「関数追加は安全、既存シグネチャの変更は破壊的」が基本線です。
- COMインターフェースに後からメソッドを追加してはいけないのはなぜですか?
- COMのインターフェースは公開したら不変(immutable)というのがCOM仕様上のルールだからです。インターフェースはvtable(関数ポインタの並び)というバイナリレイアウトの契約であり、メソッドを挿入・削除・並べ替えすると、古いバイナリはコンパイル時に焼き込んだ位置で別のメソッドを呼んでしまいます。末尾への追加なら既存スロットの位置は変わりませんが、今度は新しいクライアントが「追加済みのはず」の実装として古いコンポーネントを掴み、存在しないスロットを呼ぶ事故が起きるため、同じIIDのままの追加はやはり許されません。機能を増やしたい場合は、新しいIIDを持つ新インターフェース(IFoo2)を追加し、既存のIFooはそのまま残します。呼び出し側はQueryInterfaceで新旧どちらに対応しているかを安全に判別できます。
- .NETのAssemblyVersion・FileVersion・InformationalVersionはどう使い分けますか?
- AssemblyVersionはランタイムがアセンブリを識別・ロードするために使う唯一のバージョンで、強い名前付きでは.NET Frameworkが厳密一致を要求するため、上げるたびにバインディングリダイレクトが必要になります。このためメジャーバージョンのみを反映させる運用が公式ガイダンスで提案されています。FileVersionはエクスプローラーのプロパティに表示されるだけでランタイムの動作には影響せず、CIのビルド番号などを埋めるのに向きます。InformationalVersionは人間向けの自由な文字列で、semver形式のバージョンやコミットハッシュを記録します。
- セマンティックバージョニング(semver)を導入すれば互換性問題は解決しますか?
- semverだけでは解決しません。semverは「後方互換性のない変更をしたらメジャーを上げる」という規約ですが、その前提として「何が公開APIで、何が破壊的変更か」を宣言することを要求しています。この定義がないままバージョン番号だけを付けても、判断が人によってぶれて機能しません。ネイティブDLLなら本記事の判断表のような基準、.NETならMicrosoftの互換性変更ルールを「自社の破壊的変更の定義」として採用し、リリース手順に組み込むことで初めてsemverが意味を持ちます。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク