更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- バイナリ契約の構造と、QueryInterfaceによる新旧の組み合わせを示す図を追加しました。本文の説明は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- コード例の章を新設しました。C++の生COMで`CoCreateInstance`から`QueryInterface`、`Release`までを示し(`E_NOINTERFACE`が正常系である点を含む)、同じGUIDを使うC#版を並べています。In-procとOut-of-procの対比表、3要素と4つの強みの対応表、COMと現代技術の対応表を追加しました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589577)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「COM とは何か - Windows COM の設計が今でも美しい理由」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589577 https://staging.comcomponent.com/blog/2026/01/25/001-why-com-is-beautiful/
- DOI(最新版)
- 10.5281/zenodo.21589577
- DOI(この版)
- 10.5281/zenodo.21732573
COMとは?
COM(Component Object Model)とは、Windows上でコンポーネント同士がやりとりするための「バイナリ契約」 です。言語やコンパイラの違いを超えて、インターフェースという厳密な契約で通信する仕組みで、「実装ではなく、契約に対してプログラミングする」という設計思想が根底にあります。
この記事の知識マップ
COMはWindows上でコンポーネント同士がやり取りするためのバイナリ契約で、すべてのインターフェースが継承するIUnknownがQueryInterfaceによる機能確認とAddRef/Releaseによる参照カウント管理を提供します。CLSIDとIIDというGUIDでコンポーネントとインターフェースを一意に識別することで名前の衝突を避け、既存インターフェースを変えずに新しいインターフェースを追加してQueryInterfaceで確認する設計がバージョン共存を実現します。呼び出し側と同じプロセスに読み込むIn-proc(DLLサーバー)はビット数の一致が必須で相手のクラッシュに道連れになりますが、別プロセスで動くOut-of-proc(LocalServer)はその制約から自由な代わりに、相手が消えたときのRPC_E_DISCONNECTEDのような切断エラーへの備えが必要になります。HRESULTという戻り値による成否表現とProxy/Stub、IDLによる契約の事前定義が、この仕組み全体を言語やプロセスの境界から独立させています。
flowchart LR
accTitle: COMの設計が美しい理由の知識マップ
accDescr: COMがIUnknownによる参照カウントとQueryInterface、CLSID・IIDによる識別を土台に、バイナリ互換性・インターフェース分離・バージョン共存・プロセス境界を越えた再利用という強みを実現し、In-procとOut-of-procの配置形態やHRESULT、.NETとの相互運用へつながる関係を示す図。
com["COM(コンポーネントオブジェクトモデル)"]
iunknown["IUnknown"]
queryinterface["QueryInterface"]
com_reference_counting["COMの参照カウント(AddRef/Release)"]
clsid["CLSID(Class ID)"]
iid["IID(インターフェース識別子)"]
hresult["HRESULT"]
com_binary_compatibility["COMのバイナリ互換性"]
com_interface_versioning["COMのバージョン共存"]
in_proc_com["In-proc COM(DLLサーバー)"]
com_localserver["COM LocalServer(別プロセスCOMサーバー)"]
bitness_match_requirement["bitness一致要件"]
in_proc_crash_propagation["In-procのクラッシュ道連れ"]
out_of_proc_com_disconnection_error["Out-of-proc COMの切断エラー"]
proxy_stub["Proxy/Stub"]
idl["IDL(インターフェース定義言語)"]
dotnet_com_interop[".NETのCOM相互運用(COM Interop)"]
progid["ProgID(Programmatic Identifier)"]
activex["ActiveX"]
com -->|"利用する"| iunknown
com -->|"利用する"| queryinterface
com -->|"利用する"| com_reference_counting
com -->|"利用する"| clsid
com -->|"利用する"| iid
com -->|"利用する"| hresult
com -->|"実装を担う"| com_binary_compatibility
com_binary_compatibility -->|"前提とする"| iunknown
com -->|"実装を担う"| com_interface_versioning
com_interface_versioning -->|"前提とする"| queryinterface
com -.->|"利用する"| in_proc_com
com -.->|"利用する"| com_localserver
in_proc_com -->|"前提とする"| bitness_match_requirement
in_proc_com -->|"原因になり得る"| in_proc_crash_propagation
com_localserver -->|"防止する"| in_proc_crash_propagation
com_localserver -.->|"原因になり得る"| out_of_proc_com_disconnection_error
com -.->|"利用する"| proxy_stub
com -->|"利用する"| idl
dotnet_com_interop -.->|"利用する"| progid
dotnet_com_interop -->|"利用する"| queryinterface
dotnet_com_interop -->|"利用する"| hresult
dotnet_com_interop -->|"利用する"| com_reference_counting
activex -->|"前提とする"| com
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全23件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
COMの3つの重要な要素
この章で挙げるのは、COMという仕組みを構成している部品です。このあとの「COMの4つの強み」は、これらの部品を組み合わせた結果として得られる性質なので、同じことを2回説明しているわけではありません。対応関係は強みの章の最後に表で示します。
1. インターフェース中心の設計
COMでは「実装より契約が先」。オブジェクトの内部実装を知らなくても、公開されたインターフェースさえ知っていれば利用できます。
2. GUID(CLSID / IID)による識別
すべてのコンポーネントとインターフェースには世界で一意なID(GUID) が付与されるので、名前の衝突がそもそも起きません。
3. IUnknown
すべてのCOMインターフェースが継承する基本インターフェース。以下の3つの機能を提供します。
| メソッド | 役割 |
|---|---|
QueryInterface |
別のインターフェースを持っているか尋ねる |
AddRef |
参照カウントを増やす |
Release |
参照カウントを減らす(0になったら自分を破棄) |
この3つの要素が組み合わさると、呼び出し側と実装のあいだに残るのは「GUIDで識別された、メソッドの並び順が決まっているインターフェース」だけになります。言語もコンパイラも、この契約には現れません。
flowchart LR
subgraph CALLER["呼び出し側 ── 言語は問わない"]
A1["C++ のアプリ"]
A2["C# のアプリ"]
A3["VBA / Python など"]
end
CONTRACT["契約(バイナリで固定されている部分)<br/>・IID(GUID)で「どの契約か」を一意に決める<br/>・IUnknown の3メソッドから始まる並び順<br/>・メソッドごとの引数・戻り値の型と呼び出し規約"]
subgraph IMPL["実装 ── 言語は問わない"]
B1["C++ で書いたコンポーネント"]
B2["C# で書いたコンポーネント"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
図1: COMのバイナリ契約。呼び出し側の言語も実装の言語も契約には現れないので、どちらを入れ替えても相手を再ビルドしなくてよい
コードで見る「契約に対してプログラミングする」
文章だけだと分かりにくいので、最小のコードにしてみます。足し算をするだけのコンポーネント ICalcService を、実装を一切知らずに使う例です。
まずC++(生のCOM)から。ここに書かれた ICalcService の定義が契約そのもので、実装がC++で書かれているのかC#で書かれているのかは、呼び出し側のコードのどこにも出てきません。
#include <objbase.h>
// 契約の定義。通常はIDLからこの形のヘッダーが生成される
// IUnknown を継承しているので QueryInterface / AddRef / Release を必ず持つ
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};
// あとから追加された拡張版の契約。GUIDは別物になる
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
// 実装コンポーネントのCLSID。通常はIDLから生成されるヘッダーで定義される
static const CLSID CLSID_CalcService =
{ 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };
// 呼び出し側
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }
ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
__uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// ここで返ってきたポインターは AddRef 済み(参照カウントは1)
if (SUCCEEDED(hr))
{
int sum = 0;
hr = calc->Add(1, 2, &sum); // 実装を知らずに呼べる
// 「拡張版の契約も持っているか」を実行時に尋ねる = バージョン共存の実装
ICalcServiceEx* calcEx = nullptr;
if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
reinterpret_cast<void**>(&calcEx))))
{
// 新しい版のコンポーネントだったときだけここに来る
calcEx->Release(); // 受け取った側が返す
}
// 持っていなければ E_NOINTERFACE が返るだけで、古い版でも動き続ける
calc->Release(); // 参照カウントが0になり破棄される
}
CoUninitialize();
読みどころは3つです。
AddRefを自分で書く場面はほとんどありません。CoCreateInstanceもQueryInterfaceも、返すポインターに対してすでにAddRefを済ませています。つまり「ポインターを受け取った側がReleaseする」が原則で、AddRefを明示的に呼ぶのは、同じポインターをもう1か所で保持し始めるときだけです。QueryInterfaceの失敗は異常ではありません。「その契約は持っていません」(E_NOINTERFACE)は正常な答えで、これが「古いコンポーネントを壊さずに新しい機能を足す」を成立させています。- 戻り値はすべて
HRESULT。例外ではなく戻り値で成否を返すのは、言語をまたぐために必要な取り決めです。例外の投げ方は言語ごとに違いますが、整数の戻り値なら誰でも解釈できます。
同じ契約をC#から使うとこうなります。GUIDが同じなら同じ契約とみなされるので、相手がC++実装でも構いません。
using System;
using System.Runtime.InteropServices;
// C++側と同じGUIDを書く。これが「同じ契約」の宣言
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// 呼び出し側
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
?? throw new InvalidOperationException("COMサーバーが登録されていません。");
object server = Activator.CreateInstance(t)!;
try
{
var calc = (ICalcService)server; // このキャストが QueryInterface に相当する
int sum = calc.Add(1, 2);
Console.WriteLine(sum); // 3
}
finally
{
Marshal.ReleaseComObject(server); // Release に相当する
}
C#側で Add の戻り値が int になっているのは、.NETのCOM相互運用が「最後の [out, retval] 引数を戻り値にし、失敗した HRESULT は例外に変換する」という決まった変換をしてくれるからです。AddRef/Release も呼び出し側では見えなくなりますが、消えたわけではなく、.NETが用意する薄いラッパー(RCW)が代わりに呼んでいます。同じ契約を、言語ごとの自然な書き方で使える──これが「バイナリ契約」の実際の姿です。
COMの4つの強み
1. バイナリ互換性
一度ビルドしたコンポーネントは、プログラミング言語やランタイムを問わず再利用できます。C++で作ったCOMコンポーネントをC#やPythonから呼び出す、といったことが普通にできるわけです。
2. インターフェース分離
実装を完全に隠して契約だけを公開するため、内部実装を自由に変更しても呼び出し側には影響しません。
3. バージョン共存
後方互換性を保ったまま機能追加するために、新しいインターフェースを追加していく設計が基本です。古いインターフェースを変更せずに新機能を提供できます。
新旧の呼び出し側と新旧のコンポーネントを組み合わせると、4通りのうち3通りがそのまま動き、残る1通りも「持っていない」という答えが返るだけで済みます。
flowchart LR
OLDC["古い呼び出し側<br/>ICalcService しか知らない"]
NEWC["新しい呼び出し側<br/>ICalcServiceEx を尋ねてみる"]
OLDS["古い版のコンポーネント<br/>ICalcService だけを実装"]
NEWS["新しい版のコンポーネント<br/>両方を実装"]
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK"| OLDS
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK ── 更新しても壊れない"| NEWS
NEWC -->|"QueryInterface(IID_ICalcServiceEx)<br/>S_OK ── 新機能が使える"| NEWS
NEWC -.->|"QueryInterface(IID_ICalcServiceEx)<br/>E_NOINTERFACE ── 旧機能で続行"| OLDS
図2: 既存のインターフェースを変えずに新しいIIDを足すと、新旧どの組み合わせでも動き続ける。点線は「その契約は持っていない」という正常な答え
4. プロセス境界を越えた再利用
COMコンポーネントの置き場所には2種類あります。呼び出し側と同じプロセスにDLLとして読み込む In-proc(DLLサーバー)と、別プロセスのEXEとして起動する Out-of-proc(EXEサーバー、LocalServer)です。呼び出すコードの見た目はどちらも変わりません。
| In-proc(DLLサーバー) | Out-of-proc(EXEサーバー) | |
|---|---|---|
| 実行される場所 | 呼び出し側と同じプロセス | 別プロセス |
| 呼び出しの実際 | 関数ポインター経由の直接呼び出し | 引数を詰め直して(マーシャリング)プロセス間通信で渡す |
| 速度 | 速い | プロセス間通信のぶん遅い |
| 相手が落ちたとき | 呼び出し側も道連れになる | 呼び出し側は生き残る(呼び出しが失敗として返る) |
| ビット数(32/64bit) | 一致していないと読み込めない | 違っていてもよい |
Out-of-proc COM(EXEサーバー)を使えば、別プロセスの機能を安全に呼び出せます。「32bit/64bitが違っていてもよい」という最後の行は実務で効く場面があり、32bitアプリから64bit DLLを使う具体的な組み方は「32bitアプリから64bit DLLを呼ぶCOMブリッジ実例」で扱っています。
ただし、別プロセスであることは相手がいつでも消えうることも意味します。サーバープロセスがクラッシュしたり終了したりすると、呼び出し側には次のような失敗が返ります。これらは「壊れた」ではなく「相手がもういない」を表す、Out-of-proc特有のエラーです。
| エラーコード | 意味 |
|---|---|
RPC_E_DISCONNECTED |
呼び出そうとしたオブジェクトが、クライアントから切り離されている(相手のオブジェクトがすでに無い) |
RPC_S_SERVER_UNAVAILABLE |
呼び出し先のサーバー(プロセス)に到達できない |
In-procなら「落ちたら自分も落ちる」ので考えなくてよかった話が、Out-of-procでは再起動や再接続の復旧処理として設計に入ってきます。安全性と引き換えに増える仕事、と考えるのが正確です。
3要素と4つの強みの対応
冒頭で触れたとおり、「3つの要素」は部品、「4つの強み」はその結果です。どの部品がどの強みを支えているかを並べると、重なって見えていたものの役割分担がはっきりします。
| 強み | 主に効いている要素 |
|---|---|
| 1. バイナリ互換性 | インターフェース中心の設計 + IUnknown(呼び出しの並び順がバイナリレベルで決まる) |
| 2. インターフェース分離 | インターフェース中心の設計(契約だけを公開する) |
| 3. バージョン共存 | GUID + QueryInterface(別の契約を実行時に尋ねられる) |
| 4. プロセス境界を越えた再利用 | インターフェース中心の設計(実装の場所まで契約から切り離せる) |
COMは今でも現役
「古い技術」と思われがちですが、COMはWindowsの中核で今も使われ続けている仕組みです。
COMが使われている場所
- エクスプローラー拡張(右クリックメニュー、プレビュー表示)
- Office自動化(ExcelやWordの外部制御)
- .NETとの相互運用(COM Interop)
- ActiveXを含む既存システム
- DirectX、Windows Shell API など多数のWindows API
「自分には関係ない」と思っていても、Windows開発をする限りCOMはどこかに登場します。
まとめ
COMの設計の中心は 「言語・プロセス・実装からの独立」 です。言語に中立なインターフェース設計、GUIDによる一意識別とバージョン管理、IUnknownによる参照カウント、そしてプロセス間通信を透過的に扱える仕組み。
「美しい」というのは主観的な言い方ですが、そう評価する根拠は具体的です。COMが解こうとした問題と、その解き方を並べるとこうなります。
| 当時(そして今も)ある制約 | COMの答え |
|---|---|
| C++には標準のバイナリ規約(ABI)がなく、コンパイラが違うだけで再利用できない。名前修飾もオブジェクトの配置も揃わない | 「仮想関数テーブルの並び順」だけを規約にした。この1点に絞ったから、言語もコンパイラも問わなくなった |
| ライブラリを差し替えると、それを使う全部を再ビルドしなければならない | 契約(インターフェース)が変わらない限り再ビルド不要。バイナリのまま差し替えられる |
| 機能を足したいが、既存の呼び出し側を壊せない | インターフェースを変えずに追加し、QueryInterface で「持っているか」を実行時に尋ねる |
| 名前が衝突する(同じクラス名の別物が同居する) | GUIDで一意に識別する。名前の調整をそもそも不要にした |
| 別プロセス・別マシンの機能を呼ぶと、呼び出しの書き方がまるで変わる | Proxy/Stubを挟んで、呼び出し側のコードを同じ形に保つ |
つまりCOMの設計は、「どうすればきれいか」から始まったのではなく、再利用を阻んでいた具体的な障害を1つずつ潰した結果として今の形になっています。制約と解の対応がここまで素直に追える設計はそう多くありません。
そして、この解き方は現代のコンポーネント指向開発にそのまま残っています。
| COM | 現代の対応物 |
|---|---|
| IDLで契約を先に決め、そこから両側のコードを生成する | OpenAPIやProtocol Buffersのスキーマからクライアント/サーバーを生成する |
| 既存インターフェースを変えず、新しいインターフェースを追加する | Protocol Buffersでフィールド番号を再利用せず追加する、APIをバージョン分けする |
| 実装言語を問わない(バイナリ契約) | 実装言語を問わない(ネットワーク越しのメッセージ契約) |
| Proxy/Stubがプロセス境界を隠す | RPCのクライアントスタブがネットワーク境界を隠す |
違うのは境界の種類(同一マシンのプロセス境界か、ネットワークか)と契約の表現(バイナリか、テキスト/スキーマか)だけです。COMを理解しておくと、マイクロサービス間のインターフェース設計で「なぜ契約を先に決めるのか」「なぜ既存フィールドを消してはいけないのか」が、流行ではなく必然として腑に落ちます。
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
COM/OCX/ActiveX開発でハマる登録とbitnessの罠
COM、OCX、ActiveX開発でハマりやすい32bit/64bit、Visual Studio 2022、regsvr32/Regasm、管理者権限、HKCR、STA/MTAを実務目線で整理します。
COM / ActiveX / OCX とは何か - 違いと関係をまとめて解説
COMとは何か、ActiveXとは何か、OCXとは何かを、違いと関係、OLEとのつながり、どこで使われるのか、今どう捉えるべきかまで実務目線で整理します。
ActiveX / OCX を今どう扱うか - 残す・包む・置き換える判断表
ActiveX / OCX を見つけたときに、残す・包む・置き換えるのどれを選ぶべきかを、32bit / 64bit、登録、ブラウザ依存、ベンダー保守まで含めて整理します。
Arm版Windowsで業務アプリは動くのか ── x64エミュレーション(Prism)とネイティブDLL・COMの現実
「Arm版Windowsで業務アプリは動くのか」に開発者・情シス向けに答えます。x64エミュレーション(Prism)の仕組み、ドライバーなど動かない層、.NETのAnyCPUとP/Invokeの問題、Arm対応チェックリストまで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
既存資産活用・移行支援
COM の設計や互換性を理解することは、既存 Windows 資産をどう活かすかを考える入口として相性がよいです。
技術相談・設計レビュー
IUnknown、GUID、境界設計の見方を踏まえて方針整理したい場合は、技術相談・設計レビューにつながります。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- COMとは何ですか?
- COM(Component Object Model)とは、Windows上でコンポーネント同士がやりとりするための「バイナリ契約」です。言語やコンパイラの違いを超えて、インターフェースという厳密な契約で通信する仕組みです。根底には「実装ではなく、契約に対してプログラミングする」という設計思想があります。
- IUnknownとは何ですか?
- すべてのCOMインターフェースが継承する基本インターフェースです。別のインターフェースを持っているか尋ねる QueryInterface、参照カウントを増やす AddRef、参照カウントを減らして0になったら自分を破棄する Release の3つの機能を提供します。COMのオブジェクト寿命管理はこの参照カウントで成り立っています。
- COMは今でも使われていますか?
- はい、COMはWindowsの中核で今も使われ続けている仕組みです。エクスプローラー拡張(右クリックメニューやプレビュー表示)、ExcelやWordのOffice自動化、.NETとのCOM Interop、ActiveXを含む既存システム、DirectXやWindows Shell APIなど多数の場所で登場します。Windows開発をする限り、COMはどこかに登場します。
- COMの強みは何ですか?
- 大きく4つあります。一度ビルドしたコンポーネントを言語やランタイムを問わず再利用できるバイナリ互換性、実装を隠して契約だけを公開するインターフェース分離、新しいインターフェースを追加して後方互換を保つバージョン共存、そしてOut-of-proc COM(EXEサーバー)による別プロセスの機能の安全な呼び出しです。