このトピックはまだ評価されていません - このトピックを評価する

Microsoft .NET Compact Framework での高度な P/Invoke

Jon Box, Dan Fox

寄稿 :
Jonathan Wells
Microsoft Corporation

March 2003
日本語版最終更新日 2003 年 6 月 24 日

対象 :
    Microsoft® .NET Compact Framework 1.0
    Microsoft® Visual Studio® .NET 2003

概要 : .NET Compact Framework での高度な相互運用性について調査します。

目次

はじめに
複合型のマーシャリング
構造体内の文字列のマーシャリング
構造体内の固定長文字列のマーシャリング
まとめ

はじめに

前回の資料「Microsoft .NET Compact Framework の P/Invoke とマーシャリング入門」では、 Microsoft .NET Compact Framework と Microsoft .NET Framework 両方の Platform Invoke サービスを使用して、 マネージ コードでアンマネージ DLL に存在する関数を呼び出す方法について説明しました。 この方法によって、カスタム API およびオペレーティング システム (Windows CE) API は両方とも、 どちらのフレームワーク用に記述されたアプリケーションでもアクセスできるようになります。 2 つのフレームワーク間では、 サービスの多くの機能がまったく同じですが、.NET Compact Framework は完全な .NET Framework のサブセットなので、 いくつかの相違点があります。 この相違点の一部については、 前回の資料で詳しく説明しました。 今回の資料では、構造体をマーシャリングするときに生じる 2 つの特定の問題、 および .NET Compact Framework でその問題に対処する方法について説明します。

複合型のマーシャリング

前回の資料で説明したとおり、 .NET Compact Framework のマーシャラと完全な .NET Framework のマーシャラの主な相違点の 1 つとして、 .NET Compact Framework の簡易なマーシャラが構造体またはクラス内部の複合オブジェクト (参照型) をマーシャリングできないことがあります。 つまり、構造体またはクラス内で .NET Compact Framework とアンマネージ コードの間で共通の表記を持たない型 (共通の表記を持つ型は "高速転送型" と呼ばれ、前回の資料で列挙しました) を含む任意のフィールドが定義されると、 構造体またはクラスを完全にマーシャリングできません。 実際には、 文字列ポインタまたは固定長文字バッファのいずれかを含む構造体やクラスが正確にマーシャリングされないことを意味します。

一例として、Windows CE で使用可能なユーザー通知 API を考えます。 この API を使用すると、アプリケーションは通知のダイアログを表示したり、 特定の場面 (すなわち、同期などのイベントへの応答時) または PC カードの変更時にアプリケーションを実行させることができます。 .NET Compact Framework にはこの機能を実行するマネージ クラスが含まれていないので、 この機能が必要な開発者は、P/Invoke で適切なオペレーティング システム呼び出しを行う必要があります。

Windows CE 通知 API (CeSetUserNotificationEx) を使用するには、 通知をアクティブにするイベントを定義する際に使用する構造体 CE_NOTIFICATION_TRIGGER をマネージ コード内で宣言して、 下記のように、VB .NET に直接変換します。 この場合、SYSTEMTIME は高速転送型で完全に構成された別の構造体で、 NotificationTypes および EventTypes は整数にマップされる列挙型です。


                      Private Structure CE_NOTIFICATION_TRIGGER
  Dim dwSize As Integer
  Dim dwType As NotificationTypes
  Dim dwEvent As EventTypes
  Dim lpszApplication As String
  Dim lpszArguments As String
  Dim startTime As SYSTEMTIME
  Dim endTime As SYSTEMTIME
End Structure

                    

残念ながら、実行するアプリケーションを指定するために使用する 2 つの文字列値、 およびそのコマンド ライン引数は、NULL で終わる Unicode 文字列 (WCHAR *) へのポインタとしてアンマネージ コードで定義されます。 String は参照型 (System.String) なので、 .NET Compact Framework のマーシャラは構造体を正しくマーシャリングしません。

注意    前回の資料で説明したとおり、.NET Compact Framework ではすべての文字列を Unicode として扱うので、 System.String は高速転送型になります。 ただし、このことは、String がアンマネージ関数に直接渡される場合のみ当てはまります。 String を構造体またはクラス内部で使用する場合は該当しません。

完全な .NET Framework では、 マーシャラは MarshalAsAttribute を含んでいるので、 この状況に対応できます。 この属性を使用して、構造体を次のように書き換えることができます。


                      Private Structure CE_NOTIFICATION_TRIGGER
  Dim dwSize As Integer
  Dim dwType As NotificationTypes
  Dim dwEvent As EventTypes
  <MarshalAs(UnmanagedType.LPWStr)> Dim lpszApplication As String
  <MarshalAs(UnmanagedType.LPWStr)> Dim lpszArguments As String
  Dim startTime As SYSTEMTIME
  Dim endTime As SYSTEMTIME
End Structure

                    

結果として、完全な NET Framework では、 アンマネージ関数での予想どおり、 構造体で参照した文字列を NULL で終わる Unicode 文字の文字列としてマーシャリングします。 .NET Compact Framework ではこの動作が含まれないので、 この問題を回避する必要があります。

構造体内の文字列のマーシャリング

.NET Compact Framework で構造体内またはクラス内の文字列ポインタを正しくマーシャリングできるようにするためには、 サンク層での呼び出し、unsafe ブロックの使用、 文字列ポインタを処理するカスタム クラスの作成という 3 つの主なソリューションがあります。

サンク層の使用

サンクという用語は、 これまで引数や戻り値を 16 ビット表現から 32 ビット表現に変換するコード、 逆に 32 ビット表現から 16 ビット表現への変換するコードに適用されてきました。 ただし、もう少し総称的に使用するならば、 この用語は単にデータ変換を処理する中間コードを作成することを意味します。 .NET Compact Framework および P/Invoke に関するサンク層とは、 構造体を構成する引数を受け取り、アンマネージ構造体を作成して、 適切な関数を呼び出すアンマネージ関数になります。 これは、Visual Studio .NET のヘルプおよび MSDN で説明した構造体で複合オブジェクトを渡す技法です。

上記の通知の例でこの技法を使用するために、CE_NOTIFICATION_TRIGGER 構造体のすべての引数を受け取る関数を公開する eMbedded Visual C++ のアンマネージ DLL を作成します。 たとえば、次のように、 通知サービス (CeSetUserNotificationEx 関数) を使用して、 特定の時刻にアプリケーションを実行するようにスケジュールを設定する C のアンマネージ関数を作成します。


                      HANDLE RunApplicationThunk (WCHAR *lpszApplication, 
  WCHAR *lpszArguments, SYSTEMTIME startTime)
{
  HANDLE returnCode;
  CE_NOTIFICATION_TRIGGER trigger;
// 構造体に値を設定します。
  trigger.dwSize = sizeof(trigger);
  trigger.dwType = 2; //CNT_TIME
  trigger.dwEvent = 0; //NONE
  trigger.lpszApplication = lpszApplication;
  trigger.lpszArguments = lpszArguments;
  trigger.startTime = startTime;
trigger.endTime = 0;  // 空、使用しません。

// ネイティブ Windows CE API 関数を呼び出します。
  returnCode = CeSetUserNotificationEx(0,&trigger, 0);
  return returnCode;
}

                    

上記のコードでは、 アンマネージ関数が既定で構造体の dwType メンバ、 dwEvent メンバ、 および endTime メンバを設定し、 適切な引数を CeSetUserNotificationEx 関数に渡していることに注意します。

C# では、 マネージ コードから DLLImportAttribute を使用して、 この関数を次のように宣言します。 .NET Compact Framework では、 文字列をアンマネージ関数に直接渡すときに、 高速転送型として扱うことができるので、 宣言では簡単に文字列を使用できます。


                      [DLLImport("MyNotification.DLL", SetLastError=true"]
private static extern IntPtr RunApplicationThunk( 
  string lpszApplication, string lpszArguments, 
  SYSTEMTIME startTime);

                    

前回の資料で説明したように、C# で示したこのアンマネージ サンク関数を、 マネージ クラスにカプセル化し、 クラスの (VB で共有する) スタティック メソッドを使用して公開する必要があります。


                      public static void RunApplication(string application, string arguments, 
  DateTime startTime)
{
// DateTime 値をマネージ SYSTEMTIME 構造体に変換します。
  SYSTEMTIME startStruct = DateTimeToSystemTime( start );

  try
  {
// サンク層を呼び出します。
    IntPtr hNotify = RunApplicationThunk(application, arguments,
      startStruct);
// エラーを処理します。
    if(hNotify == IntPtr.Zero)
    {
       int errorNum = Marshal.GetLastWin32Error();
       HandleCeError(New WinCeException("Could not set notification ",
         errorNum), "RunApplication");
    }
  }
  catch (Exception ex)
  {
    HandleCeError(ex, "RunApplication");
  }
}

                    

この場合、適切な引数すべてを受け取るマネージ メソッドを記述できるので、 マネージ コードで CE_NOTIFICATION_TRIGGER 構造体を宣言する必要もなくなります。 そのため、基本的なオペレーティング システムの操作を完全にカプセル化していることに注意してください。 代わりに、以前説明したように、マネージ バージョンの構造体を作成し、 その構造体を簡単にメソッドに渡すことができます。 エラーが発生する場合 (返されたハンドルがゼロの場合)、 前回の資料で説明したように、 オペレーティング システムから取得したエラー コードを使用して、 カスタム例外を作成して設定します。

注意    マネージ コードで DateTime 変数を SYSTEMTIME 構造体に変換するために使用するコードが示されておらず、カスタム メソッドの DateTimeToSystemTime に含められていることに気付きます。 このメソッドは、coredll.dll に存在する FileTimeToLocalFileTime および FileTimeToSystemFileTime Windows CE API 関数を呼び出します。

この選択肢の明らかな欠点は、 開発者が eMbedded Visual C++ をインストールして使用する必要があることです。 これは、多くの Visual Basic および .NET Framework の開発者にとって困難な作業です。 そのため、.NET Compact Framework でマネージ コードのみを使用して、 この問題を解決する 2 つの他の方法について検討します。

Unsafe 文字列ポインタの使用

構造体の文字列を渡すための 2 番目の選択肢として、 C# では unsafe および fixed キーワードを使用します (VB にはこれに相当するものがありません)。 この選択肢ではマネージ コードのみを記述できますが、 共通言語ランタイムのコード検証機能を無効にするという犠牲を払います。 共通言語ランタイムはコード検証機能を使用して、 マネージ コードが割り当てられたメモリのみにアクセスしていること、 およびすべてのメソッドの呼び出しが引数の数と型の両方においてメソッドのシグネチャに従っていることを検証します。 Unsafe コードの使用は、 ポインタによる直接的なメモリ管理を行うことを意味するので、 このことが当てはまります。

注意    前回の資料で説明したように、 検証できないコードの作成を追加し、 完全な .NET Framework の Unsafe コードは信頼済みの環境のみで実行できます。 ただし、.NET Compact Framework Version 1.0 には、 コード アクセス セキュリティ (CAS) が同梱されていないので、 このことはまだ問題にはなりません。

fixed キーワードは unsafe キーワードに関連して使用します。 このキーワードは、 アンマネージ関数がオブジェクトにアクセスしている間に、 共通言語ランタイムのガベージ コレクタ (GC) がそのオブジェクトの割り当て解除を実行しないようにするために使用します。

コード検証を行わなくなるという欠点に加えて、 この技法を VB から直接使用できません。 ただし、Unsafe コードを使用する C# のアセンブリを作成して、VB からそのアセンブリを呼び出すことができます。

unsafe および fixed キーワードを使用するには、 ポインタを使用するメソッドだけでなく、 ポインタで宣言した構造体も Unsafe としてマークする必要があります。 たとえば、Unsafe コードを使用して同様の RunApplication メソッドを作成するには、 以下のようになります。


                      [DllImport("coredll.dll",SetLastError=true)]
private static extern IntPtr CeSetUserNotificationEx( 
IntPtr h, 
   ref CE_NOTIFICATION_TRIGGER nt, 
   IntPtr un 
);

private unsafe struct CE_NOTIFICATION_TRIGGER
{
   public uint   dwSize;
public CNT_TYPE NotificationTypes; // 列挙値
public NOTIFICATION_EVENT EventTypes; // 列挙値
   public char *lpszApplication;
   public char *lpszArguments;
   public SYSTEMTIME   startTime;
   public SYSTEMTIME   endTime;
}

public static unsafe void RunApplication( string application, 
  string arguments,  DateTime start )
{
   CE_NOTIFICATION_TRIGGER nt = new CE_NOTIFICATION_TRIGGER();
   nt.dwSize  = (uint)Marshal.SizeOf( typeof(CE_NOTIFICATION_TRIGGER) );
   nt.dwType  = NotificationTypes.Time;
   nt.dwEvent = EventTypes.None;
   nt.startTime = DateTimeToSystemTime( start );
nt.endTime = new SYSTEMTIME(); // このメンバをスキップします。

   try
   {
      if ((application == null) || (application.Length == 0))
      {
         throw new ArgumentNullException();
      }
      fixed (char *pApp = application.ToCharArray( ))
      {
         if ((arguments == null) || (arguments.Length == 0) )
         { 
            arguments = " ";
         }
         fixed (char *pArgs = arguments.ToCharArray())
         {
            nt.lpszApplication = pApp;
            nt.lpszArguments = pArgs;
// ネイティブ関数を呼び出します。
            IntPtr hNotify = CeSetUserNotificationEx(IntPtr.Zero, 
              ref nt, IntPtr.Zero);
               
            if( hNotify == IntPtr.Zero )
            {
       int errorNum = Marshal.GetLastWin32Error();
             HandleCeError(New WinCeException( 
               "Could not set notification ", errorNum),
               "RunApplication");
            }
         }
      }
   }
   catch (Exception ex)
   {
     HandleCeError(ex, " RunApplication");
   }
}

                    

この例では、DllImportAttribute を使用して、 最初に CeSetUserNotificationEx Windows CE API 関数が宣言されていることがわかります。 CE_NOTIFICATION_TRIGGER を構造体として定義すると、 ref 引数 (VB では ByRef) としてマークされます。 .NET Compact Framework では、 構造体のアドレスが ref 引数として宣言されている場合このアドレスが渡されるだけなので、 この作業が必要になります。 CE_NOTIFICATION_TRIGGER がクラスとして宣言された場合、 .NET Compact Framework が参照型のアドレスを自動的に渡すので、 CeSetUserNotificationEx への引数は値として渡すことができます。 多くの場合、アンマネージ関数で期待される構造体を、 マネージ コードの構造体またはクラスとして宣言しても問題ありませんが、 この場合、SYSTEMTIME 構造体は例外です。 ここでは、SYSTEMTIME を構造体として宣言する必要があります。 CeSetUserNotificationEx 関数が 構造体全体をインラインで保持することを CE_NOTIFICATION_TRIGGER に求めるためです。 SYSTEMTIME がクラスとして宣言されると、 クラスへの 4 バイトのポインタのみがマーシャリングされます。

次に、lpszApplication および lpszArguments メンバの文字配列へのポインタで CE_NOTIFICATION_TRIGGER 構造体を宣言し、 unsafe キーワードを指定します。

注意    完全な .NET Framework とは異なり、 .NET Compact Framework では StructLayoutAttribute を使用して構造体を修飾する必要がないことを忘れないでください。ゆえにすべての構造体は自動的に LayoutKind.Sequential になります。

最後に、 前の例と同じシグネチャを使用して RunApplication メソッドを宣言します。 今回のみ、メソッドは CE_NOTIFICATION_TRIGGER のインスタンスを作成して、 続いてそのメンバを設定しています。 前の例と同様に、 いくつかのメンバが特定のアプリケーションを実行するために必要な値に設定されます。 SYSTEMTIME 構造体は、 カスタム DateTimeToSystemTime メソッドで作成されます。

メソッドは、構造体に埋め込まれた文字列ポインタを設定するために、 最初に、 アプリケーション名が Null または空の文字列ではないことを確認します。 どちらかの条件が当てはまる場合、ArgumentNullException がスローされます。 どちらの条件も当てはまらない場合は、 文字配列へのポインタが fixed ステートメントで宣言され、 String クラスの ToCharArray メソッドを使用して設定されます。 文字配列は fixed ブロックのスコープ内のメモリ固定されたままになることに注意してください。 その後、引数ポインタが同じ方法で設定されます。 構造体の lpszApplication メンバと lpszArguments メンバはポインタで設定され、 構造体が CeSetUserNotificationEx 関数に渡されます。

注意    この例では、実際、CE_USER_NOTIFICATION 型の構造体が呼び出されるときに、 CeSetUserNotificationEx 関数の宣言に 3 番目の引数として IntPtr があります。 RunApplication メソッドがその引数の関数に IntPtr.Zero を渡すことを可能にするには、この宣言が必要になります。 他に CeSetUserNotificationEx を使用する場合は、実際に構造体を渡す必要があります (たとえば、通知ダイアログを表示します)。 このような場合、最初の宣言をオーバーロードする CeSetUserNotificationEx の 2 番目の宣言を行うことができます。 コンパイラは、引数に基づいて、適切な宣言を選択します。

エラーが発生する (関数が返したハンドルが空である) 場合、 カスタムの WinCeException が作成されて、エラー ハンドラに渡されます。

マネージ文字列ポインタの使用

.NET Compact Framework で、 構造体内の文字列のマーシャリングに使用できる最後の技法として、 マネージ文字列ポインタ クラスを独自に作成します。 この方法の利点として、C# および VisualBasic の両方で使用できることと、 文字列ポインタ クラスを一度作成したらさまざまな状況で利用できることがあります。 ただし、Windows CE オペレーティング システムのメモリ割り当て API を少し操作する必要があります。

最初にこの技法を使用して、 必要なメモリ管理 API の宣言と呼び出しを行うマネージ クラスを作成できます。 このクラスは、アンマネージ メモリ ブロックを割り当て、 そのメモリを解放し、 アンマネージ メモリ ブロックのサイズを変更し、 マネージ文字列をアンマネージ メモリにコピーする共有メソッドを公開できます。 VisualBasic の Memory クラスは次のとおりです。


                      Public Class Memory
    <DllImport("coredll.dll", SetLastError:=True)> _
    Private Shared Function LocalAlloc(ByVal uFlags As Integer, _
      ByVal uBytes As Integer) As IntPtr
    End Function

    <DllImport("coredll.dll", SetLastError:=True)> _
    Private Shared Function LocalFree(ByVal hMem As IntPtr) As IntPtr
    End Function

    <DllImport("coredll.dll", SetLastError:=True)> _
    Private Shared Function LocalReAlloc(ByVal hMem As IntPtr, _
      ByVal uBytes As Integer, ByVal fuFlags As Integer) As IntPtr
    End Function

    Private Const LMEM_FIXED As Integer = 0
    Private Const LMEM_MOVEABLE As Integer = 2
    Private Const LMEM_ZEROINIT As Integer = &H40

    Private Const LPTR = (LMEM_FIXED Or LMEM_ZEROINIT)

' LocalAlloc を使用して、メモリ ブロックを割り当てます。
    Public Shared Function AllocHLocal(ByVal cb As Integer) As IntPtr
        Return LocalAlloc(LPTR, cb)
    End Function

' AllocHLocal で割り当てられたメモリを解放します。
    Public Shared Sub FreeHLocal(ByVal hlocal As IntPtr)
        If Not hlocal.Equals(IntPtr.Zero) Then
            If Not IntPtr.Zero.Equals(LocalFree(hlocal)) Then
                Throw New Win32Exception(Marshal.GetLastWin32Error())
            End If
            hlocal = IntPtr.Zero
        End If
    End Sub

' 以前に AllocHLocal で割り当てられたメモリ ブロックのサイズを変更します。
    Public Shared Function ReAllocHLocal(ByVal pv As IntPtr, _
      ByVal cb As Integer) As IntPtr
        Dim newMem As IntPtr = LocalReAlloc(pv, cb, LMEM_MOVEABLE)
        If newMem.Equals(IntPtr.Zero) Then
            Throw New OutOfMemoryException
        End If
        Return newMem
    End Function

' マネージ文字列の内容をアンマネージ メモリにコピーします。
    Public Shared Function StringToHLocalUni( _
     ByVal s As String) As IntPtr
        If s Is Nothing Then
            Return IntPtr.Zero
        Else
            Dim nc As Integer = s.Length
            Dim len As Integer = 2 * (1 + nc)
            Dim hLocal As IntPtr = AllocHLocal(len)
            If hLocal.Equals(IntPtr.Zero) Then
                Throw New OutOfMemoryException
            Else
                Marshal.Copy(s.ToCharArray(), 0, hLocal, s.Length)
                Return hLocal
            End If
        End If
    End Function
End Class

                    

コードで示すとおり、LocalAllocLocalFree、 および LocalRealloc アンマネージ関数は、 DllImportAttribute で宣言され、 それぞれ AllocHLocalFreeHLocal、 および ReAllocHLocal メソッドでラップされます。 最後に、StringToHLocalUni メソッドは、 特定の文字列に対して適切なサイズのアンマネージ メモリ ブロックを割り当てます (.NET Compact Framework は Unicode しかサポートしていないので、各文字に 2 バイト割り当てます)。 その後、アンマネージ ブロックを示す IntPtr に文字配列をコピーします。

Memory クラスを配置した後は、 次に示すように、 簡潔なマネージ文字列ポインタの構造体を作成できます。


                      Public Structure StringPtr
   Private szString As IntPtr

   Public Sub New(ByVal s As String)
       Me.szString = Memory.StringToHLocalUni(s)
   End Sub

   Public Overrides Function ToString() As String
       Return Marshal.PtrToStringUni(Me.szString)
   End Function

   Public Sub Free()
       Memory.FreeHLocal(Me.szString)
   End Sub
End Structure

                    

お気付きのように、StringPtr 構造体には、 アンマネージ メモリ内の文字列へのポインタを監視する際に使用するプライベート IntPtr が含まれています。 ポインタは、 マネージ文字列に渡す Memory クラスの StringToHLocalUni メソッドを呼び出すことにより、 コンストラクタで設定されます。 ToString メソッドは、 System.ToString メソッドをオーバーライドして、 ポインタが指定するマネージ文字列を返しますが、 Free メソッドは文字列に割り当てられたアンマネージ メモリを解放します。

Memory クラスと StringPtr 構造体は両方とも 1 つのアセンブリに配置され、 マネージ文字列ポインタを必要とする任意のスマート デバイス プロジェクトから参照されます。

CE_NOTIFICATION_TRIGGER 構造体の場合、 StringPtr 構造体を使用するには、 その構造体の lpszApplication メンバと lpszArguments メンバ用に StringPtr 構造体を使用して、 構造体を宣言します。 さらに、StringPtr クラスの Free メソッドを使用して、 これらのメンバの割り当てを解除する必要があるので、 CE_NOTIFICATION_TRIGGERIDisposable インターフェイスを実装することは十分理解できます。 Dispose メソッドを使用して、 Free メソッドの 2 つのメンバを呼び出すことができます。 さらに、パブリック コンストラクタをクラスに追加して、StringPtr メンバを作成するだけではなく、 通知のセットアップに必要な他のメンバを既定値に設定することもできます。 構造体の C# コードは次のとおりです。


                      private struct CE_NOTIFICATION_TRIGGER: IDisposable
{
   public uint   dwSize;
   public CNT_TYPE dwType;
   public NOTIFICATION_EVENT dwEvent;
   public StringPtr lpszApplication;
   public StringPtr lpszArguments;
   public SYSTEMTIME startTime;
   public SYSTEMTIME endTime;

   public CE_NOTIFICATION_TRIGGER( string application, string arguments,  
      DateTime start )
   {
      dwSize  = (uint)Marshal.SizeOf( typeof(CE_NOTIFICATION_TRIGGER) );
      dwType  = CNT_TYPE.CNT_TIME;
      dwEvent = NOTIFICATION_EVENT.NONE;
      
      lpszApplication   = new StringPtr( application );
      lpszArguments   = new StringPtr( arguments );

      startTime = DateTimeToSystemTime( start );
      endTime = new SYSTEMTIME();
   }

// ここに、その他の可能性のあるコンストラクタを記述します。

   public void Dispose()
   {
      lpszApplication.Free();
      lpszArguments.Free();
   }
}

                    

最後に、RunApplication メソッドは、 単に CE_NOTIFICATION_TRIGGER 構造体の新しいインスタンスを作成して、 CeSetUserNotificationEx 関数を呼び出すことだけできます。 C# では、 構造体が IDisposable インターフェイスを実装すると、 using ステートメントを使用してアンマネージ関数が呼び出された後に、 コンパイラは自動的に Dispose メソッドを呼びます。


                      public static void RunApplication( string application, string arguments,  
   DateTime start )
{
   CE_NOTIFICATION_TRIGGER nt = 
      new CE_NOTIFICATION_TRIGGER( application, arguments, start );
   using( nt )
   {
      IntPtr hNotify = CeSetUserNotificationEx( 
IntPtr.Zero, ref nt, IntPtr.Zero );
            
      if( hNotify == IntPtr.Zero )
      {
          int errorNum = Marshal.GetLastWin32Error();
           HandleCeError(New WinCeException( 
             "Could not set notification ", errorNum),
             "RunApplication");
      }
   }
}

                    

構造体内の固定長文字列のマーシャリング

.NET Compact Framework の P/Invoke サービスを使用する場合に生じる二つめの状況は、 構造体内の固定長文字列または文字配列のマーシャリングに関係します。 たとえば、システム トレイからアプリケーション アイコンを配置および削除する際に使用する Shell_NotifyIcon 関数は、 以下のような、Windows CE SDK で定義された構造体へのポインタを受け取ります。


                      typedef struct _NOTIFYICONDATA { 
DWORD cbSize;
HWND hWnd; 
UINT uID; 
UINT uFlags; 
UINT uCallbackMessage; 
HICON hIcon; 
WCHAR szTip[64]; 
} NOTIFYICONDATA, *PNOTIFYICONDATA; 

                    

構造体の最後のフィールド (カーソルをアイコン上に置くときに表示されるツールヒントのテキストを保持します) は、 64 要素の文字配列として定義されます。 この構造体を VB .NET にわかりやすく変換すると、次のようになります。


                      Private Structure NOTIFYICONDATA
    Public cbSize As Integer
    Public hWnd As IntPtr
    Public uID As Integer
    Public uFlags As Integer
    Public uCallbackMessage As Integer
    Public hIcon As IntPtr
    Public szTip() As Char

    Public Sub New(ByVal toolTip As String)
        szTip = toolTip.ToCharArray
    End Sub
End Structure

                    

残念ながら、System.Char が高速転送型の場合でも、 文字の配列が実行時に配列への 4 バイト ポインタとしてマーシャリングされるので、 この簡単な構造体は機能しません。 以前と同様に、 完全な .NET Framework では、MarshalAs 属性を使用してこの状況をサポートします。 この場合は、属性で配列を修飾し、 そのコンストラクタに UnmangedType 列挙値の ByValTStr 値を渡します。

この動作のため、本来、2 つのオプションがあります。 最初のオプションでは、 正確な合計サイズのバイト配列を作成した後、 構造体の各フィールドをバイト配列にコピーしたり、バイト配列からコピーします。 この操作は、必要に応じて、Marshal クラスのメソッドを使用するか、 または unsafe コードのポインタを直接使用する (つまり、手動でマーシャリングする) ことによって行います。 ポインタをバイト配列に設定して、 アンマネージ関数に渡すことができます。 ただし、この技法は複雑なので、 この資料の残りの部分では、.NET Compact Framework マーシャラを使用する際に、 カスタム マーシャリングを組み合わせる技法について説明します。

注意    この技法で重要な点は、 NOTIFYICONDATA の szTip メンバが構造体の最後のメンバであることです。 そうではない場合は、さらにカスタマイズした方法が必要になります。

この技法を使用するには、 最初に、クラス内で適切なアンマネージ宣言を行う必要があります。 以前と同様、推奨事例は、 機能を実行するために (VB で共有する) スタティック メソッドを公開するクラスのプライベート関数として宣言することです。 この場合、次に示すように、NOTIFYICONDATA 構造体やいくつかの定数に加えて、 DestroyIconRegisterWindowMessage、 および Shell_NotifyIcon 関数を宣言する必要があります。


                      Private Structure NOTIFYICONDATA
    Public cbSize As Integer
    Public hWnd As IntPtr
    Public uID As Integer
    Public uFlags As Integer
    Public uCallbackMessage As Integer
    Public hIcon As IntPtr
End Structure

<DllImport("coredll", SetLastError:=True)> _
Private Shared Function RegisterWindowMessage(ByVal lpMessage As String) As Integer
End Function

<DllImport("coredll", SetLastError:=True)> _
Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As IntPtr
End Function

<DllImport("coredll", SetLastError:=True)> _
Private Shared Function Shell_NotifyIcon( _
 ByVal dwMessage As TrayConstants, _
 ByVal lpData As IntPtr) As Boolean
End Function

Private Enum TrayConstants As Integer
    NIM_ADD = 0
    NIM_DELETE = 2
    NIF_ICON = 2
    NIF_MESSAGE = 1
    NIF_TIP = 4
    NIF_ALL = NIF_ICON Or NIF_MESSAGE Or NIF_TIP
End Enum

                    

ここで注意すべき重要な点は、szTip フィールドは手動でマーシャリングする必要があるので、 NOTIFYICONDATA 構造体に含まれていないということです。 さらに、RegisterWindowMessage 関数を使用して、 一意なメッセージ識別子を作成します。 この識別子を使用して、uCallbackMessage フィールドを配置し、 最終的に、ユーザーが通知アイコンを操作するときにアプリケーションに通知します。 DestroyIcon 関数を使用して、 システム トレイでアイコンが使用するメモリの割り当てを解除します。 最後に、Shell_NotifyIcon メソッドが TrayConstants 型の引数を受け取るように宣言されていることがわかります。 TrayConstants 型の引数は、 アイコンの追加または削除などの関数による操作、 および NOTIFYICONDATA 構造体を含むポインタで作成するアンマネージ ポインタを定義します。

Shell_NotifyIcon 関数の呼び出しは、 下に示すように、CreateNotifyIcon と呼ばれる共有メソッドでラップできます。


                      ' 通知アイコンをセットアップし、イベントをフックするためにメッセージを返します。
Public Shared Function CreateNotifyIcon( _
 ByVal notifyWindow As MessageWindow, _
 ByVal icon As IntPtr, _
 ByVal Tooltip As String) As Integer

    Dim structPtr As IntPtr

' 構造体を作成します。
    Dim nid As New NOTIFYICONDATA

' 必要な構造体のサイズを計算します。
    Dim size As Integer = Marshal.SizeOf(nid) + _
     (64 * Marshal.SystemDefaultCharSize)

    Try
' コールバック イベントを登録します。
        Dim msg As Integer = RegisterWindowMessage("NotifyIconMsg")

' 構造体のフィールドを入力します。
        With nid
           .cbSize = size
           .hIcon = icon
           .hWnd = notifyWindow.Hwnd
           .uCallbackMessage = msg
           .uID = 1 'Application defined identifier
           .uFlags = TrayConstants.NIF_ALL
        End With

' メモリを割り当てます。
        structPtr = Memory.AllocHLocal(size)

' 構造体をポインタにコピーします。
        Marshal.StructureToPtr(nid, structPtr, False)

' ツールヒントを追加します。
        Dim arrTooltip() As Char = Tooltip.ToCharArray()
        Dim toolOffset As New IntPtr(structPtr.ToInt32() + _
         Marshal.SizeOf(nid))
        Marshal.Copy(arrTooltip, 0, toolOffset, arrTooltip.Length)

' 関数を呼び出します。
        Dim ret As Boolean = Shell_NotifyIcon( _
         TrayConstants.NIM_ADD, structPtr)

        If ret = False Then
' エラーが発生した場合。
            Dim errorNum As Integer = Marshal.GetLastWin32Error()
            Throw New WinCeException("Could not set the notify icon ", _
             errorNum)
        Else
' 成功した場合。
            Return msg
        End If

    Catch ex As Exception
        HandleCeError(ex, "CreateNotifyIcon", False)
    Finally
' メモリを解放します。
        Memory.FreeHLocal(structPtr)
    End Try

End Function

                    

Microsoft.WindowsCe.Forms 名前空間から MessageWindow オブジェクトとして定義されたアイコンが操作されるときに通知されるウィンドウ、 システム トレイに配置するアイコン (実際にはアイコン ハンドル) へのアンマネージ ポインタ、 および表示されるツールヒントのテキストをこのメソッドが受け取っていることがわかります。 メソッドは、通知アイコン上で行うクリックなどのイベントを探すために MessageWindow で使用する Windows メッセージ ハンドルを返します。

注意    アイコンへのポインタを作成する 1 つの方法として、ExtractIconEx Windows CE 関数を使用し、実行可能ファイルまたは DLL からアイコン ハンドルを抽出できます。

最初に、このメソッドは、 構造体へのポインタを保持する際に使用するアンマネージ ポインタを宣言して、 NOTIFYICONDATA の新しいインスタンスを作成し、 そのインスタンスを参照変数 nid に配置します。 次に、マーシャラ (Marshal.SizeOf) で計算したサイズをツールヒント テキストのサイズ (この場合、64 要素の配列) に追加することで、 アンマネージ関数で予想される構造体の正確なサイズを計算します。 この特定の事例では、szTip フィールドが構造体に含まれていた場合、 SizeOf メソッドが 28 (7 つのフィールドそれぞれに 4 バイト) を返します。 ただし、手動で配列のサイズを追加すると、92 という正しい合計 (他の 6 つのフィールドの 24 バイト、さらに文字配列の 64 バイトに加えて) が計算されます。 SystemDefaultCharSize を使用して、 システムの正確な文字のサイズを考慮に入れるようにします。

構造体の正確なサイズが決定するとすぐに、 RegisterWindowMessage 関数が呼び出されて、 いつアイコンのイベントを生成するか決定するためにアプリケーションで使用される Windows メッセージ ハンドルを登録します。 構造体の残りの部分 (計算されたサイズ、アイコン ハンドル、通知される Windows のハンドル、 生成されたメッセージ ハンドル、アプリケーション定義の識別子、 最後に、有効なデータを含む他のフィールドを示すフラグなど) が作成されます。

この時点で、 構造体をアンマネージ メモリにコピーして、 ポインタを返すことができます。 この操作行うには、 この資料の最初のセクションで説明した Memory クラスの AllocHLocal メソッドを使用します。 このメソッドに、既に計算された構造体の合計サイズが渡されます。 返された IntPtr (この場合は structPtr) は、 Marshal クラスの StructureToPtr メソッドを使用して配置できます。 このコードでは、.NET Compact Framework のマーシャラを利用して、 NOTIFYICONDATA 構造体の高速転送型を適切にマーシャリングします。

ただし、この時点では、structPtr で指定された最初の 24 バイトのメモリしか初期化されていません。 残りの 64 バイトを設定するには、カスタム マーシャリングが必要な場合があります。 最初に、メソッドに渡されたツールヒントの引数を文字配列にコピーします。 次に、structPtr で指定されたメモリ領域内の適切なオフセットを示す新しいポインタを、 ToInt32 メソッドで返されたポインタ自体の整数値に NOTIFYICONDATA 構造体のサイズ (この場合は 24 バイト) を追加することによって作成します。 最後に、Marshal クラスの Copy メソッドを使用して、 文字配列の内容を新しいポインタにコピーします。

Shell_NotifyIcon 関数を呼び出して、 アイコンをトレイに追加できます。 関数が False を返すと、(前回の資料で説明したように) Marshal クラスの GetLastWin32Error メソッドを呼び出し、 カスタム例外をスローできます。 呼び出しが成功すると、フックするメッセージ ハンドルが返されます。 PInvoke サービスでスローされる例外のイベントでは、(前回の資料でも説明した) カスタム HandleCeError メソッドを使用して、 適切な操作を行います。

ただし、Finally ブロックには、 Memory クラスの FreeHLocal メソッドの呼び出しが含まれているので、 構造体に割り当てられたメモリが、常に、解放されることに注意することが重要になります。

注意    この資料で説明したように参照型と文字配列のマーシャリングが必要な開発者は、代替手段として、NET Compact Framework のカスタム 属性の作成に時間を費やす場合があります。 この方法を使用すると、すべてのメモリとポインタの操作を属性コードに設定できます。

完全を期すために、 ここでは、関連する DestroyNotifyIcon メソッドについても説明します。 確かに同様の方法を使用しますが、TrayConstants 列挙値の NIM_DELETE 値を使用して、 トレイからアイコンを削除します。 また、DestroyIcon アンマネージ関数を呼び出して、 アイコン ハンドルの割り当てを解除します。


                      ' 通知アイコンをクリアにして、アイコンの割り当てを解除します。
Public Shared Sub DestroyNotifyIcon(ByVal notifyWindow As MessageWindow, _
 ByVal icon As IntPtr)

    Dim structPtr As IntPtr
    Dim nid As New NOTIFYICONDATA
    Dim size As Integer = Marshal.SizeOf(nid) + _
     (64 * Marshal.SystemDefaultCharSize)

    nid.cbSize = size
    nid.hWnd = notifyWindow.Hwnd
    nid.uID = 1

    Try
' メモリを割り当てます。
        structPtr = Memory.AllocHLocal(size)
' 構造体へのポインタを作成して、アイコンを削除します。
        Marshal.StructureToPtr(nid, structPtr, False)
        Dim ret As Boolean = Shell_NotifyIcon( _
         TrayConstants.NIM_DELETE, structPtr)
        If ret = False Then
' エラーが発生した場合。
            Dim errorNum As Integer = Marshal.GetLastWin32Error()
            Throw New WinCeException("Could not destroy icon ", errorNum)
        End If
    Catch ex As Exception
        HandleCeError(ex, "DestroyNotifyIcon", False)
    Finally
' メモリを解放します。
        Memory.FreeHLocal(structPtr)
' アイコンを破棄して、リソースを復元する必要があります。
        DestroyIcon(icon)
    End Try
End Sub

                    

注意    アイコンを削除するときに Shell_NotifyIcon 関数が True を返すように、アプリケーション識別子 (構造体の uID メンバ) は、最初に渡されたときと同じ値に設定する必要があります。

CreateNotifyIcon メソッドおよび DestroyNotifyIcon メソッドを使用するために、 以下の Form クラスと MessageWindows クラスを作成できます。


                      using Atomic.CeApi;
public class Form1 : System.Windows.Forms.Form
{
   private EventWindow eventWnd;
   private IntPtr hIcon;
   private int iconMsg;

public Form1()
   {
      this.Text = "Form1";
      this.Load += new System.EventHandler(this.Form1_Load);
      this.Closed += new System.EventHandler(this.Form1_Closed);
   }

   private void Form1_Load(object sender, System.EventArgs e)
   {
      hIcon = IntPtr.Zero;
// ここでアイコン ハンドルを取得します。

// MessageWindow を作成して、メッセージを処理します。
      eventWnd = new EventWindow();

// アイコンをトレイに配置します。
      iconMsg = Forms.CreateNotifyIcon(eventWnd,
       hIcon,"My application tooltip");

// イベント ハンドラをセットアップして、受信メッセージを処理します。
      eventWnd.msgId = iconMsg;
      eventWnd.MsgProcssedEvent += new 
EventWindow.MsgProcessedEventHandler( 
eventWnd_MsgProcessed);
   }

   private void Form1_Closed(object sender, System.EventArgs e)
   {
// アイコンを消去します。
      Forms.DestroyNotifyIcon(eventWnd,hIcon);
   }

   private void eventWnd_MsgProcessed (ref Message msg)
   {
// メッセージを処理します。
   }
}

// トレイアイコンからメッセージを受け取るクラス。
public class EventWindow: MessageWindow
{
   public delegate void MsgProcessedEventHandler(ref Message msg);
   public event MsgProcessedEventHandler MsgProcessedEvent;
   public int msgId

   protected override void WndProc(ref Message m)
   {
      base.WndProc (ref m);
      if (m.Msg == this.msgId)
      {
         MsgProcessedEvent(ref m);
      }
   }
}

                    

ご覧のとおり、Form にはイベントを処理する MessageWindow クラスを参照するプライベート フィールド、 アイコンへのポインタ、通知に使用するメッセージの識別子が含まれています。 Form1_Load メソッドは、アイコンへのハンドルを作成した後、 (MessageWindow から派生した) EventWindow クラスのインスタンスを作成して、 そのインスタンスおよびアイコン ポインタの両方を CreateNotifyIcon メソッドに渡します。 メソッドはスタティックなので、 うまくコードを作成するために Atomic.CeApi.Forms クラスにカプセル化していることに注意してください。 この時点で、アイコンはシステム トレイに追加され、通知を受け取るように準備されます。

ただし、このような通知を受け取る場合、 メソッドがメッセージ識別子を返すと、 それが EventWindow クラスの msgId フィールドに配置されます。 メッセージ識別子により、EventWindow クラスはメッセージをフィルタして、 アイコンの通知に関するメッセージのイベントのみを発生させることができます。 あるいは、メッセージ識別子は CreateNotifyIcon メソッドと EventWindow クラスの両方にハードコーディングされているので、 RegisterWindowMessage を呼び出す必要性をなくすことができます。

eventWindow クラスの MsgProcessedEvent イベントを処理するために、 イベント ハンドラとして eventWnd_MsgProcessed メソッドを Form1 に作成します。 EventWindow クラスが対応するデリゲートおよびイベントを定義していることがわかります。 オーバーライドされた WndProc メソッドは、 メッセージ識別子が通知メッセージであるかどうか確認します。 確認できると、Form1 でキャッチされる MsgProcessedEvent が発生します。 フォームでは、フォームを前面に移動したり、ショートカット メニューを表示するなど、いくつかの操作を行うことができます。

まとめ

P/Invoke サービスで使用する .NET Compact Framework のマーシャラには、 完全な .NET Framework のマーシャラのすべての機能が含まれていませんが、 このマーシャラは、かなり複雑な場合でも、さまざまな技法を使用して 、依然として効果的に使用できます。 この資料では、サンク層、ポインタ、および Unsafe コードを使用して、 独自の文字列ポインタ クラスを作成し、 カスタム マーシャリングを実行して、 構造体内の文字列ポインタおよび埋め込み文字配列の両方を渡す方法について説明しました。 .NET Compact Framework ベースの優れたアプリケーションを作成するときに、 これらの技法を使用したり、応用したりできるでしょう。


この情報は役に立ちましたか。
(残り 1500 文字)