2015/11/18

MFCで作成されたWin32 DLLをATL/WTL DLLに移植する

始めに
Visual Studio 2010でMFCをスタティックリンクするDLLを作成すると、巨大なDLLになってしまう。UnAceV2J.DLLはVisual Studio 2005と(ほとんど)同じソースをビルドするだけなのに、1.5MBを超えるようなサイズです。目を疑わんばかりの巨大さだ。昔だったら「フロッピーディスクに入りません。」と阿鼻叫喚の地獄絵図だ。(^_^;)
Microsoft Office級のアプリケーションなら許されても、ちょっとしたAPIを提供するだけのWin32 DLLとしてのこのファイルサイズは許しがたいものがある。
そこで、MFCよりもWindows APIの薄いラッパであるATL/WTLを使うようにソースを書き換えようという欲求が発生することと思う。
ただ、MFCのDLLをATL/WTLに移植するための方法がネットでなかなか発見できず、自分以外にもきっと困っている人がいるだろうという事で、ここにメモをする。
上記で触れた通り、きっかけはUnAceV2J.DLLなので、例としてUnAceV2J.DLLのソースを使用する。

作成方法のとっかかり
WTLはオープンソースであるので、ネット上から手に入れて開発環境にインストールしておく必要がある。
ATLも使用するので、Visual StudioはStandard Edition以上。筆者の環境はVisual Studio 2010 Professional Editionである。

DLLの初っ端であるDllMainについて
MFCは、グローバルなクラスとしてCWinAppのサブクラスを作成して、そのCWinAppクラスのInitInstance(), ExitInstance()などをオーバーライドしてDllMainの代わりとする。
ATL/WTLではWTL::CAppModuleクラスのサブクラスを作成してそのクラスをグローバルなクラスとして使用する。

(例)

// UnAceV2JModule.h
#pragma once

#include "atlapp.h"

class CUnAceV2JModule :
   public WTL::CAppModule
{
public:
   // DLL内のグローバルで使用する変数の宣言
   BOOL g_Running;
   HWND g_ParenthWnd;

};
 
WTL::CAppModuleクラスには、MFCのようなInitInstance(), ExitInstance()は無く、DllMain関数で定型的な処理もされないため、自分でDllMain()を実装する必要がある。
Visual Studio 2010の「新しいプロジェクト」でWin32の「Win32 プロジェクト」を選択し、ウィザードでDLLを選ぶとひな形のソースが作成されるが、dllmain.cpp内にDllMain()が作成されている。そこにWTLを使用するためのお約束事なコードを追加する。

(例)

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "stdafx.h"

CUnAceV2JModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()

BOOL APIENTRY DllMain( HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved
                    )
{
   switch (ul_reason_for_call)
   {
   case DLL_PROCESS_ATTACH:
       _Module.Init(ObjectMap, hModule, NULL);
       DisableThreadLibraryCalls(hModule);

       break;
   case DLL_THREAD_ATTACH:
   case DLL_THREAD_DETACH:
       break;
   case DLL_PROCESS_DETACH:
       _Module.Term();
       break;
   }
   return TRUE;
}

stdafx.hについて
stdafx.hには、ATL/WTLで使用するためのヘッダファイル群を定義しておき、各ヘッダファイルの先頭で必ずstdafx.hをインクルードしておく。

(例)

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Windows ヘッダーから使用されていない部分を除外します。

// TODO: プログラムに必要な追加ヘッダーをここで参照してください。
#define _ATL_APARTMENT_THREADED
#define _WTL_NO_AUTOMATIC_NAMESPACE 1
#define _SECURE_ATL 1
#include < atlbase.h>

#include < atlwin.h>       // ATL

#include < atlapp.h>       // WTL
#include < atlframe.h>     // WTL
#include < atlctrls.h>     // WTL
#include < atlctrlw.h>     // WTL
#include < atlmisc.h>      // WTL
#include < atlgdi.h>       // WTL
#include < atlcrack.h>
#include < atlddx.h>       // DDX/DDVを使用するため
#include < ATLComTime.h>
#include < atlcoll.h>
#include "UnAceV2JModule.h"

//extern WTL::CAppModule _Module;
extern CUnAceV2JModule _Module;  // グローバル変数を保持したいので、
                                // CAppModuleを継承したサブクラスを_Moduleとして使用する。

#include < windows.h>
ライブラリクラスの移植
MFCに存在するがATL/WTLには存在しないクラスを何とか移植しなければならない。
過去にも書いたが、CStringListは以下のマクロで対処できるみたい。

(例)

typedef CAtlList< CString, CStringElementTraits< CString > > CStringList;
例にしているUnAceV2J.DLLでは、CObListに圧縮ファイル内のファイル情報を保持する「CArchiveFileInfoクラスのポインタ」を格納して、FindFirst(), FindNext()メソッド内で参照している。hArcはCObList内に保持しているポインタなんですね~!(^^ゞ
ATL/MFCでは、CObListは存在しないが、CArchiveFileInfoのポインタを格納するCAtlListとしてテンプレート定義をすれば、既存のソースがほぼ使えるようだ。

(例)


// CObList m_list;
CAtlList< CArchiveFileInfo *> m_list;
CStdioFileクラスはないので頑張って書き換えるしかない。
AfxMessageBoxは::MessageBox関数に置き換えるが、オミットしていた親ウィンドウハンドルやタイトルを呼び出し元から調達してこなければならない。

モーダル・ダイアログ表示について
WTLのセオリ通り、ATL::CDialogImplのテンプレートとして継承したクラスを使用する。また、DDXを利用するためにWTL::CWinDataExchangeのテンプレートとして継承したクラスも使用する。

(例)
// ConfigDlg.h
#pragma once

#include "stdafx.h"
#include "resource.h"

class CConfigDlg : public CDialogImpl< CConfigDlg>, public
WTL::CWinDataExchange< CConfigDlg>
{
public:
   enum { IDD = IDD_CONFIG };

   WTL::CString m_Version;
   WTL::CString m_Copyright;

   BEGIN_DDX_MAP(CConfigDlg)
       DDX_TEXT(IDC_VERSION, m_Version)
       DDX_TEXT(IDC_COPYRIGHT, m_Copyright)
   END_DDX_MAP()

   BEGIN_MSG_MAP(CConfigDlg)
       MSG_WM_INITDIALOG(OnInitDialog)
       COMMAND_ID_HANDLER_EX(IDOK, OnOK)
       COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
   END_MSG_MAP()

   CConfigDlg(void);
   ~CConfigDlg(void);

   BOOL OnInitDialog(ATL::CWindow wndFocus, LPARAM lInitParam);

   void OnOK(UINT uNotifyCode, int nID, ATL::CWindow wndCtl);

   void OnCancel(UINT uNotifyCode, int nID, ATL::CWindow wndCtl);

};
例での通り、CStringはデフォルトでATL::CStringを使用するが一部WTL::CStringを使用しなければならないことがある。
ATL::CStringとWTL::CStringの間のやり取りは普通に代入で済むが、ここ何とかならないのだろうか?
MFCでは、OnOk()やOnCancel()では、自動的にダイアログのクローズ処理を行ってくれるが、ATL/WTLでは明示的に書いてやらなければ、ボタンを押しても何も起こらないので、DLLを使用するアプリケーションを強制終了する羽目になる。
またBEGIN_DDX_MAP, BEGIN_MSG_MAPマクロの引数のクラス名、コピペで作成すると別のクラスを設定していて、実行してみるとちゃんと動かない!って事になりがちなので注意。(^_^;)

(例)

// ConfigDlg.cpp
#include "StdAfx.h"
#include "ConfigDlg.h"

CConfigDlg::CConfigDlg(void)
   : m_Version(_T(""))
   , m_Copyright(_T(""))
{
}

CConfigDlg::~CConfigDlg(void)
{
}

BOOL CConfigDlg::OnInitDialog(CWindow wndFocus, LPARAM lInitParam){
   // スクリーンの中央に配置
   CenterWindow();

   // コントロール設定
   DoDataExchange(FALSE);

   return TRUE;
}

void CConfigDlg::OnOK(UINT uNotifyCode, int nID, CWindow wndCtl){
   EndDialog(nID);
}

void CConfigDlg::OnCancel(UINT uNotifyCode, int nID, CWindow wndCtl){
   EndDialog(nID);
}
上記のように定義したダイアログのクラスを以下のようにして呼ぶ。
ちなみに、MFCと違い、OnOK()やOnCancel()には引数がある。

(例)

BOOL WINAPI UnAceConfigDialog(const HWND _hwnd, LPSTR _szOptionBuffer,
const int _iMode)
{
   if (UnAceGetRunning())
       return FALSE;
   CConfigDlg dlg;
   CString s;
   s.Format("UnAceV2J.DLL Version %d.%.2d.%.2d.%.2d",
                   UnAceGetVersion() / 100,
                   UnAceGetVersion() % 100,
                   UnAceGetSubVersion() / 100,
                   UnAceGetSubVersion() % 100);
   dlg.m_Version = s;
   dlg.m_Copyright = "Copyright (C) 2004-2010 Niiyama(HEROPA)";
   return (dlg.DoModal() == IDOK);
}

MFCを使用した場合と全く同じで、このメソッド内は修正不要だった。

モードレス・ダイアログについて
モードレスダイアログも上記のモーダルダイアログと同じ感じに実装したクラスをMFCの時と同じように呼べばよい。
ただし、引数に親ウィンドウハンドルが必要で、NULLで済ませていた場合は、::GetDesktopWindow()の戻り値をセットする。
(例)

       if (m_ShowDlg) {
           if (_Module.g_ParenthWnd == NULL) {
               _Module.g_ParenthWnd = ::GetDesktopWindow();
           }
           // 状況表示ダイアログのモードレス表示
           m_ProgressDlg.Create(_Module.g_ParenthWnd);
           // モードレスは、これがないと親ウィンドウ中央に表示されない。
           m_ProgressDlg.CenterWindow(_Module.g_ParenthWnd);
           // SW_SHOWNA以外だとウィンドウが閉じた後にフォーカスが変わってアプリが困る。
           m_ProgressDlg.ShowWindow(SW_SHOWNA);
       }
(例)
       if (m_ShowDlg) {
           m_ProgressDlg.DestroyWindow();
       }

 
終わりに
というわけで、知ってしまえば僅かな手間でMFCからATL/WTLを使用したDLLに書き換えることができるはず。
作成されたバイナリファイルを比較すると
UnAceV2J.DLL 0.06 Visual Studio 2005でのMFC DLL 282,112バイト
UnAceV2J.DLL 0.07 Visual Studio 2010でのMFC DLL 1,651,712バイト
UnAceV2J.DLL 0.08(試作) Visual Studio 2010でのATL/WTL DLL 188,928バイト
てな具合で、ファイルサイズの問題が解決できるというだけでも、ATL/WTLで書き直すメリットはあるのではないかと思った。

(2010/07/26 旧ブログ掲載分を転載)

0 件のコメント: