2006年03月08日

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

いつまでもデスクトップを使っているのは電気代にも優しくないので、そう言えば、と初代ノート PC を引っ張り出してきました。液晶モニタが割れたので新しいのに買い換えたんですが、逆に言えば液晶モニタの問題だけでしたので、デスクトップで使っているモニタが二系統の入力を受け付けているのに目を付け、バルクのモニタケーブルを調達してきてでかいモニタでノートです。操作感的には大して不都合もないのですが(意外に液晶部分もそんな気になりません)、難点は致命的に重いってこと。やたら HDD へのアクセスが発生してるところからみてスワップしまくってんでしょうね。HDD も 4200rpm と旧式ですし。メモリが 240 MB( 16 MB はビデオメモリに割り当て)ってのはやはり厳しいものがあるようです。特に開発に使うものじゃありません。あまつさえ .NET となるともう、OpenFileDialog 開けるだけでなんか 30 秒とか待たされたり、ってなんかほかの要因もあるような気が。

さて、前回の続き。今回はインターフェイスの残りをまとめてやってしまいましょう。TaskScheduler の項目にある六つのインターフェイスのうち、一つは無視すると初めに宣言しました。さらに IScheduledWorkItem は完全に ITask に吸収させることにしました。ので、残りは二つ。いずれも小さい単純なインターフェイスです。

まずは IEnumWorkItems 。タスクスケジューラに登録しているタスクアイテムを列挙するためのインターフェイスですね。COM にはこの IEnum*** ってインターフェイスはかなりたくさん転がっています。それらのメソッドは共通していて、次のアイテムの取得、列挙のスキップ、現在位置の初期化、クローン作成、の四つを持っています。が、正直列挙なら .NET の IEnumerator みたいに MoveNext(と Current)だけ持ってれば良いんじゃね? と思ったり。

もう一つが ITaskTrigger 。トリガってのは要するに起動させるための条件ですね。一見大がかりなインターフェイスになりそうな感じがするんですが、このインターフェイスが持っているメソッドは三つだけ。しかもそのうち二つは取得系ですから、設定するのは一つだけということになります。まあその一つだけの設定メソッドで使う構造体が大変なことになるわけですが……。

四十ぐらいメソッドを持つ ITask と違って三つ四つしか持たないインターフェイスですから、ここでささっと書き下ろして次にいくことにしましょう。

using System;
using System.Runtime.InteropServices;

[Guid("148BD528-A2AB-11CE-B11F-00AA00530503"), //IID_IEnumWorkItems
 ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IEnumWorkItems {
    [PreserveSig] int Next(
        [MarshalAs(UnmanagedType.I4)] int taskNumber,
        [MarshalAs(UnmanagedType.SysInt)] out IntPtr fileNames,
        [MarshalAs(UnmanagedType.SysInt)] IntPtr fetchedNumber);
    [PreserveSig] int Skip(
        [MarshalAs(UnmanagedType.I4)] int numberOfToSkip);
    [PreserveSig] int Reset();
    [PreserveSig] int Clone(
        [MarshalAs(UnmanagedType.Interface)] out IEnumWorkItems cloned);
}
[Guid("148BD52B-A2AB-11CE-B11F-00AA00530503"), //IID_ITaskTrigger
 ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ITaskTrigger {
    [PreserveSig] int SetTrigger(
        [MarshalAs(UnmanagedType.LPStruct), In] object trigger);
    [PreserveSig] int GetTrigger(
        [MarshalAs(UnmanagedType.LPStruct), Out] object trigger);
    [PreserveSig] int GetTriggerString(
        [MarshalAs(UnmanagedType.SysInt)] out IntPtr trigger);
}

補足。このコード片で動かす余地はありませんが、ITaskTrigger の SetTrigger / GetTrigger は不完全です。おそらくこのままでは実行時例外がでるはず。object は MarshalAs.AsAny でマークしなければならないと言う制限があるので。ここで object を使っているのはプレースホルダといったところです。実際には SystemTimeClass と同じように構造体的 class を使用する予定。

ところで、以前から気になっていたことがありました。Microsoft.mshtml.dll を使って mshtml を操作するってのはまあありがちなことですが、WebBrowser を介さずに直接 DOM を作成する手段として HTMLDocument のインスタンスを作成して write で HTML を書き込む、とかやります。一見自然な流れですが、Microsoft.mshtml.dll を ILDASM でみてみると、なんと HTMLDocument はインターフェイスとなっているのです。ちなみにコクラスは HTMLDocumentClass 。でも、ごく普通に HTMLDocument doc = new HTMLDocument とか書いている。インターフェイスを new しちゃっています。C# の常識的にはあり得ないですよね。これは一体どうなっているのでしょうか。

さて、第一回で定義した TaskSchedulerClass コクラスは new するためだけに定義されていました。何せ実装を持っていませんからね。持とうにも禁止されていますし。仕方ないので ITaskScheduler インターフェイスにキャストしてこのコクラスのインスタンスに操作できる口を作ったわけです。しかし考えてみればこれは無駄です。コクラスのインスタンス作成は CoCreateInstance の呼び出しと等価であると言われますが、この現状ではそんなことありません。C++ で CoCreateInstance を呼び出す場合、CLSID と IID の二つを渡し、IID の示すインターフェイスを受け取るのが基本です。ところが C# におけるコクラス作成では、IID を渡すこともなく当然ながらインターフェイスを受け取ることもありません。受け取るのは CLSID によって作られた操作口のわからないオブジェクト一つです。ここをどうにかすれば、つまり CLSID と IID(要するにインターフェイス定義)を両方持つクラスを new してやれば CoCreateInstance と等価の呼び出しと言うことになるでしょう。

ではどうやるのでしょうか。ILDASM で前述の HTMLDocument インターフェイスと HTMLDocumentClass コクラスを調べてみましょう。

まず「なぜか new できるインターフェイス」HTMLDocument の方はというと、import すなわち ComImport 属性、Guid 属性、CoClass 属性の三つが特徴的な感じです。一方、「実装持ってるコクラス」HTMLDocumentClass は、ComImport 属性、ComSourceInterface 属性、TypeLibType 属性、ClassInterface 属性、Guid 属性といったところ。

ComImport 属性は第一回でも調べたとおり、COM で定義済みであることを表します。インターフェイスやコクラスには通常付ける属性ですね。TypeLibType 属性も特に意味がない属性。ClassInterface 属性は IUnknown とは関係なさそうな雰囲気です。ComSourceInterfaces 属性はイベント用の属性で、今回は関係なさそう。はてそうすると CoClass 属性しか怪しいのが残ってないじゃない。

CoClass 属性は解説を読んでも今ひとつピンときません。コンストラクタはコクラスの Type を渡すだけ。たしかにコクラスの情報を持っているのなら、自前の IID である Guid 属性と併せて CoCreateInstance に必要な情報を完全に持つことができることになりますが……。

そう言えば、インターフェイスへのキャストも、COM インターフェイスの場合は単純なキャストではなく QueryInterface が呼ばれるという事実がありました。new にしても特別扱いされる可能性は十分にあります。

まあ考えるのは後にして、とりあえず試してみましょう。

using System;
using System.Runtime.InteropServices;

[ComImport, Guid("148BD52A-A2AB-11CE-B11F-00AA00530503")]
internal class TaskSchedulerClass {
}

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 ComImport, CoClass(typeof(TaskSchedulerClass)),
 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);
}
public class Program {
    public static void Main() {
        ITaskScheduler sch = new ITaskScheduler();
        try {
            IntPtr ptr;
            sch.GetTargetComputer(out ptr);
            using (CoTaskMem mem = ptr) {
                Console.WriteLine(mem.ReadString(CharSet.Unicode));
            }
        }
        finally {
            if (sch != null)
                Marshal.ReleaseComObject(sch);
        }
    }
}

宣言は第一回・第二回で使ったものをそのまま流用しています。そう言えば out object でやってたの、ITask に置き換えでき……ないですね(笑)。ITask は未掲載でした。文字列化には以前定義した CoTaskMem を使って解放処理もやってもらいます。IntPtr を直接 CoTaskMem 型に代入しているのは暗黙の型変換を定義してるからですね。今回は COM の参照カウンタのデクリメントも正しく try-catch で処理しています。

そう言えば CoTaskMem の実装では解放処理を自動的にやりますけど、StreamReader みたいにポインタからインスタンスを作った場合は解放しない(各種メソッドを使いたいだけだ!)と言うクラスがあっても良いかしら。んー、それって SimpleMemory に実装させればいいか?

では実際に実行してみましょう。果たしてインターフェイスを new することはできるのか。……おお、普通に動きましたよ奥さん。しかしすごく気持ち悪い挙動ですね、インターフェイスを new 。

さて今回はこれくらいで。次回はまた脇道。COM オブジェクトの面倒くささについて考察します。

posted by Hongliang at 03:43| Comment(0) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

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


※画像の中の文字を半角で入力してください。

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

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

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

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

×

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