更新履歴(8件・最終更新 2026年08月30日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 3章の手前に一段落を足しました。64bit 側で使いたいものが、もともと in-proc の COM サーバー(InprocServer32 で登録する DLL)である場合は、CLSID に AppID を付けてその AppID キーへ空文字の DllSurrogate を書くだけで Windows 付属の dllhost.exe に載せられ、EXE サーバーを書かずに済むことがある、という補足です。立つサロゲートの bitness はクライアントではなく DLL 側で決まること、LocalServer32 があるとサロゲートは使われないことも添えています。ただしこの記事のように呼びたいのがただのネイティブ DLL であれば、載せるべき COM サーバーがそもそも無いので、以降で説明する EXE サーバー方式が必要です。登録手順と、サロゲートで足りるか自前の EXE を書くかの線引きは、それぞれ別の記事へのリンクに任せています。
- レビュー指摘に対応し、今日追加した図のうち幅が大きすぎたものを縦向きの構成に直し、一部の図とキャプションの表現を本文の記述に合わせて正確にしました。本文の文章は変えていません。
- ビット数の制約、ブリッジの構成、レジストリ登録の落とし穴、採用判断などを図でも追えるように、Mermaid図を10点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。本文の文章は変えていません。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- `LocalServer32`に実行ファイルのパスを引用符なしで格納していたのを直しました。`/d "%SERVER%"`と書くと引用符は`reg.exe`の引数解析で消えるため、`C:\Program Files\...`は「`C:\Program`に`Files\...`を渡す」とも解釈でき、`C:\Program.exe`を置ける環境ではそちらが起動され得ます。引用符ごと格納する形にし、確認用の`reg query`も添えました。
- 冒頭に対象読者と前提環境を追加し、レジストリ登録の手順を新設しました(必要な3キーの表、`reg add /reg:64`と`/reg:32`による登録と解除、EXE側でのクラスファクトリ登録、管理者権限なしでHKCUに登録する代替)。ProgIDが何のためにあるかの説明、用語ミニ辞書、.NET Frameworkと.NET 5以降の差の対比表、まとめの章を追加しました。
- サンプルコードのクライアントが ProgID でサーバーを取得しているのに、サーバー側に ProgID を指定する属性が書かれておらず、そのままでは再現できませんでした。属性を補いました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589580)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「32bitアプリから64bit DLLを呼ぶCOMブリッジ実例」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589580 https://comcomponent.com/blog/2026/01/25/002-com-case-study-32bit-to-64bit/
- DOI(最新版)
- 10.5281/zenodo.21589580
- DOI(この版)
- 10.5281/zenodo.22170268
32bit アプリから 64bit DLL を呼び出したい、という要件は Windows ではかなり典型的です。 特に既存資産を残したまま 64bit 側の機能だけ使いたいとき、COM ブリッジの構成が現実的な解になりやすいです。
対象読者: 32bit の既存 Windows アプリを保守していて、64bit 側の DLL やライブラリを使いたい方。COM を「聞いたことはあるが自分で組んだことはない」程度の前提で読めるように書いています。
前提環境: 64bit 版の Windows(x64)と、C# を書ける開発環境(Visual Studio など)。COM サーバーをマシン全体(HKEY_LOCAL_MACHINE 配下)に登録する作業には管理者権限が必要です。COM の基本的な考え方は「COM とは何か - Windows COM の設計が今でも美しい理由」で整理しています。
目次
この記事の知識マップ
32bitアプリはビット構成が一致しないin-proc COMとして64bit DLLを直接ロードできないため、この記事では64bit側の処理をCOM LocalServer(EXEサーバー)として別プロセスに分離し、IDLとTypeLibで共有したCOMインターフェース越しに型付きで呼び出します。マーシャリングとProxy/Stubがプロセス境界をまたぐ呼び出しを橋渡しし、CLSIDとProgIDの登録、そしてCLSIDサブキーが32bit viewと64bit viewで別になるWOW64レジストリリダイレクターへの両面登録が前提になります。マシン全体への登録には管理者権限が必要で、.NET(5以降)は標準のEnableComHostingがin-proc専用のためOut-of-procサーバーの登録処理は自前で書く必要があります。
flowchart LR
accTitle: 32bit/64bit COMブリッジの知識マップ
accDescr: 32bitアプリが64bit DLLをCOM LocalServerのEXEサーバーとして呼び出す構成が、型ライブラリ・マーシャリング・CLSID/ProgIDの登録・WOW64レジストリリダイレクターとどう関係するかを示す図
com_bridge_32_64["32bit/64bit COMブリッジ"]
com_localserver["COM LocalServer(別プロセスCOMサーバー)"]
type_library["型ライブラリ(TLB)"]
com_marshaling["マーシャリング"]
proxy_stub["Proxy/Stub"]
in_proc_com["In-proc COM(DLLサーバー)"]
bitness_match_requirement["bitness一致要件"]
clsid["CLSID(Class ID)"]
progid["ProgID(Programmatic Identifier)"]
wow64_registry_redirector["WOW64レジストリリダイレクター"]
wow6432node["WOW6432Node"]
reg_exe["reg.exe(/reg:32 /reg:64)"]
class_factory_registration["クラスファクトリの登録"]
admin_rights["管理者権限"]
regasm["Regasm.exe"]
dotnet[".NET(Core以降)"]
dotnet_framework[".NET Framework"]
dotnet_comhost[".NET(5以降)のEnableComHosting/.comhost.dll"]
com["COM(コンポーネントオブジェクトモデル)"]
com_bridge_32_64 -->|"利用する"| com_localserver
com_bridge_32_64 -->|"前提とする"| type_library
com_localserver -->|"利用する"| com_marshaling
com_marshaling -->|"利用する"| proxy_stub
in_proc_com -->|"前提とする"| bitness_match_requirement
com_localserver -->|"軽減する"| bitness_match_requirement
com_localserver -->|"前提とする"| clsid
progid -->|"前提とする"| clsid
com_localserver -.->|"前提とする"| wow64_registry_redirector
clsid -.->|"に保存される"| wow6432node
wow64_registry_redirector -->|"で構成できる"| reg_exe
com_localserver -->|"前提とする"| class_factory_registration
com_bridge_32_64 -.->|"前提とする"| admin_rights
regasm -->|"用いるのは非推奨"| dotnet
regasm -->|"前提とする"| dotnet_framework
dotnet_comhost -->|"利用する"| dotnet
dotnet_comhost -->|"用いるのは非推奨"| com_bridge_32_64
dotnet -.->|"前提とする"| clsid
com -.->|"利用する"| type_library
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全19件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. 想定状況
32bitの既存アプリはそのままに、64bit DLLの処理を使いたいというケースです。 ところが、32bitプロセスは64bit DLLを読み込めません。これはOSレベルの制約で、工夫すればどうにかなるという話ではありません。
よくあるのは、こんな状況です。
- 既存の32bitアプリは資産として大きく、すぐには移行できない
- 64bit DLL側に新機能がある、または依存ライブラリが64bitのみ
- 32bit側から「型付きで」呼び出したい
この組み合わせでは、同一プロセス内で呼び出す道は最初から閉ざされています。
flowchart TB
accTitle: 想定状況の構図
accDescr: 32bitの既存アプリが64bit DLLの処理を使いたいのに、32bitプロセスは64bit DLLを読み込めないというOSレベルの制約で、同一プロセス内で呼び出す道が閉ざされていることを示す図。
app["32bitの既存アプリ"] --> want["64bit DLLの処理を使いたい"]
want --> deny["同一プロセスでは読み込めない"]
deny -.-> os["OSレベルの制約で回避できない"]
図1: 32bitプロセスは64bit DLLを読み込めないため、同一プロセス内で呼び出す道は最初から無い。
2. 解決方法
この章から専門用語が続くので、先に最小限の言葉を並べておきます。
| 用語 | 意味 |
|---|---|
| In-proc COM(DLLサーバー) | COMコンポーネントを呼び出し側と同じプロセスに読み込んで使う形。速いがビット数が一致していないと読み込めない |
| Out-of-proc COM(EXEサーバー) | COMコンポーネントを別プロセスとして起動して使う形。ビット数が違っても連携できる |
| LocalServer | 同じPC内の別プロセスとして動くCOMサーバーのこと。レジストリの LocalServer32 キーにEXEのパスを登録する |
| IDL / TypeLib | インターフェースの形(メソッド名・引数の型)を書いた定義(IDL)と、それをバイナリ化したもの(TypeLib)。両側が同じ「契約」を見るために使う |
| マーシャリング | プロセス境界を越えるために、引数や戻り値を送れる形に詰め直すこと。逆はアンマーシャリング |
| Proxy / Stub | マーシャリングを実際に行う代理コード。呼び出し側にProxy、サーバー側にStubが立つ |
| WOW6432Node | 64bit Windows 上で、32bit アプリ向けのレジストリ内容が物理的に置かれる場所。同じキー名でも32bit側と64bit側で中身が別になる |
解決の基本は、Out-of-proc COM(EXEサーバー)で分離することです。 64bit DLLは64bitのCOMサーバー(EXE)から呼び出し、32bitアプリはCOM経由で使います。
flowchart TB
accTitle: COMブリッジの基本構成
accDescr: 32bitアプリがCOM経由で64bitのCOMサーバーのEXEを呼び出し、そのサーバーが内部で64bit DLLを呼び出すという別プロセス分離の構成を示す図。
a32["32bitアプリ"] -->|"COM経由で呼び出す"| srv["64bit COMサーバー(EXE)"]
srv -->|"内部で呼び出す"| dll["64bit DLL"]
図2: 64bit DLLは64bitのEXEサーバーに持たせ、32bitアプリはCOM経由でそのサーバーを使う。
流れは次の通りです。
- 64bitのCOM LocalServer(EXE)を用意し、内部で64bit DLLを呼び出す
- COMインターフェース(IDL/TypeLib)を共有し、型を公開する
- 32bitアプリはCOMを「型付き」で呼び出す(Proxy/Marshalでやり取り)
ただし注意点もあります。
- 32bit/64bitの登録は別(WOW6432Node含む)
- 独自構造体はマーシャリング設計が必要
- IPCのオーバーヘッドがあるため、高頻度呼び出しは注意
つまり、「64bitの処理を別プロセスに逃がして、COMで橋渡しする」のが王道です。
flowchart TB
accTitle: ブリッジを組む3つの手順
accDescr: 64bitのCOM LocalServerを用意して内部で64bit DLLを呼び、IDLとTypeLibでインターフェースの型を公開し、32bitアプリが型付きで呼び出すという3つの手順の流れを示す図。
s1["64bitのCOM LocalServerを用意"] --> s2["IDL / TypeLibで型を公開"]
s2 --> s3["32bitアプリが型付きで呼び出す"]
s3 -.-> ps["Proxy / Marshalでやり取り"]
図3: LocalServerの用意、型の公開、型付きの呼び出しという3段階でブリッジが成立する。
なお、64bit 側で使いたいものが もともと in-proc の COM サーバー(InprocServer32 で登録する DLL) である場合は、EXEサーバーを書かずに済むことがあります。CLSID に AppID を付け、その AppID キーへ空文字の DllSurrogate を書くと、その DLL は Windows 付属のサロゲートプロセス(64bit の DLL なら System32\dllhost.exe)に載り、32bit クライアントからは別プロセスで動く out-of-proc の COM サーバーとして見えます(LocalServer32 に EXE のパスを登録するわけではありません。むしろ LocalServer32 があるとサロゲートは使われません)。逆向き(64bit アプリから 32bit の COM DLL を使う)でも仕組みは同じで、立つサロゲートの bitness はクライアントではなく DLL 側で決まります。ただし、この記事のように 呼びたいのがただのネイティブ DLL なら、サロゲートに載せる COM サーバーがそもそも無いので、以降の EXE サーバー方式が必要です。登録手順は COM/OCX/ActiveX開発でハマる登録とbitnessの罠 の 3.5(あちらは 32bit DLL 前提なので、AppID 値を書き込む view だけ読み替えてください)、サロゲートで足りるか自前の EXE を書くかの線引きは ActiveX / OCX を今どう扱うか - 残す・包む・置き換える判断表 の 5.2 にまとめています。
3. 処理の流れ(シーケンス図)
以下は、32bitアプリが64bit DLLの処理を呼び出すときの流れです。
sequenceDiagram
participant App as 32bit クライアントアプリ
box rgba(100,100,255,0.1) 登録済みのCOMマーシャリング基盤が処理
participant Proxy as COM Proxy<br/>(32bit側)
participant RPC as RPC/IPC<br/>(プロセス間通信)
participant Stub as COM Stub<br/>(64bit側)
end
participant Server as 64bit COM Server<br/>(EXE)
participant DLL as 64bit DLL
App->>Proxy: ICalcService.Add(1, 2)
rect rgba(100,100,255,0.1)
Note over Proxy: パラメータをマーシャリング
Proxy->>RPC: シリアライズされたデータ
RPC->>Stub: プロセス境界を越えて転送
Note over Stub: パラメータをアンマーシャリング
end
Stub->>Server: Add(1, 2)
Server->>DLL: ネイティブ関数呼び出し
DLL-->>Server: 結果: 3
Server-->>Stub: 結果: 3
rect rgba(100,100,255,0.1)
Note over Stub: 戻り値をマーシャリング
Stub-->>RPC: シリアライズされた結果
RPC-->>Proxy: プロセス境界を越えて転送
Note over Proxy: 戻り値をアンマーシャリング
end
Proxy-->>App: 結果: 3
図4: 32bitアプリの呼び出しはProxy・プロセス間通信・Stubを経て64bitサーバーと64bit DLLに届き、結果が同じ経路で戻る。
ポイント:
- 32bitアプリは
ICalcServiceインターフェースを通じて型安全に呼び出せる - COMランタイムは、登録済みの Proxy/Stub DLL、TypeLib マーシャラー、標準マーシャラーなどを使ってプロセス境界を越える
- プロセス間通信のオーバーヘッドがあるため、細かい呼び出しより一括処理が望ましい
flowchart TB
accTitle: 呼び出し粒度の考え方
accDescr: プロセス間通信にはオーバーヘッドがあるため、細かい呼び出しを高頻度で行うと積み重なり、一括処理に寄せるのが望ましいという考え方を示す図。
ipc["プロセス間通信のオーバーヘッド"] --> fine["細かい呼び出しの繰り返しでは積み重なる"]
ipc --> batch["一括処理に寄せれば影響が小さい"]
batch -.-> rec["望ましいのはこちら"]
図5: プロセス境界を越えるたびにコストがかかるので、呼び出しは粗い粒度にまとめるのが望ましい。
4. サンプルコード(イメージ)
4.1. 共有インターフェースとサーバー・クライアント
以下は概念のイメージです。動かすには、このあと 4.2 の登録が必要です。
// 共有インターフェース(IDL相当)
[ComVisible(true)]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// 64bit COM LocalServer(EXE側)
[ComVisible(true)]
[Guid("1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("KomuraSoft.CalcService")]
public class CalcService : ICalcService
{
public int Add(int a, int b)
{
// ここで64bit DLLを呼び出す
return a + b;
}
}
// 32bitアプリ側(クライアント)
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService");
var calc = (ICalcService)Activator.CreateInstance(t);
int result = calc.Add(1, 2);
この形にすると、32bit側は「型付きで」扱えます。 COMが内部でプロキシ/スタブを使い、IPC経由で呼び出してくれます。
[ProgId("KomuraSoft.CalcService")] を付けているのは、クライアント側が Type.GetTypeFromProgID("KomuraSoft.CalcService") で探せるようにするためです。ProgID は「人間が読める別名」にすぎず、実際にサーバーを見つけるのは次に説明する CLSID の登録です。
flowchart LR
accTitle: ProgIDからサーバーに届くまで
accDescr: クライアントが指定するProgIDは人間が読める別名にすぎず、そこからCLSIDが引かれ、CLSIDの登録によって実際のサーバーが見つかるという解決の順序を示す図。
progid["ProgID(人間が読める別名)"] --> clsid["CLSIDの登録"]
clsid --> srv["サーバーが見つかる"]
図6: ProgIDは入口の別名で、サーバーを実際に指しているのはCLSIDの登録のほう。
4.2. 最低限の登録手順
COMは「レジストリに登録されているCLSIDを、COMランタイムが引いて起動する」仕組みなので、登録していないコードは絶対に動きません(Type.GetTypeFromProgID が null を返すか、CreateInstance で REGDB_E_CLASSNOTREG になります)。EXEサーバー(LocalServer)に必要な登録は、突き詰めると次の3つのキーだけです。
| 登録するもの | キー | 値 |
|---|---|---|
| ProgID → CLSID の対応 | HKEY_CLASSES_ROOT\KomuraSoft.CalcService\CLSID |
{1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11} |
| CLSID → EXEのパス | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\LocalServer32 |
64bit COMサーバーEXEのフルパス |
| CLSID → ProgID の逆引き | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\ProgID |
KomuraSoft.CalcService |
ここでこの記事の主題そのものの落とし穴があります。HKEY_LOCAL_MACHINE\SOFTWARE\Classes は32bitアプリと64bitアプリで共有されるのに対し、その配下の CLSID サブキー(および Interface など)は32bit側と64bit側で別々(32bit側の実体が WOW6432Node)と、Microsoft のドキュメントに明記されています。つまり ProgID のキーは一度書けば両方から見えますが、CLSID の登録は32bitビューと64bitビューの両方に書かないと、32bitクライアントからサーバーが見つかりません。
flowchart TB
accTitle: レジストリの共有部分と分かれる部分
accDescr: Classes直下のProgIDのキーは一度書けば32bitと64bitの両方から見えるのに対し、CLSID配下は32bitビューと64bitビューで別々なので両方に書く必要があり、32bit側が欠けると32bitクライアントからサーバーが見つからないことを示す図。
progk["ProgIDのキー(Classes直下)"] --> shared["両方から見える"]
clsk["CLSID配下の登録"] --> v64["64bitビューに書く"]
clsk --> v32["32bitビューに書く"]
v32 -.-> wow["実体はWOW6432Node"]
v32 -.-> warn["欠けると32bitから見えない"]
図7: ProgIDのキーは共有だが、CLSID配下は32bit/64bitのビューごとに別なので両方に登録する。
管理者権限のコマンドプロンプトで、reg コマンドの /reg:32 /reg:64 を使うのが確実です(Wow6432Node を自分でパスに書くのは、Microsoft が非推奨としています)。
:: 管理者権限のコマンドプロンプトで実行する
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
set SERVER=C:\Program Files\KomuraSoft\CalcServer.exe
:: 1) ProgID → CLSID(HKLM\SOFTWARE\Classes 直下は32/64で共有)
reg add "HKLM\SOFTWARE\Classes\%PROGID%\CLSID" /ve /d "%CLSID%" /f
:: 2) CLSID → LocalServer32 と ProgID(CLSID配下は32/64で別なので両方に書く)
:: LocalServer32 の値には、実行ファイルのパスを引用符ごと入れる。
:: /d "%SERVER%" と書くと引用符は reg.exe の引数解析で消え、値は
:: C:\Program Files\... のまま格納される。COM はこれをコマンドラインとして
:: 解釈するため、空白の手前で切れた C:\Program.exe を先に探しに行く
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:32
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:32
:: 格納された値を確認する。"C:\Program Files\..." と引用符付きで出れば正しい
reg query "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /reg:64
LocalServer32 の引用符は、単に行儀の問題ではありません。引用符が無いと、C:\Program Files\... は「C:\Program に Files\... を引数として渡す」という解釈が成り立ってしまいます。その結果、C:\Program.exe を作れる権限を持つ相手がいる環境では、そちらが先に起動され得ます。空白を含むパスへ入れるなら必ず引用符を付けてください。
flowchart TB
accTitle: LocalServer32の引用符の有無で変わる解釈
accDescr: LocalServer32の値に引用符なしでパスを入れると空白の手前で切れる解釈が成り立ち、C:\Program.exe を置ける環境ではそちらが先に起動され得るのに対し、引用符ごと格納すれば意図したEXEが起動されることを示す図。
noq["引用符なしで格納"] --> cut["空白の手前で切れる解釈が成り立つ"]
cut --> evil["C:\Program.exe が先に起動され得る"]
q["引用符ごと格納"] --> safe["意図したEXEが起動される"]
図8: 空白を含むパスを引用符なしで登録すると、別の実行ファイルが起動され得る余地が生まれる。
解除は同じキーを消すだけです。
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:64
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:32
reg delete "HKLM\SOFTWARE\Classes\%PROGID%" /f
加えて、EXE側は起動したときに「自分がこのCLSIDの担当だ」とCOMに名乗り出る必要があります。C/C++なら CoRegisterClassObject、.NET Framework なら RegistrationServices.RegisterTypeForComClients がこれにあたります。レジストリ登録はCOMがEXEを起動するところまでしか面倒を見てくれないので、この名乗りが抜けているとEXEは起動するのにオブジェクトが作れない、という分かりにくい失敗になります。
flowchart TB
accTitle: レジストリ登録とEXE側の名乗り
accDescr: レジストリ登録が面倒を見るのはCOMがEXEを起動するところまでで、起動したEXEが自分がこのCLSIDの担当だと名乗って初めてオブジェクトが作れるようになり、名乗りが抜けるとEXEは起動するのにオブジェクトが作れない失敗になることを示す図。
regd["レジストリ登録"] --> boot["COMがEXEを起動する"]
boot --> ann["EXEがCLSIDの担当だと名乗る"]
ann --> ok["オブジェクトが作れる"]
boot -.->|"名乗りが抜けると"| ng["起動するのに作れない失敗"]
図9: レジストリ登録はEXEの起動まで、その先はEXE自身の名乗りが無いとオブジェクトは作れない。
なお、開発中に試すだけなら HKLM の代わりに HKCU\SOFTWARE\Classes へ同じ構造で書けば管理者権限なしでも登録できます(HKEY_CLASSES_ROOT は HKLM と HKCU の合成ビューです)。ただし HKCU\SOFTWARE\Classes\CLSID も同じく32bit/64bitで別扱いなので、両方のビューに書く点は変わりません。
4.3. .NET Framework と .NET(5以降)で作り方が違う
上のコードはC#ですが、どちらの.NETを使うかで手順が大きく変わります。ここを混ぜると詰まります。
| .NET Framework | .NET(Core 3.0 / 5以降) | |
|---|---|---|
| 登録ツール | RegAsm.exe がある(ただし作られるのは in-proc 用の InprocServer32 登録なので、LocalServer32 は結局自分で書く) |
RegAsm 相当のツールはない |
| 標準的なCOM公開 | アセンブリに属性を付けて RegAsm |
<EnableComHosting>true</EnableComHosting> で *.comhost.dll を生成し regsvr32 で登録(in-proc のみ) |
| TypeLib(.tlb)の生成 | TlbExp / RegAsm /tlb で生成できる |
サポートされない。IDLを手で書いて MIDL でコンパイルする(.NET 6以降は、できあがった .tlb をcomhostに埋め込むことは可能) |
| CLSIDの指定 | 省略可 | COMから生成させるクラスには CLSID の明示が必須 |
| AnyCPU の扱い | 32bit/64bit 両方のクライアントから使える | 付随する *.comhost.dll が既定で64bitなので、64bitクライアントからしか使えない |
本記事の構成(EXEサーバー)は、.NET(5以降)では標準の EnableComHosting の範囲外なので、登録処理を自分で書くことになります。Microsoft公式のサンプルとして OutOfProcCOM が用意されているので、.NET 側で組む場合はそちらが出発点になります。
flowchart TB
accTitle: .NET(5以降)でEXEサーバーを組むときの道筋
accDescr: 本記事のEXEサーバー構成は.NET 5以降では標準のEnableComHostingの範囲外なので、登録処理を自分で書くことになり、公式サンプルのOutOfProcCOMが出発点になるという道筋を示す図。
exe[".NET(5以降)でのEXEサーバー"] --> range["標準のEnableComHostingの範囲外"]
range --> self["登録処理を自分で書く"]
self -.-> smp["公式サンプルOutOfProcCOMが出発点"]
図10: .NET(5以降)のEXEサーバーは標準機能の範囲外なので、登録処理は自前で書く前提になる。
5. 完全なサンプルコード
上記の概念を実際に動作する形で実装したサンプルを GitHub で公開しています。
Call64bitDLLFrom32bitProc - GitHub
このリポジトリには以下が含まれています:
- Call64bitDLLFrom32bitProc/ - 64bit COM LocalServer (EXE)
- X64DLL/ - 64bit DLL(実処理)
- X86App/ - 32bit クライアント (WinForms)
- scripts/ - COM サーバー登録・解除スクリプト
README に記載の手順に従ってビルド・登録すれば、実際に 32bit プロセスから 64bit DLL を呼び出す動作を確認できます。
6. まとめ
COMブリッジは万能ではなく、向く仕事と向かない仕事がはっきり分かれる構成です。採用を決める前に、次の表で自分のケースを当てはめてみてください。
| 向いているケース | 向かないケース |
|---|---|
| 32bitアプリ本体を作り直せない(改修コストが見合わない) | そもそも32bit側を64bitに再ビルドできる(それが最短) |
| 呼び出しが粗い粒度(1回で画像1枚、1回でファイル1本など) | 1要素ずつ数万回など、細かい呼び出しを高頻度で回す(IPCのオーバーヘッドが支配的になる) |
| やり取りするのが数値・文字列・配列など、マーシャリングしやすい型 | 生ポインターや複雑な独自構造体を大量に往復させる |
| 64bit側の処理が落ちても、アプリ本体は生かしておきたい(プロセス分離が利点になる) | サーバー側のクラッシュや再起動を扱う復旧処理を書きたくない |
| 型付きの呼び出し(IntelliSenseやコンパイル時チェック)を残したい | 単発のバッチ処理で済み、標準入出力やファイル経由の受け渡しで十分 |
最後の行の裏返しとして、「64bit側の処理を単なるコンソールEXEにして、引数とファイルでやり取りする」という素朴な代替案も常に検討する価値があります。COMブリッジが効くのは、型付きの呼び出しを維持したい場合と、状態を持ったサーバーを何度も呼びたい場合です。
flowchart TB
accTitle: COMブリッジと素朴な代替案の分かれ目
accDescr: 型付きの呼び出しの維持や状態を持ったサーバーを何度も呼ぶことが必要ならCOMブリッジが効き、単発のバッチ処理で済むならコンソールEXEにして引数とファイルでやり取りする素朴な代替案も検討する価値があるという分かれ目を示す図。
q{"型付き呼び出しや状態の保持が必要?"} -->|"はい"| br["COMブリッジ"]
q -->|"いいえ"| alt["コンソールEXEで引数とファイル受け渡し"]
図11: 型付きの呼び出しと状態の保持が要るかどうかが、ブリッジと素朴な代替案の分かれ目になる。
次のアクションとしては、この順番をおすすめします。
- まず 5章のサンプルリポジトリを clone して、README のとおりにビルド・登録し、動く状態を1つ手元に作る。
- 自分の 64bit DLL の関数を1つだけ選び、サンプルの
ICalcServiceに相当するインターフェースにメソッドを1本足して通す。 - 通ったら、呼び出し回数と1回あたりのデータ量を測る。ここで「粗い粒度に寄せる」設計判断(複数回の呼び出しを1回にまとめる)を先に済ませておくと、後戻りが減ります。
7. 参考資料
- Component Object Model (COM) の概要 https://learn.microsoft.com/en-us/windows/win32/com/component-object-model–com–portal
- COM LocalServer32 の登録 https://learn.microsoft.com/en-us/windows/win32/com/localserver32
- COM インターフェースの基本 https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
- COM Interop(.NETからの利用) https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
- WOW64 のレジストリリダイレクター(
HKLM\SOFTWARE\Classesは共有、CLSID配下は32/64で別) https://learn.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys - .NET(Core / 5以降)のコンポーネントをCOMに公開する https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
COM/OCX/ActiveX開発でハマる登録とbitnessの罠
COM、OCX、ActiveX開発でハマりやすい32bit/64bit、Visual Studio 2022、regsvr32/Regasm、管理者権限、HKCR、STA/MTAを実務目線で整理します。
Office 2024/Microsoft 365でActiveXが動かない原因と確認手順
Office 2024/Microsoft 365でActiveXが動かない時に、既定無効化、32bit/64bit、COM登録、依存DLL、IEモード、Click-to-Runログを切り分ける順番を整理します。
WinRTはCOMである ── IInspectable・.winmd・言語プロジェクション、そしてWinUIが今もバイナリ契約の上に乗っている理由
WinRTはマネージドランタイムではなく、COMにメタデータ(.winmd)と言語プロジェクションを足したABIです。IUnknownとIInspectableの関係から、デスクトップアプリでのHWND初期化やpackage identityの詰まりどころまで解説します。
OLEオブジェクトとは何か ── 埋め込み・リンクの仕組みと業務文書の落とし穴
WordにExcelの表を埋め込む機能の正体がOLEオブジェクトです。埋め込みとリンクの違い、複合ファイルと構造化ストレージ、In-Place Activationの仕組みから、リンク切れ・肥大化・セキュリティ対策まで実務目線で解説します。
クリップボードとドラッグ&ドロップの仕組み ── OLEデータ転送を業務アプリで正しく扱う
Excelの表を貼ると崩れる・コピー元を閉じると貼れない――正体は同じ内容を複数形式で置くクリップボードの仕組みです。標準形式・遅延レンダリング・OLEドラッグ&ドロップから履歴とクラウド同期のポリシーまで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
32bit / 64bit テーマ
32bit / 64bit、ネイティブ連携、C++/CLI まわりを整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
既存資産活用・移行支援
32bit 資産を残しながら 64bit 側へ橋をかける話なので、既存資産活用・移行支援 に直結するテーマです。
技術相談・設計レビュー
COM ブリッジやプロセス境界の切り方を先に整理したい段階なら、技術相談・設計レビューとして比較検討できます。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 32bitアプリから64bit DLLを直接呼び出せますか?
- できません。32bitプロセスは64bit DLLを読み込めず、これはOSレベルの制約で、工夫すれば回避できるという話ではありません。同一プロセス内で呼び出す道は最初から閉ざされているため、64bit側の処理を別プロセスへ分離する構成が必要になります。
- 32bitアプリから64bit DLLの機能を使うにはどうすればよいですか?
- Out-of-proc COM(EXEサーバー)で分離するのが王道です。64bit DLLは64bitのCOM LocalServer(EXE)から呼び出し、32bitアプリはCOMインターフェース経由で型付きで利用します。COMインターフェース(IDL/TypeLib)を共有して型を公開し、COMランタイムがプロキシ/スタブとプロセス間通信で境界を越えてくれます。
- COMブリッジ構成の注意点は何ですか?
- 主に3点あります。32bit/64bitの登録は別である(WOW6432Node含む)こと、独自構造体はマーシャリング設計が必要なこと、そしてプロセス間通信のオーバーヘッドがあるため高頻度の細かい呼び出しには注意が必要なことです。細かい呼び出しを大量に流すより、一括処理に寄せるのが望ましいです。
- 実際に動作するサンプルコードはありますか?
- あります。GitHubのCall64bitDLLFrom32bitProcリポジトリに、64bit COM LocalServer(EXE)、64bit DLL、32bitクライアント(WinForms)、COMサーバー登録・解除スクリプトを含むサンプル一式が公開されています。READMEの手順に従ってビルド・登録すれば、32bitプロセスから64bit DLLを呼び出す動作を確認できます。