2006 年 10 月 11 日 23 時 56 分

ショートカットを読む


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


昨日の続き。

最近の言語では、クラスが名前空間で整理されており、
利用側が空間やクラス単位で宣言を取り込むことができる。

C 言語では、関数を使う側のファイルで、
関数のプロトタイプ宣言が含まれたヘッダファイルを、
必要なだけ選択して取り込む必要がある。

そのためには、どの関数がどのヘッダに宣言されているか、
事前にしっておく必要がある。
まあ、片っ端から取り込むことはできるのだが、
それをするとコンパイル時間が増加してしまう。

リンクを読み込む関数の中では、
IPersistFile と IShellLinkW インタフェースを使う。
そのため、これらの宣言を取り込んでおく必要がある。

では、関数の実装直前までの部分を先に書こう。

========== lnkfile.c (1st half) ==========

/* 制御マクロ宣言 */

#define UNICODE      /* UNICODE 用プログラム */
#define STRICT      /* 厳密な型を利用 */
#define WIN32_LEAN_AND_MEAN /* コンパイルを高速化 */

#define COBJMACROS  /* C 言語用 COM マクロを利用 */

/* 基本ヘッダの取り込み */
#include "lnkfile.h"

/* 各種 GUID, CLSID, IID */
#include <shlguid.h>

/* CoInitialize */
#include <objbase.h>

/* IPersistFile */
#include <objidl.h>

/* IShellLinkW */
#include <shobjidl.h>

========== end of lnkfile.c (1st half) ==========

インタフェースの定義は、MSDN ライブラリで確認する。
IPersistFile は objidl.h、IShellLinkW は shobjidl.h。

そして、C 言語では GUID を直接扱う必要があるので、
定義済みの インタフェース ID やクラス ID の宣言が必要。
これらは、shlguid.h に記載されているので取り込む。

更に、COM を使う場合、その初期化と後処理の関数がある。
CoInitialize/CoUninitialize という関数だ。
これ以外にも COM 用の関数が色々存在する。
これらは、objbase.h にあるので取り込んでおく。

では、いよいよ実装を書いてみよう。

========== lnkfile.c (2nd half) ==========

/* 関数の実装 */
HRESULT ShortcutGetTarget(const wchar_t *fileName,
        wchar_t *target, unsigned int bufferSize) {

    /* C 言語では、変数は全て先に宣言が必要 */
    HRESULT      result = S_OK;
    IShellLinkW  *link  = NULL;
    IPersistFile *pf    = NULL;

    /* COM を使う場合は CoInitialize で初期化 */
    result = CoInitialize(NULL);
    if (FAILED(result)) return result;

    /* ShellLink オブジェクトを作成し、IShellLinkW のポインタを取得 */
    result = CoCreateInstance(&CLSID_ShellLink, NULL,
            CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **)&link);
    if (FAILED(result)) goto cleanup;

    /* IPersistFile のポインタも取得 */
    result = IUnknown_QueryInterface(link,
            &IID_IPersistFile, (void **)&pf);
    if (FAILED(result)) goto cleanup;

    /* IPersistFile#Load を利用して読み込む */
    result = IPersistFile_Load(pf, fileName,
            STGM_READ | STGM_SHARE_DENY_WRITE);
    if (FAILED(result)) goto cleanup;

    /* IShellLinkW#GetPath を利用してリンク先ファイル名を得る */
    result = IShellLinkW_GetPath(link, target,
            bufferSize, NULL, SLGP_UNCPRIORITY | SLGP_RAWPATH);

cleanup:
    /* COM ポインタは、IUnknown#Release で解放 */
    if (pf)  IUnknown_Release(pf);
    if (link) IUnknown_Release(link);

    /* CoInitialize と CoUninitialize は必ず対に */
    CoUninitialize();

    return result;
}

========== end of lnkfile.c (2nd half) ==========

C# での実装と比較すると分かりやすい。
C# が裏で色々とやってくれているのがよく分かる。
http://mixi.jp/view_diary.pl?id=166475473&owner_id=2300658

C 言語では、あらゆる型が値型であるため、
インターフェースやクラスなどは、
ポインタとして宣言されることになる。

変数 link は、IShellLinkW * 型だ。
IShellLinkW の実体のメモリ領域を示すアドレスである。
C 言語では生々しいアドレスなのが高級言語との違いだ。
(といっても、物理メモリのアドレスではないが)

最初に CoInitialize を呼び出して COM を初期化する。
どのタイミングでもいいのだが、
COM を使うスレッドでは必ず呼び出す必要がある。
戻り値は HRESULT 型だ。

HRESULT がエラーを示すかどうかは、
FAILED マクロ関数を使って判断できる。
逆に成功を表すのは、SUCCEEDED マクロ関数だ。

C# では、COM のコクラスを、
Shortcut オブジェクトとして宣言して作成した。
C で作成するには、CoCreateInstance 関数を使う。

引数が多いが、重要なのは、CLSID と IID だ。
NULL と CLSCTX_INPROC_SERVER はほぼ固定値である。

CLSID_ShellLink は作成したいコクラスの識別子だ。
この場合、シェルの ShellLink オブジェクトである。
IID_IShellLink は、新しく作成した ShellLink の
インスタンスを操作するための、インタフェースの指定だ。

両方とも、GUID 構造体だが、
関数はポインタを要求するため、
アドレス演算子 & を前につける必要がある。
参照型のある C++ では不要だが、C では必須だ。

COM クラスは、基本的にクラス自身の型を持たないため、
ShellLink 型として扱うことはできないが、
クラスは最低 1 つのインタフェースを実装しているので、
クラスが実装しているインタフェースを指定して操作する。
Java の無名クラスにちょっと似ている。

そのため、インスタンスを作成した後、
それをインタフェース型として受け取る必要がある。
ここでは、そのインタフェースの型を指定するのだ。

一般的には、ここで IUnknown を使うことが多い。
IUnknown は、COM インタフェースの基底であり、
あらゆる COM インタフェースがこれを継承しているため、
結果的にあらゆる COM クラスで使えるからである。

しかしながら、ここでは実装しているのが明確なので、
IShellLinkW の ID を直接指定している。

最後の引数は、新しいインスタンスを操作するための、
IShellLinkW * ポインタを受け取る変数のアドレスだ。
C 言語では、引数を使って値を受け取ることが多いが、
その際、呼び出す側が変数のアドレスを渡すことで、
呼び出された関数が変数の値を変更できるようにする。

アドレス演算子 & を使って変数のアドレスを得て、
それを (void **) にキャストして渡すというのは、
COM ではよく見られる光景である。

* や & などは C 言語の一番ややこしい部分だ。
高級言語ではこういった要素をうまく隠し、
プログラマが意識する必要はない。

続いて、IPersistFile インタフェースにキャストする。
C 言語にもキャストの構文はあるが、
COM インタフェースでは、キャスト構文は禁じ手であり、
代わりに、QueryInterface メソッドを呼び出す。
QueryInterface は基底の IUnknown で定義されており、
あらゆるインタフェースが実装している。

C 言語では、クラスのメソッドを呼べないため、
COM マクロがここで役に立つ。
IUnknown の QueryInterface メソッドは、
IUnknown_QueryInterface というマクロ関数として、
第 1 引数にオブジェクトを渡すことで利用できる。

QueryInterface は、オブジェクトに対して、
引数に指定した IID のポインタを要求する。
オブジェクトがインタフェースを実装していれば、
適切なポインタが返却される仕組みだ。
ここでは、IID_IPersistFile を渡し、
IPersistFile インタフェースへのポインタを得る。

そして、IPersistFile の Load メソッドを呼び、
ファイルをオブジェクトに読み込み、
IShellLinkW の GetPath メソッドを呼び、
ショートカットの示すパスを文字列バッファに取得する。
このあたりは C# の時とあまり変わらない。

これで関数の仕事は終わりなのだが、
COM の後処理をする必要がある。

まず、COM インタフェースのポインタは、
自らの Release メソッドを呼んで解放する。
これも、IUnknown で定義されている。
そして、最後に CoUninitialize を呼んで終了となる。

C# では、10 行も使わないコードで済んだが、
C では、非常に長いコードになっていることが分かる。

まあ、C# ではインタフェース定義を書く必要があったので、
公平な比較ではないかもしれないが、
C# の定義はあらかじめ用意しておけば参照設定で済む。
C では、これ以上コードを省略することはできないのだ。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.