2006年03月03日

COM クライアント実装の道程 for TaskScheduler その番外編1 〜アンマネージドメモリへの手抜き

予告通り、三回目にして速攻で脇道に入ります。

アンマネージドとの相互運用において、面倒なものの一つにアンマネージドメモリの管理があります。.NET ではアンマネージドメモリを扱う場合 System.Runtime.InteropServices.Marshal クラスを使用してアクセスしますが、問題の一つに、解放忘れ即メモリリークにつながるため try-finally が欠かせない、と言うのがあります。面倒です。他にも、バイト配列との相互コピーも事前にバイト配列を確保する必要があったり、文字列を書き込んでも結局何バイト書き込んだのか分からなかったりとか、色々扱いづらい点があります。そこで今回、この辺をクラス化して、多少便利に扱えるようにしたいと思います。もちろん、便利さと速度はプログラミングにおいて大抵は相反するものでして、今回作るのも実行速度に対するパフォーマンスという点は目を瞑っています。ボトルネックになる部分では使用するのも少し考えた方が良いかもしれません。逆に言えばボトルネックでないのなら(私は)気にせず使います。もっとも一回の呼び出しで何秒も使うようなものでもないですから、これの使用そのものがボトルネックになることはあまり考えられませんけど。

ところでこのコンセプトで以前記事を書いたことがあります。05/10/22 の 多少は使いやすい共有メモリクラス、とその VB.NET 向け記事 共有メモリクラス for VB.NET です。今回作るクラスはこれを踏まえて、更に改良を加えて(ると思いますけど)作成しています。この共有メモリクラスも今回作ったクラスに応じて書き換え……は今回の記事にするには容量がはみ出るかな。

それでは設計(と言うほど大層なものでもないですけど)を考えましょう。アンマネージドメモリの特徴は、System.IntPtr を介して操作するという点です。しかし全てが Marshal クラスの WriteHogehoge を使用するとは限りません。例えば VirtualAllocEx で他のプロセスに領域を確保した場合、これに対しては Write/ReadProcessMemory を使用して読み書きせねばならず、とにかく Marshal.WriteHogehoge と言うわけにも行かないでしょう。このことから、汎用性を考えると IntPtr を提供する部分と Marshal を使用して読み書きする部分は分けないといけません。しかし、内部でどう読み書きしていようと、バイト配列、文字列、構造体を読み書きするという操作では同じです。そこで、まずバイト配列、文字列、構造体を読み書きすると言う操作、それと何らかのポインタを持つ基底抽象クラスを規定します。実際の読み書きの実装は派生クラスに任せます。

この基底抽象クラスに必要なものを考えてみましょう。ポインタの操作で良くあるのが、前述のバイト配列・文字列・構造体の読み書きです。それにポインタ同士のコピーもあるでしょうか。ゼロクリアなんかも? それから、この辺は構造体の可変長配列メンバなどの厄介な仕様を考えると、任意のオフセットを指定できるオーバーロードがあると安心です。 メモリを解放する操作。当然このクラスは IDisposable を実装して using を使えるようにしましょう。そう言えば抽象クラスと無条件に決め込んでいましたが、インターフェイスにするかどうかと言う問題もありますね。

以上のことをまとめると、大体スケルトン的にはこんな感じでしょうか。引数は仮のものなので意味はありません。

class UnmanagedMemory {
    Write(byte[]);
    Write(byte[], int);
    Write(string);
    Write(string, int);
    Write(object);
    Write(object, int);
    Write(IntPtr);
    Write(IntPtr, int);
    ReadBytes();
    ReadBytes(int);
    ReadString();
    ReadString(int);
    ReadStructure();
    ReadStructure(int);
    ReadToPtr();
    ReadToPtr(int);
    Clear();
    Dispose();
    Address { get; }
}

Read 系はシグネチャが返値しか違わないことも多いので ReadHogehoge にするのは当然ですが、Write 系の方をどうするか、割と悩みます。.NET の標準ライブラリを見ても、Marshal クラスではそれぞれの型を後ろに付けてますけど、Stream 系のクラスでは基本的に全部 Write のオーバーロードで片づけている、と言うように明確には統一されていない模様。

インターフェイスにするとなると、これだけ全部派生クラスで実装しろと言うのはなかなか面倒な話ですね。ここで目を付けるのが、各メソッドにはそれぞれオフセットをとるオーバーロードがあることです。オフセットを使わないと言うことは、つまりオフセット0を使うと言うことと同義ですから、これをこの基底クラスで実装しておけば、派生クラスで定義するのはオフセットをとるメソッドの方だけで済みます。と言うことはやはりインターフェイスよりも抽象クラスの方が便利そうですね。そう言えばそれぞれオフセットをとるメソッドがあると言うことは、このオフセット済みアドレスを取得するメソッドがあると便利ですね。シグネチャも考えて、以下のようにします。

using System;
using System.Runtime.InteropServices;

public abstract class UnmanagedMemory : IDisposable {
    public abstract void ReadToPtr(IntPtr dest, int size, int offset);
    public abstract byte[] ReadBytes(int length, int offset);
    public abstract object ReadStructure(Type type, int offset);
    public abstract string ReadString(CharSet charSet, int offset);
    public abstract void Write(object value, int offset);
    public abstract void Write(byte[] value, int offset);
    public abstract void Write(IntPtr ptr, int size, int offset);
    public abstract int Write(string value, CharSet charSet, int offset);
    public abstract void Clear(int size, int offset);
    protected abstract void Dispose(bool disposing);

    protected UnmanagedMemory() {
    }
    public abstract IntPtr Address { get; }
    public virtual IntPtr Offset(int offset) {
        if (this.Address.Equals(IntPtr.Zero)) {
            throw new InvalidOperationException(
               "まだこのインスタンスにポインタが割り当てられていないか、"
               + "または既に解放済みです。");
        }
        if (offset < 0) {
            throw new ArgumentOutOfRangeException(
               "offset", offset, "0 未満にすることはできません。");
        }
        return new IntPtr(this.Address.ToInt64() + offset);
    }
    public virtual void ReadToPtr(IntPtr dest, int size) {
        this.ReadToPtr(dest, size, 0);
    }
    public virtual byte[] ReadBytes(int length) {
        return this.ReadBytes(length, 0);
    }
    public virtual object ReadStructure(Type type) {
        return this.ReadStructure(type, 0);
    }
    public virtual string ReadString() {
        return this.ReadString(CharSet.Auto, 0);
    }
    public virtual string ReadString(CharSet charSet) {
        return this.ReadString(charSet, 0);
    }
    public virtual void Write(object value) {
        this.Write(value, 0);
    }
    public virtual void Write(byte[] value) {
        this.Write(value, 0);
    }
    public virtual void Write(IntPtr ptr, int size) {
        this.Write(ptr, size, 0);
    }
    public virtual int Write(string value) {
        return this.Write(value, CharSet.Auto, 0);
    }
    public virtual int Write(string value, CharSet charSet) {
        return this.Write(value, charSet, 0);
    }
    public virtual void Clear(int size) {
        this.Clear(size, 0);
    }
    public void Dispose() {
        this.Dispose(true);
    }
    public void Free() {
        this.Dispose(true);
    }
    ~UnmanagedMemory() {
        this.Dispose(false);
    }
}

オフセットをとらないメソッドを virtual にしているのは、派生クラスで独自に実装したい場合もあるかと考えてです。また、文字列に関しては文字セットを指定して読み書きできるようにします。Auto だけにしようかとも思っていましたが、今回のタスクスケジューラのように LPWSTR を要求するものもありますからね。大半は Auto で良いでしょうから、CharSet を指定しないオーバーロードも用意。

さて、基底クラスは作れました。では実装クラス……なんですが、もう一つ考えましょう。以前に定義した共有メモリクラスの、メモリマップトファイルを使用した方。あれも、読み書きは Marshal クラスのメソッドを使用していました。一般に P/Invoke に利用するために .NET でアンマネージドメモリを用意する場合、Marshal.AllocCoTaskMem や Marshal.AllocHGlobal を使用するのが一般的です。これらで確保したメモリも、同じように Marshal の各メソッドを使用します。つまり、これらの違いは確保と開放の部分だけな訳です。それなら、この操作する部分を実装するクラスを用意すれば、それから派生するクラスは確保と解放だけ用意すればいいことになります。

それではそう言うクラスを用意しましょう。この場合、抽象クラスの方が良いでしょう。別に基本となるメモリの確保法があるわけでもないですから、それは派生クラスに任せます。

using System;
using System.ComponentModel;;
using System.Runtime.InteropServices;

public abstract class SimpleMemory : UnmanagedMemory {
    protected SimpleMemory() : base() {
    }
    public static implicit operator IntPtr(SimpleMemory mem) {
        return mem.Address;
    }
    public override void ReadToPtr(IntPtr dest, int size, int offset) {
        if (size < 0) {
            throw new ArgumentOutOfRangeException(
               "size", size, "サイズを 0 よりも小さくすることはできません。");
      }
        SimpleMemory.MoveMemory(dest, this.Offset(offset), new IntPtr(size));
    }
    public override byte[] ReadBytes(int length, int offset) {
        if (length < 0) {
            throw new ArgumentOutOfRangeException(
               "length", length, "長さを 0 よりも小さくすることはできません。");
      }
        byte[] array = new byte[length];
        if (length == 0)
            return array;
        Marshal.Copy(this.Offset(offset), array, 0, length);
        return array;
    }
    public override object ReadStructure(Type type, int offset) {
        return Marshal.PtrToStructure(this.Offset(offset), type);
    }
    public override string ReadString(CharSet charSet, int offset) {
        switch (charSet) {
            case CharSet.Auto :
                return Marshal.PtrToStringAuto(this.Offset(offset));
            case CharSet.Unicode :
                return Marshal.PtrToStringUni(this.Offset(offset));
            case CharSet.Ansi :
            case CharSet.None :
                return Marshal.PtrToStringAnsi(this.Offset(offset));
            default :
                throw new InvalidEnumArgumentException(
                    "charSet", (int)charSet, typeof(CharSet));
        }
    }
    public override void Write(object value, int offset) {
        if (value == null)
            Marshal.WriteIntPtr(this.Offset(offset), IntPtr.Zero);
        else
            Marshal.StructureToPtr(value, this.Offset(offset), true);
    }
    public override void Write(byte[] value, int offset) {
        Marshal.Copy(value, 0, this.Offset(offset), value.Length);
    }
    public override void Write(IntPtr ptr, int size, int offset) {
        if (size <= 0) {
            throw new ArgumentOutOfRangeException(
                "size", size, "サイズを 0 以下にすることはできません。");
        }
        SimpleMemory.MoveMemory(this.Offset(offset), ptr, new IntPtr(size));
    }
    public override int Write(string value, CharSet charSet, int offset) {
        byte[] data = null;
        switch (charSet) {
            case CharSet.Auto :
                if (Marshal.SystemDefaultCharSize == 2)
                    goto case CharSet.Unicode;
                else
                    goto case CharSet.Ansi;
            case CharSet.Unicode :
                data = Encoding.Unicode.GetBytes(value + "\0");
                break;
            case CharSet.Ansi :
                data = Encoding.Default.GetBytes(value + "\0");
                break;
            default :
                throw new InvalidEnumArgumentException(
                    "charSet", (int)charSet, typeof(CharSet));
        }
        this.Write(data, offset);
        return data.Length;
    }

    public override void Clear(int size, int offset) {
        if (size < 0) {
            throw new ArgumentOutOfRangeException(
                "size", size, 
                "サイズを 0 よりも小さい値にすることはできません。");
        }
        this.Write(new byte[size], offset);
    }

    [DllImport("kernel32.dll", SetLastError=true)]
    private static extern bool MoveMemory(
        IntPtr dest, IntPtr source, IntPtr size);
}

本音を言うと ReadString したときも何バイト読み込んだのかって情報が欲しいんですけどね。Marshal.PtrToString... に out で受け取れるオーバーロードがあればなぁ。そうすれば NULL 文字区切りの文字列配列が格段に扱いやすくなるんですが。

ここまでくればもうできたも同然。最後にちょちょいと肝心の物である CoTaskMem を対象とした実装クラスを作成して完了です。このクラスは、ほとんどがこのクラス自身のインスタンスを作成する静的メソッドです。

こういう記事を書いておいてナンですが、未だもって CoTaskMem と HGlobal の使い分けが分かりません。意味の違いは分かるんですが、他のところで確保されていて解放関数を命じている場合はともかく、単純にアンマネージドメモリとして確保したいメモリの場合、差が分からない。直感的には CoTaskMem の方が確保や操作にコストがかかりそうな気もするんですが、以前実測してみたところ差異がないどころか微妙に CoTaskMem の方が早かったと言う結果になったり。と言うことで個人的に限定されない場合はちょっと用途が広そうな(印象の) CoTaskMem の方を使用しています。

using System;
using System.Collections;
using System.Runtime.InteropServices;

public sealed class CoTaskMem : SimpleMemory {
    private IntPtr address;
    public override IntPtr Address { get { return this.address; } }
    public CoTaskMem(int size) {
        this.address = Marshal.AllocCoTaskMem(size);
    }
    public CoTaskMem(IntPtr ptr) {
        this.address = ptr;
    }
    public static implicit operator CoTaskMem(IntPtr ptr) {
        return new CoTaskMem(ptr);
    }
    public static implicit operator IntPtr(CoTaskMem mem) {
        return mem.Address;
    }
    protected override void Dispose(bool disposing) {
        if (! this.Address.Equals(IntPtr.Zero)) {
            Marshal.FreeCoTaskMem(this.Address);
            this.address = IntPtr.Zero;
        }
    }
    public static CoTaskMem FromString(string value) {
        if (value == null) {
            throw new ArgumentNullException(
                "value", "null を指定することはできません。");
        }
        return new CoTaskMem(Marshal.StringToCoTaskMemAuto(value));
    }
    public static CoTaskMem FromString(string value, CharSet charSet) {
        if (value == null) {
            throw new ArgumentNullException(
                "value", "null を指定することはできません。");
        }
        switch (charSet) {
          case CharSet.Auto :
              return CoTaskMem.FromString(value);
          case CharSet.Unicode :
              return new CoTaskMem(Marshal.StringToCoTaskMemUni(value));
          case CharSet.Ansi :
          case CharSet.None :
              return new CoTaskMem(Marshal.StringToCoTaskMemAnsi(value));
          default :
              throw new InvalidEnumArgumentException(
                            "charSet", (int)charSet, typeof(CharSet));
        }
    }
    public static CoTaskMem FromStructure(object value) {
        if (value == null) {
            throw new ArgumentNullException(
                "value", "null を指定することはできません。");
        }
        IntPtr ptr = Marshal.AllocCoTaskMem(Mashal.SizeOf(value));
        Marshal.StructureToPtr(value, ptr, false);
        return new CoTaskMem(ptr);
    }
    public static CoTaskMem FromBytes(byte[] value) {
        if (value == null) {
            throw new ArgumentNullException(
                "value", "null を指定することはできません。");
        }
        IntPtr ptr = Marshal.AllocCoTaskMem(value.Length);
        Marshal.Copy(value, 0, ptr, value.Length);
        return new CoTaskMem(ptr);
    }
}

さて、それではお試しプログラムを最後に載せて今回は終了です。次回は小休止といいますか、今回のクラス群の VB.NET 版を掲載することにしましょう。

VB8 から .NET の文字が消えたので、VB.NET 以降の VB をなんと呼称すればいいのか困りますね。単純に VB と呼ぶとまだまだ VB6 の意味を指すことが多いでしょうし。「 VB7 以降」では微妙に冗長。そもそも VB7 と言うのは正しいんでしょうか? 取りあえず私はしばらく「 VB.NET 」で「バージョン8 以降の VB を含む .NET Framework 対応 VB 」というものを呼称することにします。

public struct Test {
    private int id;
    private int value;
    public Test(int id, int value) {
        this.id = id;
        this.value = value;
    }
    public override string ToString() {
        return this.id + ":" + this.value;
    }
    public static void Main() {
        byte[] data = Encoding.Default.GetBytes("てすと。");
        using (CoTaskMem mem = CoTaskMem.FromBytes(data)) {
            Console.WriteLine(BitConverter.ToString(mem.ReadBytes(data.Length)));
        }
        using (CoTaskMem mem = CoTaskMem.FromString("てすと。")) {
            Console.WriteLine(mem.ReadString());
            //FromString(string) で作ったインスタンスに対して、
              //NT 系ではやっちゃいけない
            Console.WriteLine(mem.ReadString(CharSet.Ansi));
              //9x 系ではやっちゃいけない
            Console.WriteLine(mem.ReadString(CharSet.Unicode));
        }
        using (CoTaskMem mem = CoTaskMem.FromStructure(new Test(12, 43))) {
            Console.WriteLine(mem.ReadStructure(typeof(Test)));
            //メモリレイアウトが同じだからできる暴挙
            Console.WriteLine(mem.ReadStructure(typeof(Point)));
        }
        using (CoTaskMem mem = new CoTaskMem(4)) {
            //int を構造体として書き込んでいる、って点に注意
            //CoTaskMem.Write(object) のオーバーロード呼び出しになる
            mem.Write(128);
            Console.WriteLine(mem.ReadStructure(typeof(int)));
        }
    }
}
posted by Hongliang at 01:34| Comment(0) | TrackBack(0) | C# | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

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


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

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

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

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

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

×

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