COMサーバー、忘れてたのでメモ

前回書いたような、過去の出荷アプリ(当時の担当者が行方不明)のメンテを任されるような業務が今後も発生しそうです。
最近はずっとC++/Win32だけ知っていれば十分、な世界にいたので、だいぶ記憶も鈍ってしまったようで、今回COMサーバーを復習してみたのでメモっておきます。
メモなんで、内容薄いのと間違っているかもしれません＞＜

復習に際して、以下を参考にしました。

[C++でATLを使わずにレジストリフリーのCOMサーバーを作成してWSHから利用する方法 - seraphyの日記
](https://seraphy.hatenablog.com/entry/2018/05/31/130124)
いつもの[EternalWindows](http://eternalwindows.jp)さん


###0. 下記説明で共通して使用するクライアントの例

```
#include <windows.h>
#include <tchar.h>

#include <comdef.h>
#include "..\COMsampleServer\COMSampleServer.h"

int main()
{
	TCHAR szBuf[256];
	CoInitialize(nullptr);
	{
    HRESULT hr;

    ITestServerA *p;
    hr=CoCreateInstance(CLSID_TestServerA, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&p));
    if(FAILED(hr)){
      wsprintf(szBuf, TEXT("ITestServerAの取得に失敗しました。%08x"),  hr);
      MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
      goto exitproc;
    }
    p->Test();
    p->Release();
  }
 exitproc:
	CoUninitialize();
	return 0;
}

```

###1. 普通なCOMサーバ(inproc_server)
上記のseraphyさんのとこのソースを理解して、ほぼ丸コピーすればOKですね！


#####＜手順＞
(1) VisualStudioでdllのプロジェクトを作成します
(2) idlファイルを作成してプロジェクトに追加します
　idlファイルに、GUIDのツールで Interface, TypeLib, CLSID のguidを生成して埋め込み、idlファイル単体でコンパイルしておきます。
　tlbファイルが作成されます。

```
import "oaidl.idl";
import "ocidl.idl";

// Interface
[object, dual, uuid(8037957A-E0DA-4F34-A580-605FEC8E0D95)]
interface ITestServerA : IDispatch
{
	[id(1)] HRESULT Test();
}

// TypeLib
[uuid(5686C776-212E-4B34-BD46-A5C05397AEC9), version(1.0)]
library LTestServerA
{
	importlib("stdole2.tlb");

  // CLSID
	[uuid(246A7D70-0F7C-4781-BA9D-578FA6404090)]
	coclass TestServerA
	{
		[default] interface ITestServerA;
	}
}
```

(3) defファイルを作成してプロジェクトに追加します

```
EXPORTS
	DllGetClassObject	PRIVATE
	DllCanUnloadNow		PRIVATE
	DllRegisterServer 	PRIVATE
	DllUnregisterServer PRIVATE
```
(4) rcファイルを作成してプロジェクトに追加します

　(2)で作成したtlbファイルを追記します。

```
1 TYPELIB "プロジェクト名.tlb"
```

(5) 実装コードファイル(*.c, *.h) の追加します
　seraphyさんのとこのコードを参考に作成します。
　DllRegisterServer(), DllUnregisterServer()の実装については、EternalWindowsさんとこのコードを参考に作成します。

　**ポイントは下記**
　　DllRegisterServer()にて、(4)のrcファイルによってdllファイルに埋め込んだtlbファイルの読み込みに成功したら、その情報(dllファイルのパス)をレジストリに登録します。
　　(LoadTypeLib(), RegisterTypeLib()のパスの引数は、Unicode文字列なことに注意)

```
  WCHAR szModulePathW[MAX_PATH];
  GetModuleFileNameW(g_hinstDll, szModulePathW, MAX_PATH);
  ITypeLib *pTypeLib;
  if(SUCCEEDED(LoadTypeLib(szModulePathW, &pTypeLib))){
    RegisterTypeLib(pTypeLib, szModulePathW, NULL);
    ...

```

(6) ビルドして完成、管理者になってレジストリに登録します

```
> regsvr32 プロジェクト名.dll
```

　以降、COMサーバーを起動する際には、(5)にてレジストリに記載したdllファイルを実行するようになります。

#####＜評価＞
　1.は、レジストリ登録が面倒ですね。
　出来上がったdllファイルをexeファイルと同じ位置にコピーしておくだけで使えるようにしたいですよね
　ということで、2.のレジストリフリーなCOMサーバです。(seraphyさんとこの情報そのまま)

###2. レジストリフリーなCOMサーバ(inproc_server)
以下、変更点を追記していきます。

#####＜手順＞
(1) VisualStudioでdllのプロジェクトを作成します(1.と同じ)
(2) idlファイルを作成してプロジェクトに追加します(1.と同じ)
(3) defファイルを作成してプロジェクトに追加します(1.と同じ)
(4) rcファイルを作成してプロジェクトに追加します(1.と同じ)
(5) 実装コードファイル(*.c, *.h) の追加(1.と同じですが、DllRegisterServer(), DllUnregisterServer()は使いません)
(6) manifestファイルを作成してプロジェクトに追加します
　プロジェクトのプロパティ->リンカー->マニフェストファイルで、"ユーザーアカウント制御(UAC)を有効にする"をいいえにします。

```
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity name="プロジェクト名" type="win32" version="1.0.0.0" />
  <file name="プロジェクト名.dll">
    <comClass clsid="{84C36579-EF9B-4DF7-AD52-ED157380520B}" threadingModel="Apartment" progid="COM TestServerA" />
    <typelib  tlbid="{A3F4673F-7328-4034-8F53-E74E7A2C07EB}" version="1.0" helpdir="" />
  </file>
</assembly>
```
(7) ビルドして完成します
(8) クライアント側にもmanifestファイルを追加してビルドします

```
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity name="COMsampleClient" type="win32" version="1.0.0.0" /> 
  <!-- -->
  <dependency>
    <dependentAssembly>
      <assemblyIdentity name="プロジェクト名" type="win32" version="1.0.0.0" />
    </dependentAssembly>
  </dependency>
</assembly>

```

#####＜評価＞
　結局、レジストリフリーかどうかという違いは、どのdllファイルを使用するかの指定方法の違いだけでしかないようですね。

###3. サロゲートなCOMサーバ(local_server)
通常、COMサーバー(dll)は呼び出し元の実行ファイルと同じプロセス空間で実行するものですが、別プロセスで実行したほうがいい場合があります。
例えば、COMは32bitプロセス、本体は64bitプロセス、のハイブリッドとか、COMがエラー起こしたら本体が介錯してあげるとか、そういう使い方。
サロゲートサーバとは、`dllhost.exe`がCOMサーバー(dll)を実行してクライアントと通信する形態です。
1.のCOMサーバをそのまま使いまわします。

#####＜手順＞
(1) レジストリに以下の項目を追加します
　レジストリのCLSIDの下に、AppIDを追加、そのAppIDに、DllSurrogate=""を追加します。
　登録に`regedit.exe`を使用する場合、
　64bitOSで、`dllhost.exe`を32bit(Wow64)で動かしたいときには、
　　`c:\Windows\syswow64\regedit.exe`
　の方を使用します。

```
Windows Registry Editor Version 5.00 

[HKEY_CLASSES_ROOT\AppID\{308B9966-063A-48B8-9659-4EBA6626DE5D}] 
"DllSurrogate"="" 

[HKEY_CLASSES_ROOT\CLSID\{246A7D70-0F7C-4781-BA9D-578FA6404090}]
"AppID"="{308B9966-063A-48B8-9659-4EBA6626DE5D}" 
```

(2) クライアント側のCoCreateInstance()を書き換えます

```
    ITestServerA *p;
    hr=CoCreateInstance(CLSID_TestServerA, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&p));
    // hr=CoCreateInstance(CLSID_TestServerA, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&p));

```
#####＜評価＞
　この変更で、`dllhost.exe`が起動してCOMサーバdllを実行し、クライアントと通信するようになります。
　予想外にあっさりしてますねｗ