続・Unix C がちょっと書けるだけの俺による令和最新式 C++/MFC プログラミング入門

今回は CCtHelperDlg クラスのスケルトンを読んでみる。

MFC の最新版だとデフォルトで CDialogEx を継承した CCtHelperDlg クラスが生成されるのだがこいつは MFC 9.0 FeaturePack 以降の機能だそうで CtHelper.exe の書かれた時代には CDialog しか無かったはずである。 最初のウィザードでどちらを継承できるか選択できるようになってるの気づかずプロジェクト作り直したが、吐かれるスケルトンは継承元以外同じなのでそこだけ直せばよかったっぽいね。

  • CtHelperDlg.h
// CtHelperDlg.h : ヘッダー ファイル
//

#pragma once


// CCtHelperDlg ダイアログ
class CCtHelperDlg : public CDialog
{
// コンストラクション
public:
	CCtHelperDlg(CWnd* pParent = nullptr);	// 標準コンストラクター

// ダイアログ データ
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_CTHELPER_DIALOG };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV サポート


// 実装
protected:
	HICON m_hIcon;

	// 生成された、メッセージ割り当て関数
	virtual BOOL OnInitDialog();
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	DECLARE_MESSAGE_MAP()
};
  • CtHelperDlg.cpp
// CtHelperDlg.cpp : 実装ファイル
//

#include "pch.h"
#include "framework.h"
#include "CtHelper.h"
#include "CtHelperDlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// CCtHelperDlg ダイアログ



CCtHelperDlg::CCtHelperDlg(CWnd* pParent /*=nullptr*/)
	: CDialog(IDD_CTHELPER_DIALOG, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CCtHelperDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CCtHelperDlg, CDialog)
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
END_MESSAGE_MAP()


// CCtHelperDlg メッセージ ハンドラー

BOOL CCtHelperDlg::OnInitDialog()
{
	CDialog::OnInitDialog();

	// このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、
	//  Framework は、この設定を自動的に行います。
	SetIcon(m_hIcon, TRUE);			// 大きいアイコンの設定
	SetIcon(m_hIcon, FALSE);		// 小さいアイコンの設定

	// TODO: 初期化をここに追加します。

	return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

// ダイアログに最小化ボタンを追加する場合、アイコンを描画するための
//  下のコードが必要です。ドキュメント/ビュー モデルを使う MFC アプリケーションの場合、
//  これは、Framework によって自動的に設定されます。

void CCtHelperDlg::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this); // 描画のデバイス コンテキスト

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// クライアントの四角形領域内の中央
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// アイコンの描画
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialog::OnPaint();
	}
}

// ユーザーが最小化したウィンドウをドラッグしているときに表示するカーソルを取得するために、
//  システムがこの関数を呼び出します。
HCURSOR CCtHelperDlg::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}

うん、要らねえもんばっかりだな。

  • アイコンとその描画ルーチン
  • ダイアログ上のコンポーネントの値とメンバ変数を紐付ける DDX/DDV サポート

こいつらは今回は不要なので全部消してしまってよい、どこが C++/MFC 入門なんだよこれ。

その結果が以下となる。

  • CtHelperDlg.h
#pragma once

class CCtHelperDlg : public CDialog
{
public:
	CCtHelperDlg(CWnd* pParent = nullptr);

#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_CTHELPER_DIALOG };
#endif

protected:
	virtual BOOL OnInitDialog();
	DECLARE_MESSAGE_MAP()
};
  • CtHelperDlg.cpp
#include "pch.h"
#include "framework.h"
#include "CtHelper.h"
#include "CtHelperDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

CCtHelperDlg::CCtHelperDlg(CWnd* pParent /*=nullptr*/)
	: CDialog(IDD_CTHELPER_DIALOG, pParent)
{
}

BEGIN_MESSAGE_MAP(CCtHelperDlg, CDialog)
END_MESSAGE_MAP()

BOOL CCtHelperDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	return TRUE;
}

そもそも最初に書いた通り MFC なんか使う意味無いので、framework.h から MFC の基本ヘッダである afxwin.h 以外の拡張機能のインクルードも全部消していい。 リソースファイルからも OK キャンセルボタンとかラベルとか消していい、ほんと何なんだこれ。

ということでダイアログが初期化された時だけ発火する OnInitDialog だけが残った、これは何かというと初期化される時に飛ぶ WM_INITDIALOG イベントのハンドラである、終わり!閉廷!

他の GUI フレームワークしか知らんとコンストラクタあるのに初期化イベント?と思ってしまうが、コンストラクタの時点ではダイアログ画面上のコントロールがまだ生成されてないはず。 コントロールの配置はリソースファイルで管理してるからね、コンストラクタなどでコンテナにシコシコ add していく Java AWT/Swing とは違うのだ(うろ覚え)。 それにこの時代はイベントドリブン全盛期で何でもかんでもメッセージ飛ばすのがカッコいいだろ!って時代だったから深く考えてはならない。

本来はダイアログ上のコンポーネントに初期値をセットしたりするのに使われるものなんだけど、ここに以下のなんも GUI 関係ないロジックを実装していくことになる。

CCtHelperDlg のコードを復元する

Ghidra は前回触れたように C++ の解析が不得意なので、いろいろクラスの型情報を自分で考えて足していかないとひどい疑似コードになってしまう。 まぁでも一度も C++ に触れたことの無い俺でもこの程度のコードなら復元できるので、C++ 経験豊かなプログラマなら簡単だろう。

  • CtHelperDlg.h
class CCtHelperDlg : public CDialog
{
public:
	CCtHelperDlg(CWnd* pParent = nullptr);

#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_CTHELPER_DIALOG };
#endif

protected:
	virtual BOOL OnInitDialog();
	DECLARE_MESSAGE_MAP()

private:
	struct AttachedDevice {
		BYTE deviceDescription[0x100];
		HDEVNOTIFY deviceDetachedNotification;
	} attachedDevices[4];
	DWORD attachedDeviceCount;
	HDEVNOTIFY deviceAttacedNotification;
	HANDLE registryWatchdogThread;
	void RegisterCtDeviceNotification();
	int DoSomething1();
	void DoSomething2();
	void RunPatchMixDSP();
	void LoadCtBurstLibrary();
	void RunMidiDef();
};

extern BOOL registryWatchdogCanceled;
  • CtHelperDlg.cpp
CCtHelperDlg::CCtHelperDlg(CWnd* pParent /*=nullptr*/)
	: CDialog(IDD_CTHELPER_DIALOG, pParent)
{
	ZeroMemory(attachedDevices, sizeof(attachedDevices));
	attachedDeviceCount = 0;
	deviceAttacedNotification = NULL;
	registryWatchdogThread = NULL;
}

BOOL CCtHelperDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	RegisterCtDeviceNotification();
	if (DoSomething1() < 0)
		DoSomething2();
	RunPatchMixDSP();
	LoadCtBurstLibrary();
	RunMidiDef();
	LPVOID param = NULL;
	DWORD threadId = 0;
	registryWatchdogThread = CreateThread(NULL, 0, &RegistryChangedWatchdogThreadProc, &param, 0, &threadId);
	return TRUE;
}

それぞれ以下の処理を行っている。

  • RegisterCtEventNotification … E-MU サウンドデバイスの着脱通知イベントハンドラの登録
  • DoSomething1 … 謎の処理その 1
  • DoSomething2 … 謎の処理その 2
  • RunPatchMixDSP … PatchMixDSP プロセスの起動
  • LoadCtBurstLibrary … CtBurst ライブラリのロード
  • RunMidiDef … MidiDef プロセスの起動
  • RegistryChangedWatchdogThreadProc … レジストリ変更監視スレッド

メンバ変数やメソッド名は勝手に俺が名前をつけただけ、デバッグ情報残ってないからしかたないね。 また現在判明してるクラス変数やメソッドは OnInitDialog から呼ばれてたりするものだけなので完全ではない。

謎の処理その 1~2 については COM 経由で別の DLL を呼び出しているので、現時点では何やってるのかさっぱり判らん。

COM を一切使ってないメソッドはわりと簡単に復元可能であった。

  • RegisterCtEventNotification
void CCtHelperDlg::RegisterCtDeviceNotification()
{
	GUID guid = KSCATEGORY_CAPTURE;
	HDEVINFO classDevs = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);
	SP_DEVICE_INTERFACE_DATA deviceInterface;
	deviceInterface.cbSize = sizeof(deviceInterface);
	for (DWORD i = 0; /**/; ++i) {
		if (SetupDiEnumDeviceInterfaces(classDevs, NULL, &guid, i, &deviceInterface) == FALSE) {
			if (classDevs != INVALID_HANDLE_VALUE)
				SetupDiDestroyDeviceInfoList(classDevs);
			if (deviceAttacedNotification == NULL) {
				DEV_BROADCAST_DEVICEINTERFACE deviceInterfaceFilter;
				ZeroMemory(&deviceInterfaceFilter, sizeof(deviceInterfaceFilter));
				deviceInterfaceFilter.dbcc_size = sizeof(deviceInterfaceFilter);
				deviceInterfaceFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
				deviceInterfaceFilter.dbcc_classguid = guid;
				HDEVNOTIFY notification = RegisterDeviceNotification(m_hWnd, &deviceInterfaceFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
				if (notification != NULL)
					deviceAttacedNotification = notification;
			}
			return;
		}
		DWORD deviceInterfaceDetailDataSize;
		if (SetupDiGetDeviceInterfaceDetail(classDevs, &deviceInterface, NULL, 0, &deviceInterfaceDetailDataSize, NULL) == TRUE || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
			PSP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData;
			deviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)LocalAlloc(LPTR, deviceInterfaceDetailDataSize);
			if (deviceInterfaceDetailData != NULL) {
				deviceInterfaceDetailData->cbSize = sizeof(*deviceInterfaceDetailData);
				if (SetupDiGetDeviceInterfaceDetail(classDevs, &deviceInterface, deviceInterfaceDetailData, deviceInterfaceDetailDataSize, &deviceInterfaceDetailDataSize, NULL) == TRUE || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
					if (SetupDiOpenDeviceInterface(classDevs, deviceInterfaceDetailData->DevicePath, 0, &deviceInterface) == TRUE) {
						SP_DEVINFO_DATA devinfoData;
						ZeroMemory(&devinfoData, sizeof(devinfoData));
						devinfoData.cbSize = sizeof(devinfoData);
						if (SetupDiGetDeviceInterfaceDetail(classDevs, &deviceInterface, NULL, 0, NULL, &devinfoData) == TRUE || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
							DWORD propertyRegDataType = 0;
							BYTE propertyBuffer[0x100];
							ZeroMemory(&propertyBuffer, sizeof(propertyBuffer));
							if (SetupDiGetDeviceRegistryProperty(classDevs, &devinfoData, SPDRP_FRIENDLYNAME, &propertyRegDataType, &propertyBuffer[0], sizeof(propertyBuffer), NULL) == FALSE)
								SetupDiGetDeviceRegistryProperty(classDevs, &devinfoData, SPDRP_DEVICEDESC, &propertyRegDataType, &propertyBuffer[0], sizeof(propertyBuffer), NULL);
							CString path = CString(deviceInterfaceDetailData->DevicePath);
							path.MakeUpper();
							CString desc = CString((LPCSTR)(PCHAR)&propertyBuffer[0], sizeof(propertyBuffer));
							desc.MakeUpper();
							if ((path.Find(_T("VEN_1102")) != -1 && path.Find(_T("WAVE")) != -1 && (path.Find(_T("DEV_0002")) != -1 || path.Find(_T("DEV_0004")) != -1 || path.Find(_T("DEV_0008")) != -1)) || (desc.Find(_T("CREATIVE")) != -1 && desc.Find(_T("E-MU")) != -1)) {
								HANDLE deviceFile = CreateFile(deviceInterfaceDetailData->DevicePath, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
								if (deviceFile != INVALID_HANDLE_VALUE) {
									DEV_BROADCAST_HANDLE handleFilter;
									ZeroMemory(&handleFilter, sizeof(handleFilter));
									handleFilter.dbch_size = sizeof(handleFilter);
									handleFilter.dbch_devicetype = DBT_DEVTYP_HANDLE;
									handleFilter.dbch_handle = deviceFile;
									HDEVNOTIFY notification = RegisterDeviceNotification(m_hWnd, &handleFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
									if (notification != NULL) {
										attachedDevices[attachedDeviceCount].deviceDetachedNotification = notification;
										strcpy_s((PCHAR)&attachedDevices[attachedDeviceCount].deviceDescription[0], sizeof(attachedDevices[attachedDeviceCount].deviceDescription[0]), (PCHAR)propertyBuffer);
										++attachedDeviceCount;
									}
									CloseHandle(deviceFile);
								}
							}
						}
					}
				}
				LocalFree(deviceInterfaceDetailData);
			}
		}
	}
	SetupDiDestroyDeviceInfoList(classDevs);
}
  • RegistryChangedWatchdogThreadProc
#define WM_USER_DEFINED_UNKNOWN	0x445

BOOL registryWatchdogCanceled = FALSE;

DWORD WINAPI RegistryChangedWatchdogThreadProc(LPVOID param)
{
	static LPTSTR subkeys[3] = {
		_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0002\\Enable"),
		_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0004\\Enable"),
		_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0008\\Enable")
	};
	static HKEY hkeys[3];
	static HANDLE events[3];
	for (int i = 0; i < 3; ++i) {
		hkeys[i] = 0;
		events[i] = CreateEvent(0, 1, 0, NULL);
	}
	for (;;) {
		for (int i = 0; i < 3; ++i) {
			if (RegOpenKey(HKEY_CURRENT_USER, subkeys[i], &hkeys[i]) == ERROR_SUCCESS)
				RegNotifyChangeKeyValue(hkeys[i], FALSE, REG_NOTIFY_CHANGE_NAME|REG_NOTIFY_CHANGE_ATTRIBUTES|REG_NOTIFY_CHANGE_LAST_SET, events[i], TRUE);
		}
		DWORD ret = MsgWaitForMultipleObjects(3, events, FALSE, INFINITE, QS_ALLEVENTS|QS_RAWINPUT);
		if (registryWatchdogCanceled == TRUE)
			break;
		HWND window = FindWindow(NULL, ident);
		if (window != NULL)
			SendNotifyMessage(window, WM_USER_DEFINED_UNKNOWN, 0, 0);
		ResetEvent(events[ret]);
	}
	return 0;
}

80 字で折り返すと余計に読みづらくなるけどみんな 8k ディスプレイくらい持ってるだろうしそのまま、うちはまだ 4k どころか FullHD すら使ってないけど。

完全な復元ではないことに注意、スタックガード的なコードが部分的に入ってるのはコンパイラが足してると思われるので無視したりしてる。 あと ZeroMemory はインラインで展開されてるっぽいし memset もチャンポンで使われてるようだが統一した、あとメモリアロケーション周りも Windows プログラミング側に寄せた。

それプラス元の実装者が明らかに INVALID_HANDLE(=-1) かどうかチェックすべきケースを NULL(=0) でチェックしてるバグも修正してある。 そんでバッファオーバーラン対策してない部分が散見されるけど、今の Visual C/C++ だと strcpy → strcpy_s の置換しないとエラーになるのでそこだけ修正しそれ以外の Out Of Bounds (OOB) チェックは放置。

デバイスの着脱イベントハンドラ登録については MSDN とにらめっこしながら書けば誰でもこうなる著作性皆無のコードであるが、もうちょっと可読性の為に分割してほしいところである。 もしかしたらインライン展開されてるだけで元コードはスッキリしてる可能性もなくはないが。

もう一つケチつけるなら SetupDiGetDeviceRegistryProperty で取得するプロパティを固定長で 256 バイト決め打ちにしてるとこか。 ただでさえネストが深いのに長さ取得に動的アロケーションしたらあと 3 段くらいネスト深くなるから書いたやつ限界だったんだろうなと察するべきなんだろうか。 まぁ E-MU サウンドデバイスに絞り込んでるし、そいつら 256 バイト超えないだろうからどうでもいいっちゃいい。

着脱監視するデバイスが最大 4 決めうちだけど、そもそも複数 E-MU サウンドデバイスが存在する場合、ちゃんとドライバや PatchMix DSP が扱えるのか謎である。 手元に 0404 PCI が 3~4 枚と 1616M PCI-E と 1820M PCI があるだし試せなくもないのだが、デスクトップはデカくて邪魔だから押入れの奥にしまっちゃったからな。 1616M CardBus も 3 枚ほどあるけど物理的に 2 枚も刺さらねえし。

ちなみに同じチップの SoundBlaster X-Fi Premium のドライバで CtHelper.exe に相当する機能を実装し直した CtxfiHlp.exe では Windows Management Interface (WMI) 使ってる。 SQL 文ライクなクエリ使えるからデバイスの有無を調べるならこっちの方が楽よね。 ただしデバイス脱着は想定してないから WMI を使えてる感じかな、こっちは 1616M CardBus や 0404/0202 USB みたいな取り外し可能な製品無いし。

レジストリ変更監視スレッド周りについてはよく判らん、スレッド起こして自分でイベントループ書く必要あるんかなこれすでに CtHelperApp クラスの継承元の CWinThread がイベントループしてんのに。 しかもユーザ定義メッセージ 0x445 を投げる相手はスレッド起こした CtHelperDlg 自身だし、なんならスレッドパラメーターとして自身を渡せばいいよね。

そんで CtxfiHlp.exe ではこのレジストリ変更監視スレッドは無いのはやはり取り外し可能デバイスでないからか、別途 CtxfiReg.exe というレジストリを読むだけのアプリが別にあるみたいだが。

次回

謎の処理その 1 と 2 を追うため Unix C がちょっと書けるだけの俺による令和最新式 C++/COM プログラミング入門編がはじまるはずだが飽きてきた。