Posts filed under 'アプリケーション開発'
今回は、ネイティブコード(アンマネージコード)から C# のメソッド、つまりマネージドコードをコールバックする方法について紹介します。
■C# のコールバック実装
ネイティブコードから C# のメソッドをコールバックさせる場合の手順を説明する前に、C# でコールバック関数を実装する方法について、簡単に説明します。C#/CLR の delegate についてご存じの方は、以下の説明は飛ばして、次の「ネイティブコードからコールバックさせる場合の手順」をご覧になって下さい。
C# でコールバック関数を実装する場合は、delegate を定義して、コールバック関数として用いるメソッドを、その delegate に割り当てます。delegate については、C# のプログラミングガイドの、次のページで説明されています。
delegate (C# Reference)
http://msdn.microsoft.com/en-us/library/900fyy8e%28v=VS.80%29.aspx
Delegates (C# Programming Guide)
http://msdn.microsoft.com/en-us/library/ms173171(v=vs.80).aspx
上のページで説明されているように、delegate は、メソッドに対する参照型(reference type)です。delegate は、delegate キーワードを使って、通常のメソッドと同じように宣言しますが、delegate と同じシグネチャを持つメソッドを、その実体として割り当てることができます。この時、宣言された delegate に対して、System.Delegate クラスの派生クラスがコンパイラによって生成され、インスタンス化されます。Delegate クラスのリファレンスは、次のページにあります。
Delegate Class
http://msdn.microsoft.com/en-us/library/system.delegate(v=vs.90).aspx
なお、delegate に割り当てることのできるメソッドは、static である必要はなく、インスタンスメソッドを割り当てることが可能です。たとえば、System.Threading の Thread クラスは、コンストラクタの引数に ThreadStart という delegate を受け取り、その delegate に割り当てられたメソッド実体を、インスタンス内部で生成するスレッドによって実行します。ここで、インスタンスメソッドを delegate に割り当てることができるので、C++ の場合とは違い、クラスのインスタンスは渡す必要がない、というわけです。
■ネイティブコードからコールバックさせる場合の手順
さて、本題です。ネイティブコードから C# のメソッドをコールバックさせる手順ですが、実は簡単です。必要な手順は、次の三つです:
- コールバックさせるメソッドに対応する delegate を定義する。
- 定義した delegate にメソッドを割り当てたのち、System.Runtime.InteropServices.GCHandle 構造体の Alloc() メソッドを適用して、delegate 実体に対する GCHandle インスタンスを生成する。
- delegate 実体に、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() を適用して、その戻り値を、関数ポインタとしてネイティブコードに渡す。
C のネイティブコード API で、コールバック関数の関数ポインタを受け取るものを使ってコールバックする場合ですと、関数ポインタの引数を IntPtr として P/Invoke の宣言を行い、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() の戻り値(IntPtr 型)を、関数ポインタの引数として渡せばよいのです。ただし、それだけですと、その関数ポインタに関連づけられた delegate の実体、つまり C# のメソッド(インスタンスメソッドの場合のインスタンスおよび、クラス)が、ガベージコレクション(GC)によって解放されてしまう可能性があります。そのため、メソッドを割り当てた delegate 実体に対して GCHandle のインスタンスを生成し、GC の対象外にする、というわけです。
GCHandle 構造体および、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() については、リファレンスの次のページをご覧ください。
GCHandle Structure
http://msdn.microsoft.com/en-us/library/khk3k17t(v=vs.90)
Marshal.GetFunctionPointerForDelegate Method
http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.getfunctionpointerfordelegate(v=vs.90)
ここで述べた、ネイティブコード(アンマネージコード)からマネージドコードを呼び出す手順については、Bojan Resnik という人の Blog でも紹介されています:
Passing C++/CLI delegate to native code
http://resnikb.wordpress.com/2009/05/18/passing-ccli-delegate-to-native-code/
この Blog エントリでは、C# ではなく、C++/CLI の場合の例を説明していますが、C# の場合も同様です。上の説明と合わせ、参考にしてみて下さい。
■おまけ: .NET CF のガベージコレクション
上の説明で、ガベージコレクション(GC)について述べましたので、ついでに、.NET CF の GC について留意点を書いておきます。
まず、.NET CF の GC は、フル版の .NET Framework の GC とは実装が異なります。フル版の .NET Framework では(より正確には、フル版の .NET Framework の CLR では)、世代別 GC かつ、コンカレントまたはバックグラウンド GC が実装されています。詳しくは、次のページをご覧ください。
Fundamentals of Garbage Collection
http://msdn.microsoft.com/en-us/library/ee787088
一方、.NET CF では、コンパクション付きのマーク&スイープ GC です。これについては、.NET CF チームの Steven Pratschner という人が MSDN Blog に書いたエントリが参考になります:
An Overview of the .Net Compact Framework Garbage Collector
http://blogs.msdn.com/b/stevenpr/archive/2004/07/26/197254.aspx
The Design of the .Net Compact Framework CLR, Part III: GC Heap Management
http://blogs.msdn.com/b/stevenpr/archive/2005/12/14/503818.aspx
.NET CF の GC は、コンカレント GC ではなく、また、世代別 GC でもないため、GC が起きた際の、GC 実行中におけるマネージドコード全スレッドの停止と、停止期間について留意する必要があります。一般論としては、マネージドコードでは、リアルタイム処理に関わる動作を行わず、リアルタイム処理は全てアンマネージコードに任せるか、または、頻繁に GC が起きないように設計する方が良いと思います。GC が頻繁に起きないようにする方策としては、マネージドコードでも、バッファやオブジェクトのプールを実装し、過度に GC に頼らないようにすることが考えられます。もちろん、GC に頼らないということにこだわり過ぎて、全てのオブジェクトのメモリ管理を自前で行おうとして複雑な設計や実装をするのは、本末転倒です。とはいえ、数百KB以上のサイズのオブジェクトやバッファ領域を頻繁に使用する場合には、それらを都度 new で生成して GC に回収させるのではなく、あらかじめ必要な個数を生成しておき(プーリング)、それらを使いまわすプール方式で実装する方が良い場合は、少なくないと思います。
GC の場合も、チューニングを行う場合には、定性的な評価・予測に頼るのではなく、実測に基づく、定量的な評価・分析が重要です。たとえば、「GC.Collect() をアプリケーションが呼ぶのは間違いか?」という意味のタイトルの次の質問に対する回答でも、「それが有効な場合は、レアケースとして考えられる。ただし、十分に実測して、そのうえで効果を判定すべし。」と書かれています:
What’s so wrong about using GC.Collect()?
http://stackoverflow.com/questions/118633/whats-so-wrong-about-using-gc-collect
2012/05/14
koga
WEC/WinCE で、.NET CF (.NET Compact Framework) のクラスライブラリでは提供されていない API を使う場合は、CLR (Common Language Run-time) の P/Invoke を利用する必要があります。P/Invoke の仕組み自体は、.NET Framework と同じですが、.NET CF における制約や、WEC/WinCE での Win32 API の細かな違いがあり、.NET Framework と全く同じにはできない場合があります。
以下では、それらの注意点のうち、二点紹介します。
■Win32 API の「実際の」名前が違う場合
P/Invoke を使って Win32 API を呼び出す場合に、.NET Framework では動作したコードが、.NET CF では動作しない、という場合があります。つまり、Windows 7 などでは動作した C# のコードを、WEC/WinCE 用にビルドして動かそうとすると、例外送出が起きて動作しない、という場合があるのです。これは、その API の実際の名前が、WEC/WinCE では違っていることが原因です。
例1)InternetConnect()
wininet.dll で提供されている InternetConnect() は、マクロであり、ネイティブコード(アンマネージコード)の ANSI ビルドでは InternetConnectA(), Unicode ビルドでは InternetConnectW() の呼び出しに置換されます。ここで、.NET Framework の場合には、ANSI ビルド用の InternetConnectA() も使うことができますが、WEC/WinCE は Unicode のみであるため、InternetConnectA() は提供されておらず、使うことが出来ません。従って、InternetConnectW という名前を指定しなければいけません。
例2)SetEvent()
Windows 7 などでは、SetEvent() は kernel32.dll で提供されている一方、WEC/WinCE では、SetEvent() は coredll.dll で提供されています。DLL の名前が違うことに加え、.NET CF で SetEvent に対して P/Invoke 呼び出しを行うと、そのような関数は実装されていないというエラーになります(例外が起きます)。実は、WEC/WinCE では、SetEvent() という名前の API 関数の実体は、存在しないのです。何が違うのかは、両者のリファレンスから辿って、関数宣言のヘッダファイルを調べてみると分ります:
・デスクトップ版の SetEvent()
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686211(v=vs.85).aspx
・WEC/WinCE 版の SetEvent()
http://msdn.microsoft.com/en-us/library/ee488785.aspx
上の二つのページで、”Requirements” の項にある表の、”Header” を見て下さい。デスクトップ版では、WinBase.h ですが、WEC/WinCE 版では kfuncs.h となっています。WEC/WinCE のソースツリーにある winbase.h を見てみると(%_WINCEROOT%/public/COMMON/sdk/inc/ にあります)、SetEvent() の宣言は、#ifndef UNDER_CE … #endif で囲われており、WEC/WinCE では無効になっています。そして、それらの関数は、kfuncs.h で宣言/定義されているというコメントがあるのです。そこで、kfuncs.h を見てみると、SetEvent() は、_inline 修飾子を使ってインライン関数として定義され、EventModify() という関数を呼び出していることが分かります。EventModify() は、二つの引数を取り、SetEvent() の場合は EVENT_SET という定数(値は3)を第二引数に渡します。つまり、WEC/WinCE では、SetEvent() という名前の関数の実体は、存在しないのです。SetEvent() の呼び出しは、EventModify() の呼び出しに展開されますので、P/Invoke する場合は、EventModify という名前を指定しなければいけません。
このように、同じ名前の Win32 API であっても、WEC/WinCE では実体が異なる場合があるため、.NET Framework では動いたコードが、.NET CF では動かない、という場合があるのです。Win32 API を P/Invoke する場合には、リファレンスを見て、WEC/WinCE では実体が異なっていないかどうか確認してみて下さい。リファレンスだけでは分かりづらいですから、見落とす場合もあると思います。従って、Win32 API を P/Invoke する場合は、必ず単体テストして、期待通りに動作するかどうかを確認し、エラーが起きる場合には、ヘッダファイルをチェックしてみるのが良いでしょう。
■P/Invoke のマーシャリングに関する制約
P/Invoke における、関数引数のマーシャリングは、.NET Framework と .NET CF で一部異なっています。次のページに、その違いがまとめられています:
An Introduction to P/Invoke and Marshaling on the Microsoft .NET Compact Framework
http://msdn.microsoft.com/en-us/library/aa446536.aspx
ここで述べられている違いの中で、Win32 API を P/Invoke する際に注意が必要なのは、.NET CF の場合、構造体メンバ中に参照型(reference type)があっても、マーシャリングされない、という点です。上のページの、”Passing Structures” という項に書かれています。これは、文字列型のメンバを持つ構造体を引数に受け取る API 関数を P/Invoke する際に、問題となります。
たとえば、Windows ファイルサーバ(CIFS サーバ)との接続などを行う、WNetAddConnection3() は、NETRESOURCE 構造体を引数に受け取りますが、この NETRESOURCE 構造体は、文字列型のメンバを持ちます。なお、WNetAddConnection3() の実体は、WNetAddConnection3W() です。
ここで、関数の引数が文字列を受け取る場合は、.NET CF の場合、string オブジェクトをそのまま渡せます。前述した InternetConnectW() の引数や、WNetAddConnection3W() の引数 lpPassword と lpUserName が、その例です。しかし、構造体のメンバが文字列の場合には、マーシャリングが行われないので、特別な対応が必要です。この詳細については、次のページで説明されています。
Advanced P/Invoke on the Microsoft .NET Compact Framework
http://msdn.microsoft.com/en-us/library/aa446529.aspx
上のページでは、いくつかの方策が示されていますが、真ん中ほどにある “Using a Managed String Pointer” という項で説明されている方策が、お手頃ではないかと思います。この方策は、IntPtr 型のメンバを持つ CLR の struct(構造体)を定義して、マネージドコードの文字列(string)とアンマネージコードの文字列の間の変換(マーシャリング)を自前で行う、というものです。上のページの例では、文字列マーシャリング用の StringPtr という CLR の構造体と、StringPtr が使う Memory というクラスを VB .NET で定義しています。同様のことを、C# でもう少し単純に定義すると、次のようにも書けます:
using System;
using System.Runtime.InteropServices;
public struct NativeString {
private IntPtr szString;
// constructor
public NativeString(string strObj) {
int len = (strObj.Length + 1) * 2;
szString = Marshal.AllocHGlobal(len);
Marshal.Copy(strObj.ToCharArray(), 0, szString, strObj.Length);
Marshal.WriteByte(szString, len - 2, 0);
Marshal.WriteByte(szString, len - 1, 0);
}
// release the resource
public void Dispose() {
if (IntPtr.Zero != szString)
{
Marshal.FreeHGlobal(szString);
szString = IntPtr.Zero;
}
}
}
この NativeString を使って、NETRESOURCE を次のように定義できます。
public struct NETRESOURCE {
public int dwScope;
public int dwType;
public int dwDisplayType;
public int dwUsage;
public NativeString lpLocalName;
public NativeString lpRemoteName;
public IntPtr lpComment;
public IntPtr lpProvider;
// constructor
public NETRESOURCE(string localName, string remoteName) {
dwScope = 0;
dwType = 1; // RESOURCETYPE_DISK
dwDisplayType = 0;
dwUsage = 0;
lpLocalName = new NativeString(localName);
lpRemoteName = new NativeString(remoteName);
lpComment = IntPtr.Zero;
lpProvider = IntPtr.Zero;
}
// release the resource
public void Cleanup() {
lpLocalName.Dispose();
lpRemoteName.Dispose();
}
}
なお、NativeString は、クラスではなく、構造体(struct)でなければならないことに注意して下さい。NativeString は、Win32 API、つまりアンマネージコードと同じメモリレイアウトを持った NETRESOURCE 構造体のメンバとして使いますので、参照型であるクラスではなく、値型(value type)である構造体として定義する必要がある、というのがその理由です。
以上、.NET CF から Win32 API を P/Invoke する場合に注意すべき点を二つ紹介しました。.NET CF からのアンマネージコードの呼び出しについては、以下の Technical Articles セクションに、他にも記事がありますので、興味のある方は、ご覧になってみて下さい。
Native Interoperability
http://msdn.microsoft.com/en-us/library/aa145821.aspx
上の記事の中には、.NET CF 2.0 を扱ったものもあり(※現時点での .NET CF の最新版は、3.5 です)、少し古く感じられるものもありますが、今でも通用する内容がほとんどだと思います。
2012/05/02
koga
前回の続きです。アプリケーションとカーネルランドを一緒にデバッグするやり方に関連して、いくつか補足します。
■カーネルデバッガと一緒に動かす場合の設定(※ネットワークインタフェースが一つだけの場合)
さて、カーネルデバッガとアプリケーションデバッガを一緒に動かす場合、一つ注意しなければいけません。それは、WinCE/WEC に固定 IP アドレスを設定する場合のレジストリキーです。前回の「ターゲットボードとホスト PC を直結する場合」の説明で、IP アドレスを設定する場合のレジストリキーが
[HKEY_LOCAL_MACHINE\Comm\<アダプタ名>\Parms\Tcpip]
だと書きました。カーネルデバッガを動かす場合、この「アダプタ名」が、動かさない時と違う場合があるのです。それは、ターゲットボードにネットワークインタフェースが一つしかなく、アプリケーションデバッガの Ethernet 接続と、カーネルデバッガの Ethernet 接続が、一つのネットワークインタフェースを共有する(つまり、Ethernet ケーブルを一本しか使わない)場合です。
その場合、カーネルデバッガを動かす時は、アダプタ名として “VMINI1″ を指定しなければいけません。VMINI1 というのは、前回の例で示した “FEC1″ のような、Ethernet コントローラを直接制御するデバイスドライバではなく、仮想のネットワークデバイス(論理デバイス)のアダプタ名です。この仮想ネットワークデバイス(VMini)は、リファレンスの次のページで説明されています:
Ethernet Debugging Services (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-us/library/ee478406(v=winembedded.60).aspx
WEC 7 のリファレンスには、VMini について説明した個所が見当たりません。もしかすると、WEC 7 からは、VMini の使用を推奨しておらず、そのため、説明を省略しているのかも知れません。なお、KITL についてご存じの方は、ここと、次の説明(「KITL について、もう少しだけ」)は飛ばして下さい。
以下は、KITL について詳しくない方のための説明です。
上のページの図を見ると、”VBridge” というモジュールが、”Vmini.dll” と “EDBG” の二つのモジュールに繋がっており、”Vmini.dll” は、NDIS インタフェースを介して TCP/IP スタックと繋がっています。この “VBridge” が、カーネルデバッガとアプリケーションデバッガが一つのネットワークインタフェースを共有するためのモジュールで、”Vmini.dll” が VMini のことです。VMini は、ネットワークインタフェース、つまり Ethernet コントローラを直接制御せず、VBridge に委譲して間接的に制御します。VBridge は、VMini に加えて、”EDBG”、つまり、カーネルデバッガが使用する Ethrnet 通信機能のモジュールに対してもネットワークインタフェース機能を提供します。
この VBridge の仕組みから分かるように、カーネルデバッガは、Ethernet 経由でホスト PC と通信しますが、TCP/IP スタックは使用せず、独自のプロトコルで通信します。TCP/IP スタックは、カーネルの上位にありますから、カーネルデバッガが TCP/IP スタックを利用することは、できないのです。カーネルデバッガがホスト PC と通信するプロトコルは、KITL (Kernel Independent Transport Layer) と名付けられており、上のページの一つ上のドキュメント階層で説明されています。
Kernel Independent Transport Layer (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-us/library/ee479323(v=winembedded.60).aspx
Kernel Independent Transport Layer (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee479323.aspx
KITL の説明は、WEC 7 のリファレンスにもあり、どちらも同じ図が載っています。この図の “KITL” の、”Device hardware” に近い側の部分(下側の部分)に先ほどの図の “VBridge” があり、そして、”Kernel debugger” に近い側の部分(上側の部分)に “EDBG” があると考えて下さい。
本題に戻ります。ネットワークインタフェースが一つしかなく、それを KITL と TCP/IP スタックで共有する場合、通常の Ethernet コントローラのドライバでは対応できません。そのため、通常の Ethernet コントローラのドライバ(先の例の “FEC1″)は使われず、VMini が代わりに OS イメージに組み込まれ、VBridge の働きにより、TCP/IP スタックと KITL がネットワークインタフェースを共有する、というわけです。そのため、固定 IP アドレスを設定する場合、レジストリキーで指定する TCP/IP スタックのアダプタ名を、VMini のアダプタ名である “VMINI1″ にしなければいけないのです。
■KITL について、もう少しだけ
ところで、KITL は Ethernet 接続でしか使えないかといえば、そうではありません。Kernel *Independent* Transport Layer という名前の通り、Ethernet 以外の様々な伝送路に対応できます。そして、Ethernet の KITL を使用して、TCP/IP スタックと Ethernet コントローラを共有する場合は、TCP/IP スタックが Ethernet コントローラを占有する場合に比べて、Ethernet による通信のパフォーマンスが低下します。これは、同時に二種類の通信が同じ伝送路を使うせいでもでありますし、また、VMini と VBrige、および、VBridge が呼び出す Ethernet コントローラの制御ルーチン(※この制御ルーチンは、OAL の一部として実装されます)の組み合わせによる動作は、通常の Ethernet コントローラのドライバの単体動作に比べてオーバーヘッドが大きいため、パフォーマンスを出しづらい、という事情もあると思われます。
そのため、WinCE/WEC のネットワークのパフォーマンス評価を行う場合は、VMini を無効にした状態で行うようにという注意書きがリファレンスに書かれています。上の KITL の説明ページにも、KITL と OS(TCP/IP スタック)がハードウェアを共有するとパフォーマンスに悪影響がある、という但し書きがあります。
年々、組み込み機器用のプロセッサは高機能化が進み、多くの周辺機器のコントローラを内蔵するようになってきています。そのため、カーネルデバッガ/KITL と TCP/IP スタックが一つのネットワークインタフェースを共有しなくても、たとえば KITL にはシリアルや USB での接続を割り当て、Ethernet コントローラは TCP/IP スタックに占有させる、といった構成は可能です。逆に、プロセッサに内蔵された Ethernet コントローラはデバッグ専用にして KITL に割り当て、TCP/IP スタック用には、USB や SD の WiFi モジュールを使う、というような構成や、あるいは、安価な Ethernet コントローラを外付けして、そちらをデバッグ専用に使う、という方策も考えられます。
いずれにせよ、VMini による、TCP/IP スタックと KITL の共存は、デバッグ専用のものであり、通常構成で使うものではありません。もちろん、Ethernet コントローラのドライバをデバッグする場合には、VMini は利用できません(※Ethernet コントローラのドライバをデバッグしようとすると、同じコントローラを使う KITL の動作に影響を与えるから・・・ではなく、そもそも、デバッグしたいドライバの代わりに VMini が動作するので、デバッグする方法がありません)。
■ソースコードも、見てみましょう
上で述べた VBridge は、KITL (kitl.dll) に組み込まれるモジュールとなっており、そのソースコードは、WEC 7 と WinCE 6.0 のソースツリーの、それぞれ次の場所にあります:
・WEC 7
%_WINCEROOT%/platform/common/src/common/kitl/
・WinCE 6.0
%_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/ETHDBG/VBRIDGE/
WinCE 6.0 では、VBridge は vbridge.lib という static library でしたが(※ソースファイルが vbridge.c 一つだけのライブラリ)、WEC 7 では、oal_kitl.lib という static library にまとめられています。oal_kitl.lib は、WinCE 6.0 にもありますが、VBridge は別のライブラリになっていました。WEC 7 になって、VBridge(vbridge.c) が oal_kitl.lib にまとめられた、というわけです。
ちなみに、カーネルデバッガの OS 側モジュール(kd.dll)のソースコードは、WEC 7 も WinCE 6.0 も、次の場所にあります:
%_WINCEROOT%/private/winceos/COREOS/nk/kdstub/
興味のある方は、どのような実装になっているのか、ソースコードをご覧になってみると面白いんじゃないかと思います。
■おまけ:ネイティブコードのアプリケーションの場合
さて、.NET CF アプリケーションではなく、ネイティブコード(アンマネージドコード)のアプリケーションをデバッグする場合は、どうなのでしょうか?
Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションをデバッグする手順は、.NET CF アプリケーションの場合と同様です。前回と今回で説明した手順を使えば、ネイティブコードのアプリケーションも、カーネルランドと一緒にデバッグできます。試してみて下さい。
ここで、注意深い方であれば、「あれ?」と思われたかも知れませんね。前回の説明では、冒頭で次のように書いたからです:
皆さんご存じの通り、ネイティブコードのアプリケーションの場合には、アプリケーションもカーネルランドも、どちらもカーネルデバッガでデバッグできます。
これは、何だったのでしょうか?前回の冒頭で書いたのは、OS Design のサブプロジェクトとしてアプリケーションを作成した場合のことを念頭に置いていました。つまり、OS イメージをビルドする際に、OS と一緒にビルドして、デフォルト設定では OS イメージに組み込まれるようにした場合のことを指していたのです。その場合は、カーネルデバッガを使って、カーネルランドと同時にアプリケーションをデバッグできます。
では、OS Design のサブプロジェクトで作成したアプリケーションではなく、Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションは、カーネルデバッガを使ってデバッグできないのでしょうか?実は、できます。
Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションを、カーネルデバッガでデバッグするには、アプリケーションの実行ファイル(.exe)とプログラムデータベースファイル(.pdb)を、カーネルデバッガが認識できる場所に置けばよいのです。OS Design のカタログ項目で、”Target Control Support(Shell.exe)” か “Release Directory File System” を選択して OS イメージをビルドしておき、Flat Release Directory に、デバッグしたいアプリケーションの .exe と .pdb をコピーする、というのが一番お手軽だと思います。こうしておけば、わざわざアプリケーションデバッガを別に動かさなくとも、カーネルデバッガだけで、アプリケーションとカーネルランドを一緒にデバッグできます。
たとえば、OS を開発するチームとアプリケーションを開発するチームが分かれていて、アプリケーションで不具合が起きた場合に、不具合の要因がアプリケーション側にあるのか OS 側にあるのか切り分け調査を行うなどの場合、この方法は便利かも知れません。アプリケーション開発チームは、OS Design のサブプロジェクトではなく、Visual Studio の「スマートデバイス」プロジェクトでアプリケーションを作成するのが一般的だと思います。そのような場合に、カーネルランドとアプリケーションを同時または一緒にデバッグする方策は、次のものが考えられます:
- カーネルデバッガとアプリケーションデバッガを各々動かして、カーネルランドとアプリケーションを一緒にデバッグする。
- 「スマートデバイス」プロジェクトを OS Design のサブプロジェクトに作り直し、アプリケーションを OS Design に組み込んだうえで、カーネルデバッガを使ってカーネルランドとアプリケーションを一緒にデバッグする。
- 「スマートデバイス」プロジェクトは、そのままにして、アプリケーションの .exe と .pdb を Flat Release Directory にコピーしてからカーネルデバッガを動かす(上述した方法)。
上の1は、前回と今回の主題である、.NET CF アプリケーションをカーネルランドと一緒にデバッグするのと同じ方策です。2と比べると、3の方が手間が少ないのは明らかです。1と3を比べても、カーネルデバッガだけで作業できるという点で、3の方が、より手間が少ない方法です。
■おまけ2:その他の参考資料
前回と今回では、カーネルランドと一緒(または同時に)アプリケーションをデバッグするにはどうすればよいか、ということを説明しました。「スマートデバイス」プロジェクトで作ったアプリケーションだけをデバッグする場合については、Visual Studio のリファレンスに説明があります。いくつかのトピックが取り上げられていますので、ご覧になってみて下さい:
Debugging Device Projects
http://msdn.microsoft.com/en-us/library/ms180772(v=vs.90).aspx
ネイティブコードのアプリケーションに関しては、MFC や ATL を使ったアプリケーションをビルド・デバッグする場合の注意点や手順についても、解説されています。
Building and Debugging Visual C++ Device Projects
http://msdn.microsoft.com/en-us/library/c5fc53wa(v=vs.90).aspx
2012/04/13
koga
今回は、マネージドコードのアプリケーション、つまり、.NET CF のアプリケーションとカーネルランドを、Ethernet 経由で一緒にデバッグする方法を説明します。「一緒にデバッグする」というのは、”「同時にデバッグする」ことはできないが、アプリケーションデバッガとカーネルデバッガを各々動かすことにより、アプリケーションとカーネルランドを、それぞれのデバッガでデバッグする”という意味です。
皆さんご存じの通り、ネイティブコードのアプリケーションの場合には、アプリケーションもカーネルランドも、どちらもカーネルデバッガでデバッグできます。たとえば、アプリケーションからデバイスドライバを呼び出した場合、デバイスドライバのソースコードにブレークポイントを設定してブレークした時、アプリケーションからデバイスドライバまでのコールスタック(「呼び出し履歴」)を見ることができます。アプリケーションがデバイスドライバを呼び出す場合、システムコールを経由しますが、カーネルデバッガは、その経路を把握して、あたかも通常の関数呼び出しのように、コールスタックを表示してくれるのです。僕は、このように、一つのデバッガだけでアプリケーションとカーネルランドをデバッグすることを指して「同時にデバッグする」と表現しています。
一方、マネージドコード(.NET CF)のアプリケーションの場合には、一つのデバッガだけでアプリケーションとカーネルの両方をデバッグすることは、できません。アプリケーションのデバッガとカーネルデバッガを各々動かす必要があるのです。このことを指して、ここでは「一緒にデバッグする」と表現します。
■Ethernet 経由でのアプリケーションデバッガ接続
Ethernet 経由で WinCE/WEC のアプリケーションデバッガを接続する場合は、ActiveSync は使用しません。ActiveSync は、2005年にリリースされた 4.0 から TCP/IP 接続をサポートしていませんから、使えないのです。ActiveSync を使わずに、TCP/IP で WinCE/WEC に接続する手順は、Visual Studio のリファレンスのページで説明されています:
How to: Connect to Windows CE Device Without ActiveSync
http://msdn.microsoft.com/en-US/library/ms228708(v=vs.90).aspx
上のページは、Visual Studio 2008 のリファレンスです。日本語の説明を読みたい場合は、Visual Studio 2005 ですが、ユニダックス社の「CE6.0 技術情報」に日本語の説明があります:
11. アプリケーションのリモートデバッグ
http://www.unidux.co.jp/embedded/techinfo/ce6/000437.php
上のページで説明されているように、Visual Studio に付属している次の5つのファイルを、WinCE/WEC の /Windows ディレクトリへコピーして、Visual Studio のデバイス設定を行うことにより、TCP/IP でアプリケーションデバッガを接続できます。
Clientshutdown.exe
ConmanClient2.exe
CMaccept.exe
eDbgTL.dll
TcpConnectionA.dll
WEC 7 で、AMR9 コアの CPU のボードに接続する場合ですと、上記のファイルは、次の場所にあります。
C:\Program Files\Common Files\microsoft shared\CoreCon\1.0\Target\wce400\armv5\
これらのファイルを、WinCE/WEC が起動した後に、/Windows ディレクトリに USB メモリなどを使ってコピーするか、または、OS Design の project.bib ファイルを編集して OS イメージに組み込めば、使えます。
または、OS Design のカタログ項目で、”Target Control Support(Shell.exe)” か “Release Directory File System” を選択している場合は、Flat Release Directory(環境変数 _FLATRELEASEDIR が指すディレクトリ)に上記5つのファイルを置いても、使えます。Target Control Support を有効にしてカーネルデバッガを使っている場合には、これが一番お手軽でしょう。
■OS Design から作った SDK との関連づけ
マネージドコードのアプリケーションも、ネイティブコードのアプリケーションも、WinCE/WEC のアプリケーションは、Visual Studio の「スマートデバイス」プロジェクトで作成します。それらのアプリケーションをデバッグする場合は、上で紹介したページで説明されている手順で、WinCE/WEC と接続する必要があります。
Visual Studio 2008 日本語版と WEC 7 の場合ですと、以下のような手順になります。
- 上記の5つのファイルを、WEC 7 の /Windows ディレクトリへコピーする。
- WEC 7 を DHCP 有効で動かしている場合は、コマンドプロンプトで ipconfig を実行して、WEC 7 の IP アドレスを調べる。
- Visual Studio 2008 の [ツール] > [オプション...] メニューを選択し、「オプション」ダイアログを開く。
- 「オプション」ダイアログの左端にあるツリービューで、[デバイスツール」を展開し、その下にある「デバイス」を選択する。
- 「オプション」ダイアログの右端に表示された「デバイス」画面にある、「デバイス(V):」というリストボックスから、アプリケーションのビルドに使った SDK のデバイスを選択する。
- 「デバイス(V):」リストボックスの右側にある「プロパティ」ボタンをクリックして、選択したデバイスのプロパティダイアログを開く。
- プロパティダイアログの「トランスポート(R):」というドロップダウンリストから、「TCP 接続トランスポート」を選択する。
- 「トランスポート(R):」ドロップダウンリストの右側にある「構成(C)...」ボタンをクリックして、「TCP/IP トランスポートの構成」ダイアログを開く。
- 「TCP/IP トランスポートの構成」ダイアログにある二つのラジオボタンのうち、「特定の IP アドレスを使用(S):」を選択する。
- 「特定の IP アドレスを使用(S):」の下にあるエディットフィールド兼ドロップダウンリストに、WEC 7 の IP アドレスを入力して設定する。
- 「TCP/IP トランスポートの構成」ダイアログ、プロパティダイアログ、および「オプション」ダイアログの OK ボタンを順にクリックして、設定内容を確定する。
- WEC 7 で、ConmanClient2.exe と CMaccept.exe を順に実行する。
- Visual Studio 2008 の [ツール] > [デバイスへの接続...] メニューを選択する。
■ターゲットボードとホスト PC を直結する場合
さて、上の手順では、WinCE/WEC を DHCP 有効で動かしている場合(デフォルトでは、DHCP 有効です)、WinCE/WEC を起動するたびに IP アドレスを調べて Visual Studio の「TCP/IP トランスポート構成」の設定に反映しなければいけません。DHCP 有効ではなく、固定 IP アドレスで動かせば、WinCE/WEC を起動するたびに設定する必要がなくなり、少しだけ手間を減らせます。
あるいは、WinCE/WEC とホスト PC をハブ経由で接続せず、クロスの Ethernet ケーブル(*)で直接接続する場合は、DHCP サーバにアクセスできませんから、WinCE/WEC とホスト PC 共に、固定 IP アドレスを割り当てる必要があります。
※最近のネットワークインタフェースは、ストレートの Ethernet ケーブルで直接接続しても、自動認識してくれるものが多くなっています。従って、WinCE/WEC とホスト PC を Ethernet ケーブルで直接接続する際、クロスケーブルがなければ、ストレートケーブルで接続しても大丈夫な場合があります。手元にクロスケーブルがない場合は、試してみて下さい。
なお、Ethernet 経由でリモートデバッグする場合、WinCE/WEC とホスト PC をハブ経由で接続するよりも、直接接続する方が、一般的には通信速度が上がりますので、より快適にデバッグできます。
Visual Studio を動かすホスト PC、つまり、Windows 7 や Vista/WindowsXP に固定 IP アドレスを設定する手順は、皆さんご存じだと思いますので、ここでは、WinCE/WEC に固定 IP アドレスを設定する方法を述べます。
WEC 7 と WinCE 6.0 の IP アドレス設定のレジストリは、リファレンスの以下のページで説明されています:
TCP/IPv4 Configurable Registry Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee494881.aspx
TCP/IPv4 Configurable Registry Settings (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee494881(v=winembedded.60).aspx
上のページの中ほどにある、”Adapter-specific Values” という表を見て下さい。ここに、”EnableDHCP”, “DefaultGateway”, “IpAddress”, “Subnetmask” が載っています。これらのレジストリ項目を使って、WinCE/WEC に固定 IP アドレスを設定できます。レジストリキーは、これらのページに書かれている通り、
[HKEY_LOCAL_MACHINE\Comm\<アダプタ名>\Parms\Tcpip]
です。たとえば、弊社が提供している Armadillo-400 シリーズ用の BSP であれば、以下の行を .reg ファイルに書いて OS イメージをビルドすると、クラス C の固定 IP アドレス 192.168.0.50 が設定されます:
[HKEY_LOCAL_MACHINE\Comm\FEC1\Parms\Tcpip]
"EnableDHCP"=dword:0
"DefaultGateway"="192.168.0.1"
"IpAddress"="192.168.0.50"
"Subnetmask"="255.255.255.0"
ここまでの設定は、Visual Studio のスマートデバイスプロジェクトのアプリケーションを Ethernet 経由でデバッグするための手順です。次に、今回の本題である、マネージドコード(.NET CF)のアプリケーションをカーネルランドと一緒にデバッグする方法を説明します。
■カーネルデバッガも一緒に動かす方法
今回の冒頭で、「.NET CF のアプリケーションとカーネルランドを一緒にデバッグするとは、アプリケーションデバッガとカーネルデバッガを各々動かすことにより、それぞれをデバッグすること」だと述べました。アプリケーションデバッガとカーネルデバッガを各々動かすというのは、どういうことかと言えば、Visual Studio を二つ起動して、一方でアプリケーションデバッガ、もう一方でカーネルデバッガを動かす、ということです。
以下に、もう少し具体的な手順を述べます。以下のようにすれば、アプリケーションデバッガとカーネルデバッガを各々動かして、.NET CF のアプリケーションとカーネルランドを一緒にデバッグできます。
- Visual Studio を起動して、OS Design のプロジェクトを開く。
- OS Design のプロジェクトをビルドしてできた OS イメージを、ホスト PC と Ethernet 接続したターゲットボードに転送し、カーネルデバッガを有効にして WinCE/WEC を起動する。
- Visual Studio をもう一つ起動して、デバッグしたいアプリケーションのプロジェクトを開く。
- 上で述べた手順を使って、アプリケーションのプロジェクトを開いた Visual Studio から WinCE/WEC に接続する。
- アプリケーションのデバッグを開始する。
このようにすれば、二つ起動した Visual Studio のうち、OS Design のプロジェクトを開いた方でカーネルデバッガが動き、アプリケーションのプロジェクトを開いた方でアプリケーションデバッガが動きます。なお、アプリケーションデバッガで設定したブレークポイントでアプリケーションが停止した場合、OS(WinCE/WEC)は動作し続けていますが、カーネルデバッガで設定したブレークポイントで停止した場合は、OS 全体が停止しますので、アプリケーションの動作も停止します。これは、アプリケーションデバッガとカーネルデバッガで違う点です。
以上で、アプリケーションとカーネルランドを一緒にデバッグする方法の説明は、一応終わりです。ただし、いくつか補足が必要なので、次回に続けます。もし、上の手順を試してみたが、アプリケーションデバッガとカーネルデバッガを一緒に動かせない、という方は、次回の説明もあわせて読んでみて下さい。
2012/04/03
koga
C# や Visual Basic で WinCE/WEC のアプリケーションを開発する際に、デバッグメッセージを出力する方法は、いくつかあります:
- System.Console クラスの Write(), WriteLine() を呼び出す
- System.Diagnostics.Debug クラスの Write()/WriteIf(), WriteLine()/WriteLineIf() を呼び出す
- P/Invoke を使って NKDbgPrintfW() を呼び出す
上に挙げた三つの方法について、それらがどう違うのか、以下に述べます。なお、ここでは、.NET CF (.NET Compact Framework) の 3.5 について記します。
■System.Console クラスを使う場合
このクラスのメソッド一覧は、次のリファレンスページに載っています:
Console Methods
http://msdn.microsoft.com/en-us/library/system.console_methods(v=vs.90).aspx
System.Console クラスの Write() や WriteLine() を呼び出すと、コンソールウィンドウが開いて、引数に渡した文字列が表示されます。これは、ネイティブコードの場合に、printf() などを使って標準出力へ出力した場合と同じ振る舞いです。クラス名の通り、コンソールデバイスに出力する API というわけです。
■System.Diagnostics.Debug クラスを使う場合
このクラスのメソッド一覧は、次のリファレンスページに載っています:
Debug Methods
http://msdn.microsoft.com/en-us/library/system.diagnostics.debug_methods(v=vs.90).aspx
System.Diagnostics.Debug クラスの Write() や WriteLine() を呼び出すと、アプリケーションをデバッガから実行している場合には、デバッガの出力ウィンドウに文字列が出力されます。デバッガから実行していない場合(通常実行の場合)には、何も起きません。この振る舞いは、OutputDebugString() を呼び出した場合と同様です。
このクラスの Write() や WriteLine() の呼び出しが、OutputDebugString() の呼び出しと同様になるのは、System.Diagnostics.DefaultTraceListener クラスの働きによるものです。DefaultTraceListener クラスの説明は、次のリファレンスページに載っています。
DefaultTraceListener Class
http://msdn.microsoft.com/en-us/library/system.diagnostics.defaulttracelistener(v=vs.90).aspx
System.Diagnostics.DefaultTraceListener のインスタンスは、System.Diagnostics.Debug クラスの Listeners プロパティのデフォルト要素であり、そのため、Write() や WriteLine() によるデバッグメッセージ出力は、通常は System.Diagnostics.DefaultTraceListener の Write(), WriteLine() に渡されます。そして、System.Diagnostics.DefaultTraceListener は、Write() や WriteLine() に渡された文字列を引数にして OutputDebugString() を呼び出す、というわけです。
ただし、ネイティブコードで OutputDebugString() を直接呼び出した場合とは異なり、アプリケーションをデバッガから実行していないと、System.Diagnostics.Debug の Write() や WriteLine() を呼び出しても、一切出力されません。一方、ネイティブコードから OutputDebugString() を呼び出した場合は、アプリケーションデバッガから実行していなくても、WinCE/WEC のカーネルデバッガを実行していれば、カーネルデバッガの出力ウィンドウに文字列が出力されます。.NET CF のアプリケーションから P/Invoke で直接 OutputDebugString() を呼び出した場合にどうなるかは、確認していませんが、おそらく、ネイティブコードから OutputDebugString() を呼び出した場合と同じになるのではないかと思います。興味のある方は、試してみて下さい。
System.Diagnostics.Debug クラスの使い方については、次のサポートページも参考になるでしょう:
How to trace and debug in Visual C#
http://support.microsoft.com/kb/815788/en-us
■NKDbgPrintfW() を P/Invoke で呼び出す場合
さて、ネイティブコードの場合、デバッグメッセージの出力は、DEBUGMSG() マクロを使うのが普通です。C# や Visual Basic では、このマクロは使えませんので、その代わりに、DEBUGMSG() マクロが使っている NKDbgPrintfW() を、P/Invoke で直接呼び出す必要があります。
たとえば、次のページにあるフォーラムの質問でも、そのような回答がなされています:
DebugMsg macro in C#
http://us.generation-nt.com/answer/debugmsg-macro-c-help-51715972.html
.NET CF アプリケーションから、P/Invoke で NKDbgPrintfW() を直接呼び出した場合は、アプリケーションをデバッガから実行していない場合でも、WinCE/WEC のカーネルデバッガを実行していれば、カーネルデバッガの出力ウィンドウに文字列が出力されます。.NET CF アプリケーションを、ネイティブコードのアプリケーションやライブラリと一緒にデバッグしていて、追跡したい個所の呼び出しタイミングの関係を知りたい場合には、NKDbgPrintfW() を使うと、デバッグメッセージをカーネルデバッガの出力に渡すことができます。つまり、.NET CF アプリケーションのデバッグメッセージも、ネイティブコードのデバッグメッセージも(さらには、デバイスドライバのデバッグメッセージも)同じ出力に出せますので、それらを同時に追跡したい場合には、便利でしょう。
なお、P/Invoke で NKDbgPrintfW() を直接呼び出すと、ネイティブコードで DEBUGMSG() マクロを使った場合とは異なり、リリースビルドでもデバッグメッセージが出力されてしまいます。その点は、注意して下さい。デバッグビルドの時にだけデバッグメッセージが出力されるようにするには、System.Diagnostics.ConditionalAttribute を使ってデバッグビルドに対してのみ有効にするか、または、#if … #endif を使って条件コンパイル指定して下さい。ちなみに、System.Diagnostics.Debug クラスの Write() や WriteLine() では、System.Diagnostics.ConditionalAttribute を使っています。
2012/03/29
koga
Visual Studio 2005 以降の Visual C++ は、OpenMP 2.0 をサポートしています。OpenMP は、共有メモリ型の並列処理を記述するための仕組みで、C/C++ と Fortran に対する仕様が、OpenMP Architecture Review Board (OpenMP ARB) によって策定・公開されています。OpenMP の詳細は、OpenMP ARB の Web サイト、および、Visual Studio のリファレンスをご覧ください:
Open MP
http://openmp.org/
OpenMP C and C++ Application Program Interface
http://msdn.microsoft.com/en-us/library/8y6825×5(v=vs.90).aspx
なお、OpenMP 仕様の最新版は、2011年の 3.1 ですが、Visual C++ がサポートしているのは 2.0 です。
さて、この OpenMP 2.0 は、Windows Embedded Compact 7 (WEC 7) で対応が追加されました。OpenMP のカタログ項目は、Platform Builder の Catalog Items View に表示されるツリーの、以下の位置にあります:
Core OS
Windows Embedded Compact
Application and Services Development
C Libraries and Runtimes
★ OpenMP Runtime
対応する Sysgen 変数は、SYSGEN_OPENMP です。これについては、リファレンスの次のページに書かれています:
C Libraries and Runtimes Catalog Items and Sysgen Variables (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/gg155191.aspx
ただし、デスクトップ版の Windows(Windows7 など)とは異なり、WEC 7 の OpenMP 対応では、C/C++ コンパイラの pragma に制限があり、以下のものは使えません。
omp dynamic
omp threadprivate
これについては、リファレンスの次のページをご覧ください:
C/C++ Libraries for Windows Embedded Compact (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee479345.aspx
さらに、WEC 7 の場合は、SMP 対応のボード、つまり、カーネルがマルチコアに対応していなければ、OpenMP を使用できないということが、先ほど述べたカタログ項目のリファレンスのページに書かれています。
マルチコアのプロセッサを搭載した WEC 7 対応のボードをお持ちの方は、試してみると面白いかも知れませんね。Visula C++ で OpenMP を使う場合は、以下のリファレンスページが参考になるでしょう:
OpenMP in Visual C++
http://msdn.microsoft.com/en-us/library/tt15eb9t(v=vs.90).aspx
2012/02/29
koga
アプリケーションをフルスクリーン表示したい場合、つまり、タスクバーを表示しないで、アプリケーションのウィンドウを全画面に表示したい場合、WinCE 6.0 では、いくつかの方法があります。標準シェル(explorer.exe)を使わずに、カスタムのシェルを組み込んだ OS イメージを作る、というのは、その方法の一つです。しかし、今回は、OS をカスタマイズせず、レジストリ設定や、アプリケーションからの API 呼び出しだけで対応する方法を述べます。
以下では、OS をカスタマイズせずにタスクバーを非表示にする方法を、三通り紹介します。一つは、レジストリ設定を変更することにより、全てのアプリケーションをフルスクリーン表示可能にする方法で、残り二つは、アプリケーションが API 呼び出しを行ってフルスクリーン表示する方法です。では、順に見ていきましょう。
■レジストリ設定による方法
まず最初は、レジストリ設定による方法です。この方法では、タスクバーが最前面(TOPMOST)に表示されないように設定することで、スクリーンと同サイズのウィンドウを表示した際、ウィンドウの下端がタスクバーで隠されてしまうのを防ぎます。具体的には、
[HKEY_LOCAL_MACHINE\Software\Microsoft\Shell\OnTop]
キーのデフォルト値を dword:0 に設定します。この設定を行うと、CreateWindow() でウィンドウの幅と高さに 0 を指定して、幅と高さにデフォルト値を使う場合や、それから、SystemParametersInfo() に SPI_GETWORKAREA を指定して work area を取得して、work area と同サイズでウィンドウを表示するアプリケーションは、全てフルスクリーン表示となります。たとえば、シェルのエクスプローラーウィンドウ(フォルダウィンドウ)でも、タスクバーが表示されず、フルスクリーン表示されます。シェルのエクスプローラーウィンドウもフルスクリーン表示されると都合がよくない場合は、以下で述べる、API 呼び出しによる方法を使ってみて下さい。
なお、シェルの OnTop というキーに対する説明は、WinCE のリファレンスには記載されておらず、MSDN フォーラムの投稿などでしか見当たりません。たとえば、次のページです:
http://social.msdn.microsoft.com/Forums/en-US/netfxcompact/thread/a5293f70-0587-4812-b50f-cbfdee9e6d0e
WinCE のリファレンスには、[HKEY_LOCAL_MACHINE\Software\Microsoft\Shell\AutoHide] というキーに関する説明が載っていますが、このキーを設定しても、タスクバーは完全に隠れず、隠れた状態では6ピクセルの高さだけ、画面下端にタスクバーが表示されます:
http://msdn.microsoft.com/en-US/library/ee501693(v=WinEmbedded.60).aspx (Enabling Slide Animation for the Taskbar)
■AYGShell API による方法
二番目は、AYGShell の API を使う方法です。この方法では、フルスクリーン表示用の API を呼び出したうえで、アプリケーションのウィンドウのサイズを、スクリーンと同サイズにします。ただし、WinCE 6.0 付属の標準シェルでは、フルスクリーン表示用の AYGShell API に対する応答動作の実装に不足があると思われ、追加の API 呼び出しが必要です。
フルスクリーン表示用の AYGShell API は、SHFullScreen() という関数です:
http://msdn.microsoft.com/en-US/library/ee499392(v=WinEmbedded.60).aspx (SHFullScreen)
上のリファレンスページには、タスクバーを非表示にしてフルスクリーン表示する処理、および、タスクバーを表示してフルスクリーン表示を解除する処理のサンプルコードも記載されています。しかし、WinCE 6.0 付属の標準シェルでは、SHFullScreen() に対する応答動作の実装が、リファレンス記載の仕様とは合っておらず、そのため、上のページ記載のサンプルコードは、期待通りに動作しません(タスクバーが非表示となりません)。SHFullScreen() のリファレンスには、この関数の第二引数に SHFS_HIDETASKBAR を渡した場合、「タスクバーが、z オーダーの底」に移動する、つまり、ウィンドウの重なり位置の最背面に移動すると書かれていますが、WinCE 6.0 付属の標準シェルの実装は、そうなっていないのです。
標準シェルのソースファイルは、%_WINCEROOT%/PUBLIC/shell/oak/hpc/explorer/ 配下にあり、タスクバーのソースファイルは、taskbar/ サブディレクトリに配置されています。そして、SHFullScreen() に対する応答動作は、taskbar,cpp の CTaskBar::TaskBarWndProc() で実装されています。CTaskBar::TaskBarWndProc() の中の switch 文で、SPECIAL_HIDE_MESSAGE と SPECIAL_SHOW_MESSAGE に対する case 節が、当該個所です。ソースを追ってもらえば分かると思いますが、SHFullScreen() の呼び出しによって SPECIAL_HIDE_MESSAGE が送られても、標準シェルのタスクバーの実装(CTaskBar クラス)では、タスクバーがウィンドウメッセージに応答しないようにするだけで(※受け取ったメッセージを、SHFullScreen() の第1引数に渡されたウィンドウに全て転送します)、タスクバーの表示変更は、何も行いません。つまり、リファレンスに書かれている「タスクバーを z オーダーの底」に移動する処理は、行わないのです。
SHFullScreen() の呼び出しに対するタスクバーの動作が、リファレンスに記載された通りではないことへの対応策の一つは、標準シェルの実装を修正することです。しかし、その対応では、OS のカスタマイズが必要であり、アプリケーションだけでは対応できません。アプリケーションだけで対応する場合は、SHFullScreen() の呼び出しの後、SetWindowPos() を使って、アプリケーションのウィンドウを最前面に移動することで解決できます。
SHFullScreen() のリファレンスページに記載されているサンプルコードの場合で言うと、次の対応を追加することにより、タスクバーの非表示/表示切り替えによる、アプリケーションのウィンドウのフルスクリーン表示/解除ができるようになります:
・タスクバーを非表示/アプリケーションのウィンドウをフルスクリーン表示する時
SHFullScreen() の呼び出しの後に、次の行を追加する:
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
・タスクバーを表示/アプリケーションのウィンドウのフルスクリーン表示を解除する時
SHFullScreen() の呼び出しの後に、次の行を追加する:
SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
■レジストリ設定の動的変更による方法
最後の三番目は、一番目に述べた OnTop キーの値を動的に変更することにより、フルスクリーン表示するアプリケーションの動作中にだけ、タスクバーの最前面表示を行わないようにする、という方法です。この方法は、AYGShell API が OS イメージに組み込まれていない場合に有効です。AYGShell API は、WinCE 6.0 の SKU のうち、最もライセンス価格が安価な “Core”、および “Core Plus” には含まれません。そのため、AYGShell を組込まないコンフィグレーションの OS イメージがデバイスに搭載されている場合があります。
さて、レジストリの [HKEY_LOCAL_MACHINE\Software\Microsoft\Shell\OnTop] キーの値を動的に変更する場合は、単にキー値を変更するだけでは、タスクバーの非表示・表示の切り替え(最前面表示の無効化・有効化の切り替え)を行うことは、できません。レジストリのキー値を変更した際に、タスクバーに通知して、レジストリ設定をロードし直させる必要があるのです。標準シェルのソースを読むと分かりますが、この通知は、WM_WININICHANGE メッセージを使って行うことができます。メッセージのパラメータは、wParam が 0、lParam が 5000 です。
SHFullScreen() のリファレンスページ記載のサンプルコードを真似て書くと、次のようになります。
LRESULT CALLBACK SHFullScreenWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static fFullScreen = FALSE;
switch (message)
{
case WM_KEYDOWN:
{
// Toggle between full screen and normal mode when the user presses the space bar.
if (VK_SPACE == wParam)
{
HWND tbWndH;
HKEY keyH;
DWORD onTop = (!fFullscreen ? 0 : 1);
DWORD dwState;
RECT rc;
if (0 != RegOpenKeyEx(HKEY_LOCAL_MACHINE,
L"Software\\Microsoft\\Shell\\OnTop",
0, KEY_ALL_ACCESS, &keyH))
{
DWORD disp;
if (0 != ::RegCreateKeyEx(HKEY_LOCAL_MACHINE,
L"Software\\Microsoft\\Shell\\OnTop",
0, NULL, 0, KEY_ALL_ACCESS, NULL, &keyH, &disp))
{
break; // error
}
}
(void)RegSetValueEx(keyH, L"", 0, REG_DWORD, (LPBYTE)&onTop, sizeof(DWORD));
(void)RegCloseKey(keyH);
tbWndH = FindWindow(L"HHTaskBar", NULL);
if (NULL == windowH)
{
break; // error
}
SendMessage(windowH, WM_WININICHANGE, 0, 5000);
if (fFullScreen)
{
// resize the main window to the size of the work area.
SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, FALSE);
MoveWindow(hwnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, TRUE);
fFullScreen = !fFullScreen;
}
else
{
// resize the main window to the size of the screen.
SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
MoveWindow(hwnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, TRUE);
fFullScreen = !fFullScreen;
}
}
}
break;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
2011/08/15
koga