プログラムのモジュール化－COMとMFC DLLの比較－

MFCでとあるプログラムを書いていたところ、最初は1000行くらいだったのに機能を追加しすぎてあっという間に数万行に・・・。というわけでプログラムの分割、モジュール化について「ATL COM」と「MFC DLL」とについて、今更ですがちょっと検討しました。検討といっても極簡単なサーバーを作成して、クライアントから利用するだけです。

以下を参考にさせていただきました。ありがとうございます。
https://qiita.com/tadnakam/items/77ccb15c83a05b356b0c

George Shepherd (2002) プログラミングMicrosoft Visual C++.NET Vol.1基礎編　
（もう売ってない・・・）

Richard Grimes,Alex Stockton et al. (1999) ATL COM プログラミング 翔泳社


#各種比較

①.ATL COM
>長所
・VC++以外からでも利用できる。
・ATLを使用するだけなら小さくて早い。
・処理を更新するときにクライアントプログラムの変更が不要。
短所
・覚えなきゃならないことがたくさん！
・インストーラーやregsvr32を使用してDLLをレジストリに登録する必要がある。

<br>
②MFCを使用する通常のDLL（暗黙的リンク）
>長所
・覚えることは少ない。
・ライブラリとインクルードファイルをクライアントプログラムにリンクさせるだけで簡単に使える。
・頑張ればVC++以外からも利用できる。
短所
・C言語形式の関数しかエクスポートできない。
・クライアントを起動する時にdllが無いとクライアントが起動できない。

<br>
③MFCを使用する通常のDLL（明示的リンク）
>長所
・覚えることは少ない。
・頑張ればVC++以外からも利用できる。
・クライアント起動時にdllが無くてもクライアントは起動可能。
短所
・C言語形式の関数しかエクスポートできない。
・クライアントからインポートするときにちょっと面倒（関数へのポインタを使う）。

<br>
④MFCの拡張DLL（作りません）
>長所
・覚えることは少ない。
・クラスをエクスポートできる。
・DLLのサイズが小さい。
短所
・クライアントもMFCで作る必要がある。
・MFCのバージョンもDLLとクライアントで同じでなければならない。
・MFC DLLは動的リンクしかできない。（MFCのDLLをインストールしておく必要がある）

<br>
以上の長所と短所があります。
④の各種バージョン合わせやインストールする必要があることが、こちらの環境では致命的なので今回は除外します。
②と③はDLLは同じでクライアントからの利用の仕方が違うだけです。
このため、今回は①②③の方法でDLLを作成し、クライアントから利用してみます。


#作成するプログラムについて
以下のように、

```C++:
double AddNum(double first, double second)
{
    return first + second;
}
```

double型を足してそれを出力するDLLを①②③で作成します。
今回DLLはVC++2015(Update3),クライアントはVC++2017(15.9.27)で作成しています。
<br>
<br>

#1.ATL COM でDLLの作成
COMのDLLを作ろうと思って色々調べるとIUnknownがどうのとか、QueryInterface()やマーシャリング・・・等といきなり難しいことが書かれています。しかし、今回作成するのはdouble型の足し算をするだけなので、難しいことをせずに簡単にできることで作ります。慣れたら色々なお作法を覚えてください（いきなり難しいことをやろうと思っても挫折するので、簡単なところからやりましょ。とよく理解できなかったので言い訳してみる・・・）
>今回のCOMサーバーは
①「calcクラス」を「Icalcインターフェース」を使用して
②「AddNum」メソッドで計算する、まで行います。
③細かいところはIDEにおまかせで！
という動けばいいをコンセプトに作成します。

<br>

1.新規作成
　1-1.ファイル→新規作成→プロジェクト
　　以下のようにATLプロジェクトを選び、名前を「com_dll_test」にする。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/d590c1eb-9f7c-91a5-86fb-2281a6a1db90.png)
<br>

　1-2.その後は変更せずに「完了」しちゃってください。こうしてcom_dll_testプロジェクトを作成します。
![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/3b3c54b2-a05e-7058-dfd3-8ac9537dfceb.png)
<br>

　1-3.ソリューションエクスプローラーでcom_dll_testを選び右クリック
　　クラスの追加→ATLシンプルオブジェクトで「追加」
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/0342cd05-4c31-b16f-a1fc-1e273b2eda87.png)
<br>

　1-4.短い名前に「calc」と入れるとProgID以外が埋まるので「次へ」
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/c6b3cfff-455a-ed28-c3a3-93a292c89dfc.png)
<br>

　1-5.ハンドラーのオプション何も変更せず「次へ」→オプションも何も変更せずに「次へ」（画像はハンドラーのオプションを飛ばしています）
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/200a59ea-c410-890a-abec-d94c4a29e711.png)
ウイザードで進めてきましたが、これで実はuuidの追加とかの面倒な作業をしてくれてます。
ここまでで、calcクラスとIcalcインターフェースを作ってくれたので、次はメソッドを追加します。
<br>

　1-6.クラスビューから「Icalc」で右クリック。追加→メソッドの追加
![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/8cc68811-29bb-efdc-f378-52fed9aed715.png)
<br>

　1-7.引数のfirstとsecondはパラメータの属性「in」にチェック、パラメータの型は「DOUBLE」を選び、パラメータ名にそれぞれを追加し「追加」で引数の追加。
![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/7bb7dd89-2532-da0e-0ff3-0ec9401aee31.png)
<br>

　1-8.戻り値はパラメータの型を「DOUBLE*」にすると「out」と「retval」が選べるようになるので「retval」をチェック。パラメータ名を入れて追加
![008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/9cde9b71-27e8-10f1-0db9-990cfdb8240f.png)
<br>

　1-9.これで「calc.cpp」にAddNumが以下のように追加されているはず。

```C++:calc.cpp

// calc.cpp : Ccalc の実装

#include "stdafx.h"
#include "calc.h"

// Ccalc

STDMETHODIMP Ccalc::AddNum(DOUBLE first, DOUBLE second, DOUBLE* result)
{
	// TODO: ここに実装コードを追加してください。

	return S_OK;
}

```

<br>
　1-10.COMのメソッドはすべてHRESULT型を戻り値となります。このため、以下のように書き換えます。

```C++:calc.cpp

// calc.cpp : Ccalc の実装

#include "stdafx.h"
#include "calc.h"

// Ccalc

STDMETHODIMP Ccalc::AddNum(DOUBLE first, DOUBLE second, DOUBLE* result)
{
	*result = first + second;
	return S_OK;
}

```

これでdoubleを戻り値とするAddNumの完成です。これでビルドをすればCOMサーバーができあがり！　ほとんどコードを書くことも無く、簡単にできます。

ビルドの条件は以下としました。
構成
　　Release x86
全般
　　プログラム全体の最適化：リンク時のコード生成を使用
C/C++ 最適化
　　最適化：実行速度の最大化
　　速度またはサイズを優先：実行速度を優先

この段階ではまだ登録していないのでエラーが発生します・・・


<br>
　1-11.COMを登録します。コマンドプロンプトを管理者権限で立ち上げて、今回作成したdllのあるフォルダへ移動し、
「regsvr32 com_dll_test.dll」と入力。「成功しました」と出てくれば成功です。
![011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/3816d95e-e713-2a46-e3ad-786a5b033f1b.png)
管理者権限が無いと失敗するので、必ずコマンドプロンプトは管理者権限で立ち上げてください。

<br>
　1-12.クライアントからの利用の仕方：最初に#importしてインターフェースを宣言してCoCreateInstanceしてメソッドを呼び出すだけとすごく簡単です。CComPtrがスマートポインタとなっており、自動的に処理してくれるので、Release()やAddRef()等はコールしません。

VS2017の新規作成→VisualC++→MFC/ATL→MFCアプリ、より名前はdll_clientにでもしておきます。ダイアログベースにして、スタティックライブラリでMFCを使用します。これで完了してクライアントを作成。リソースビューからButton1を追加して、イベントハンドラーをdll_clientDlg.cppへ追加します。さっき作成したcom_dll_test.dllをdll_clientフォルダへコピペして、以下を追加。

```C++:dll_clientDlg.cpp

#include "pch.h"
#include "framework.h"
#include "dll_client.h"
#include "dll_clientDlg.h"
#include "afxdialogex.h"

//以下の#import文を追加
#import "com_dll_test.dll" no_namespace
※中略

//イベントハンドラーの中身を追加
void CdllclientDlg::OnBnClickedButton1()
{
	CoInitialize(NULL);                                   //COMの初期化
	CComPtr<Icalc> calc1;                                 //スマートポインタを使用
	HRESULT hh = calc1.CoCreateInstance(__uuidof(calc));  //作成して
	double res = calc1->AddNum(0.1, 0.2);                 //使用して
	CoUninitialize();                                     //後始末
}

```

<br>
ウイザードだけで簡単にCOMサーバーができました。
でもCOMの登録がいる等まだまだ面倒ですね。

<br>
<br>
<br>
#2.MFC DLLでDLLの作成
2-1.ファイル→新規作成→プロジェクト
以下のようにMFC DLLを選び、名前を「mfc_dll_test」にする。
![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/3bc9da5f-1de5-6095-e553-96e74517e5ad.png)
<br>

2-2.色々と面倒なので、私は大体スタティックリンクで、SDLチェックは外す設定でやります。これで完了。
![2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/e8226dc8-66ab-f4d9-5cda-288e6fbe2148.png)

<br>
2-3.ソリューションエクスプローラーから見るとこんなにたくさん作ってくれてます。ですがこれらはほとんど使いません（実はMFCの拡張DLLでは使います）。
![2-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131051/e99fb725-4696-7476-4a74-1306bdb37ea9.png)

<br>

2-4.使用するのは「mfc_dll_test.h」と「mfc_dll_test.cpp」です。こんなソースを書いてくれますが、使いません（これもMFCの拡張DLLでは使います）。

```C++:mfc_dll_test.h

// mfc_dll_test.h : mfc_dll_test.DLL のメイン ヘッダー ファイル
//

#pragma once

#ifndef __AFXWIN_H__
	#error "PCH に対してこのファイルをインクルードする前に 'stdafx.h' をインクルードしてください"
#endif

#include "resource.h"		// メイン シンボル


// Cmfc_dll_testApp
// このクラスの実装に関しては mfc_dll_test.cpp を参照してください。
//

class Cmfc_dll_testApp : public CWinApp
{
public:
	Cmfc_dll_testApp();

// オーバーライド
public:
	virtual BOOL InitInstance();

	DECLARE_MESSAGE_MAP()
};

```

```C++:mfc_dll_test.cpp

// mfc_dll_test.cpp : DLL の初期化ルーチンです。
//

#include "stdafx.h"
#include "mfc_dll_test.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//
//TODO: この DLL が MFC DLL に対して動的にリンクされる場合、
//		MFC 内で呼び出されるこの DLL からエクスポートされたどの関数も
//		関数の最初に追加される AFX_MANAGE_STATE マクロを
//		持たなければなりません。
//
//		例:
//
//		extern "C" BOOL PASCAL EXPORT ExportedFunction()
//		{
//			AFX_MANAGE_STATE(AfxGetStaticModuleState());
//			// 通常関数の本体はこの位置にあります
//		}
//
//		このマクロが各関数に含まれていること、MFC 内の
//		どの呼び出しより優先することは非常に重要です。
//		これは関数内の最初のステートメントでなければな 
//		らないことを意味します、コンストラクターが MFC
//		DLL 内への呼び出しを行う可能性があるので、オブ
//		ジェクト変数の宣言よりも前でなければなりません。
//
//		詳細については MFC テクニカル ノート 33 および
//		58 を参照してください。
//

// Cmfc_dll_testApp

BEGIN_MESSAGE_MAP(Cmfc_dll_testApp, CWinApp)
END_MESSAGE_MAP()


// Cmfc_dll_testApp コンストラクション

Cmfc_dll_testApp::Cmfc_dll_testApp()
{
	// TODO: この位置に構築用コードを追加してください。
	// ここに InitInstance 中の重要な初期化処理をすべて記述してください。
}


// 唯一の Cmfc_dll_testApp オブジェクトです。

Cmfc_dll_testApp theApp;


// Cmfc_dll_testApp 初期化

BOOL Cmfc_dll_testApp::InitInstance()
{
	CWinApp::InitInstance();

	return TRUE;
}

```

<br>

2-5.「mfc_dll_test.h」と「mfc_dll_test.cpp」に関数の宣言と定義を追加します。コメントで「これを追加」とした部分のちょっとだけです。

```C++:mfc_dll_test.h

// mfc_dll_test.h : mfc_dll_test.DLL のメイン ヘッダー ファイル
//

#pragma once

#ifndef __AFXWIN_H__
	#error "PCH に対してこのファイルをインクルードする前に 'stdafx.h' をインクルードしてください"
#endif

#include "resource.h"		// メイン シンボル


// Cmfc_dll_testApp
// このクラスの実装に関しては mfc_dll_test.cpp を参照してください。
//

class Cmfc_dll_testApp : public CWinApp
{
public:
	Cmfc_dll_testApp();

// オーバーライド
public:
	virtual BOOL InitInstance();

	DECLARE_MESSAGE_MAP()
};

extern "C" __declspec(dllexport) double AddNum(double first, double second);   //これを追加

```

```C++:mfc_dll_test.cpp

// mfc_dll_test.cpp : DLL の初期化ルーチンです。
//

#include "stdafx.h"
#include "mfc_dll_test.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//
//TODO: この DLL が MFC DLL に対して動的にリンクされる場合、
//		MFC 内で呼び出されるこの DLL からエクスポートされたどの関数も
//		関数の最初に追加される AFX_MANAGE_STATE マクロを
//		持たなければなりません。
//
//		例:
//
//		extern "C" BOOL PASCAL EXPORT ExportedFunction()
//		{
//			AFX_MANAGE_STATE(AfxGetStaticModuleState());
//			// 通常関数の本体はこの位置にあります
//		}
//
//		このマクロが各関数に含まれていること、MFC 内の
//		どの呼び出しより優先することは非常に重要です。
//		これは関数内の最初のステートメントでなければな 
//		らないことを意味します、コンストラクターが MFC
//		DLL 内への呼び出しを行う可能性があるので、オブ
//		ジェクト変数の宣言よりも前でなければなりません。
//
//		詳細については MFC テクニカル ノート 33 および
//		58 を参照してください。
//

// Cmfc_dll_testApp

BEGIN_MESSAGE_MAP(Cmfc_dll_testApp, CWinApp)
END_MESSAGE_MAP()


// Cmfc_dll_testApp コンストラクション

Cmfc_dll_testApp::Cmfc_dll_testApp()
{
	// TODO: この位置に構築用コードを追加してください。
	// ここに InitInstance 中の重要な初期化処理をすべて記述してください。
}


// 唯一の Cmfc_dll_testApp オブジェクトです。

Cmfc_dll_testApp theApp;


// Cmfc_dll_testApp 初期化

BOOL Cmfc_dll_testApp::InitInstance()
{
	CWinApp::InitInstance();

	return TRUE;
}

//これを追加
extern "C" __declspec(dllexport) double AddNum(double first, double second)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return first + second;
}


```

これでビルド。
ビルドの条件は以下としました。
構成
　　Release x86
全般
　　プログラム全体の最適化：リンク時のコード生成を使用
C/C++ 最適化
　　最適化：実行速度の最大化
　　速度またはサイズを優先：実行速度を優先


ここまでで、MFC DLLの作成は終了です。次はクライアントからの利用の仕方

<br>
<br>

2-6.暗黙的リンク（静的リンク）でMFC DLLを使用する。
暗黙的リンクではビルドして作成した
①mfc_dll_test.dll
②mfc_dll_test.libと
③mfc_dll_test.h
を使用します。この三つを1.で作成したdll_clientフォルダへコピペしてください。
dll_clientのプロパティー→構成プロパティー→リンカー→追加の依存ファイルに「mfc_dll_test.lib」を追加します。
そこまで行ったら、リソースビューからbutton2を追加、イベントハンドラーを追加して、以下のコードを追加します。

```C++:mfc_dll_test.h

#include "pch.h"
#include "framework.h"
#include "dll_client.h"
#include "dll_clientDlg.h"
#include "afxdialogex.h"

#import "com_dll_test.dll" no_namespace

//これを追加
#include "mfc_dll_test.h"
※中略

void CdllclientDlg::OnBnClickedButton2()
{
	double res = AddNum(0.1, 0.2);
}


```

今回は暗黙的リンクでは、普通に関数を呼び出す感覚で呼び出せます。簡単ですね。でもこの機能をクライアントが使用しなくてもdllが無いとクライアントは起動できません。

<br>
<br>

2-7.明示的リンク（動的リンク）でMFC DLLを使用する。
明示的リンクでは
①mfc_dll_test.dll
のみ使用します。これはすでにコピペされているので、コードの追加のみ行います。
2-6.と同等に今度はbutton3を追加して、イベントハンドラーも追加します。
そして、以下のコードを追加

```C++:mfc_dll_test.h

void CdllclientDlg::OnBnClickedButton3()
{
	typedef double (ADDNUM)(double, double);

	HINSTANCE hinst;
	ADDNUM* pfunc;
	VERIFY(hinst = ::LoadLibrary(_T("mfc_dll_test.dll")));
	VERIFY(pfunc = (ADDNUM*)::GetProcAddress(hinst, "AddNum"));
	double res = pfunc(0.1, 0.2);
}

```

これで簡単ですが、①COM DLL、②MFC DLL（暗黙的リンク）、③MFC DLL（明示的リンク）のDLL作成方法と利用の仕方です。
<br>
<br>

3.各速度について

簡単な処理しかしていませんが、これを100万回繰り返すと以下の時間がかかります。
CPUはcorei7-10700K、ストレージはWD BLUE 2TBという環境です。
クライアントもDLLと同じく以下でビルドしています。
構成
　　Release x86
全般
　　プログラム全体の最適化：リンク時のコード生成を使用
C/C++ 最適化
　　最適化：実行速度の最大化
　　速度またはサイズを優先：実行速度を優先

|　|呼び出し無|呼び出し有|ファイルサイズ|
|:---:  |:---:  |:--:  |:--:  |
|計算させるだけ|29 mS|-|-|
|COM DLL|33 mS|908 mS|44kB|
|MFC DLL（暗黙的）|55 mS|-|1984KB|
|MFC DLL（明示的）|55 mS|408 mS|1984KB|
この程度のスピードが出るのなら、私の目的にはどれでも使えるかなぁという印象。
<br>
<br>
検討してみましたが、COMはDLLを登録するのが面倒、暗黙的リンクはDLLが無いと起動できないという制約があるので、MFCDLLの明示的リンクを採用しようかなと。