Custom WebOC in Non-MFC Dialog-Based App

WebOC と呼ばれる ActiveX コントロールを使うと、IE が利用している HTML レンダリング エンジン (Trident; mshtml.dll) を、IE 以外のアプリケーションからも利用することができます。余談ですが、このページに、"Web Browser ActiveX control (WebOC)" と書かれていますが、どう略したら WebOC になるのか謎です。Web Browser OLE Control の略じゃないのか、と思っていますが真相は不明です。

MFC を使うと、あまりコードを書かなくても、ダイアログ ベースのアプリケーションに WebOC を追加できます。MSDN にある以下のサンプルが分かりやすいです。この情報は、VC++6.0/IE4 時代に書かれたようですが、今でも問題なく動きます。

Using MFC to Host a WebBrowser Control (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa752046(v=vs.85).aspx

ほぼ MSDN 通りに書いた MFC アプリがこれ。Visual Studio 2012/IE11 の環境で動きました。

https://github.com/msmania/miniweboc

宗教上の理由で MFC が使えない場合でも、書くコードは増えますが、WebOC は利用可能です。今の時代、WebOC を使う人が早々いないので、サンプルを探すのに苦労しましたが、いろいろと切り貼りして、こんなコードを書いてみました。ダイアログ ベースの非 MFC ネイティブ アプリケーションで WebOC を使う例です。

https://github.com/msmania/minibrowser

なお、単に HTML コンテンツを表示するダイアログを表示させたいときは、mshtml.dll がエクスポートしている ShowHTMLDialog 関数を呼ぶ方法もあります。ただし、WebOC を使う場合と比べると、できることは限られます。

ShowHTMLDialog function (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa741858(v=vs.85).aspx

例えば WebOC では、IDocHostUIHandler::GetExternal を独自実装することで、ホストしているページの DOM ツリーを拡張して、JavaScript から window.external オブジェクト経由で C++ の処理を実行できるようにしています。

サンプルを書くにあたって、以下のサイトを参考にしました。

How to disable the default pop-up menu for CHtmlView in Visual C++
https://support.microsoft.com/en-us/kb/236312
—-> MFC を使っている場合で、IDocHostUIHandler を独自実装するサンプル

Use an ActiveX control in your Win32 Project without MFC with CreateWindowEx or in a dialog box – CodeProject
http://www.codeproject.com/Articles/18417/Use-an-ActiveX-control-in-your-Win-Project-witho
—-> 非 MFC のダイアログ ベースのアプリケーションから WebOC を使うサンプル

WebBrowser Customization (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa770041(v=vs.85).aspx
—-> WebOC リファレンス。

IDocHostUIHandler interface (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa753260(v=vs.85).aspx
—-> IDocHostUIHandler インターフェース リファレンス。

プログラムにおける主要なポイントは以下の通り。

  1. ダイアログ リソース上で、適当なコントロール クラス名を指定してカスタム コントロールを配置。
    サンプルでは "MINIHOST" という名前を指定。
  2. カスタム コントロールが WM_CREATE メッセージを処理する際の初期化コードにおいて、WebOC オブジェクトを紐付けた OLE オブジェクトをコントロール ウィンドウに登録
  3. OLE オブジェクトの IDocHostUIHandler::GetExternal が呼ばれたときに、独自実装した IDispatch のオブジェクトを返すように実装
  4. IDispatch::GetIDsOfNames と IDispatch::Invoke を実装

3. と 4. は window.external の実装で、上述の KB236312 に書いてある内容です。最大のポイントは 2. で、以下 2 つのメソッドを実行して WebOC をダイアログ上のカスタム コントロールに登録します。

IOleObject::SetClientSite method (COM)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684013(v=vs.85).aspx
—-> WebOC のオブジェクトを CoCreateInstance で作成し、"サイト" として IOleObject を実装したコンテナー オブジェクトに登録

IOleObject::DoVerb method (COM)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms694508(v=vs.85).aspx
—-> OLE オブジェクトを、ウィンドウ上で有効化

WebOC のコンテナーとなる OLE オブジェクトは SetWindowLongPtr を使ってウィンドウ ハンドルに紐づけました。これは必須ではなく、例えばグローバル変数として保存しておくだけでもいいですが、SetWindowLongPtr を使う方法が一般的だと思います。

WebOC に限らず COM オブジェクト全般におけるポイントですが、参照カウントを使ってオブジェクトの確保/解放を管理しないといけません。まず、参照カウンターとなるメンバー変数を適当に定義し、IUnknown::AddRef と IUnknown::Release の中でインクリメント/デクリメントする処理を書きます。

単に ++ と — を使うだけでも動きますが、マルチ スレッド環境を考慮して InterlockedIncrement/InterlockedDecrement を使うのが一般的です。

STDMETHODIMP_(ULONG) MiniBrowserSite::AddRef(void) {
    return InterlockedIncrement(&_ulRefs);
}

STDMETHODIMP_(ULONG) MiniBrowserSite::Release(void) {
    ULONG cref = (ULONG)InterlockedDecrement(&_ulRefs);
    if (cref > 0) {
        return cref;
    }
    delete this;
    return 0;
}

オブジェクトの作成は、new 演算子を使いますが、ポインターを SetWindowLongPtr で登録するときに、AddRef() を自分で呼び出して参照カウントを増やさないといけません。さらに、WM_DESTROY でオブジェクトを破棄するときには、delete 演算子で直接オブジェクトを解放するのではなく、Release() を使って、参照カウントを減らすだけにしておきます。

case WM_CREATE:
    BrowserSite = new MiniBrowserSite(h);
    if (BrowserSite == nullptr || FAILED(BrowserSite->InPlaceActivate())) {
        if (BrowserSite)
            delete BrowserSite;

        return -1;
    }

    SetWindowLongPtr(h, GWLP_USERDATA, (LONG_PTR)BrowserSite);
    BrowserSite->AddRef();
    break;

case WM_DESTROY:
    BrowserSite = (MiniBrowserSite*)GetWindowLongPtr(h, GWLP_USERDATA);
    if (BrowserSite) {
        BrowserSite->Cleanup();
        BrowserSite->Release(); // do not ‘delete BrowserSite’ directly
    }
    break;

WebOC のコンテナーとなる OLE オブジェクトの参照カウントは、IWebBrowser2::Navigate などの WebOC に対する操作を行ったときに、mshtml.dll 内部で頻繁に変更されます。これは、mshtml.dll 内部でもオブジェクトへのポインターが複数個所に保存されていることが予想されるためで、勝手にアプリケーションから delete してしまうと、mshtml.dll 内部のポインターが Use-After-Free を誘発する可能性があります。また、WM_CREATE で AddRef を忘れると、逆に WM_DESTROY で deleete を実行するときに double free でクラッシュする可能性があります。

minibrowser!MiniBrowserSite::Release にブレークポイントを置いてスタックを見てみると、Navigate を呼ぶだけで 20  回以上はヒットします。その中の一つを例として以下に示します。スタックの中に ieframe!CWebBrowserOC::Navigate が含まれていない代わりに、jscript9 の処理が見えます。

Child-SP          RetAddr           Call Site
00000000`002be298 000007fe`f10647ab minibrowser!MiniBrowserSite::Release
00000000`002be2a0 000007fe`f13ce7f5 ieframe!IUnknown_SafeReleaseAndNullPtr<IACList>+0x2b
00000000`002be2d0 000007fe`f13cdcd8 ieframe!CIEFrameAuto::_GetParentFramePrivate+0xc5
00000000`002be320 000007fe`f142cfbe ieframe!CIEFrameAuto::GetParentFrame+0x18
00000000`002be360 000007fe`f12d34cf ieframe!CWebBrowserSB::QueryService+0x13e
00000000`002be3a0 000007fe`ff101c82 ieframe!CIEFrameAuto::QueryService+0x10d4df
00000000`002be3d0 000007fe`f11c6105 SHLWAPI!IUnknown_QueryService+0x5a
00000000`002be430 000007fe`f10db378 ieframe!GetTopWBConnectionPoints+0x52
00000000`002be480 000007fe`f10db323 ieframe!FireEvent_BeforeScriptExecute+0x38
00000000`002be4e0 000007fe`ea00faf3 ieframe!CBaseBrowser2::FireBeforeScriptExecute+0x33
00000000`002be510 000007fe`ea00f9e7 mshtml!CWebOCEvents::BeforeScriptExecute+0xf3
00000000`002be590 000007fe`e9d72a88 mshtml!CScriptCollection::FireFirstScriptExecutionEvent+0xa0
00000000`002be5d0 000007fe`e7b4aee1 mshtml!CActiveScriptHolder::OnEnterScript+0xaa
00000000`002be600 000007fe`e7b4ae17 jscript9!ScriptEngine::OnEnterScript+0xbd
00000000`002be660 000007fe`e7b4ade4 jscript9!ScriptSite::ScriptStartEventHandler+0x27
00000000`002be690 000007fe`e7b4b075 jscript9!Js::ScriptContext::OnScriptStart+0x7d
00000000`002be6d0 000007fe`e7b4acc7 jscript9!Js::JavascriptFunction::CallRootFunction+0xc5
00000000`002be7b0 000007fe`e7b4ac1c jscript9!ScriptSite::CallRootFunction+0x63
00000000`002be810 000007fe`e7bd1346 jscript9!ScriptSite::Execute+0x122
00000000`002be8a0 000007fe`e7bd080d jscript9!ScriptEngine::ExecutePendingScripts+0x208
00000000`002be990 000007fe`e7bd1fb4 jscript9!ScriptEngine::ParseScriptTextCore+0x4a5
00000000`002beaf0 000007fe`ea00f6e1 jscript9!ScriptEngine::ParseScriptText+0xc4
00000000`002beba0 000007fe`ea00df0b mshtml!CActiveScriptHolder::ParseScriptText+0xc1
00000000`002bec20 000007fe`ea00db91 mshtml!CJScript9Holder::ParseScriptText+0xf7
00000000`002becd0 000007fe`ea00ef9d mshtml!CScriptCollection::ParseScriptText+0x28c
00000000`002bedb0 000007fe`ea00e9ae mshtml!CScriptData::CommitCode+0x3d9
00000000`002bef80 000007fe`ea00e731 mshtml!CScriptData::Execute+0x283
00000000`002bf040 000007fe`ea58fb75 mshtml!CHtmScriptParseCtx::Execute+0x101
00000000`002bf080 000007fe`e9d7db8d mshtml!CHtmParseBase::Execute+0x241
00000000`002bf170 000007fe`e9ff8e1f mshtml!CHtmPost::Exec+0x534
00000000`002bf380 000007fe`e9ff8d70 mshtml!CHtmPost::Run+0x3f
00000000`002bf3b0 000007fe`e9ffa6f8 mshtml!PostManExecute+0x70
00000000`002bf430 000007fe`e9ffe0c3 mshtml!PostManResume+0xa1
00000000`002bf470 000007fe`e9d54917 mshtml!CHtmPost::OnDwnChanCallback+0x43
00000000`002bf4c0 000007fe`e9c9295c mshtml!CDwnChan::OnMethodCall+0x41
00000000`002bf4f0 000007fe`e9c8aa74 mshtml!GlobalWndOnMethodCall+0x246
00000000`002bf590 00000000`77269bd1 mshtml!GlobalWndProc+0x186
00000000`002bf620 00000000`772698da USER32!UserCallWinProcCheckWow+0x1ad
00000000`002bf6e0 00000000`77274c1f USER32!DispatchMessageWorker+0x3b5
00000000`002bf760 00000000`77274edd USER32!DialogBox2+0x1b2
00000000`002bf7f0 00000000`77274f52 USER32!InternalDialogBox+0x135
00000000`002bf850 00000000`7726d476 USER32!DialogBoxIndirectParamAorW+0x58
00000000`002bf890 00000001`3f141fe4 USER32!DialogBoxParamW+0x66
00000000`002bf8d0 00000001`3f142ec4 minibrowser!wWinMain+0x94
00000000`002bf960 00000000`771459cd minibrowser!__tmainCRTStartup+0x148
00000000`002bf9a0 00000000`7737b981 kernel32!BaseThreadInitThunk+0xd
00000000`002bf9d0 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

参照カウントの処理が意図通りに動作しているかどうかを確認する方法として、Application Verifier でメモリのフラグを有効にして動作確認を行う方法があります。ただし Application Verifier は、モジュールが解放されるときにリークしているメモリを検出するので、モジュールのアンロードを行なわずにいきなりプロセスが終了するプログラムだと、メモリ リークを検出できません。

image

このような場合、クラスのデストラクターが呼ばれるかどうかをデバッガーから確認するの方法が確実です。ただし、今回のサンプル プログラムのようにデストラクターが空の場合、minibrowser!MiniBrowserSite::~MiniBrowserSite や minibrowser!CExternalDispatch::~CExternalDispatch にはヒットしないので注意が必要です。(最適化しないでコンパイルすれば使えるかも・・)

Release() から delete 演算子を呼んでいるところで止めて、そのシンボルを見ると、minibrowser!MiniBrowserSite::`scalar deleting destructor’ という不思議な関数が呼ばれています。

0:002> uf 00000001`3f801c80
minibrowser!MiniBrowserSite::Release:
00000001`3f801c80 4883ec28        sub     rsp,28h
00000001`3f801c84 83c8ff          or      eax,0FFFFFFFFh
00000001`3f801c87 f00fc14118      lock xadd dword ptr [rcx+18h],eax
00000001`3f801c8c ffc8            dec     eax
00000001`3f801c8e 7512            jne     minibrowser!MiniBrowserSite::Release+0x22 (00000001`3f801ca2)

minibrowser!MiniBrowserSite::Release+0x10:
00000001`3f801c90 4885c9          test    rcx,rcx
00000001`3f801c93 740b            je      minibrowser!MiniBrowserSite::Release+0x20 (00000001`3f801ca0)

minibrowser!MiniBrowserSite::Release+0x15:
00000001`3f801c95 488b01          mov     rax,qword ptr [rcx]
00000001`3f801c98 ba01000000      mov     edx,1
00000001`3f801c9d ff5048          call    qword ptr [rax+48h]

minibrowser!MiniBrowserSite::Release+0x20:
00000001`3f801ca0 33c0            xor     eax,eax

minibrowser!MiniBrowserSite::Release+0x22:
00000001`3f801ca2 4883c428        add     rsp,28h
00000001`3f801ca6 c3              ret

0:002> bp 00000001`3f801c9d
0:002> g
Breakpoint 2 hit
minibrowser!MiniBrowserSite::Release+0x1d:
00000001`3f801c9d ff5048          call    qword ptr [rax+48h] ds:00000001`3f820a20={minibrowser!Mini
BrowserSite::`scalar deleting destructor’ (00000001`3f8013a0)}
0:000> ln poi(rax+48)
(00000001`3f8013a0)   minibrowser!MiniBrowserSite::`scalar deleting destructor’   |  (00000001`3f801420)   minibrowser!CExternalDispatch::AddRef
Exact matches:
    minibrowser!MiniBrowserSite::`scalar deleting destructor’ (void)
0:000>

そこで、この `scalar deleting destructor’ という関数にブレークポイントを置いてダイアログを閉じたときの動作を見ると、コンテナー OLE オブジェクトも IDispatch オブジェクトも解放されていることが確認できました。

0:000> k
Child-SP          RetAddr           Call Site
00000000`0024efc8 00000001`3fd21c70 minibrowser!CExternalDispatch::`scalar deleting destructor’
00000000`0024efd0 00000001`3fd213ec minibrowser!CExternalDispatch::Release+0x20
00000000`0024f000 00000001`3fd21ca0 minibrowser!MiniBrowserSite::`scalar deleting destructor’+0x4c
00000000`0024f040 00000001`3fd219be minibrowser!MiniBrowserSite::Release+0x20
00000000`0024f070 00000000`77269bd1 minibrowser!MiniBrowserSite::MiniHostProc+0xee
00000000`0024f500 00000000`772672cb USER32!UserCallWinProcCheckWow+0x1ad
00000000`0024f5c0 00000000`77266829 USER32!DispatchClientMessage+0xc3
00000000`0024f620 00000000`7739dae5 USER32!_fnDWORD+0x2d
00000000`0024f680 00000000`7725cbfa ntdll!KiUserCallbackDispatcherContinue
00000000`0024f708 00000000`7727505b USER32!ZwUserDestroyWindow+0xa
00000000`0024f710 00000000`77274edd USER32!DialogBox2+0x2ec
00000000`0024f7a0 00000000`77274f52 USER32!InternalDialogBox+0x135
00000000`0024f800 00000000`7726d476 USER32!DialogBoxIndirectParamAorW+0x58
00000000`0024f840 00000001`3fd21fe4 USER32!DialogBoxParamW+0x66
00000000`0024f880 00000001`3fd22ec4 minibrowser!wWinMain+0x94
00000000`0024f910 00000000`771459cd minibrowser!__tmainCRTStartup+0x148
00000000`0024f950 00000000`7737b981 kernel32!BaseThreadInitThunk+0xd
00000000`0024f980 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

`scalar deleting destructor’ を始め、COM オブジェクトのデバッガーからの見え方については次回ってことで。

[Win32] [COM] How to read PDB Symbol file using DIA SDK

デバッグに不可欠なものといえば、デバッガーとデバッグ シンボルです。最近の Windows の世界でシンボルといえば、拡張子が pdb のファイルです。遠い昔の話ですが、SAP カーネルのカーネル パッチである sar ファイルを解凍すると、大抵は pdb ファイルも一緒に入っていた記憶があります。今も同じなんでしょうかね、あの仕組み。

linux や OS X におけるデバッグ シンボルは、実行可能ファイルに含まれていたり、含まれていなかったりします。linux カーネルを含め、シンボルが入っていないモジュールをデバッグするときには、シンボルを含むようにビルドし直すという作業が必要なはずです。間違ってたらごめんなさい。

開発環境でない限り、実行可能ファイルは free ビルド (release ビルド) と呼ばれる、コンパイラによる最適化を施したものを使っていて、Windows では多くの場合、実行可能ファイルにデバッグ情報は含まれていないはずです。モジュールに合ったシンボル ファイルをうまいこと入手してデバッガーにロードさせれば、モジュールをビルドし直さなくてもシンボルを使ったデバッグが可能です。

さて、この pdb  ファイルとは一体何ぞや、という話です。pdb ファイルをプログラムで読み出す必要があって、簡単にできたので紹介します。最初に思いついたのは、Windows デバッガーに付属してくる dbghelp.dll に何かあるんじゃね?ということで MSDN を探してこんな関数を見つけました。

SymSearch function (Windows)
http://msdn.microsoft.com/en-us/library/windows/desktop/ms681363(v=vs.85).aspx

それっぽいことはできそうですが、第一引数がプロセスのハンドルであるあたりからして、調べたいモジュールのイメージが予めロードされている必要がありそうです。もっと直接的に pdb を開く方法はないんかい、ということで DIA SDK とかいうものを見つけました。Visual Studio と一緒に勝手にインストールされているらしい。手元の環境だと C:\Program Files (x86)\Microsoft Visual Studio 11.0\DIA SDK に入っていました。

Debug Interface Access SDK
http://msdn.microsoft.com/en-us/library/x93ctkx8.aspx

付属の dia2dump というサンプルを使うと、pdb ファイルの内容を詳細にダンプできます。シンボルって結局こういう情報だったのね、と長年の疑問を解消させることができます。

これだけだと話が終わってしまいますが、もう一点。DIA SDK の API は COM インターフェース経由で呼び出すのですが、COM を呼び出すちょっと便利な方法も見つけたので、それもついでに紹介する意味で次のプログラムを。

いつも通り、まずは main.cpp から。

//
// main.cpp
//

#include <windows.h>
#include <stdio.h>

#define LOGERROR(fmt, …) wprintf(fmt, __VA_ARGS__)
#define LOGINFO LOGERROR
#define LOGDEBUG

#define MSDIADLL L"msdia110.dll" // msdia100.dll does not work

BOOL DumpSymbolsFromDll(LPCWSTR DllPath, LPCWSTR PdbFile, LPCWSTR SymbolName);
BOOL DumpSymbolsViaCOM(LPCWSTR PdbFile, LPCWSTR SymbolName);

void ShowUsage() {
    LOGINFO(L"\n  USAGE: SYMDUMP <pdb file> <symbol pattern> [path to msdia110.dll]\n\n", 0);
}

int wmain(int argc, wchar_t *argv[]) {
    if ( argc<3 ) {
        ShowUsage();
    }
    else if ( argc>3 ) {
        DumpSymbolsFromDll(argv[3], argv[1], argv[2]);
    }
    else {
        DumpSymbolsViaCOM(argv[1], argv[2]);
    }

    return 0;
}

そして symdump.cpp。

//
// symdump.cpp
//

#include <windows.h>
#include <stdio.h>

#include "..\diasdk\include\dia2.h"

#define LOGERROR(fmt, …) wprintf(fmt, __VA_ARGS__)
#define LOGINFO LOGERROR

#define HANDLE_ERROR(fmt, …) \
    if ( FAILED(hr) ) { LOGERROR(fmt, __VA_ARGS__); goto cleanup; }

#define HANDLE_ERROR_CONTINUE(fmt, …) \
    if ( FAILED(hr) ) { LOGERROR(fmt, __VA_ARGS__); continue; }

typedef HRESULT (__stdcall *DLLGETCLASSOBJECT)(
  _In_   REFCLSID rclsid,
  _In_   REFIID riid,
  _Out_  LPVOID *ppv
);

VOID DumpSymbols(IDiaEnumSymbols *EnumSymbols) {
    HRESULT hr = 0;
    BOOL Ret = FALSE;
    ULONG Retrieved = 0;

    for (;;) {
        IDiaSymbol *Symbol = NULL;
        BSTR Name = NULL;
        BSTR Undecorated = NULL;
        DWORD Rva = 0;
        DWORD SymTag = 0;

        hr = EnumSymbols->Next(1, &Symbol, &Retrieved);
        HANDLE_ERROR_CONTINUE(L"IDiaEnumSymbols::Next failed – 0x%08x\n", hr);
       
        if ( Retrieved==0 ) break;

        hr = Symbol->get_relativeVirtualAddress(&Rva);
        hr = Symbol->get_name(&Name);
        hr = Symbol->get_undecoratedName(&Undecorated);
        hr = Symbol->get_symTag(&SymTag);

        LOGINFO(L"%4d RVA=%08x %-30s %-30s\n", SymTag, Rva, Name, Undecorated);
       
        if ( Name ) SysFreeString(Name);
        if ( Undecorated ) SysFreeString(Undecorated);
        if ( Symbol ) Symbol->Release();
    }
}

BOOL DumpSymbolsInternal(IDiaDataSource *MsdiaInstance, LPCWSTR PdbFile, LPCWSTR SymbolName) {
    HRESULT hr = 0;
    BOOL Ret = FALSE;
    IDiaSession *Session = NULL;
    IDiaSymbol  *Global = NULL;
    IDiaEnumSymbols *EnumSymbols = NULL;

    hr = MsdiaInstance->loadDataFromPdb(PdbFile);
    HANDLE_ERROR(L"IDiaDataSource::loadDataFromPdb failed – 0x%08x\n", hr);

    hr = MsdiaInstance->openSession(&Session);
    HANDLE_ERROR(L"IDiaDataSource::openSession failed – 0x%08x\n", hr);
   
    hr = Session->get_globalScope(&Global);
    HANDLE_ERROR(L"IDiaSession::get_globalScope failed – 0x%08x\n", hr);
   
    hr = Global->findChildren(SymTagNull, SymbolName, nsfRegularExpression, &EnumSymbols);
    HANDLE_ERROR(L"IDiaSymbol::findChildren failed – 0x%08x\n", hr);

    DumpSymbols(EnumSymbols);

cleanup:
    if ( EnumSymbols ) EnumSymbols->Release();
    if ( Global ) Global->Release();
    if ( Session ) Session->Release();

    return Ret;
}

BOOL DumpSymbolsFromDll(LPCWSTR DllPath, LPCWSTR PdbFile, LPCWSTR SymbolName) {
    HRESULT hr = 0;
    BOOL Ret = FALSE;
    HMODULE MsDiaDll = NULL;
    DLLGETCLASSOBJECT MsDiaDllGetClassObject = NULL;
    IClassFactory *Factory = NULL;
    IDiaDataSource *Source = NULL;

    MsDiaDll = LoadLibrary(DllPath);
    if ( !MsDiaDll ) {
        LOGERROR(L"LoadLibrary failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }
   
    MsDiaDllGetClassObject = (DLLGETCLASSOBJECT)GetProcAddress(MsDiaDll, "DllGetClassObject");
    if ( !MsDiaDllGetClassObject ) {
        LOGERROR(L"LoadLibrary failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    hr = MsDiaDllGetClassObject(__uuidof(DiaSource), __uuidof(IClassFactory), (void**)&Factory);
    HANDLE_ERROR(L"DllGetClassObject failed – 0x%08x\n", hr);

    hr = Factory->CreateInstance(NULL, __uuidof(IDiaDataSource), (void**)&Source);
    HANDLE_ERROR(L"IClassFactory::CreateInstance failed – 0x%08x\n", hr);
   
    Ret = DumpSymbolsInternal(Source, PdbFile, SymbolName);

cleanup:
    if ( Source ) Source->Release();
    if ( Factory ) Factory->Release();
    if ( MsDiaDll ) FreeLibrary(MsDiaDll);

    return Ret;
}

BOOL DumpSymbolsViaCOM(LPCWSTR PdbFile, LPCWSTR SymbolName) {
    HRESULT hr = 0;
    BOOL Ret = FALSE;
    IDiaDataSource *Source = NULL;

    CoInitialize(NULL);

    hr = CoCreateInstance(__uuidof(DiaSource),
        NULL,
        CLSCTX_INPROC_SERVER,
        __uuidof(IDiaDataSource),
        (void**)&Source);
    HANDLE_ERROR(L"CoCreateInstance failed – 0x%08x\n", hr);
   
    Ret = DumpSymbolsInternal(Source, PdbFile, SymbolName);

cleanup:
    if ( Source ) Source->Release();

    CoUninitialize();

    return Ret;
}

やっていることはとても単純で、IDiaSymbol::findChildren で検索して、各シンボルの IDiaSymbol インスタンスを引っ張ってくるだけです。

IDiaSymbol::findChildren
http://msdn.microsoft.com/en-us/library/yfx1573w.aspx

出力例はこんな感じ。自分自身である symdump のシンボルを読ませてみました。

E:\VSDev\Projects\symdump\Release>symdump

  USAGE: SYMDUMP <pdb file> <symbol pattern> [path to msdia110.dll]

E:\VSDev\Projects\symdump\Release>symdump symdump.pdb *main*
   2 RVA=00000000 E:\VSDev\Projects\symdump\Release\main.obj (null)
   7 RVA=00003014 __native_dllmain_reason        (null)
   7 RVA=00003018 mainret                        (null)
   5 RVA=00001000 wmain                          _wmain
   5 RVA=000014d8 __tmainCRTStartup              (null)
   5 RVA=0000163d wmainCRTStartup                _wmainCRTStartup
  10 RVA=00001000 _wmain                         _wmain
  10 RVA=00003014 ___native_dllmain_reason       ___native_dllmain_reason
  10 RVA=0000163d _wmainCRTStartup               _wmainCRTStartup
  10 RVA=00002038 __imp____wgetmainargs          __imp____wgetmainargs

E:\VSDev\Projects\symdump\Release>

findChildren に渡している nsfRegularExpression ですが、名前からすると、「シンボルの検索に正規表現使えるのか、やるじゃん」 と思いますが、以下のページに "Applies a case-sensitive name match using asterisks (*) and question marks (?) as wildcards." と書いてあり、どうやら * と ? しか使えないようです。いかにも Windows らしいところ。

NameSearchOptions
http://msdn.microsoft.com/en-us/library/yat28ads.aspx

RVA というのは、Relative Virtual Address の略で、そのシンボルが示すデータのモジュール ベースからのオフセットを示しています。この値が分かることで、モジュールのベース アドレスが分かれば、関数の先頭にブレークポイントを設定できるわけです。例えば wmainCRTStartup のオフセットは 163d なので、以下のようにして確かめられます。

IDiaSymbol::get_relativeVirtualAddress
http://msdn.microsoft.com/en-us/library/vstudio/xf7wwak5(v=vs.100).aspx

E:\VSDev\Projects\symdump\Release>C:\debuggers\x86\cdb -y . symdump.exe

Microsoft (R) Windows Debugger Version 6.12.0002.633 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: symdump.exe
Symbol search path is: .;srv*c:\websymbols*
http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 012c0000 012c6000   symdump.exe
ModLoad: 77570000 776f0000   ntdll.dll
ModLoad: 76fa0000 770b0000   C:\Windows\syswow64\kernel32.dll
ModLoad: 76930000 76977000   C:\Windows\syswow64\KERNELBASE.dll
ModLoad: 75850000 759ac000   C:\Windows\syswow64\ole32.dll
ModLoad: 75750000 757fc000   C:\Windows\syswow64\msvcrt.dll
ModLoad: 76ac0000 76b50000   C:\Windows\syswow64\GDI32.dll
ModLoad: 74ff0000 750f0000   C:\Windows\syswow64\USER32.dll
ModLoad: 76ea0000 76f40000   C:\Windows\syswow64\ADVAPI32.dll
ModLoad: 770b0000 770c9000   C:\Windows\SysWOW64\sechost.dll
ModLoad: 75a20000 75b10000   C:\Windows\syswow64\RPCRT4.dll
ModLoad: 74f90000 74ff0000   C:\Windows\syswow64\SspiCli.dll
ModLoad: 74f80000 74f8c000   C:\Windows\syswow64\CRYPTBASE.dll
ModLoad: 76e90000 76e9a000   C:\Windows\syswow64\LPK.dll
ModLoad: 75480000 7551d000   C:\Windows\syswow64\USP10.dll
ModLoad: 75280000 7530f000   C:\Windows\syswow64\OLEAUT32.dll
ModLoad: 65610000 656e6000   C:\Windows\SysWOW64\MSVCR110.dll
(3fa0.4054): Break instruction exception – code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=e5cb0000 edx=0024dde8 esi=fffffffe edi=00000000
eip=7761103b esp=003ff580 ebp=003ff5ac iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2c:
7761103b cc              int     3
0:000> ln 012c0000+0000163d
*** WARNING: Unable to verify checksum for symdump.exe
(012c163d)   symdump!wmainCRTStartup   |  (012c1647)   symdump!__raise_securityfailure
Exact matches:
    symdump!wmainCRTStartup (void)
0:000>

逆に言えば、シンボルが間違っていると、変なところにブレークポイントを貼ってしまう可能性があるということです。

DIA SDK の話はここまでですが、symdump.cpp のもう一つのポイントは DumpSymbolsFromDll と DumpSymbolsViaCOM です。通常、COM インターフェースを使うときは、実体となるクラスが実装された COM サーバーを予め regsvr32 でレジストリに登録しておき、COM クライアントが CoCreateInstance を呼び出すと、GUID による検索でどこからともなくインスタンスが表れるという仕組みになっています。つまり GUID さえ分かっていれば、COM サーバーが誰なのかを知る必要がありません。ただし、regsvr32 による登録が行われていないと動きません。

regsvr32 による登録を行わずに、COM オブジェクトのインスタンスを取得する方法が DumpSymbolsFromDll です。DIA SDK の COIM サーバーは、DIA SDK\bin フォルダーにある msdia110.dll で、この中のエクスポート関数である DllGetClassObject を直接呼び出すことで、必要なオブジェクトのファクトリー オブジェクトを取得できます。というか以下のページにまんま書いてありました。これは便利。

Windows/C++: how to use a COM dll which is not registered – Stack Overflow
http://stackoverflow.com/questions/2466138/windows-c-how-to-use-a-com-dll-which-is-not-registered

便利な反面、DLL のパスを自分で指定しないといけないところが不便です。例えば、32bit と 64bit では当然 DLL が別になるので、それぞれ別の DLL を指定する必要があります。いずれにしても、覚えておくといつか役に立ちそうな方法ですね。

[Win32] [COM] VDS Object Enumeration

NTFS、パーティション マネジメントあたりがマイブームなので、VDS の COM インターフェースを使って各種オブジェクトを列挙するプログラムを書いてみた。

サンプルはこのへん。

Loading VDS (Windows)
http://msdn.microsoft.com/en-us/library/aa383037.aspx

Working with Enumeration Objects (Windows)
http://msdn.microsoft.com/en-us/library/aa383988.aspx

コードは下に貼りますが、特に捻りもない普通のコードです。
ConsumeVDS で IVdsService インターフェースを取得して、EnumVdsObjects でソフトウェア プロバイダーの,インターフェース (IVdsPack) を取得して EnumVdsVolumes, EnumVdsDisks でそれぞれボリュームとディスクを列挙。
Dump***Prop 関数は、GetProperties で取ってきた構造体をパースして表示しているだけなので省略。

そう言えば、衝撃の事実が上記 MSDN ページに。
VDS とダイナミック ディスクは星になるようです。

[Both the Virtual Disk Service and dynamic disks are deprecated as of Windows 8 Consumer Preview and Windows Server 8 Beta, and may be unavailable in subsequent versions of Windows. For more information, see Windows Storage Management API.]

VDS サービスはスタートアップ種別が手動であり、IVdsVolume インターフェースを取得するため、IVdsServiceLoader::LoadService を呼んだときに起動され、インターフェースを破棄するとサービスが停止します。このデザインはパフォーマンス的にあまりよくない気がします。

そんなこんなでソース。

//
// vds.cpp
//
//
http://msdn.microsoft.com/en-us/library/aa383037.aspx
// http://msdn.microsoft.com/en-us/library/aa383988.aspx
//

#include <initguid.h>
#include <vds.h>
#include <stdio.h>

#pragma comment( lib, "rpcrt4.lib" )

void Logging(LPCWSTR fmt, DWORD err) {
    //SYSTEMTIME st;
    //GetSystemTime(&st);
    //wprintf(L"[%d/%02d/%02d %02d:%02d:%02d.%03d] ",
    //    st.wYear,
    //    st.wMonth,
    //    st.wDay,
    //    st.wHour,
    //    st.wMinute,
    //    st.wSecond,
    //    st.wMilliseconds);

    wprintf(fmt, err);
}

#define GET_LODWORD(ll) (((PLARGE_INTEGER)&ll)->LowPart)
#define GET_HIDWORD(ll) (((PLARGE_INTEGER)&ll)->HighPart)

void EnumVdsVolumes(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumVolumes= NULL;

    Ret= VdsPack->QueryVolumes(&EnumVolumes);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryVolumes failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsVolume *Volume= NULL;
        IVdsVolumeMF *VolumeMF= NULL;
               
        VDS_VOLUME_PROP PropVol;
        VDS_FILE_SYSTEM_PROP PropFs;

        Ret= EnumVolumes->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsVolume, (void**)&Volume);
        Unknown->Release();
        if ( FAILED(Ret) ) {
            Logging(L"IID_IVdsVolume::QueryInterface failed – 0x%08x\n",
              Ret);
            continue;
        }

        Ret= Volume->GetProperties(&PropVol);
        if ( Ret==S_OK || Ret==VDS_S_PROPERTIES_INCOMPLETE )
            DumpVolumeProp(&PropVol);
        if ( Ret==VDS_S_PROPERTIES_INCOMPLETE )
            wprintf(L"      ** IID_IVdsVolume::GetProperties returned VDS_S_PROPERTIES_INCOMPLETE(0x%08x)\n\n", VDS_S_PROPERTIES_INCOMPLETE);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolume::GetProperties failed – 0x%08x\n", Ret);

        Ret= Volume->QueryInterface(IID_IVdsVolumeMF, (void**)&VolumeMF);
        Volume->Release();
        if ( Ret!=S_OK )
            Logging(L"IID_IVdsVolumeMF::QueryInterface failed – 0x%08x\n",
              Ret);

        Ret= VolumeMF->GetFileSystemProperties(&PropFs);
        if ( Ret==VDS_E_NO_MEDIA )
            wprintf(L"      ** IID_IVdsVolumeMF::GetProperties returned VDS_E_NO_MEDIA(0x%08x)\n\n", VDS_E_NO_MEDIA);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolumeMF::GetFileSystemProperties failed – 0x%08x\n", Ret);
        else
            DumpFileSystemProp(&PropFs);

    } while(1);
   
cleanup:
    if ( EnumVolumes )
        EnumVolumes->Release();

    return;
}

void EnumVdsDisks(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumDisks= NULL;

    Ret= VdsPack->QueryDisks(&EnumDisks);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryDisks failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsDisk *Disk= NULL;
        VDS_DISK_PROP DiskProp;

        Ret= EnumDisks->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsDisk, (void**)&Disk);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Disk->GetProperties(&DiskProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsDisk::GetProperties failed – 0x%08x\n", Ret);
        else
            DumpDiskProp(&DiskProp);

    } while (1);

cleanup:
    if ( EnumDisks )
        EnumDisks->Release();

    return;
}

void EnumVdsObjects(IVdsService *VdsService) {
    HRESULT Ret;

    ULONG Fetched= 0;
    IEnumVdsObject *EnumSwProviders= NULL;
    IEnumVdsObject *EnumPacks= NULL;
    IUnknown *Unknown= NULL;

    IVdsProvider *Provider= NULL;
    IVdsSwProvider *SwProvider= NULL;
    IVdsPack *VdsPack= NULL;

    VDS_PROVIDER_PROP ProviderProp;
    VDS_PACK_PROP PackProp;

    Ret= VdsService->QueryProviders(VDS_QUERY_SOFTWARE_PROVIDERS,
      &EnumSwProviders);
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::QueryProviders failed – 0x%08x\n", Ret);
        return;
    }

    do {
        Ret= EnumSwProviders->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsProvider, (void**)&Provider);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Provider->GetProperties(&ProviderProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsProvider::GetProperties failed – 0x%08x\n",
              Ret);
        else
            DumpProviderProp(&ProviderProp);

        Ret= Provider->QueryInterface(IID_IVdsSwProvider,
          (void**)&SwProvider);
        Provider->Release();
        if ( FAILED(Ret) ) continue;

        Ret= SwProvider->QueryPacks(&EnumPacks);
        SwProvider->Release();
        if ( FAILED(Ret) ) continue;

        do {
            Ret= EnumPacks->Next(1, &Unknown, &Fetched);
            if ( Ret==S_FALSE ) break;
            if ( FAILED(Ret) ) goto cleanup;

            Ret= Unknown->QueryInterface(IID_IVdsPack, (void**)&VdsPack);
            Unknown->Release();
            if ( FAILED(Ret) ) continue;

            Ret= VdsPack->GetProperties(&PackProp);
            if ( FAILED(Ret) )
                Logging(L"IID_IVdsPack::GetProperties failed – %08x\n", Ret);
            else
                DumpPackProp(&PackProp);
           
            EnumVdsDisks(VdsPack);
            EnumVdsVolumes(VdsPack);

        } while (1);

        EnumPacks->Release();
        EnumPacks= NULL;

    } while (1);

cleanup:
    if ( EnumPacks )
        EnumPacks->Release();

    if ( EnumSwProviders )
        EnumSwProviders->Release();

    return;
}

void ConsumeVDS() {
    HRESULT Ret= 0;
    IVdsServiceLoader *VdsServiceLoader= NULL;
    IVdsService *VdsService= NULL;
   
    Ret = CoInitialize(NULL);
    if ( Ret!=S_OK ) {
        Logging(L"CoInitialize failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= CoCreateInstance(CLSID_VdsLoader,
        NULL,
        CLSCTX_LOCAL_SERVER,
        IID_IVdsServiceLoader,
        (void **) &VdsServiceLoader);
    if ( Ret!=S_OK ) {
        Logging(L"CoCreateInstance(IVdsServiceLoader) failed – 0x%08x\n",
          Ret);
        goto cleanup;
    }
   
    Ret= VdsServiceLoader->LoadService(NULL, &VdsService);
    VdsServiceLoader->Release();
    VdsServiceLoader= NULL;
   
    if ( Ret!=S_OK ) {
        Logging(L"IVdsServiceLoader::LoadService failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= VdsService->WaitForServiceReady();
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::WaitForServiceReady failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    EnumVdsObjects(VdsService);

cleanup:
    if ( VdsService )
        VdsService->Release();

    if ( VdsServiceLoader )
        VdsServiceLoader->Release();

    CoUninitialize();

    return;
}

[COM] [Win32] [C++] IWindowsUpdateAgentInfo::GetInfo を使ってみる

Microsoft の更新プログラムで、Windows Update Agent を更新するものがあります。Windows Update そのものを更新する、というやつです。Windows Update にもバージョンがあり、それがインクリメントされるのですが、そのバージョンの確認方法はあまり有名ではありません。検索すると、以下のようなページが出てきたりします。

http://msdn.microsoft.com/en-us/library/bb680319.aspx
http://technet.microsoft.com/en-us/library/bb680319.aspx

なんで SQL が出てくるんだよ、って話です。って、System Center の話なのか。個人ユーザーとは関係なさ過ぎて困る、と。

もうちょっと頑張ると、次のページを見つけました。

http://msdn.microsoft.com/ja-jp/library/aa387091.aspx

API 使えってか。というわけで、Windows Update Agent バージョン確認プログラムを書きました。COM クライアントは久々なので忘れすぎ。VARIANT 構造体とか、使い方が特殊なくせに、COM の世界では当り前っぽいので、その使い方にたどり着くのにずいぶんと時間がかかった。サンプルプログラムがすぐに見つからんし。そんなこんなで、以下のルーチンを書いてみた。

//
// wuaver.cpp
//

#include <windows.h>
#include <tchar.h>
#include <wuapi.h>
#include <stdio.h>

static const OLECHAR g_ApiMajorVersion[]= L"ApiMajorVersion";
static const OLECHAR g_ApiMinorVersion[]= L"ApiMinorVersion";
static const OLECHAR g_ProductVersionString[]= L"ProductVersionString";

void DoMyJob() {
    if ( SUCCEEDED(CoInitialize(NULL)) ) {
        IWindowsUpdateAgentInfo *pIWUA= NULL;
        DWORD err= 0;
        HRESULT ret= CoCreateInstance(
            CLSID_WindowsUpdateAgentInfo,
            NULL,
            CLSCTX_INPROC_SERVER,
            IID_IWindowsUpdateAgentInfo,
            (LPVOID*)&pIWUA);

        if ( SUCCEEDED(ret) ) {
            VARIANT varin, varout;

            // major version
            varin.vt= VT_BSTR;
            varin.bstrVal= SysAllocString(g_ApiMajorVersion);
            ret= pIWUA->GetInfo(varin, &varout);
            if ( SUCCEEDED(ret) ) {
                wprintf_s(L"Major version: %d\r\n", varout.lVal);
            }
            SysFreeString(varin.bstrVal);

            // minor version
            varin.vt= VT_BSTR;
            varin.bstrVal= SysAllocString(g_ApiMinorVersion);
            ret= pIWUA->GetInfo(varin, &varout);
            if ( SUCCEEDED(ret) ) {
                wprintf_s(L"Minor version: %d\r\n", varout.lVal);
            }
            SysFreeString(varin.bstrVal);

            // ProductVersionString
            varin.vt= VT_BSTR;
            varin.bstrVal= SysAllocString(g_ProductVersionString);
            ret= pIWUA->GetInfo(varin, &varout);
            if ( SUCCEEDED(ret) ) {
                wprintf_s(L"Product version: %s\r\n", varout.bstrVal);
            }
            SysFreeString(varin.bstrVal);

            pIWUA->Release();
        }

        CoUninitialize();
    }
}

int _tmain(int argc, _TCHAR* argv[]) {
    DoMyJob();
    return 0;
}

雑ですね、ええ。とりあえずは動きます。

と、ここまで完成したところで、WUA のバージョンってのは wuapi.dll のバージョンを調べればいいということを知った。おいおいおい!