2006年03月02日

COM クライアント実装の道程 for TaskScheduler その2

では、第二回目です。

第一回では、コクラスは作れましたけど独自のメソッドも何もない、ただ本当に作っただけでした。これを何とか使えるように持っていきましょう。

COM においては、主役はインターフェイスです。インターフェイスを通すことで異言語間などでも問題なく相互運用できるようにし、内部の実装に関わる必要が無くなるわけです。この辺はオブジェクト指向の考えと同じですね。

さて、タスクスケジューラ絡みでは、6つの COM インターフェイスが定義されています。

ITaskScheduler
タスクスケジューラのシステムに関するインターフェイス。
IScheduledWorkItem
スケジュールするアイテムに関する基本機能を提供するインターフェイス。
ITask
タスクスケジューラにおいてスケジュールするアイテムに関する機能を提供するインターフェイス。IScheduledWorkItem インターフェイスを継承しています。
ITaskTrigger
指定されたアプリケーションを起動する条件を取得・設定するためのインターフェイス。
IEnumWorkItems
タスクスケジューラに登録されているアイテムを列挙するためのインターフェイス。
IProvideTaskPage
タスクのプロパティダイアログから特定のページのプロパティシートを取得するためのインターフェイス。

先に言っておきますが、このうち IProvideTaskPage インターフェイスに関しては関知しません。あんまり .NET と関係があるとも思わないし。

さて、では COM インターフェイスの宣言です。ここで前回挙げた MSDN の参考ページ、COM 相互運用性 - 第 1 部 : C# クライアント チュートリアル をもう一度見てみましょう。これによると、COM インターフェイスの宣言には、Guid 属性と InterfaceType 属性の二つを付け、それからそのインターフェイスで定義されているメソッド群を再宣言すればいいようです。

まず先に InterfaceType 属性の方を考えると、この属性は基本的に IUnknown と IDispatch 、どちらから派生したインターフェイスなのかを指定するもののようです。MSTask.h に書かれてある各インターフェイスの宣言を見てみると、全て IUnknown から派生している(まあ ITask は IScheduledWorkItem からですが)ので、今回は全て ComInterfaceType.InterfaceIsIUnknown を指定すればいいでしょう。

Guid 属性はどうでしょうか。あるオブジェクトからインターフェイスを取得するには、C# においてはそのインターフェイスへキャストするのが一般的です。前述の MSDN には、これは要するに QueryInterface の呼び出しと同じだそうです。QueryInterface は IUnknown が持つメソッドで、引数には REFIID と変換済みインターフェイスを受け取るポインタの二つが必要です。ということは、ここの Guid 属性には REFIID を指定すればいいようです。まあ要するに、MSTask.h で宣言されている各インターフェイスのすぐ上にある MIDL_INTERFACE("********-****-****-****-************") の部分を書けばいいわけですね。

属性の他に、コクラスと違ってこちらでは各メソッドも宣言する必要があります。今度はこちらを考察してみましょう。まず、IUnknown のメソッドは自動的にフレームワークが追加する、と書いています。ですから IUnknown が定義する三つのメソッドは書かなくて良いようです。あとは基本的にそのインターフェイスが直接持っているメソッドですから、これらは宣言しなければなりません。さて、ではそのまま MSTask.h に書かれてるまま宣言すればいいのでしょうか。その前に、まあ当然ながら、相互運用を行うためには型を向こうに分かる形にしなければなりません。実際に運用するときの変換作業はフレームワークが持っているマーシャラが行ってくれますが、そのマーシャラにどう変換するのかを伝えるため、MarshalAs 属性をメソッド宣言に付加する必要があります。まあ、int みたいな基本的な型は別に必要ないんですけどね。

それだけか? それだけじゃありません。.NET Framework は、デフォルトでは、COM との相互運用において、COM メソッドが通常返すメソッドの返値 HRESULT を例外機構を使用して通知するように置き換え、代わりに返値として [out,retval] としてマークされているメソッド引数を割り当てます。この場合宣言するメソッド引数が一つ減ります。これを防ぎ、返値を HRESULT のまま受け取るようにするには、PreserveSig 属性をメソッドに付加します。今回のインターフェイスはどうでしょうか? 返値になりそうな各種メソッドは……軒並み [out] だけです。これは困った。果たして返値にマーシャリングされるのかどうかも分かりません。こういう場合は多少面倒でも安全策をとって PreserveSig を付けて、返値を参照渡し( C# の out )でメソッド引数として受け取るべきでしょう。

もう一つ、メソッドの宣言順も重要です。IUnknown は並んでる順でメソッドを識別します。ITask::GetApplicationName と ITask::GetParameters を入れ替えると、GetParameters でアプリケーションパスが手に入ってしまったりします。MSTask.h での宣言を見ながら、気を付けて宣言しましょう。

さて、基本は分かりました。ではまず、前回作成したコクラスが持っているインターフェイスである、ITaskScheduler を実装してみましょう。

using System;
using System.Runtime.InteropServices;

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("148BD527-A2AB-11CE-B11F-00AA00530503")]
public interface ITaskScheduler {
    [PreserveSig] int SetTargetComputer(
        [MarshalAs(UnmanagedType.LPWStr)] string name);
    [PreserveSig] int GetTargetComputer(
        [MarshalAs(UnmanagedType.SysInt)] out IntPtr name);
    [PreserveSig] int Enum(
        [MarshalAs(UnmanagedType.IUnknown)] out object items);
    [PreserveSig] int Activate(
        [MarshalAs(UnmanagedType.LPWStr)] string name,
        [MarshalAs(UnmanagedType.Struct), In] ref Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object activated);
    [PreserveSig] int Delete(
        [MarshalAs(UnmanagedType.LPWStr)] string name);
    [PreserveSig] int NewWorkItem(
        [MarshalAs(UnmanagedType.LPWStr)] string taskName,
        [MarshalAs(UnmanagedType.Struct), In] ref Guid clsid,
        [MarshalAs(UnmanagedType.Struct), In] ref Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object activated);
    [PreserveSig] int AddWorkItem(
        [MarshalAs(UnmanagedType.LPWStr)] string taskName,
        [MarshalAs(UnmanagedType.IUnknown)] out object workItem);
    [PreserveSig] int IsOfType(
        [MarshalAs(UnmanagedType.LPWStr)] string name,
        [MarshalAs(UnmanagedType.Struct), In] ref Guid riid);
}

いくつか補足する必要がありますかね。

  • 現状では他のインターフェイスは宣言していませんので、代わりに object を使用します。なお object と COM インターフェイスでは 適用する UnmanagedType が異なります( object の場合 IUnknown 、COM インターフェイスの場合 Interface )。
  • string は 通常 LPTStr (要するに LPTSTR )としてマーシャリングされますが、タスクスケジューラ関係のインターフェイスは全て LPWSTR 、プラットフォームに関わらず Unicode を使用します。
  • Guid のパラメータに対しての In 属性の指定は、マーシャラに呼び出し先での変更がないことを指示します。C/C++ における const と大体同じと考えても良いかもしれません。この辺の動作については、MSDN の コピーと固定 に解説があります。
  • 一番興味を引かれるのは、GetTargetComputer メソッドの引数に string ではなく IntPtr を使用している点でしょう。このメソッドは、引数に LPWSTR のポインタを要求します。内部では CoTaskMemAlloc を使用してメモリを確保しており、その確保したメモリのポインタが返値と言うことになります。そしてここが重要なところですが、当然の事ながらこのメモリは解放しなければならないわけですが、その仕事は呼び出し側が行わなければならないのです。さてここで問題なのは、string で受け取ったとすると、果たしてこの解放作業は行われるのか? 簡単なテストコードを書いて IntPtr と string で比較してみたけど結論は出せませんでした。ですので、安全を重視するために IntPtr で受け取り確実に Marshal.FreeCoTaskMem を使用して解放する事にしたわけです。どこかで解放してくれるみたいなことをちらっと目にしたような気もするんですけど。

さて、それでは実際に動かしてみましょう。前回のコクラスの定義も合わせて再掲載。

using System;
using System.Runtime.InteropServices;

[ComImport, Guid("148BD52A-A2AB-11CE-B11F-00AA00530503")]
public class TaskSchedulerClass {
}
public class TaskTest {
    public static void Main() {
        TaskSchedulerClass scheduler = new TaskSchedulerClass();
        ITaskScheduler ischeduler = (ITaskScheduler)scheduler;
        IntPtr ptr;
        ischeduler.GetTargetComputer(out ptr);
        Console.WriteLine(Marshal.PtrToStringUni(ptr));
        Marshal.FreeCoTaskMem(ptr);
        Marshal.ReleaseComObject(ischeduler);
    }
}

Marshal.FreeCoTaskMem と Marshal.ReleaseComObject は try-finally でやるべきですが、まあこれぐらいなら例外が投げられることも無いですから手を抜きます。また QueryInterface を呼び出した場合は Release が一回必要ですが、C# のキャストではフレームワークが自動的にやってくれます。

次回はいきなり脇道に入ります。

posted by Hongliang at 14:23| Comment(2) | TrackBack(0) | C# | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
こんにちは、MIDL_INTERFACEをGoogleで調べてる途中こちらを拝見しました。直接今調べてる内容とは違いますが資料性の高そうなページですのであとで読もうとブックマークさせていただきました。
一点、というかじつはこっちが本題ですが「第一回」のリンクがseesa.netとなって間違っているようですのでお知らせします。
Posted by yamamoto at 2006年03月24日 12:29
わざわざありがとうございます。リンクは修正しておきました。
// なんでそんなことになってたんだろう……。
特にジャンル分けとかしてるわけでもなく興味の赴くままに書きつづっているので資料性という点ではどうかと思いますが、ここの記事が何かの参考になれば幸いです。
Posted by Hongliang at 2006年03月24日 13:27
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/14030332

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。