COM とは何か - Windows COM の設計が今でも美しい理由

· 更新日: · · COM, ActiveX, Windows開発

更新履歴(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による契約の事前定義が、この仕組み全体を言語やプロセスの境界から独立させています。

COMの設計が美しい理由の知識マップCOMがIUnknownによる参照カウントとQueryInterface、CLSID・IIDによる識別を土台に、バイナリ互換性・インターフェース分離・バージョン共存・プロセス境界を越えた再利用という強みを実現し、In-procとOut-of-procの配置形態やHRESULT、.NETとの相互運用へつながる関係を示す図。利用する利用する利用する利用する利用する利用する実装を担う前提とする実装を担う前提とする利用する利用する前提とする原因になり得る防止する原因になり得る利用する利用する利用する利用する利用する利用する前提とするCOM(コンポーネントオブジェクトモデル)IUnknownQueryInterfaceCOMの参照カウント(AddRef/Release)CLSID(Class ID)IID(インターフェース識別子)HRESULTCOMのバイナリ互換性COMのバージョン共存In-proc COM(DLLサーバー)COM LocalServer(別プロセスCOMサーバー)bitness一致要件In-procのクラッシュ道連れOut-of-proc COMの切断エラーProxy/StubIDL(インターフェース定義言語).NETのCOM相互運用(COM Interop)ProgID(Programmatic Identifier)ActiveX

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全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で識別された、メソッドの並び順が決まっているインターフェース」だけになります。言語もコンパイラも、この契約には現れません。

実装 ── 言語は問わない呼び出し側 ── 言語は問わないC++ で書いたコンポーネントC# で書いたコンポーネントC++ のアプリC# のアプリVBA / Python など契約(バイナリで固定されている部分)・IID(GUID)で「どの契約か」を一意に決める・IUnknown の3メソッドから始まる並び順・メソッドごとの引数・戻り値の型と呼び出し規約

図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 を自分で書く場面はほとんどありません。CoCreateInstanceQueryInterface も、返すポインターに対してすでに 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通りも「持っていない」という答えが返るだけで済みます。

QueryInterface(IID_ICalcService)S_OKQueryInterface(IID_ICalcService)S_OK ── 更新しても壊れないQueryInterface(IID_ICalcServiceEx)S_OK ── 新機能が使えるQueryInterface(IID_ICalcServiceEx)E_NOINTERFACE ── 旧機能で続行古い呼び出し側ICalcService しか知らない新しい呼び出し側ICalcServiceEx を尋ねてみる古い版のコンポーネントICalcService だけを実装新しい版のコンポーネント両方を実装

図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を理解しておくと、マイクロサービス間のインターフェース設計で「なぜ契約を先に決めるのか」「なぜ既存フィールドを消してはいけないのか」が、流行ではなく必然として腑に落ちます。

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

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サーバー)による別プロセスの機能の安全な呼び出しです。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る