Search Results for ‘エラー’

OS Design から生成した SDK インストーラの問題(日本語ロケール)

■はじめに
Windows Embedded Compact (WEC) では、OS のコンフィグレーション、つまり OS Design に対応した SDK を作成できることは、皆さんご存知の通りです。もし、OS Design に対応した SDK を作成する方法をご存知ないのであれば、開発者ガイドの次のページが参考になります:

 Create an SDK (Compact 7)
 http://msdn.microsoft.com/en-us/library/jj200346(v=winembedded.70).aspx

 Build an SDK (Compact 2013)
 http://msdn.microsoft.com/en-us/library/dn197930.aspx

OS Design に対応した SDK を使ってアプリケーションを作成することにより、アプリケーションと OS のコンフィグレーションの食い違いが起きるのを防ぐことができる、というわけです。たとえば、DirectShow を組み込んでいない OS で動かすアプリケーションを作成する際に、誤って DirectShow API を呼び出すコードを書いてしまうという手違いが、なくなります。そのようなコードを書いても、OS に組み込まれていない API を宣言したヘッダファイルやライブラリは、SDK には組み込まれていないので、アプリケーションをビルドする際にビルドエラーとなるからです。

ところで、WEC 7 の開発環境では、この SDK に少しだけ問題があります。作成した SDK のインストーラで、画面表示が不適切になってしまう箇所があるのです。下の図を見て下さい。

  OEM EULA Dialog

この図は、SDK のインストーラの、使用許諾契約書 (EULA; End-User License Agreement) の表示・同意確認画面ですが、「同意しない」 (Decline) のラジオボタンが途中で切れてしまい、見えていません(※「同意」までしか見えていません)。これは、ユーザに不親切で、適切ではありません。SDK をビルドする際、日本語ロケールに設定せず、英語ロケールにした場合には、”Accept” と “Decline” という二つのラジオボタンが正しく表示されるのですが、日本語ロケールだと、上の図のように、表示が途中で切れてしまうのです。

これは、どちらかといえば、WEC 7 の開発ツールの不具合ですが、少なくとも2014年8月までにリリースされたアップデータでは、修正されていません。しかし、ご安心下さい。この不具合は、自分で修正することができます。

■SDK インストーラのテンプレート
上で紹介した開発者ガイドのページで説明されているように、SDK を作成してビルドすると、SDK のインストーラ(.msi ファイル)が生成されるのですが、この .msi ファイルは、ビルドのたびに一から生成されるのではなく、テンプレートを元にして生成されます。このテンプレートは、WEC 7/Visual Studio 2008 の場合ですと、次のディレクトリに配置されています:

 C:/Program Files (x86)/Microsoft Platform Builder/7.00/cepb/ideVS/SdkTools/RollerFiles

注:上のディレクトリパスは、64bit OS の場合です。32bit OS では、’Program Files’ の後の ‘(x86)’ は、ありません。

上のディレクトリには、.msi ファイルのテンプレートの他、使用許諾契約書のテンプレートや、SDK のビルド時に実行される VB スクリプト(.vbs ファイル)も収録されています。興味のある方は、それらの内容をご覧になってみて下さい。

さて、今回重要なのは、.msi ファイルのテンプレートです。日本語ロケール用のテンプレートファイルは、拡張子の前に ‘_1041′ が付いており、
 template_1041.msi
というのが、日本語ロケール用のものです。このテンプレートには、SDK のダイアログのリソースも入っており、それを修正することで、上述した画面表示の問題を解消できます。では、.msi ファイルの内容を、どうやって修正すればよいのでしょうか?

ご安心下さい。Visual Studio には、.msi ファイルの内容を編集できる orca というツールが付属しています。このツールを使えば、.msi ファイルを開いて、インストーラのダイアログのリソースを編集できるのです。

■インストーラのテンプレートを修正する
orca を使うには、まずインストールしなければいけません。Visual Studio や WEC の開発環境(Platform Builder)をインストールしただけでは、orca はインストールされないのです。orca のインストーラが付属しているので、それを使ってインストールして下さい。orca のインストーラは、C:/Program Files (x86)/ ディレクトリ(※32bit OS の場合は、C:/Program Files/ ディレクトリ)で、’orca’ を含む名前のファイルを検索すれば見つけることができます。VS 2008 + WEC 7 をインストールしていれば、
 C:/Program Files (x86)/Windows Kits/8.0/bin/x86/
というディレクトリの中に、Orca-x86_en-us.msi というファイルが見つかるでしょう。もし、この他に、
 C:/Program Files (x86)/Orca/
というディレクトリと、その中に入っている Orca.exe というファイルが見つかる場合には、既に orca がインストールされています。orca がインストールされていない場合は、見つかったインストーラを実行して、インストールして下さい。

orca をインストールして使えるようになったら、いよいよ、日本語ロケールの SDK のインストーラのテンプレート(.msi ファイル)を開いて修正です。おっと、その前に、修正する .msi ファイルのバックアップをとっておいて下さい。万が一、修正作業で手違いが起きても、元の状態に戻せるようにするためです。

orca の使い方については、次のページが参考になるでしょう:

 Orca データベース エディタを使用して Windows インストーラ ファイルを編集する方法
 http://support.microsoft.com/kb/255905/ja

問題の、使用許諾契約書の表示・同意確認画面のラジオボタンの表示設定は、’Control’ テーブルに入っています。下に、’Control’ テーブルの該当行を編集しようとしている様子を写した画面キャプチャを示します。

 orca edit window

上の図の、選択されて青くハイライトされているのが、Control テーブルの該当行です。この行は、’OEM_EULA_Dlg’ というダイアログの、Control カラムの値が ‘Buttons’、Type カラムの値が ‘RadioButtonGroup’ という行です。この行の ‘Width’ というカラムに、二つのラジオボタンで構成されたラジオボタングループの表示幅が格納されています。日本語ロケールの SDK インストーラで表示が切れてしまうのは、この幅が、英語表示だと足りるものの、日本語表示だと足りない値だからなのです。従って、この幅の値を増やしてやれば、表示が切れなくなります。上の画面キャプチャで、黄色くハイライトされているカラムは、値が 135 ですが、これを 170 に変更すると、「同意しない」というキャプションが、正しく表示されるようになります。

.msi ファイルの編集中に、編集結果を確認したい場合には、orca の ‘Tools’ メニューから ‘Dialog Preview…’ を選択すれば、ダイアログ表示をプレビューできます。試してみて下さい。

Add comment 2014/08/22 koga

ネイティブコードから C# へのコールバックについて補足

今回は、2012/05/14 のエントリ(「ネイティブコードから C# のメソッドをコールバック」)についての補足を書きます。補足するのは、次の二点です:

・コールバック関数のコーリングコンベンション
・ネイティブコードへのデリゲートオブジェクト渡し

それぞれについて、以下に記します。

■コールバック関数のコーリングコンベンション
2012/05/14 のエントリでは、マネージドコードからアンマネージコードに渡すコールバック関数の関数ポインタを得る API として、GetFunctionPointerForDelegate() を紹介しました。この API で取り出す関数ポインタに対しては、アンマネージコード側からみた場合、留意すべき点があります。リファレンスページの “Remarks” の項にある注意書きです。

 Marshal.GetFunctionPointerForDelegate Method
 http://msdn.microsoft.com/en-us/library/at4fb09f(v=vs.90).aspx

上のページの “Remarks” の項には、次の注意書きがあります:

The delegate d is converted to a function pointer that can be passed to unmanaged code using the __stdcall calling convention.

つまり、アンマネージコード(ネイティブコード)から見た場合、マネージコードから渡される関数ポインタのコーリングコンベンション(呼出規約)は __stdcall になるので、アンマネージコード側では、受け取る関数ポインタの型を、__stdcall 付きで宣言しておく必要がある、というわけです。__stdcall をはじめ、Visual C++ の C/C++ コンパイラにおけるコーリングコンベンションの説明は、リファレンスの次のページをご覧ください。

 Argument Passing and Naming Conventions
 http://msdn.microsoft.com/ja-jp/library/984×0h58(v=vs.90).aspx

デフォルトのコーリングコンベンションは、スタックに積まれた手続きの引数を、手続きを呼び出した側がスタックから取り除く __cdecl であり、呼び出された手続きが取り除く __stdcall とは異なりますので、その点に注意して下さい。

さて、アンマネージコードの既存のライブラリがあり、そのライブラリに対してコールバック関数を設定するマネージドコードを実装しようとした際、既存のライブラリが受け取るコールバック関数が __cdecl であった場合は、どうすればよいのでしょうか?そのままでは、コーリングコンベンションが合っていないため、実行時エラーを引き起こす可能性があります。

ご安心下さい。その問題は、マネージドコードからの呼び出し方を変えることで解決できます。これは、冒頭に示した二点目の補足に関係します。

注意:実は、WinCE/WEC では、後述するようにコーリングコンベンションの問題はありません。この問題を考慮する必要があるのは、フル版の .NET Framework の場合だけなのです。

■ネイティブコードへのデリゲートオブジェクト渡し
2012/05/15 のエントリでは、マネージドコードからアンマネージコードへ渡す関数ポインタを得るために、delegate 実体に GetFunctionPointerForDelegate() を適用する必要があると述べました。しかし、この呼び出しは必須ではなく、delegate 実体をそのまま渡すことができます。MSDN にある .NET Framework プログラミングガイドにある、「アンマネージコードとの相互運用」にあるサンプルコードでも、delegate 実体をそのまま渡しています。2012/05/15 のエントリを書いた時点では、そのことを理解していませんでした。ごめんなさい。

MSDN にあるサンプルコードは、次のページをご覧ください:

 Callback のサンプル(マネージドコード)
 http://msdn.microsoft.com/ja-jp/library/5zwkzwf4(v=vs.90)

 PinvokeLib.dll(アンマネージコード)
 http://msdn.microsoft.com/ja-jp/library/as6wyhwt(v=vs.90).aspx

さて、コーリングコンベンションに話を戻します。.NET Framework には、マネージドコードとアンマネージコードの間での、(アンマネージコードにおける)コーリングコンベンションを表現する UnmanagedFunctionPointerAttribute というクラスがあります。このクラスを使って、delegate のコーリングコンベンションを変更できます。具体的には、delegate を宣言する際、次のように属性記述すれば、delegate のコーリングコンベンションを __cdecl に設定できるようです:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate bool FPtr( int value );


しかし、この UnmanagedFunctionPointerAttribute は、WinCE/WEC 環境、つまり .NET Compact Framework ではサポートされていません。さらに、UnmanagedFunctionPointerAttribute クラスのコンストラクタ引数である、enumeration 型の CallingConvention は、.NET Compact Framework の場合には、メンバとして Winapi しか持たず、これは __cdecl に対応するとリファレンスに書かれているのです。

 CallingConvention Enumeration
 http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.callingconvention(v=vs.90).aspx

このリファレンスには、デフォルトのコーリングコンベンションを示す Winapi は、「Windows 環境」、つまりフル版の .NET Framework では __stdcall に対応し、WinCE/WEC 環境(.NET Compact Framework)では __cdecl に該当するという旨の説明があります。これは一体、どいうことなのでしょう?Win32 API は、WINAPI 付きで宣言されており、__stdcall のコーリングコンベンションを設定されています。一方、UnmanagedFunctionPointerAttribute が __cdecl にしか対応しないのでは、両者が整合しないということです。

実は、WinCE/WEC のコーリングコンベンションは __cdecl だけなのです。このことは、次のヘッダファイルを見れば分かります:
 %_WINCEROOT%/public/common/sdk/inc/windef.h

windef.h には、次の行があり、’__stdcall’ が __cdecl の別名として定義されているのです。

#ifdef UNDER_CE
#define __stdcall __cdecl // Note this doesn't match the desktop definition
#define _stdcall __cdecl // Note this doesn't match the desktop definition
#endif


従って、.NET Compact Framework の場合には、コーリングコンベンションのことを意識する必要は、ありません。とはいえ、.NET Compact Framework 版と .NET Framework 版とで、アンマネージコードを含めてソースコードを共有できるように、可搬なコードを書く場合には、.NET Framework 版でも問題なく動作するよう、コーリングコンベンションを意識する必要があります。

■まとめ
今回は、2012/05/14 のエントリに対して二つの補足を書きました。二つのうち、コーリングコンベンションについては、WinCE/WEC では考慮せずともよいのですが、Win7 や Win8 でも動かせる可搬なコードを書く場合には、考慮する必要があります。

もう一つの、デリゲート渡しについては、わざわざ GetFunctionPointerForDelegate() を呼ぶ必要はない、というのが結論です。これは、2012/05/14 のエントリで、どちらかといえば、不適切な内容だったと思います。同様の見解が、.NET Framework に関する次の記事にも書かれています:

 C# からコールバック関数を使う C の関数を呼ぶ
 http://sgry.jp/pgarticles/cs_pinvoke_callback.html

今回の補足ですが、2012/05/15 のエントリに頂いていたコメントを拝見して書きました。コメントを頂いたのは、2013/05/14 ですから、半年以上の時間が経ってしまったのですが、参考になりました。有難うございます。

Add comment 2014/02/16 koga

WEC 7 付属の gdiplus.dll

■WinCE/WEC と GDI+
Windows XP で導入された 2D 描画用の API である GDI+ は、WinCE では利用できません。しかし、Windows Mobile では、gdiplus.dll という DLL が組み込まれており、この DLL により、GDI+ の API を利用できたようです。

たとえば、MSDN フォーラムの “Visual Basic and C# Projects” に投稿された “How to use GDI+ on Windows Mobile?” という質問に対して、Windows Mobile において非公式 API ではあるものの、GDI+ を利用できるというコメントが寄せられています:

 How to use GDI+ on Windows Mobile?
 http://social.msdn.microsoft.com/forums/en-US/vssmartdevicesvbcs/thread/e01dfedb-9b79-468b-9e89-44f59952b2d8

そのコメントでも紹介されている次のページは、GDI+ の Flat API を .NET CF アプリケーションから使うための wrapper ライブラリを提供しています。

 Using GDI+ on Windows Mobile
 http://community.opennetcf.com/articles/cf/archive/2007/10/31/using-gdi-on-windows-mobile.aspx

御存知の方は多いと思いますが、フル版の .NET Framework では、描画 API の実装に GDI+ が用いられています。たとえば、System.Drawing.Graphics クラスの DrawArc() や DrawBezier() などです。しかし、.NET Compact Framework では、System.Drawing.Graphics クラスには DrawArc() や DrawBezier() メソッドが存在しません。上記の wrapper ライブラリは、GDI+ の Flat API を P/Invoke によりマネージドコードから呼び出すためものです。

上のページでは、gdiplus.dll が提供している GDI+ API は、デスクトップ版のサブセットであり、「未実装」エラーを返す関数が多くあると述べています。しかし、サブセットであっても、GDI の API だけでは不足する 2D 描画機能を利用したいと思うことは、少なくありません。gdiplus.dll を WinCE でも使いたいと思う人は多いようで、’gdiplus.dll WinCE’ などで検索すると、「WinCE には gdiplus.dll が付属していないのか?」という質問のページが多く見つかります。

実は、WEC 7 には、gdiplus.dll が付属しています。

■WEC 7 の gdiex/ ディレクトリ
冒頭でも述べたように、WinCE には gdiplus.dll が付属しておらず、GDI+ の API を利用できません。しかし、WEC 7 では、
 %_WINCEROOT%/public/gdiex/
ディレクトリ配下に gdiplus.dll と gdiplus.lib が収録されているのです。このディレクトリは、WinCE 6.0 にも存在しますが、配下に収録されているのは、Imaging API のヘッダファイルとライブラリファイル(.lib)だけです。

gdiplus.dll に関する説明は、WEC 7 のリファレンスには存在しません。Windows Mobile でそうだったように、非公式な API という位置づけのようです。SKU との対応も不明ですから、もし、gdiplus.dll を組み込んだ OS イメージを製品に搭載することを検討される場合には、ランタイムライセンスの販売代理店経由で Microsoft 社に確認する方が良いでしょう。

WEC 7 に gdiplus.dll が付属していることの理由は不明ですが、もしかすると、一部の開発者からの強い要望を受けて、Windows Mobile 向けに提供していたものを WEC 7 にも収録した、ということなのかも知れません。いずれにせよ、便利な機能が付属しているわけですから、もしライセンス上も支障なければ、利用できると嬉しいところです。

■ベジェ曲線描画の動作確認
というわけで、WEC 7 付属の gdiplus.dll を使った描画について、簡単なテストコードを書いて試してみました。制御点が二つだけの単純なベジェ曲線の描画ですが、Flat API を C/C++ から呼び出すコードを書いて動かしてみたところ、問題なく動作しました。ただし、パフォーマンスは評価・実測していません。

以下に、テストコードの関連部分を引用します:

static void
DrawWithGdiPlus(HDC hdc)
{
#define MAKE_ARGB(a, r, g, b) \
    (((ARGB)(b) << 0) \
    |((ARGB)(g) << 8 ) \
    |((ARGB)(r) << 16) \
    |((ARGB)(a) << 24)) \

    GpGraphics*    graphics = NULL;
    GpPath*    path = NULL;
    GpPen*    pen  = NULL;
    PointF    points[] = {
        PointF(0.0f + 50, 0.0f + 50),
        PointF(40.0f + 50, 20.0f + 50),
        PointF(80.0f + 50, 150.0f + 50),
        PointF(100.0f + 50, 10.0f + 50),
    };

    if (0 != ::GdipCreateFromHDC(hdc, &graphics)) {
        return;
    }
    if (0 != ::GdipCreatePath(FillModeAlternate, &path)) {
        goto finish;
    }
    if (0 != ::GdipAddPathBeziers(path, points, 4)) {
        goto finish;
    }
    if (0 != ::GdipCreatePen1(MAKE_ARGB(255, 255, 0, 0), 1.0, UnitPixel, &pen)) {
        goto finish;
    }
    if (0 != ::GdipDrawPath(graphics, pen, path)) {
        goto finish;
    }

finish:
    if (NULL != pen) {
        (void)::GdipDeletePen(pen);
    }
    if (NULL != path) {
        (void)::GdipDeletePath(path);
    }
    (void)::GdipDeleteGraphics(graphics);
}


上記のテストコードを動かした画面キャプチャが、次の図です。

bezier-spline

なお、上の画面キャプチャは、2012/7/4 に書いたエントリ(「デバイスエミュレータで WEC 7 を動かす」)で紹介した、デバイスエミュレータに移植した WEC 7 でテストコードを動かした様子です。興味のある方は、ご自分でテストコードを書いて試してみて下さい。

上のテストコードについて、二点補足しておきます。

  1. GdiPlus.h のインクルードが必要。さらに、上記のコードの場合、Gdiplus と Gdiplus::DllExports に対する using ディレクティブが必要。
  2. GdiPlus.h を始め、GDI+ のヘッダファイルは WEC 7 に付属しないので、別途入手が必要。
  3. アプリケーションの初期化処理と終了処理で、それぞれ、GDI+ の初期化関数と解放関数を呼び出さなければならない。

上記のテストコードのビルドと動作確認を行う際、GDI+ のヘッダファイルは、次のページで公開されているライブラリ(LibGdiplus)に収録されているものを使いました。

 GDI+ for Windows Mobile
 http://www.ernzo.com/LibGdiplus.aspx

このライブラリは、Windows Mobile に収録されている gdiplus.dll 用の wrapper です。冒頭で紹介した wrapper ライブラリとは異なり、マネージドコードではなく、C/C++ 用の wrapper です。

GDI+ の初期化関数と解放関数は、GdiplusStartup() と GdiplusShutdown() です。詳細は、GDI+ のリファレンスをご覧下さい:

 GDI+ Reference > Functions
 http://msdn.microsoft.com/en-us/library/windows/desktop/ms534055(v=vs.85).aspx

GDI+ の Flat API のリファレンスは、次のページに記載されています。

 GDI+ Flat API
 http://msdn.microsoft.com/en-us/library/windows/desktop/ms533969(v=vs.85).aspx

■GDI+ の代替ライブラリ
WEC 7 には gdiplus.dll が付属しており、それを OS イメージに組み込むことで GDI+ の API(のサブセット)を利用できることを、上で紹介しました。では、WinCE 6.0 で GDI+ の API を利用したい場合には、何か方法はないのでしょうか?

簡単な代替策は、ありません。前の節で紹介した LibGdiplus のページ("GDI+ for Windows Mobile")では、Mono プロジェクトの LibGdiplus を紹介していますが、(その紹介でも述べられているように)そのまま使うことは、できません。Mono プロジェクトの LibGdiplus は、X Window System に依存した実装だからです:

 Libgdiplus
 http://www.mono-project.com/Libgdiplus

Mono プロジェクトの Libgdiplus は、Cairo という 2D 描画ライブラリを利用しているようですが、WinCE 6.0 で使えるようにするためには、X Window System に依存した部分の実装を、WinCE へ移植する必要があるでしょう。その移植作業は、移植対象の API 関数を一部に絞ったとしても、それなりに大きな手間だと思われます。

なお、GDI+ が提供する 2D 描画 API のうち、ベジェ曲線(Bezie-spline 曲線)の描画だけであれば、自前で実装したサンプルコードが CodeProject サイトの次のページで公開されています。

 Drawing Qubic Bezier-splines on Pocket PC
 http://www.codeproject.com/Articles/9862/Drawing-Qubic-Bezier-splines-on-Pocket-PC

このサンプルコードの動作確認は行っていませんが、おそらく、WinCE/WEC でも問題なく使えるんじゃないかと思います。

■まとめ
今回は、WEC 7 に付属している GDI+ の DLL(gdiplus.dll)について紹介し、単純なベジェ曲線であれば、問題なく動作することを述べました。gdiplus.dll が、先日発表のあった WEC の次のバージョン(Windows Embedded Compact 2013)でも提供されるのかどうかは、現時点では不明です。しかし、WEC 7 に限定すれば、GDI だけでは不足する 2D 描画機能を実現するために gdiplus.dll の利用を検討することは、価値があるかも知れません。

なお、OS やミドルウェアベンダーの近年の動きを見ていると、高機能な 2D 描画は、3D 描画のグラフィクス機能に統合されるのが一般的な方向性のように思われます。Windows の場合でも、GDI/GDI+ を置き換えていくものとして Windows 7 で導入された Direct2D は、Direct3D の上に実装されているそうです:

 Introducing Direct2D
 http://msdn.microsoft.com/en-us/magazine/dd861344.aspx

今後、Windows Embedded Compact におけるグラフィクス機能が、どのように改良ないしは変革されていくのか分かりません。WEC 7 において gdiplus.dll が追加されたことは、もしかすると、Windows Embedded Compact の歴史における、一過性のものだとして捉えておく方が確実かも知れません。

とはいえ、WEC 7 は、2011年のリリースから7年後の、2018年までは、無償サポートが継続する予定です。従って、開発のターゲットを WEC 7 に絞れば、その他の API やミドルウェアと同様に、gdiplus.dll はアプリケーション開発の有力なツールとなり得るでしょう。この点は、WinCE/WEC の Long Term Suppport の利点だと思います。

Add comment 2013/02/05 koga

CeLogFlush.exe と Kernel Tracker

WEC 7 や WinCE 6.0 のデバイスドライバや、アプリケーションの開発を行っていて、期待しているパフォーマンスが達成されなかったり、予想していたタイミングでスレッドが実行されない、などの問題が生じた場合、どこに要因があるのかを調べるには、カーネルのログ機能と Kernel Tracker を使うのが便利です。WEC/WinCE カーネルのログ機能は、デバッグメッセージを出力するなどの単純なものではなく、スレッドの切り替わりや、各種同期オブジェクトの獲得や解放、および、割り込み処理の発火など、OS 内部の詳細な動きを記録できるようになっています。

WEC/WinCE カーネルが出力したログの内容は、Kernel Tracker を使って、グラフィカルに表示できます。以下に、WEC 7 をデバイスエミュレータで動かして取得したログの内容を、Kernel Tracker で表示した様子を示します。

Kernel Tracker の画面キャプチャ

Kernel Tracker の画面キャプチャ

上の画面キャプチャを見ると、標準シェル(explorer.exe)のスレッドが 100ms の Quantum を使い果たし、servicesd.exe 内の同じ優先度を持つスレッドに切り替わった様子が分かります。また、ほぼ一定間隔で割り込み応答動作が起きていることも分かります。これは、1ms おきに発生するタイマ割り込みに対するものです。

このように、WEC/WinCE カーネルのログ機能を使うと、1ms より細かい時間精度でスレッドの動きを知ることができ、OS 内部の動作を分析するのに役立ちます。カーネルデバッガを使ってステップ実行する場合とは異なり、通常の動作での OS 内部の振る舞いを知ることができますので、うまく使えば、強力なツールとなります。また、後述するように、出荷後のデバイスに搭載されているものなど、ログ機能を有効にしない OS イメージに対しても、一時的にログ機能を有効にしてログ採取できるのも、非常に便利な点でしょう。

■CeLogFlush.exe によるログ採取(標準設定)
WEC/WinCE カーネルのログ機能については、リファレンスの次のページで説明されています:

 CeLog Event Tracking (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee479601

 CeLog Event Tracking Overview (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee480432(v=winembedded.60)

このログ機能を使って、WEC/WinCE カーネルが出力するログをファイルに保存する手順は、次の通りです。以下の説明は WEC 7 の場合ですが、WinCE 6.0 でも同様です。

  1. OS Design のカタログ項目で、Target Control Support (Shell.exe) を選択する(SYSGEN_SHELL を有効にする)。
  2. OS Design の「構成プロパティ」の Build Options で、Enable KITL を Yes に設定する。
  3. 同じく Build Options で、Flush tracked events to release directory を Yes (IMGAUTOFLUSH=1) に設定する。

ただし、WinCE 6.0 の場合は、次のいずれかの手順が追加で必要です。

  • OS Design の「構成プロパティ」の Build Options で、Enable event tracking during boot を Yes (IMGCELOGENABLE=1) に設定する。
  • project.bib の FILES セクションに、celog.dll を追加する。

つまり、WinCE 6.0 の場合は、IMGAUTOFLUSH に加えて IMGCELOGENABLE も1に設定しなければ(または、明示的に celog.dll を .bib ファイルに記述しなければ)、カーネルのログ出力が起きません。一方、WEC 7 の方は、IMGCELOGENABLE は設定しなくても、IMGAUTOFLUSH を1に設定すればカーネルのログ出力が起きるのです。実際、リファレンスを見ても、そのように説明されています:

 Including CeLogFlush in a Run-Time Image (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee481413(v=WinEmbedded.60).aspx

 Enable CeLog Event Tracking (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee480716

WinCE 6.0 と WEC 7 の、この違いは、%_WINCEROOT%/public/COMMON/oak/files/common.bib の記述内容の違いによるものです。WinCE 6.0 の common.bib には、IMGCELOGENABLE が1の場合に celog.dll を MODULES セクションに追加する行があるだけです。一方、WEC 7 の common.bib では、次のようになっています。

MODULES
;  Name            Path                                           Memory Type
;  --------------  ---------------------------------------------  -----------
…
IF IMGCELOGENABLE
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SHK
ENDIF IMGCELOGENABLE
…

; ====================================================================
; FILES section
;
; ====================================================================

FILES
…
; Setting IMGAUTOFLUSH or IMGOSCAPTURE without IMGCELOGENABLE will include
; celog.dll in the FILES section instead of MODULES, so that the DLL will
; be loaded late enough to read settings from the device registry.
IF IMGCELOGENABLE !
IF IMGAUTOFLUSH
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SH
ENDIF IMGAUTOFLUSH
IF IMGOSCAPTURE
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SH
ENDIF IMGOSCAPTURE
ENDIF IMGCELOGENABLE


WEC 7 の場合は、common.bib の内容が上のようになっているため、IMGCELOGENABLE を設定せずに IMGAUTOFLUSH だけを1に設定した場合は、celog.dll が .bib ファイルの FILES セクションに追加されますので、OS イメージに celog.dll が収録されます。これに対して、WinCE 6.0 では、IMGCELOGENABLE が1に設定されなければ celog.dll が OS イメージに収録されません。そのため、IMGCELOGENABLE も1に設定するか、または、project.bib の FILES セクションに celog.dll の行を追加する必要があるのです。

上記の設定を行った OS Design をビルドして、ターゲットデバイスにダウンロードして動かすと、Flat Release Drectory、つまり環境変数 _FLATRELEASEDIR が指すディレクトリに celog.clg というファイルが作られて、カーネルが出力したログが書き込まれます。ターゲットデバイスと Platform Builder との接続を切れば、celog.clg ファイルを開くことができるようになりますので、Kernel Tracker で開いてみて下さい。

WEC 7 の場合は、.clg ファイルが Kernel Tracker と関連づけられているため、.clg ファイルのアイコンをダブルクリックすると、Kernel Tracker が起動します。WinCE 6.0 の場合は、Kernel Tracker を起動した後、File メニューで .clg ファイルを開く必要があります。詳細は、次のページをご覧下さい:

 Remote Timeline Viewer (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/gg156030

 Starting Kernel Tracker in File Mode (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee480674(v=winembedded.60)

■CeLogFlush.exe によるログ採取(設定のカスタマイズ)
さて、KITL と Target Control Support (Shell.exe) を有効にした OS イメージで、Flat Release Directory に WEC/WinCE カーネルのログを出力できることは分かりました(Target Control Support (Shell.exe) ではなく、Release Directory File System を有効にするのでも構いません)。これらを有効にしない限り、カーネルのログをファイルへ出力することは、できないのでしょうか?

Flat Release Directory 以外のディレクトリへファイルを出力することは、レジストリ設定により可能です。WEC/WinCE カーネルのログは、celog.dll によって RAM 上のリングバッファへ格納され、その内容を、CeLogFlush.exe がファイルへ出力する、という仕組みになっています。CeLogFlush.exe および celog.dll に対するレジストリ設定は、リファレンスの次のページで説明されています。

 CeLog Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee480468

 CeLogFlush Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee481192(v=winembedded.60)

ログを出力するファイルは、上のページの説明にある通り、[HKEY_LOCAL_MACHINE\System\CeLog] キーの FileName で設定できます。たとえば、SD カードにログファイルを出力する場合は、次のような行を .reg ファイルへ追加すればよいでしょう。

[HKEY_LOCAL_MACHINE\System\CeLog]
    "BufferSize"=dword:20000
    "FileName"="\\Storage Card\\celog.clg"
    "FileSize"=dword:0
    "FileFlags"=dword:0
    "FlushTimeout"=dword:2710
    "ThreadPriority"=dword:F8
    "Transport"="LocalFile"
    "UseUI"=dword:0
    "ZoneCE"=dword:c003e2


なお、BufferSize や FlushTimeout の値は、デフォルトよりも小さくしないで下さい。これらを小さくすると、celog.dll が確保するリングバッファがすぐに満杯になったり、あるいは、ログが出力されていなくてもすぐにバッファのフラッシュ時間に達してしまい、必要以上にログ出力が増えます(CeLogFlush.exe によるファイルへのログ書き込みによっても、カーネルのログ出力が発生することに留意して下さい)。その結果、OS の実効速度が実用にならないものになってしまう場合もあるからです。

もう一点注意です。[HKEY_LOCAL_MACHINE\System\CeLog] キーの FileName 値の設定で、ログの出力先を外部ストレージにする場合、そのストレージデバイスがマウントされるよりも前に CeLogFlush.exe が起動されないようにしなければいけません。IMGAUTOFLUSH を1に設定した場合、common.reg ファイルの記述により、[HKEY_LOCAL_MACHINE\init] キーに対する以下の設定が追加されます:

[HKEY_LOCAL_MACHINE\init]
        "Launch05"="CeLogFlush.exe"


この設定では、デバイスマネージャ(device.dll)よりも先に CeLogFlush.exe がロード・起動されてしまうため、CeLogFlush.exe が初期化時に出力ファイルを作成できず、エラーで終了してしまいます。従って、device.dll の起動順序(Launch20)よりも後の起動順序を設定する必要があります。デバイスマネージャが起動してストレージがマウントされるまでに若干の時間がかかることを考慮すると、gwes.dll(Launch30)の後にする方が、より確実だと思われます。

■ログ機能を有効にしていない OS イメージでのログ採取
ここまでの説明で、celog.dll と CeLogFlush.exe を OS イメージに組み込んでいれば、WEC/WinCE カーネルのログをファイルへ出力できることが分かりました。しかし、カーネルのログが常にファイルへ出力されると、若干のオーバーヘッドを生みますし、また、セキュリティの観点からも、好ましいことでは、ありません。このことは、上で紹介した WEC 7 のリファレンスページにも注意書き(Note)として記載されています。

さて、今回のエントリの冒頭で、次のように書きました:

また、後述するように、出荷後のデバイスに搭載されているものなど、ログ機能を有効にしない OS イメージに対しても、一時的にログ機能を有効にしてログ採取できるのも、非常に便利な点でしょう。

実は、celog.dll と CeLogFlush.exe を OS イメージに組み込んでいなくても、OS の起動後に celog.dll をカーネルにロードさせて、ログ機能を有効にできるのです。従って、USB メモリなどの外部ストレージに celog.dll と CeLogFlush.exe を入れておき、OS の起動後に、手動で CeLogFlush.exe を始動すれば、必要な時にだけログを採取できます。このことは、リファレンスの次のページでも説明されています:

 Collecting Data On A Standalone Device With CeLogFlush (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee480886(v=winembedded.60)

CeLogFlush.exe は、起動すると終了要求されるまで、celog.dll が出力したログをリングバッファから読み出してファイルへ出力するループ動作を実行します。”SYSTEM/CeLogFlush Quit” という名前の event に対して SetEvent() を呼び出すことにより、CeLogFlush.exe に終了要求できます。または、CeLogFlush.exe のソースファイルと同じ場所に収録されている、CeLogStopFlush というサンプルプログラムも参考になります。CeLogStopFlush は、CeLogFlushCommon.lib というライブラリを CeLogFlush.exe と共用しており、CeLogFlushCommon.lib の FLUSH_SignalStopFlush() という関数を呼び出します。FLUSH_SignalStopFlush() は、”SYSTEM/CeLogFlush Quit” という名前の event を OpenEvent() でオープンして、SetEvent() を呼び出すだけです(※SetEvent() の後、CloseHandle() を呼び出します)。

■おまけ
いかがでしょうか?ここまでの説明で、WEC/WinCE カーネルのログ機能が、出荷用の OS イメージに対する障害解析ツールとしても利用可能な、強力なものであることを、お分かり頂けたのではないかと思います。以下に、このログ機能について、いくつか補足します。

(1)CeLogFlush.exe と celog.dll のソースコード
 CeLogFlush.exe と celog.dll のソースコードは、それぞれ次の場所にあります:

 CeLogFlush.exe
 %_WINCEROOT%/public/COMMON/sdk/samples/celog/flush/CeLogFlush/

 celog.dll
 %_WINCEROOT%/private/winceos/COREOS/nk/celog/celogdll/

 CeLogFlush.exe と celog.dll は、”SYSTEM/CeLog Data” という名前の共有メモリ領域(CreateFileMapping() によって生成されるメモリマップドオブジェクト)上に配置したリングバッファを介して、WEC/WinCE カーネルが出力したログデータを受け渡しします。リングバッファが満杯に近付くと、celog.dll が event オブジェクトを使って CeLogFlush.exe に通知して、リングバッファの内容をファイルへ出力させる、という連携です。リングバッファのサイズは、celog.dll が共有メモリ領域を生成する際に、レジストリの設定値に従って決定します。celog.dll は、共有メモリ上のリングバッファ構造体のヘッダにバッファサイズを書き込むことにより、CeLogFlush.exe にサイズを伝えます。CeLogFlush.exe は、リングバッファが満杯に近付かなくとも(つまり、celog.dll から event オブジェクトで通知されなくとも)、レジストリで設定されたタイムアウト時間が経過すると、リングバッファの内容をファイルへ出力します。従って、ログ出力の量が少ない場合でも、一定周期でログファイルへの書き出しが行われます。

(2)”Enable event tracking during boot” を設定しない場合の動作
 OS Design の「構成プロパティ」の Build Options で、”Enable event tracking during boot” を Yes (IMGCELOGENABLE=1) に設定しない場合とする場合の違いについて、もう少し詳しく述べておきます。

  IMGCELOGENABLE を1に設定した場合としない場合の違いは、WEC/WinCE カーネルの初期化が完了した後から、カーネルの起動が完了するまでの間のログ出力を採取するかどうかです。つまり、IMGCELOGENABLE を1に設定した場合は、CeLogFlush.exe が起動する前に出力されたログも採取できるのに対し、IMGCELOGENABLE を1に設定しない場合は、CeLogFlush.exe が起動した以降のログのみ採取可能となります。この違いは、celog.dll がロードされるタイミングの違いによって生じます。そのタイミングの違いを引き起こすのは、.bib ファイルの設定です。

  WEC 7 の場合、common.bib の設定で、IMGAUTOFLUSH のみ1に設定した場合(IMGCELOGENABLE は1に設定しない場合)は、celog.dll が .bib ファイルの MODULES セクションではなく、FILES セクションに配置されると述べました。この結果、WEC/WinCE カーネルの初期化が完了した直後のタイミングでは、celog.dll がロードされず、CeLogFlush.exe によって初めてロードされるのです。これについて、もう少しだけ詳しく述べます。

  WEC/WinCE カーネルの初期化が終わり、マルチスレッドモードへ遷移して最初に起動されるスレッドが実行する関数である SystemStartupFunc() の中で、LoggerInit() という、ログ機能の初期化関数を呼び出します。この LoggerInit() は、”CeLog.dll” を引数として LoadKernelLibrary() を呼び出し、celog.dll をカーネルにロードすることを要求します。しかし、この時点では、filesys.dll がロードされておらず、ファイルシステム機能が初期化されていません。そのため、カーネルの loader は、OS イメージの中の modules、つまり、.bib ファイルの MODULES セクションに配置された DLL しかロードできないのです(※ちなみに、SystemStartupFunc() は、LoggerInit() を呼び出す前に、LoaderInit() を呼び出してカーネルの loader を初期化します)。そのため、LoggerInit() による CeLog.dll のロードは成功せず、その時点では、ログ出力が有効になりません。その後、CeLogFlush.exe が起動すると、CeLogFlush.exe の初期化処理において、再び “CeLog.dll” を引数として LoadKernelLibrary() が呼び出されます。この時点では、filesys.dll が動作していますので、FILES セクションに配置された DLL もロードできる、というわけです。

  さて、celog.dll は、ロードされると、InitLibrary() という初期化関数を呼び出します。この関数の中で、IOCTL_CELOG_REGISTER を ioctl コードとする KernelLibIoControl() 呼び出しを行い、ログ出力関数群の関数テーブル(CeLogExportTable 構造体)をカーネル本体に登録します。これらの関数は、カーネル本体の中にあるログ出力部に登録されます。カーネル本体の中にあるログ出力部は、登録された関数テーブルの中の pfnCeLogQueryZones というメンバを使って、ログ出力 DLL から、出力対象とする zone の組み合わせを示すマスクビット列を得て、ログ出力の有無判定に使う、という仕組みになっています。

  上で述べた、カーネル本体の処理のソースコードは、それぞれ次の場所にあります:

  SystemStartupFunc()
  %_WINCEROOT%/private/winceos/COREOS/nk/celog/schedule.c

  カーネル本体の中のログ出力部
  %_WINCEROOT%/private/winceos/COREOS/nk/logger/logger.c

(3)Readlog
  Kernel Tracker を使うと、WEC/WinCE カーネルのログ機能が出力したログデータをグラフィカルに表示できることを、冒頭で紹介しました。このログデータ、つまり .clg ファイルの内容を解析する付属のツールは、他にもあります。それが Readlog です。Readlog について、リファレンスの次のページをご覧下さい:

  Readlog Viewing Tool (Windows Embedded Compact 7)
  http://msdn.microsoft.com/en-us/library/ee481220.aspx

  Readlog Viewing Tool (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee481220(v=winembedded.60)

  スレッドの切り替わりの様子などを直観的に見るには、Kernel Tracker が便利ですが、Kernel Tracker には表示されないログ内容をチェックしたり、ログ全体の分析結果などを手早く表示するには、Readlog の方が便利です。ちなみに、WinCE 6.0 の Kernel Tracker では、”Thread Migrate” のログを表示できませんが、WEC 7 のものでは表示できます。この “Thread Migrate” は、システムコールの発生、つまり、プロセス(カーネルである nk.exe 以外のプロセス)中のスレッドの、カーネル呼び出しによる、ユーザモードとカーネルモードの間の遷移に伴うコンテキストスイッチを示します。

(4)ログ機能のカスタマイズ
  ここまでの説明では、WEC/WinCE カーネルのログ出力は、celog.dll によって実行されると述べました。しかし、celog.dll を使わず、独自のログ出力 DLL を実装して使うことが可能です。また、独自のログ出力 DLL を celog.dll と共存させることも可能です(複数のログ出力 DLL を共存させることに意味があるかどうかは、別としてですが)。

  独自のログ出力 DLL を実装する方法について、リファレンスの次のページで説明されています。今のところ、WinCE 6.0 向けのものしかありませんが、WEC 7 でも同様の筈です。興味のある方は、カーネル本体のログ出力部や celog.dll のソースコードと併せ、ご覧になってみて下さい。

  CeLog Tool Customization (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-us/library/ee480013(v=winembedded.60)

  Implementing a Custom Event Tracking Library (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee480279(v=winembedded.60)

  Implementing an Event Tracking Library (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee480272(v=winembedded.60)

Add comment 2012/08/16 koga

デバイスエミュレータで WEC 7 を動かす

最初にことわっておきますが、今回の内容は、WinCE 6.0 (Windows Embedded CE 6.0) と WEC 7 (Windows Embedded Compact 7 ) を両方お使いの方が主な対象です。WinCE 6.0 しか使っていらっしゃらない方には、特に関係ない内容ですし、また、WEC 7 だけをお使いの方にも、必要性は低い内容だと思います。もし、あなたが、WinCE 6.0 の ARM ベースのデバイスエミュレータをご利用になっていて、同じものを WEC 7 でも使えないだろうか?と思ったことがあるのなら、今回の内容は、あなたのためのものです。お急ぎなら、途中を飛ばして、今回のエントリの最後の方にある「DeviceEmulator BSP を WEC 7 へ移植する」を読んでみて下さい。

■WinCE 6.0 と Windows Mobile のデバイスエミュレータ
Visal Studio 2008 までは、Pro 以上の版に含まれる Smart Device Development 機能の一部として、ARM ベースのデバイスエミュレータが付属しています。このデバイスエミュレータは、単体でも提供されており、最新版の 3.0(VS 2008 付属のものと同じ版)を、以下のページからダウンロードできます:

 Microsoft Device Emulator 3.0
 http://www.microsoft.com/ja-jp/download/details.aspx?id=5352

デバイスエミュレータがエミュレートしているのは、Samsung 製の、S3C2410 という ARM9 コアのプロセッサを搭載したリファレンスボードです。WinCE 6.0 の Platform Builder をお使いの方ならご存知の通り、WinCE 6.0 には、デバイスエミュレータの BSP が付属しています。そのため、WinCE 6.0 を動かす実機がなくても、デバイスエミュレータの BSP を使って OS イメージをビルドすれば、その OS イメージをデバイスエミュレータで動かし、実機に依存しない部分の開発が可能です。

WinCE 5.0 までは、デバイスエミュレータには Virtual PC が使われていました。つまり、ターゲットプロセッサは x86 でした。WinCE 6.0 では、それが ARM プロセッサになった、というわけです。しかし、後述するように、WEC 7 では、再び Virtual PC がデバイスエミュレータとして採用されました。

以下に、WinCE 6.0 のデバイスエミュレータ、つまり、S3C2410 リファレンスボードのエミュレータの特徴をまとめます:

  • Windows Mobile のエミュレータとしても利用されている。
  • 設定ファイルを作成することにより、スキン画像と入力ボタン/キーパッドを設定できる。
  • ARM プロセッサをエミュレートしている。

どれも、Virtual PC ベースのエミュレータには無いものです。専用の BSP を使って作成した OS イメージをロードして動かせるのは、どちらのエミュレータも同じです。一方、エミュレータから利用できるホスト PC の周辺機器機能は、Virtual PC ベースのエミュレータの方が豊富です。WinCE 6.0 のデバイスエミュレータの方は、実質、Ethernet とシリアルポートのみです。

WinCE 6.0 のデバイスエミュレータでは、周辺機器のエミュレート機能として、ホスト PC のマウス入力に対する、タッチパネル入力のエミュレートや、ホスト PC のディレクトリをメモリカードとしてマウント/エミュレートする、というものがあります。Virtual PC に比べると、利用できるホスト PC の周辺機器機能は貧弱ですが、入力ボタン/キーパッドのエミュレートが可能なことと、ARM プロセッサをエミュレートしているのは、便利な場合があります。

さて、WinCE 6.0 のデバイスエミュレータがエミュレート(シミュレート)しているのは、上述したように、Samsung の ARM9 コアのプロセッサ(S3C2410)のリファレンスボードです。S3C2410 の ARM9 コアは、ARM920T、つまり、命令セットが v4T である ARM9TDMI ファミリです。WinCE 6.0 までの ARM コンパイラは、ARMv4 の命令セットにしか対応していませんでしたので、それで十分でした。しかし、WEC 7 の ARM コンパイラでは、ARMv5/v6/v7 のサポートが追加された代わりに、ARMv4 には対応していません。このことは、このデバイスエミュレータで WEC 7 を動かそうとする場合に、問題となるように思われます。

しかし、心配はいりません。VS 2008 の Smart Device Development 機能に付属する、最新版(3.0)のデバイスエミュレータ(※上述したページから、単体でダウンロードできます)では、ARMv5 の命令セットにも対応しています。デバイスエミュレータのコマンドラインオプションのリファレンスを見ると、/cpucore オプションで ARMv5 を指定することにより、ARMv5 命令セットが有効になると説明されています(デフォルトは、ARMv4):

 デバイス エミュレータのコマンド ライン リファレンス
 http://msdn.microsoft.com/ja-jp/library/aa188169(v=VS.90).aspx

ちなみに、VS 2005 に付属していた版(1.0)、つまり、WinCE 6.0 用としても使われるデバイスエミュレータは、ソースコードを入手可能です:

 Shared Source Microsoft Device Emulator 1.0 Release
 http://www.microsoft.com/en-us/download/details.aspx?id=10865

デバイスエミュレータ 1.0 のソースコードは、上のページに書かれているように、Shared Source ライセンスで提供されていますので、興味がある方は、ご覧になってみて下さい。QEMU など、他のエミュレータと同様なところはあると思いますので、両者を比較しながら読んでみるのも、面白いでしょう。

■WEC 7 での Virtual PC 対応
WEC 7 では、WinCE 6.0 にあった ARM ベースのデバイスエミュレータの BSP は付属せず、代わりに、Virtual PC 用の BSP が付属しています。この BSP に対応した、Virtual PC の仮想マシンイメージ(cevm.vmc)も付属しており、BSP を使って OS イメージを作成すれば、すぐに動かせるようになっています。この仮想マシンイメージを有効にすると、Windows 7 で Windows XP Mode をお使いの場合、Windows Virtual PC の仮想マシンとして、Windows XP Mode に加えて cevm が表示されるようになる筈です。cevm には、WEC 7 のブートローダが組み込まれていますので、起動してブートローダのメニュー画面を表示させ、ネットワーク設定を行えば、Platform Builder を使って OS イメージをダウンロードできます。

WEC 7 での Virtual PC ベースのデバイスエミュレータを使う手順については、リファレンスの次のページをご覧下さい。

 Develop with Virtual CEPC (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/jj200433

お使いの PC に WEC 7 をインストール済みの方であれば、
 C:/Program Files/Windows Embedded Compact 7/Documentation/
ディレクトリの中に入っている
 Getting Started with Virtual CEPC.pdf
というドキュメントも、参考になるでしょう。

Virtual PC ベースのデバイスエミュレータを、WinCE 6.0 のデバイスエミュレータと比べると、上述したように、エミュレータから利用できるホスト PC の周辺機器機能は、Virtual PC ベース(前者)の方が豊富です。また、原理上、前者の方が高速でもあります。一見すると、Virtual PC ベースのエミュレータの方が良いことずくめで、WinCE 6.0 のデバイスエミュレータ(ARM ベースのエミュレータ)を使う理由は、ありません。

しかし、ARM ベースのエミュレータの方が便利な場合も、あります。たとえば、次の場合です。

  • 実機のプロセッサが ARM の場合。
  • 実機にハードウェアボタンを搭載予定であり、そのモック動作を、エミュレータで行いたい場合。

ハードウェアボタンのシミュレートについては、上述したように、設定ファイル(スキンファイル)へ記述することにより任意に設定できます。ARM ベースのデバイスエミュレータの設定ファイルの記述仕様は、リファレンスの次のページで説明されています:

 デバイス エミュレータのスキンの XML スキーマ リファレンス
 http://msdn.microsoft.com/ja-jp/library/aa188144(v=vs.90)

 デバイス エミュレータ構成の XML スキーマ リファレンス
 http://msdn.microsoft.com/ja-jp/library/bb531167(v=vs.90)


2012-09-05 追記:
デバイスエミュレータのスキン設定ファイルにおいて、ボタン/キーパッドに割り当てるキーコードについて、上記のリファレンスでは、特に説明されていません。割り当て可能なキーコードの説明は、WinCE 5.0 のリファレンスにある、次のページをご覧下さい:

 http://msdn.microsoft.com/en-us/library/ms905130.aspx

実機のプロセッサが ARM の場合ですが、Virtual PC ベースのデバイスエミュレータ、つまり、x86 のエミュレータで開発すると、エミュレータでの動作中には起きなかった例外送出が、実機の ARM プロセッサでは発生する、という可能性があります。これが起きる典型的なケースは、TCP/IP 通信や USB 通信でデータ転送を行う際に、転送プロトコルのパケットのペイロードを、構造体に cast して、構造体のメンバにアクセスする場合です。この時、構造体のメンバが 2Byte 以上のサイズの整数値型([unsigned] short, [unsigned] long など)であり、かつ、構造体に cast したメモリ領域のアドレスのアラインメントが、それらの整数値型のサイズに対して揃っていなければ、ARM プロセッサの場合は例外が発生します。しかし、x86 の場合には、アラインメントが揃っていなくても、例外が発生しません。

従って、Virtual PC ベースのデバイスエミュレータでは起きなかった、アプリケーションや汎用デバイスドライバ(USB のクラスドライバなど)の不具合が、ARM プロセッサの実機へ移植すると発生する、という可能性があります。同様のことは、x86 PC でしかテストされていないソフトウェアを、ARM や MIPS などのプロセッサへ移植する場合にも問題となります。これら、ワードアラインメントに関わる不具合を、デバイスエミュレータでの開発時に検出することができるのは、WinCE 6.0 において、ARM ベースのエミュレータが導入された利点の一つだったと思います。

ちなみに、WEC 7 の ARM コンパイラでは、WinCE 6.0 までの ARM コンパイラに比べて Compiler Intrinsics が強化されているため、WinCE 6.0 では発生しなかった例外送出が、WEC 7 では発生する、というケースもあります。つまり、WinCE 6.0 では、バイト単位のアクセス実行にしかコンパイルされなかったソースコードが、ワード単位でのアクセス実行にコンパイルされる場合がある、ということです。同じソースコードから生成されるバイナリの実行効率が上がるようにコンパイラが改善された一方で、その「副作用」として、不用意に書かれたソースコードによって新たに例外送出が発生する場合もある、というわけですね。

ところで、WEC 7 のデバイスエミュレータが、WinCE 6.0 での ARM ベースから Virtual PC ベースに戻ったのは、使用できるホスト PC の周辺機器や実行速度の違いが、主な要因だと思われます。それに加えて、WEC 7 からは PCMCIA がサポートされなくなったことも、要因の一つではないかと思います。WinCE 6.0 のデバイスエミュレータは、NE2000 互換の PCMCIA カードをエミュレートしているのですが、WEC 7 では PCMCIA がサポートされないため、その PCMCIA カード(NE2000 互換のネットワークカード)を利用できないのです。

このことは、Device Emulator の BSP を WinCE 6.0 から WEC 7 へ移植する際に、問題となります。さて、いよいよ、今回のエントリの本題です。

■Device Emulator BSP を WEC 7 へ移植する
これまでの説明で、WinCE 6.0 と WEC 7 では、デバイスエミュレータが異なっており、一方は、ARM プロセッサベース(Samsung S3C2410 のリファレンスボード)、他方は、Virtual PC ベースだということを述べました。また、WinCE 6.0 には付属していた、ARM プロセッサベースのデバイスエミュレータの BSP が、WEC 7 には付属していないことも述べました。そのため、Visual Studio 2008 の Smart Device Development 機能に含まれる、ARM ベースのデバイスエミュレータ(3.0; ARMv5 対応)で WEC 7 を動かそうとしても、OS イメージを作成する手段がないということについても、お分かり頂けたのではないかと思います。

では、Visual Studio 2008 付属のデバイスエミュレータで WEC 7 を動かすには、どうすればよいのでしょうか?答えは簡単です。WinCE 6.0 に付属するデバイスエミュレータの BSP を、WEC 7 に移植して、移植した BSP を使って OS イメージを作成すればよいのです。必要な作業は、以下の通りです:

  1. WinCE 6.0 の %_WINCEROOT%/platform/DEVICEEMULATOR/ ディレクトリを、WEC 7 の %_WINCEROOT%/platform/ ディレクトリへコピーする。
  2. WinCE 6.0 の %_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/PCCARD/ ディレクトリを、WEC 7 にコピーした DEVICEEMULATOR/ ディレクトリの中の SRC/DRIVERS/ ディレクトリへコピーする(※その際、DRIVERS/ の下に PCMCIA/ というディレクトリを作り、その中に PCCARD/ ディレクトリの内容をコピーするのが良いでしょう)。
  3. コピーした DEVICEEMULATOR/ ディレクトリと PCCARD/ ディレクトリの内容、つまり、WinCE 6.0 のデバイスエミュレータの BSP と PCMCIA スタックに対して、WEC 7 への移植(必要な改訂)を施す。
  4. WinCE 6.0 の %_WINCEROOT%/PUBLIC/COMMON/DDK/INC/ ディレクトリから、PCMCIA 関連のヘッダファイル(tuple.h, cardsv2.h, socksv2.h, cardserv.h)を、WEC 7 にコピーした DEVICEEMULATOR/ ディレクトリの中の SRC/INC/ へコピーする。
  5. WinCE 6.0 の %_WINCEROOT%/PUBLIC/COMMON/OAK/files/common.reg ファイルから、PCMCIA の NE2000 互換カードに関するレジストリ項目を、WEC 7 にコピーしたデバイスエミュレータの platform.reg へコピーする。

上で述べたように、デバイスエミュレータがエミュレートするネットワークインタフェースは、NE2000 互換の PCMCIA カードなのですが、WEC 7 では PCMCIA がサポートされず、PCMCIA スタックが付属していません。そのため、WinCE から PCMCIA スタックを移植する必要があります。PCMCIA スタックを移植するにあたっては、%_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/ ディレクトリへコピーせず、デバイスエミュレータ専用のコンポーネントとして、デバイスエミュレータの BSP ディレクトリへコピーするのが良いでしょう。そうすれば、WEC 7 のソースツリー全体を「汚さずに」済みます。なお、デバイスエミュレータの BSP では、DRIVERS/ の下に PCCARD/ というディレクトリがあります。そのため、WinCE 6.0 の PCMCIA スタックのディレクトリ(PCCARD/)を、そのまま DRIVERS/ の下にコピーするのではなく、DRIVERS/ の下に PCMCIA/ というディレクトリを作って、その下に、WinCE 6.0 の PCARD/ ディレクトリの内容をコピーするのが良いでしょう。

デバイスエミュレータの BSP の移植作業は、他の BSP を WinCE 6.0 から WEC 7 へ移植する場合と同様です。中間ディレクトリのパスが WEC 7 で変わったことに伴う、各コンポーネントの sources ファイルの修正や、WinCE 6.0 時点で古くなっていた型定義が WEC 7 では削られてしまったことへの対応などが必要です。たとえば、デバイスエミュレータのディスプレイドライバでは、GPERotate や GPESurfRotate という型を参照していますが、これらは、WEC 7 のヘッダファイルでは削られてしまっているため、WinCE 6.0 のヘッダファイルからドライバのソースファイルへコピーする必要があります。

また、PCMCIA スタックの移植では、%_WINCEROOT%/PUBLIC/COMMON/DDK/INC/ にある devload.h や cebuscfg.h において、PCMCIA 関連の定数の定義が削られてしまっていることへの対応(WinCE 6.0 の同じ名前のファイルから、PCMCIA 関連の定数の定義を PCMCIA スタックのソースファイルへコピーする)も必要です。

上記のように、それなりの移植の手間は必要ですが、WEC 7 のコアのソース(PRIVATE/ および PUBLIC/ ディレクトリ配下)に手を加えずに、BSP および BSP 固有のコンポーネントとして追加するだけで済みます。これは、Windows Embedded Compact が、明確にモジュール化されていることの証だと思います。

移植後の BSP の品質を確保するためには、より多くの作業が必要ですが、「とりあえず動かす」レベルであれば、それほど大変な作業では、ありません。今回は、デバイスエミュレータで WEC 7 が起動して、ホスト PC でのマウス操作でタッチパネル入力をシミュレートでき、さらに、デバイスエミュレータのネットワークアダプタを使えるようになるまでの作業で分かったことを書いています。

最後に、デバイスエミュレータのネットワークアダプタを使えるようにするための移植作業に関して、注意点を二つ書きます。移植作業の細かい説明は省きましたが、次の二点は、見落としがちな要点なので、自分でも移植してみようと思った方のために書いておきます。

  • PCMCIA の NE2000 互換カードに関するレジストリ項目を、WinCE 6.0 の common.reg からコピーする際、BusType を 0 に変更する。
  • PCMCIA ホストコントローラのドライバ(pcc_smdk2410.dll)のロード順序を、後にする。

一番目の注意点ですが、[HKEY_LOCAL_MACHINE\Comm\NE20001\Parms] キーの下にある BusType キー値のことです。WinCE 6.0 の common.reg では、8、つまり PCMCIA の bus type 値が設定されています。しかし、WEC 7 では、PCMCIA がサポートされないため、この値を指定すると、「未知/未定義の bus type」というエラーになってしまい、NE2000 ミニポートドライバの初期化に失敗します。このキー値は、NE2000 ミニポートドライバがロードされた際に、初期化動作の一つとして NdisMRegisterIoPortRange() を呼び出す際に参照されるのですが(※NdisMSetAttributes() を使って、NDIS_HANDLE に対して bus type を設定したのち、NdisMRegisterIoPortRange() を呼び出します)、その呼び出しがエラーとなってしまいます。

このエラーは、bus type を 0 (Internal) に変更することで回避できます。今回、デバイスエミュレータで WEC 7 を動かすために上記の移植を行った際、当初は、NE2000 ミニポートドライバを自前実装し、NdisMRegisterIoPortRange() を呼び出さない仕組みで実装し直す必要があるのではないかと考えました。しかし、分析してみたところ、bus type の設定変更だけで済むことが分かり、移植の手間を減らすことができました。

最後に、二番目の注意点です。オリジナルの実装では、[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\PCC_SMDK2410] キーの下にある Order キー値が 1 になっています。しかし、その設定では、デバイスエミュレータの PCMCIA ホストコントローラがネットワークカードを認識して、NE2000 ミニポートドライバをロードする際に、エラーになってしまうのです。その原因は、解明できていないのですが、pcc_smdk2410.dll がロードされる順序を遅らせることにより、エラーを回避できるようです。設定する値は、dword:10 などにしてみて下さい。

二番目の点については、今後、もし調査の時間がとれたら、さらに追ってみたいと思います。もし、正確な原因や、より根本的な対処策をご存じの方がいらしたら、ぜひ教えて下さい。

1 comment 2012/07/04 koga

.NET CF で Win32 API を P/Invoke する時の注意点

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 です)、少し古く感じられるものもありますが、今でも通用する内容がほとんどだと思います。

Add comment 2012/05/02 koga

WinCE/WEC でシリアルコンソール

Windows Embedded CE/Compact には、PC 用の Windows と同様のコマンドプロセッサ(cmd.exe)が付属しているのは、皆さんご存じの通りです。今回は、このコマンドプロセッサをシリアルケーブル経由で PC から操作する手順について述べます。

さて、Platform Builder のカタログ項目ビューで依存項目を見ると、コマンドプロセッサには、コンソールウィンドウと Telnet サーバーが依存していることが分かります。コンソールウィンドウは、「コマンドプロンプト」のウィンドウですが、コンソールアプリケーションから printf() などで標準出力に出力した場合も、コンソールウィンドウが開いて出力内容が表示されます。PC 用の Windows でも、同様にコンソールウィンドウが開きますよね。つまり、コンソールウィンドウは、標準出力と結びつく(同様に、標準入力とも結びつく)コンソールデバイスとして機能するというわけです。Telnet サーバについては、後で述べます。

■コンソールの出力先に対するレジストリ設定
コマンドプロセッサを、シリアルケーブル経由で PC から操作するには、コンソールの出力先の設定を変更します。この設定は、リファレンスの次のページで説明されています:

 Command Processor Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee506287(v=winembedded.60).aspx

 Command Processor Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee506287.aspx

上のページで説明されているように、[HKEY_LOCAL_MACHINE\Drivers\Console] キー直下の OutputTo の値により、コンソールの出力先を、コンソールウィンドウ以外に設定できるのです。たとえば、2番のシリアルポート(COM2)であれば、次のような設定になります。


[HKEY_LOCAL_MACHINE\Drivers\Console]
    "OutputTo"=dword:2
    "COMSpeed"=dword:1C200

上の例は、COM2 をコンソールの出力先とし、COM2 のボーレートを 115200 としています。

上記のように設定することで、シリアルケーブルで接続した PC から、ターミナルソフトを使ってコマンドプロセッサを操作できます。このように設定しておくと、コマンドプロセッサ(cmd.exe)を起動すると、コンソールウィンドウは開かず、代わりに、シリアルケーブルで接続した PC のターミナルソフトのウィンドウに、コマンドプロセッサのプロンプトが出力されます。そして、コマンドプロセッサのプロンプトに対して、コマンド名やプログラム名をタイプ入力すれば、Windows Embedded CE/Compact 上のコンソールウィンドウの場合と同様、コマンドやプログラムを実行できます。

なお、コンソール出力をデフォルト(0)以外に設定すると、「コマンドプロンプト」を起動しても、ウィンドウが開かず、コンソール出力に設定したデバイスにプロンプトが出力されます。この点は、注意して下さい。

■シリアルコンソールに関する注意点
シリアルケーブル経由でコマンドプロセッサを使う設定については、たとえば、次の Blog エントリでも紹介されています:

 Command line (Console) over serial
 http://ce4all.blogspot.com/2007/06/command-line-console-over-serial.html

このエントリには、いくつか読者コメントが付いており、最後のコメントにある「入力がエコーバックされない」という質問には、回答が付いていないままです。実は、エコーバックされないのが通常動作です。コマンドプロセッサ(cmd.exe)のソースを見ると分かりますが、cmd.exe 自身は、コンソールに対して入力をエコーバックしません。バッチファイル(.bat, .cmd)の中で、echo コマンドによるエコーバックの ON/OFF 切り替えは出来ますが、コンソールに対しては、echo コマンドの実行有無とは関係無く、エコーバック動作は行いません。

cmd.exe のソースコードは、WinCE/WEC のソースツリーの、以下の場所にあります:

 %_WINCEROOT%/private/winceos/UTILS/cmd2/

コンソールに対する入力処理は、cmd.cxx にある cmd_GetInput() で実装されていますが、バッチファイル(.bat, .cmd)の実行ではなく、コンソールからの入力、つまり標準入力に対する入力処理では、エコーバックは行いません。そもそも、コマンド入力を _fgetts() で取得していますので、タイプ入力の1文字ごとにエコーバックする仕組み自体がないのです。従って、シリアルコンソールで入力のエコーバックを実現するには、自前でエコーバック処理を実装する必要があります。

■タイプ入力に対するエコーバック動作
コマンドプロセッサのプロンプトに対してエコーバックする処理は、コマンドプロセッサが標準入出力を行う先の、「コンソールデバイス」が担当する役割です。たとえば、コンソールウィンドウは、コマンドプロセッサがデフォルトで使用するコンソールデバイスであり、コンソールウィンドウにおけるタイプ入力のエコーバック処理は、コンソールウィンドウが行います。

もう一つの例は、Telnet サーバです。以下のページでも言及されていますが、Telnet サーバは、自身をコンソールデバイスとして登録したうえで、コマンドプロセッサ(cmd.exe)を起動します。

 http://msdn.microsoft.com/en-us/library/aa446909.aspx (Implementing a Network Service on Windows CE)

そして、Telnet クライアントでのタイプ入力を受け取り、それを、コンソールデバイスのインタフェースを介してコマンドプロセッサへ渡し、コマンドの実行を依頼します。コマンドプロセッサがコマンドを実行し、標準出力へ出力を行うと、それが(入力とは逆の向きに)コンソールインタフェースを介して Telnet サーバに渡され、Telnet クライアントに送信される、というわけです。なお、Telnet の場合には、Telnet クライアントでのタイプ入力のエコーバック処理は、Telnet クライアント自身が行うことになるはずです。

コンソールウィンドウのソースコードは、開示されていないため、コンソールデバイスとしての振る舞いの詳細を見ることができません。一方、Telnet サーバの方は、
 %_WINCEROOT%/public/servers/sdk/samples/telnetd/
にソースコードが収録されていますので、興味のある方は、ご覧になってみて下さい。telndev.cpp において、Stream Interface Driver のインタフェース(TEL_Init(), TEL_Deinit(), TEL_Open(), TEL_Close(), TEL_Read(), TEL_Write(), TEL_Seek(), TEL_IOControl(), TEL_PowerUp(), TEL_PowerDown() 関数)を実装しており、TEL_Read() と TEL_Write() で、コンソール入出力の処理を行います。

Telnet サーバが cmd.exe を起動する処理は、telnetd.cpp にある TelnetLaunchCmd() ですが、ここでは、cmd.exe を起動する前に、SetStdioPathW() を呼び出して標準入出力を変更します。この時、コンソールデバイスとして、自分自身を実体に割り当てたデバイス(”TEL<インデックス>:” というデバイス名)を設定することにより、cmd.exe の標準入出力先として自身を設定します。Telnet サーバは、Windows の流儀でいう「サービス」、つまり、ユーザモードのデバイスドライバとして動作しますから、このような動作が可能なのです。

■エコーバック動作の実現方策
では、シリアルコンソールの場合に、タイプ入力のエコーバック動作を実現するには、どうすればよいのでしょうか?

方策は、二つ考えられます。一つは、Telnet サーバのように、自前でコンソールデバイス機能を実装し、コマンドプロセッサとシリアルポートの間に介在することにより、シリアルポートからの入力を、一文字ごとにエコーバックする、という方策です。

もう一つは、シリアルポートに対して直接入出力を行い、シリアルポートから文字列が入力されたら、入力された文字列をコマンド名として、都度コマンドプロセッサ(cmd.exe)を起動する、という方策です。

二つの方策のうち、後者の方が、より簡単に実装できます。cmd.exe にコマンドを実行させるには、/Q と /C オプション付きで cmd.exe を実行すればよいのです。つまり、
 ”/Q /C <コマンド名>[ コマンド引数]”
という文字列を、CreateProcess() の第二引数として渡し、第一引数に “cmd.exe” を渡して呼び出せば、cmd.exe がコマンドを実行し、その結果が、レジストリで設定したコマンドプロセッサのコンソール、つまりシリアルポートへ出力されます。その際、CreateProcess() が返したプロセスハンドルに対して WaitForSingleObject() を呼び出せば、コマンド実行の完了を待つことが出来ます。

ただし、二番目の方策については、次の点を注意して下さい:

・コマンド実行のたびに、都度 cmd.exe を起動するので、シリアルコンソールから cd コマンドが実行されても、その結果が保持されない。
 (cd コマンドに対応するとしたら、移動先に指定されたディレクトリを、cmd.exe を起動するプログラム自身が記録することによって、履歴動作を実現する必要がある。)

・cmd.exe を実行している間は、シリアルポートを閉じて、cmd.exe がシリアルコンソールを使用できるようにしなければならない。

二番目の注意点ですが、これは、シリアルポートのデバイスドライバの制約によるものです。シリアルポートは、一つの実体に対し、デバイスを一つしかオープンできないのです。たとえば、COM1 であれば、”COM1:” を指定して CreateFile() を呼び出すことにより、シリアルポートのデバイスをオープンします。この時、”COM1:” のデバイスをオープンしたままの状態で、同じシリアルポート(”COM1:”)に対して CreateFile() を呼び出すと、エラーとなり、オープンできません。この制約は、シリアルポートのデバイスドライバの、MDD レイヤの実装によるものです。

シリアルポートのデバイスドライバの MDD レイヤのソースコードは、
 %_WINCEROOT%/public/COMMON/oak/drivers/serial/com_mdd2/
に収録されています。このディレクトリにある mdd.c で実装されている COM_Open() を見ると、既にオープン済みのシリアルポートに対して呼び出された場合は、ERROR_INVALID_ACCESS をエラーコードとしてセットして、NULL を返すことが分かります。

ところで、シリアルコンソールにバックスペース(’\b’)が入力された場合、エコーバック処理では、入力を一文字消さなければいけません。単純に ‘\b’ をエコーバックしただけでは、PC のターミナルソフトでは、カーソルが一文字戻るだけで、文字が消えないのではないかと思います。カーソルを戻すだけでなく、直前にあった文字を消したい場合には、’\b’ を単純にエコーバックする代わりに、”\b \b” という文字列を出力すればよいでしょう。つまり、カーソルを一文字分戻した後、空白文字を出力することにより、直前にあった文字を消し、再度 ‘\b’ を出力してカーソルを戻す、というわけです。

■シリアルコンソールを利用した管理機能の実現
上で述べた、シリアルコンソールのタイプ入力に対するエコーバック動作の二つの実現方策のうち、後者の方は、シリアルコンソールを使った管理機能を実装する際には、却って向いているかも知れません。前者の方策の場合、OS (WinCE/WEC) の起動完了直後にコマンドプロセッサを起動するように設定すれば、シリアルコンソールにコマンドプロセッサのプロンプトが出力されて、Linux などと同じ感覚で使うことが可能です。しかし、そのようにしてしまうと、全てのプログラムをコマンドプロセッサから実行できてしまいます。

シェルをカスタマイズして、特定の操作しかできないようにしたデバイスや、あるいは、ヘッドレスのデバイスの場合には、限られたコマンドだけをシリアルコンソールから実行できるように制限したり、また、ログイン動作を実装してセキュリティを確保する必要があるでしょう。そのような場合は、単純にコマンドプロセッサをシリアルコンソールへ割り当てるのではなく、コマンドプロセッサを呼び出す「ラッパー」/「ドライバ」プログラムを割り当てて、そのプログラムが、ログイン動作や、シリアルコンソールから実行可能なプログラムの名前だけを受け付けて実行する、という仕組みにするのが良いと思います。

2 comments 2012/03/15 koga

OS の部分アップデート方策~その2(2/2)

前回のエントリでは、ファイルシステム用の永続記憶域を使った部分アップデートの方法を紹介しました。今回は、ROM イメージを複数に分割する方式の部分アップデートについて紹介します。

最初に断っておきますが、この方法は、WinCE/WEC の OS Design のコンフィグレーションでは対応できません。カーネル移植レイヤ(OAL)とブートローダのカスタマイズが必要です。

■複数の ROM イメージ(Multiple Region)
ROM イメージを複数に分割する仕組みは、もともと、WinCE/WEC のカーネルと Platform Builder 内蔵の ROM イメージ生成コマンド(makeimg.exe, romimage.exe)に組み込まれています。ただし、それを有効にするためには、OAL とブートローダが、複数に分割された ROM イメージに対応する必要があるのです。この対応は、必須ではないため、全ての BSP が対応しているわけではありません。対応していない BSP の場合は、OAL とブートローダの実装にカスタマイズを加えなければいけない、というわけです。

複数に分割された ROM イメージに対応した OAL とブートローダは、それぞれ、次の機能を持ちます。

・OAL
 ブートローダが Flash メモリなどから RAM にロードした(XIP の場合は、NOR Flash 上の)、複数に分割された ROM イメージのリストを、カーネルに伝える。この通知は、OAL の初期化時に(つまり、OEMInit() の中で)OEMRomChain を構築することで行う。

・ブートローダ
 Flash メモリなどから、複数に分割された ROM イメージを RAM へロードする(XIP の場合は、ロードしない)。また、ROM イメージをホスト(開発用 PC)から転送された時は、分割された ROM イメージを各々認識し、それらを、Flash メモリなどに書き込む。

ここで、「分割された ROM イメージ」というのは、ROM イメージファイルを単純に分割したものでは、ありません。「分割された ROM イメージ」の一つ一つは、それぞれが ROM イメージのヘッダを持っていて、個別の ROM イメージとなっています。これらを、region と呼びます。どのように region を構成するのかは、config.bib で設定します。makeimg.exe は、config.bib で複数の region が設定されていれば、それに従って、region ごとに ROM イメージファイル(.bin)を生成するのです。

複数の region を WinCE/WEC カーネルに認識させるために、OAL は、初期化時に region のリストを構築します。このリストが、上で述べた OEMRomChain です。OEMRomChain は、ROMChain_t 構造体のリストで、カーネルのスタートアップルーチンによって、スタートアップルーチンが所属する ROM イメージを指すように初期化されます。つまり、デフォルトでは、region が一つだけとなります。その後、スタートアップルーチンが OAL の OEMInit() を呼び出し、OEMInit() が、OEMRomChain に複数の region を登録する(OEMRomChain を初期化し直す)というわけです。

より正確には、OEMInit() が OEMRomChain をどう構築するかは、OAL における OEMInit() の実装次第です。カーネルのスタートアップルーチンは、OEMInit() を呼び出した後、OEMRomChain の内容をチェックして、自身が所属する region が登録されていない場合には、自身が所属する region を OEMRomChain の末尾に連結します。

ROMChain_t 構造体の説明は、WinCE 6.0/WEC 7 のリファレンスの、次のページをご覧ください:

 OEMRomChain
 http://msdn.microsoft.com/en-US/library/ee479246(v=WinEmbedded.60).aspx
 http://msdn.microsoft.com/en-US/library/ee479246.aspx

■Region ごとの部分アップデート
複数の ROM イメージ(Multiple Region)を使うと、部分アップデートする対象(アプリケーションなど)を収録した ROM イメージと、OS 本体を収録した ROM イメージを分けることができます。そして、ブートローダが対応していれば、アップデートしたい ROM イメージ(region)だけをブートローダへ転送し、書き換えることができます。これが、ROM イメージファイルを複数に分割する方式の部分アップデートです。

この方法の利点は、次の通りです:

・永続記憶域上にルートファイルシステムを構築する必要がないため、より耐障害性が高い。
・OS イメージ中の全てのファイルを、アップデートすることが可能。

ここで、「OS イメージ中の全てのファイルをアップデート可能」というのは、ROM イメージの Region 間で shadowing が作用することによるものです。つまり、二つの region に同じファイルが収録されている場合、上述した OEMRomChain のリストにおいて、より先頭に近い方の region 内のファイルが優先され、その後ろにある region 内のファイルが隠されるのです。

OEMRomChain の region 間での shadowin は、前回紹介した、ルートファイルシステム上のファイルによる ROM イメージ内のファイルの shadowing の仕組みと、本質的には同じものです。どちらも、FSD Manager と、カーネルのローダーによって実現されています。前回、カーネルのローダーが、OpenExecutable() から OpenFileFromFilesys() と OpenFileFromROM() を呼び出し、先に呼び出す OpenFileFromFilesys() で executable ファイル(.exe または .dll)が見つかれば、そちらを優先する、と説明しました。実は、OEMRomChain に複数の region が登録されている場合、OpenFileFromROM() は、OEMRomChain のリスト(つまり、region のリスト)を先頭から順に辿り、最初に見つかったファイルをオープンします。従って、よりリストの先頭に近い region 内のファイルが優先される、というわけです。

たとえば、OS 本体を収録した region(※この region に、カーネルのスタートアップルーチンが含まれます)と部分アップデートする対象を収録した region の二つがある場合について考えましょう。OEMRomChain のリストを構築する際、OS 本体を収録した region がリストの末尾要素となり、部分アップデートする対象を収録した region がリストの先頭要素となるようにすれば、両者に同じファイルが存在する場合、部分アップデート対象を収録した region 内のファイルが優先されます。このため、OS 本体を収録した region に収録したデバイスドライバなどをアップデートする際に、その region を書き換える代わりに、部分アップデート用の region の ROM イメージにアップデートしたいファイルを追加して、部分アップデート用の region だけを書き換える、という方策が可能です。

OS 本体を収録した region のサイズが、部分アップデートする対象を収録した region のサイズよりも、ずっと大きければ、小さい方の region(つまり、部分アップデートする対象を収録した region)の方を書き換える方が、より小さなコストで済みます。

部分アップデート用に、複数の ROM イメージ、つまり region の分割を行う方策としては、region を二つに分ける以外に、次のように、四つの region を設定する方策も考えられます(どちらかといえば、極端な例ですが):

 - NK: OS 本体を収録
 - EXT: デバイス固有(BSP 固有)のデバイスドライバやアプリケーションなどを収録
 - UNK: NK region 内のファイルの更新版を収録する(※出荷時は空)
 - UEXT: EXT region 内のファイルの更新版を収録する(※出荷時は空)

このように region を設定すれば、OS 本体(WinCE/WEC のカーネル及び、標準のデバイスドライバやアプリケーション、API の DLL など)と、デバイス(あなたが開発する製品)固有のドライバやアプリケーションを、それぞれ独立してアップデートできます。OS 本体用の ROM イメージと、デバイス固有の ROM イメージは、全体的なアップデートが必要となれば、各々の region(NK, EXT)を書き換えればよいですし、特定のファイルだけをアップデートしたい場合は、各々の部分アップデート用の region(UNK, UEXT)を書き換えればよい、というわけです。

WinCE/WEC は、月例アップデートが公開されます。もし、出荷後に重要な修正がリリースされた場合は、その修正を、出荷済みの製品の OS に反映しなければならないこともあるでしょう。そのような場合に、上の例のような region 分割を設定しておくと、OS 本体の ROM イメージを全て書き換えなくても、修正が加わった .dll や .exe を収録した、部分アップデートの ROM イメージを作り(そのイメージサイズは、OS 本体の ROM イメージに比べて、非常に小さいでしょう)、対応する region を書き換えることでアップデートできます。なお、部分アップデートを繰り返す場合、部分アップデートの ROM イメージには、最新のアップデート対象の .exe や .dll に加えて、以前の部分アップデートの際に収録した .exe や .dll も収録しなければならないことに注意して下さい。そうしなければ、部分アップデートした際に、以前の部分アップデートの内容が消えてしまうからです。

ところで、もし、部分アップデートを繰り返した結果、アップデート対象のファイルが累積して、部分アップデートの ROM イメージが大きくなった場合は、古い部分アップデート内容を破棄して、全体をアップデートする必要が生じる場合も、あると思います。つまり、上の例で言うと、UNK または UEXT region に収録するファイルが増えた場合は、それらの region を空にして、代わりに、NK または EXT region を作り直し、そちらを書き換える必要が生じるでしょう。もちろん、どのように region を分割し、部分アップデートを行うかということは、ケースバイケースですよね。

■Region 分割の手順
ここまでで、ROM イメージを複数に分割する方式の部分アップデートについて、その仕組みと、部分アップデートの方策について説明しました。残りは、実際の手順です。

複数の region に分割した ROM イメージを作成する手順の例は、WinCE 6.0 のリファレンスに載っています。現時点では、WEC 7 のリファレンスには対応するページが見当たりません。

 How to Create a Run-Time Image with Multiple XIP Regions
 http://msdn.microsoft.com/en-US/library/ee482739.aspx

このページでは、CEPC の場合の手順が載っていますが、他のボード(デバイス)の場合も、基本的には同じです。必要な作業を簡単にまとめると、次の通りです:

・config.bib ファイルの MEMORY セクションの記述を書き換える。その際、環境変数 IMGMULTIBIN に対する条件分岐を書き加えて、複数 region 対応するかどうかを、環境変数(IMGMULTIBIN)の設定有無で切り換えできるようにすると便利。
 複数 region 対応用の MEMORY セクションには、NK の他に定義する region を、RAMIMAGE 型の memory region として設定する。また、それらの region の連結内容を記録するための CHAIN region を、RESERVED の memory region として設定する。
 http://msdn.microsoft.com/en-US/library/ee482800.aspx (Modifying the Binary Image Builder Files)

・platform.bib ファイルの MODULES または FILES セクションに、NK 以外の region に収録するファイルを記述する。
 BSP 固有のデバイスドライバやアプリケーションなどを、NK 以外の region に収録する場合は、環境変数 IMGMULTIBIN に対する条件分岐を書き加えて、複数 region 対応の場合のみ、当該 region(「Region ごとの部分アップデート」で示した例で言えば、EXT region)を指定する。
 http://msdn.microsoft.com/en-US/library/ee482800.aspx (Modifying the Binary Image Builder Files)

・OS イメージをビルドする。

・ビルドしてできた .bin ファイルのうち、chain.bib を、Platform Builder から WinCE/WEC デバイスへ転送するファイルとして設定する。
 デバイスへ転送するファイルは、Platform Builder のプロパティダイアログで設定できる。Platform Builder の「プロジェクト」 > 「プロパティ」メニューで、プロジェクトのプロパティダイアログを開き、左側にあるツリービューから、「構成プロパティ」 > 「全般」を選択して表示される画面の、「デバッガ用のターゲットファイル名」というラベルのドロップダウンリストで、転送したい .bin ファイルを選択する。
 http://msdn.microsoft.com/en-US/library/ee482967.aspx (Building the Run-Time Image and Opening a Workspace with Multiple XIP Regions)

以上が、複数の region に分割した ROM イメージを作成して、WinCE/WEC デバイスのブートローダへ転送するために、Platform Builder で行う手順です。なお、上の最後のページ(“Building the Run-Time Image and Opening a Workspace with Multiple XIP Regions”)の説明では、複数に分割した ROM イメージを全て転送する場合、xip.bin を指定すると書かれていますが、これは、うまくいきません。xip.bin ではなく、CAHIN region の内容を収録した chain.bin を指定して下さい。

xip.bin は、全ての region の内容を一つに連結したファイルなのですが、WinCE 6.0 付属のブートローダのライブラリ(blcommon)の実装は、この構造に対応していないように思われます。xip.bin が、残りの .bin ファイルの内容を単純に連結した内容になっており、その結果、blcommon ライブラリでは、先頭の region しか認識されないのです。blcommon ライブラリのソースを読むと、ROM イメージのヘッダに記すマジックナンバーの値として、xip.bin 専用の値が定義されていたような形跡があるのですが、その値(X000FF)が設定されている場合、blcommon ライブラリは、エラーメッセージを出力して停止するように実装されています。

■OAL とブートローダのカスタマイズ
さて、今回の最初の方で、複数に分割した ROM イメージに対応するには、OAL とブートローダのカスタマイズが必要だと述べました。複数に分割した ROM イメージに対応している OAL とブートローダが、それぞれ、どのような働きをするのかについても、ごく簡単に説明しました。OAL とブートローダに対するカスタマイズの内容について、最後に、もう少しだけ補足します。

・OAL のカスタマイズ
 OEMInit() 中で、OEMRomChain を構築しなければならないことは、既に述べました。このことは、次のページに書かれています。

 Booting an Image with Multiple XIP Regions
 http://msdn.microsoft.com/en-US/library/ee483010.aspx

 しかし、このページの説明だけでは、あっさりし過ぎていて、具体的にどうすればよいのか、分かりません。OEMRomChain を構築する例は、x86 用の BSP 共通ソースをご覧になるのが良いでしょう。このソースは、次の場所にあります:

 %_WINCEROOT%/PLATFORM/COMMON/src/x86/COMMON/startup/oeminit.c

 oeminit.c の中にある、x86InitRomChain() が、OEMRomChain を構築する関数です。なお、x86InitRomChain() では、OEMRomChain という名前の変数の値を設定していますが、実は、OEMRomChain はマクロで、その実体は、OEMGLOBAL 構造体のメンバ pROMChain なのです。

 OEMGLOBAL
 http://msdn.microsoft.com/en-us/library/ee478176(v=WinEmbedded.60).aspx  
 http://msdn.microsoft.com/en-us/library/ee478176.aspx

 OEMRomChain を定義したヘッダファイルは、次の場所にあります:

 %_WINCEROOT%/PUBLIC/COMMON/OAK/INC/bcoemglobal.h

 OEMRomChain を構築する処理では、region の連結内容を記録した CHAIN region と、各 regionの ROM イメージヘッダ内容をアクセスする必要がありますが、それらは、以下のヘッダファイルで定義されています:

 %_WINCEROOT%/PUBLIC/COMMON/OAK/INC/romldr.h
 %_WINCEROOT%/PUBLIC/COMMON/OAK/INC/pehdr.h

 CHAIN region の構造については、次のページでも説明されています:

 XIP Chain
 http://msdn.microsoft.com/en-us/library/ee482877.aspx

・ブートローダのカスタマイズ
 ブートローダのカスタマイズについては、次のページをご覧ください。

 Adding Support for Multiple-BIN Image Notification
 http://msdn.microsoft.com/en-US/library/ee479207.aspx

 OEMMultiBINNotify
 http://msdn.microsoft.com/en-US/library/ee478943(v=WinEmbedded.60).aspx
 http://msdn.microsoft.com/en-US/library/ee478943.aspx

 OEMMultiBINNotify() を実装すると共に、CHAIN region の内容および他の region をホストから受け取って Flash メモリなどに書き込む動作、および、region が一つだけ転送された場合(部分アップデートの場合)に、それを正しい場所へ書き込む機能を実装しなければいけません。さらに、ブート動作では、Flash メモリなどに書き込んだ、CHAIN region と他の全ての region を RAM へロードする処理(XIP の場合は、ロードしない)を実装する必要があります。

Add comment 2012/01/13 koga

WinCE デバイスのリモート操作~その1(1/2)

■「リモートディスプレイアプリケーション」
どちらかといえば、デバッグ用の機能になりますが、WinCE/WEC には、ネットワーク経由で画面をリモート表示し、操作する機能が標準で付属しています。CERDisp というユーティリティが、そうです。

CERDisp を OS イメージへ追加するには、次のカタログ項目を選択して下さい:
 
  コア OS
   CEBASE
    コア OS サービス
     デバッグツール
★     リモートディスプレイアプリケーション

Platform Builder のカタログ項目ビューには、「リモートディスプレイアプリケーション」の下に、次の二つの項目があります:
       CE リモートディスプレイアプリケーション
       CE リモートディスプレイユーティリティ
「CE リモートディスプレイアプリケーション」が、CERDisp であり、リモート操作する対象の WinCE/WEC 上で動作します。「CE リモートディスプレイユーティリティ」は、リモート操作を行うホスト(開発用の PC など)で動かすユーティリティで、CERHost という名前です。

CERHost の方は、PC 用にあらかじめビルドされたものが、
 %_WINCEROOT%/PUBLIC/COMMON/OAK/BIN/I386/
に入っています。このフォルダに入っている cerhost.exe を PC で起動しておき、その後、リモート操作したい WinCE/WEC 上で、cerdisp.exe を起動して、両者を接続すると、WinCE/WEC の画面を PC に表示できます。

CERDisp の使い方については、Nicolas BESSON という人の Blog の 2007/12/28 のエントリでも説明されています:

 Enable Remote Display Application – CERDisp
 http://nicolasbesson.blogspot.com/2007/12/enable-remote-display-application.html

また、Mike Hall(Windows Embedded プロダクトグループのテクニカルプロダクトマネージャ)の Blog の 2007/01/04 のエントリでは、”AutoLaunch” というツールと一緒に、CERDisp が紹介されています:

 CE 6.0: Booting processes with Command Line Options.
 http://blogs.msdn.com/b/mikehall/archive/2007/01/04/ce-6-0-booting-processes-with-command-line-options.aspx

このページで紹介されている AutoLaunch は、レジストリの [HKEY_LOCAL_MACHINE\init] キー下に登録して WinCE/WEC の起動直後に起動されるようになっています(そのための .reg ファイルが、ソースファイルに同梱されています)。AutoLaunch は、起動すると、レジストリの [HKEY_LOCAL_MACHINE\Startup] キー下に登録された内容を見て、登録されたアプリケーションを起動します。

AutoLaunch の便利な点は、[HKEY_LOCAL_MACHINE\Startup] キー下に登録されたアプリケーションを起動する前に、WinCE/WEC に IP アドレスが設定されるまで待つ点です。つまり、WinCE/WEC デバイスのネットワークインタフェース(Ethernet コントローラなど)と TCP/IP スタックの初期化動作が完了し、さらに、DHCP が有効な場合は DHCP サーバから IP アドレスの割り当てを受けて IP アドレスが設定されるまでの間、AutoLaunch は、登録されたアプリケーションを起動しません。従って、WinCE/WEC デバイスの IP アドレスが確定する前にネットワークアプリケーションが自動起動されてしまい、そのアプリケーションがエラーを起こしてしまう、という心配がないのです。

また、[HKEY_LOCAL_MACHINE\init] キーに登録する場合とは違い、登録するアプリケーションのコマンド引数を指定できるのも、AutoLaunch の便利な点です。[HKEY_LOCAL_MACHINE\init] キーの場合、起動されたアプリケーションが SignalStarted() を呼び出す際の引数をコマンドライン引数として渡す仕組みになっているため、コマンド引数を指定することが出来ません。詳細については、上記の Mike Hall の Blog を読んでみて下さい。

■CERDisp と CERHost の動作の仕組み
さて、以下では、CERDisp と CEHost の動作の仕組みについて、簡単に説明します。これら二つのアプリケーションのソースコードは、
 %_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/CERDISP/
配下にありますので、興味のある人は、ソースコードも読んでみて下さい。

上で紹介した Nicolas BESSON の Blog では、CERHost がサーバ、CERDisp がクライアントという風に説明されていますが、実際は逆で、CERDisp の方がサーバとして動作します。CERDisp と CERHost は、それぞれ、次のように動作します。

・CERDisp の動作
 - 起動すると、スクリーンサイズと OS バージョンなどのデバイス情報を取得し、クライアント(CERHost)との接続用スレッドを始動する。

 - クライアントとの接続用スレッドは、TCP ソケットを生成して、ポート987番に対し bind() & listen() を実行した後、ブロードキャスト用のスレッドを始動し、その後、accept() を呼び出して TCP 接続されるのを待つ。

 - ブロードキャスト用のスレッドは、UDP ソケットを生成して、同じくポート987に対し、デバイス情報と自身の IP アドレスの対を5秒間隔で繰り返しブロードキャストする。
  ブロードキャスト用のスレッドは、クライアントと接続したことを検出したら、自発終了する。

 - クライアントとの接続用スレッドは、accept() の呼び出しが完了してクライアントと接続したら、ブロードキャスト用のスレッドの終了を待つ。ブロードキャスト用のスレッドが終了したら、クライアントからのイベント通知を受信するスレッドを始動し、その後、画面内容をクライアントへ転送するループ動作を実行する。
  画面内容をクライアントへ転送するループ動作では、レジストリで設定された間隔(デフォルトは、100ミリ秒)で画面内容をキャプチャして、キャプチャした画像データをクライアントに送信する。ここで、送信する画像データは、BitBlt() でキャプチャした画面内容を、行単位でランレングス圧縮したもの。

 - クライアントからのイベント通知受信スレッドは、画面内容を送信するのと同じ TCP ソケットに対して、クライアントから送られたイベントを受信して処理するループ動作を実行する。クライアントから送られたイベントは、マウスイベントであれば、mouse_event() を呼び出し、キーボードイベントであれば keybd_event() を呼び出すことにより、システムのイベントキューへ投入する。

・CERHost の動作
 - 起動すると、UDP ソケットを生成して、ポート987番に対するブロードキャストメッセージを監視し、検出したサーバ(CERDisp)のリストを構築・管理する。

 - 検出した CERDisp のうち、[File] → [Connect...] メニューのダイアログで指定された IP アドレスのものに TCP 接続し、通知されたスクリーンサイズと同じサイズになるよう、自身のウィンドウの(クライアント矩形の)サイズを調節する。

 - 以降は、TCP ソケットから画面内容を受信して、自身のウィンドウに表示する動作と、マウスイベントおよびキーボードイベントを TCP ソケットで送信する動作を繰り返す。

このようにして、CERDisp と CERHost が連携し、WinCE/WEC のリモート操作を実現します。WinCE/WEC には、RDP (Remote Desktop Protocol) のサーバ機能は付属していませんが、CERDisp と CERHost を使えば、似たことができるというわけです。

Mike Hall が彼の Blog で書いているように、この機能は、WinCE/WEC のリファレンスボードや開発ボードを使う際、ボードにモニタや液晶パネルを繋がなくても、開発用の PC だけで、WinCE/WEC と開発環境を同時に操作する際に便利でしょう。最近のボードは、液晶パネルが付属しているものが珍しくありませんが、それでも、開発用の PC だけで操作できるというのは、便利な場合が少なくないと思います。

ところで、注意深い人は、
 %_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/CERDISP/
の下に、CERDisp と CERHost の他にもソースディレクトリがあることに気づいたのではないかと思います。次回は、それについて書きます。

2 comments 2011/12/29 koga

アプリケーションからリブート(または電源 OFF)~その2(2/2)

前回の続きです。WinCE 6.0 のサスペンド動作と、電源 OFF 動作について書きます。

■サスペンド動作
前回、SetSystemPowerState() の第二引数に POWER_STATE_SUSPEND を渡せば、WinCE がサスペンド状態になると書きました。つまり、WinCE をホールト(halt)するには、SetSystemPowerState() を使えばよい、ということです。デバイスの電源を切ってよい状態にすることが目的であれば、それで問題ないでしょう。

SetSystemPowerState() に POWER_STATE_SUSPEND を渡すと、POWER_STATE_RESET を渡した場合と同様、Power Management 機能によって、レジストリやファイルシステムのフラッシュ動作が発火します。その後、Power Management 機能によって、ボードや CPU 固有のサスペンド処理が実行され、電源を切ってよい状態に遷移します。なお、「電源を切ってよい状態」と書きましたが、その状態は、Linux などの UNIX 系 OS で、shutdown コマンドを -h オプション付きで実行した場合に遷移する状態とは、異なります。

上で「サスペンド状態」と書いたように、SetSystemPowerState() に POWER_STATE_SUSPEND を渡すと、いわゆるシャットダウン動作を行うのではなく、サスペンド動作、WinXP で言えば「スタンバイ」動作を行います。つまり、ここで言うサスペンド動作は、Suspend To RAM のことです。Suspend To RAM については、たとえば
 http://e-words.jp/w/STR.html
が参考になるでしょう。Linux で言えば、shutdown コマンドではなく、uswsusp パッケージの s2ram コマンドを使ってサスペンドした状態に相当するのではないかと思います:
 http://suspend.sourceforge.net/
 http://suspend.sourceforge.net/intro.shtml
 http://sourceforge.net/projects/suspend/

さて、サスペンド動作というからには、対応するレジューム動作、つまり、サスペンド状態から通常動作状態に復帰できることを期待したくなります。もちろん、WinCE でも、サスペンド動作とレジューム動作をサポートしています。サスペンド動作による、通常状態からサスペンド状態への遷移、および、レジューム動作による、サスペンド状態から通常状態への遷移(復帰)については、WinCE のリファレンスの次のページで説明されています:

 Suspend State
 http://msdn.microsoft.com/en-us/library/aa916003.aspx

 Resume State
 http://msdn.microsoft.com/en-us/library/aa914868.aspx

上の “Suspend State” のページを見ると、サスペンド動作の発火は、SetSystemPowerState() ではなく、GwesPowerOffSystem() の呼び出しが基点となっています。OS デザインに GWES を組み込んでいる場合、サスペンドするには、SetSystemPowerState() ではなく、GwesPowerOffSystem() を呼び出す方が、より適切です。単に、デバイスの電源を切ってよい状態にするためにサスペンドするのであれば、SetSystemPowerState() を呼び出すだけでも構わないと思いますが、後でレジュームする前提でサスペンドするのなら、GwesPowerOffSystem() を呼び出す必要があります。

GwesPowerOffSystem() を呼び出す代わりに、keybd_event() の第一引数を VK_OFF にして呼び出すのでも構いません。keybd_event() に VK_OFF を渡した場合は、WinCE のスタートメニューから [サスペンド] を選択した場合と、ほぼ等価な動きになります。
GWES と Power Management を組み込んだ OS デザインの場合には(※標準シェルを組み込んでいる場合が、その典型例です)、keybd_event() に VK_OFF を渡してサスペンド動作を発火するのが、GwesPowerOffSystem() を呼び出すよりも簡単かも知れません。

GwesPowerOffSystem() に話を戻します。上の “Suspend State” のページには、GwesPowerOffSystem() を呼び出すと、システムがレジュームするまで呼び出しが完了しないという説明があります。GwesPowerOffSystem() のリファレンスにも、同じ説明が書かれています:
 http://msdn.microsoft.com/en-us/library/aa912858.aspx

レジュームするまで呼び出しから戻らないという振る舞いは、実際には、SetSystemPowerState() が行う PowerOffSystem() の呼び出しにより、カーネルが呼び出す OEMPowerOff() によって達成されます。GwesPowerOffSystem() は、GWES 自身のサスペンド処理を行った後で、SetSystemPowerState() を呼び出します。つまり、keybd_event() や GwesPowerOffSystem()、または SetSystemPowerState() のどれを使ってサスペンド動作を発火しても、サスペンド動作の最終段階で OEMPowerOff() が呼び出されます。

ところで、”Suspend State” のページには、SetSystemPowerState() の呼び出しによって Power manager が行うサスペンド動作において、PowerOffSystem() を呼び出した後、カーネルがサスペンド動作の最終段階の処理を行えるようにするために Sleep(0); を実行する、と書かれています。この説明は、WinCE 6.0 に対しては正しくないように思われます。Power Management モジュール(pm.dll)は、デバイスマネージャにリンクされてロードされますが、WinCE 5.0 までとは異なり、デバイスマネージャは、カーネル(nk.exe)にロードされ、カーネルの一部として動作します。従って、Power manager による PowerOffSystem() の呼び出しは、通常の関数呼び出しと同様にカーネル本体に引き継がれます。つまり、PowerOffSystem() を呼び出したスレッドによって、カーネル本体側で実装されているサスペンド処理が実行されるのです。

Power manager が、PowerOffSystem() を呼び出した後、Sleep(0); を実行することによって、カーネルにサスペンド動作の最終段階処理を行わせる(※スケジューラにスケジューリング処理させ、その結果として、カーネルのサスペンド動作を起動する)という説明は、WinCE 5.0 までのマイクロカーネル構造に基づく説明のように思われます。WinCE 6.0 では、デバイスマネージャはカーネルにロードされる DLL ですが(device.dll)、WinCE 5.0 までは、DLL ではなく EXE であり(device.exe)、独立したプロセスとして動作する仕組みでした。その場合、Power manager から PowerOffSystem() を呼び出すと、デバイスマネージャのプロセスからカーネルのプロセスに対する呼び出しになりますので、カーネル本体側のサスペンド処理は、PowerOffSystem() を呼び出したスレッドとは別のスレッドによって実行される筈です。このため、Power manager が PowerOffSystem() の呼び出しを行った後、カーネル本体のサスペンド処理が実行されるのを待たずに、その先の処理へ進んでしまうことがないように、Sleep() を呼び出すのではないかと思います。

ここで、「その先の処理」というのは、PowerOffSystem() の呼び出しから戻った後の処理、つまり、Power manager によるレジューム動作を指します。Power manager によるサスペンドとレジューム処理は、
 <WINCE600>/PUBLIC/COMMON/OAK/DRIVERS/PM/PDD/DEFAULT/platform.cpp
にある PlatformSetSystemPowerState() で実装されています(431行目付近)。

PowerOffSystem() に話しを戻します。PowerOffSystem() に対応するカーネル本体側のサスペンド処理は、
 <WINCE600>/PRIVATE/WINCEOS/COREOS/NK/KERNEL/kwin32.c
にある NKPowerOffSystem() で実装されています。このソースを見れば、”Suspend State” のページに書かれている、カーネルが行うサスペンド動作の最終段階処理が実際に行われているのが分かるでしょう。NKPowerOffSystem() では、まず、自身のスレッド優先度を最高値(0)に変更して、次に GWES と FileSys の Power off 動作を行った後、内部関数の CallOEMPowerOff() を介して OEMPowerOff() を呼び出します。OEMPowerOff() の呼び出しから戻ると、上の “Resume State” のページに書かれた動作を行います。

OEMPowerOff() は、OAL の関数であり、ボードや CPU ごとに異なるサスペンド処理の実装を提供します:
 http://msdn.microsoft.com/en-us/library/aa916963.aspx

OEMPowerOff() の役割は、レジュームに必要なデータ(レジスタの内容など)を RAM に退避した後、CPU をホールト(halt)状態にして、外部からの割り込みが発生するまで CPU を休眠させることです。休眠を解除するトリガとなる割り込みは、CPU をホールト状態にする前に設定します。割り込みが発生して CPU が起床すると、RAM に退避していたデータを使って、ホールト前の状態を復元し、OEMPowerOff() の呼び出し元に制御を戻す(return する)のです。

デバイスエミュレータの場合、OEMPowerOff() のソースは、
 <WINCE600>/PLATFORM/DEVICEEMULATOR/SRC/OAL/POWER/power.c
です。このソースを見ると、OEMPowerOff() の中で CPUPowerOff() という関数を呼び出しており、CPUPowerOff() が、CPU をホールト状態にする処理を行っていることが分かります。CPUPowerOff() は、
 <WINCE600>/PLATFORM/DEVICEEMULATOR/SRC/OAL/OALLIB/cpupoweroff.s
で実装されています。興味がある人は、cpupoweroff.s にある CPUPowerOff() のソースを見てみて下さい。レジュームに必要なデータを RAM へ退避した後、サスペンド状態を解除するトリガとなる割り込みに対する設定を行い、CPU に対するクロック供給を停止してホールトさせる様子が分かるでしょう。なお、CPU をホールトさせる処理自体は、cpupoweroff.s ではなく、同じディレクトリにある startup.s で実装されています。startup.s で実装されている StartUp() には、通常の呼び出しの場合に、冒頭のジャンプ命令(’b ResetHandler’)によってスキップされる箇所があるのですが、そこ(通常の呼び出しではスキップされる箇所)で、CPU をホールトさせています。

デバイスエミュレータの OEMPowerOff() の実装では、CPU をホールトさせた後(S3C2410 を SLEEP モードに設定した後)、外部割り込みによって CPU が通常モードに復帰すると、startup.s にある StartUp() のコードの、’ResetHandler’ というラベル以降を実行するようになっています。これは、ホールト状態から通常モードに復帰した後に行わなければならない、CPU 内部の再初期化処理が、パワーオンリセット時の初期化処理と共通する部分が多いため、コードを共有するための措置ではないかと思われます。StartUp() の ‘ResetHandler’ 以降のコードでは、パワーオンリセットによる呼び出しでないかどうかを要所ごとに判定しながら初期化処理を進め、レジューム用のデータが正しく退避されていると最終的に判断できたら、cpupoweroff.s の ‘Awake_address’ ラベルへジャンプするようになっています。’Awake_address’ ラベル以降のコードでは、RAM に退避していた CPU のレジスタ内容を復元した後、CPUPowerOff() の戻り先(呼び出し元)アドレスへジャンプします。これによって、CPUPowerOff() の呼び出しが完了し、OEMPowerOff() の実行が再開します。

OEMPowerOff() は、CPUPowerOff() の呼び出しから戻った後、レジューム動作を開始します。同様に、OEMPowerOff() を呼び出した NKPowerOffSystem() はカーネル本体のレジューム動作を実行し、さらに、その呼び出し元、つまり、PlatformSetSystemPowerState() や、SetSystemPowerState() ならびに GwesPowerOffSystem() において、それぞれのレジューム動作が実行され、サスペンド前の状態に復帰する、というわけです。

なお、デバイスエミュレータをサスペンドすると、デバイスエミュレータのウィンドウが閉じてしまい、以降は操作できなくなってしまいます。その状態では、デバイスエミュレータのプロセスは終了せずに残っているため、タスクマネージャを使って kill しない限り、[ターゲット] -> [デバイスの接続] メニューで Platform Builder からデバイスエミュレータを起動し直すことすら、できないのです。デバイスエミュレータをサスペンドした後、電源ボタンやキーボードによる割り込みの発生をエミュレートできれば面白いのですが、その方法は、ないようです。とりあえずの対策として、power.c を編集し、OEMPowerOff() からの CPUPowerOff() の呼び出しをコメントアウトして使ったという例が、Bruce Eitman という方の Blog エントリで紹介されています:
 http://geekswithblogs.net/BruceEitman/archive/2008/08/11/platform-builder-getting-the-emulator-to-resume.aspx
 http://geekswithblogs.net/BruceEitman/archive/2008/05/13/profile-of-bruce-eitman.aspx

サスペンドの開始からレジュームの完了に至るまでの一連の動作を、デバイスエミュレータとカーネルデバッガを使って確認するには、上の Blog エントリで紹介されているように、OEMPowerOff() からの CPUPowerOff() の呼び出しをコメントアウトするのが簡単です。
僕も、この記事を書く際に、その方法を使ってサスペンドとレジュームの動作をカーネルデバッガで追跡しました。

サスペンド動作について、もう一つだけ書いておきます。この項の冒頭で、SetSystemPowerState() によるサスペンド動作では、レジストリやファイルシステムのフラッシュ動作を発火させ、その後で、ボードや CPU 固有のサスペンド処理を実行すると書きました。レジストリやファイルシステムのフラッシュ動作を発火させるのは、メモリ上のキャッシュのみが更新されたまま電源が切られてしまい、永続記憶域上のデータに不整合が発生することを防ぐためでした。しかし、フラッシュ動作を発火させた後、サスペンドが完了するまでの間に、書き込みモードでファイルを開いているアプリケーションがファイルに対する書き込みを行ったら、どうなるのでしょうか?

心配は、不要です。ファイルシステムを管理する File System Disk Manager (fsdmgr.dll) によって、フラッシュ動作後のファイルアクセスは、ブロックされます。WriteFile() や ReadFile() などのファイルアクセス API の全ての呼び出しは、ファイルシステムの実体(ファイルシステムのドライバ)による実装が呼び出される前に、fsdmgr.dll によるチェックが行われ、そのファイルシステムが所属するボリュームが電源 OFF 状態の場合、電源が ON されて利用可能になるまでの間、ブロックされる(待たされる)ようになっています。レジストリやファイルシステムのフラッシュ動作においては、ファイルをアクセスしているスレッドが全てアクセスし終えるのを待ち、アクセスし終わった時点で、メモリ上のファイルキャッシュを全てコミットしてから、マウントされている全てのボリュームを電源 OFF 状態にします。それ以降にファイルアクセスを行おうとしたスレッドは、ファイルアクセス API 内部でブロックされますので、フラッシュ動作後に再度メモリ上のファイルキャッシュ内容が更新されてしまう、ということは起きないのです。

fsdmgr.dll による、ボリュームの電源 OFF 時の保護動作に興味がある人は、ソースをご覧になってみて下さい。fsdmgr.dll のソースは、
 <WINCE600>/PRIVATE/WINCEOS/COREOS/STORAGE/FSDMGR/
にあります。このディレクトリの、mountedvolume.{hpp,cpp} や {file,fsdmgr,volume}api.{cpp,hpp} が、関連するソースです。

■電源 OFF について
サスペンド動作の項が、だいぶ長くなってしまいました。電源 OFF については、短めにします。

実は、WinCE の Power Management では、電源 OFF 状態のサポートは、標準では提供されていません。つまり、デフォルトの実装では電源 OFF 状態がサポートされていないため、WinCE デバイスを実装する側(OEM)での対応が必要です。実際、SetSystemPowerState() に POWER_STATE_OFF を渡しても、エラーが返ります。エラーが返る直接の原因は、Power Management に関するレジストリ設定で、POWER_STATE_OFF に対応する電源状態の指定が存在しないからです。デバイスが取りうる電源状態は、D0~D4 がありますが、電源 OFF 状態を指す D4 と、POWER_STATE_OFF とを関連づける指定が存在していないため、SetSystemPowerState() に POWER_STATE_OFF を渡すと、無効な状態が指定されたものとして扱われ、エラーになるのです。

D0~D4 の電源状態の説明は、WinCE のリファレンスで “Device Power States” を見て下さい:
 http://msdn.microsoft.com/en-us/library/aa932261.aspx

Power Management に関するデフォルトのレジストリ設定は、
 <WINCE600>/PUBLIC/COMMON\OAK/FILES/common.reg
にあります。common.reg をテキストエディタで開き、’HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\State’ で検索してみて下さい。このレジストリキーによる、電源状態の指定は、WinCE のリファレンスの “Mapping of System Power States to Device Power States” で説明されています:
 http://msdn.microsoft.com/en-us/library/aa920052.aspx

common.reg での指定を見ると、”Flags” キーに POWER_STATE_OFF を指定した有効な設定が、存在しません。POWER_STATE_OFF と D4 を関連づけた、ShutDown という名前の指定があるのですが、コメントアウトされています。コメントアウトされた ShutDown に対しては、以下のコメントが記されています:

;;;; Entering this system power state shuts down the system. All devices are powered off,
;;;; resuming may require user intervention. The system will cold boot on resume.
;;;; Supporting this power state requires that the OEM customize the Power Manager
;;;; to recognize POWER_STATE_OFF and take platform-specific action to remove
;;;; power.

ここで、ShutDown の指定を有効にするには、Power Manager のカスタマイズが必要だと書かれています。これに対応するのが、Power Manager (pm.dll)のソースのうち、
 <WINCE600>/PUBLIC/COMMON/OAK/DRIVERS/PM/PDD/DEFAULT/platform.cpp
です。platform.cpp にある PlatformSetSystemPowerState() の本体で、POWER_STATE_RESET に対するチェックを行っている箇所に、以下のコメントがあります(358行目付近):

// Handle resets and shutdowns here, after flushing files. Since Windows CE does
// not define a standard mechanism for handling shutdown (via POWER_STATE_OFF),
// OEMs will need to fill in the appropriate code here. Similarly, if an OEM does
// not support IOCTL_HAL_REBOOT, they should not support POWER_STATE_RESET.

あなたがアプリケーションの開発を担当していて、お使いの WinCE デバイスがソフトウェアによる電源 OFF 機能をサポートしている場合には、SetSystemPowerState() に POWER_STATE_OFF を渡すことによって、アプリケーションから電源 OFF できるかも知れません。それができるかどうかは、お使いの WinCE デバイスのハードウェアが、ソフトウェアによる電源 OFF 機能を持っているか(※CPU から電源回路を OFF するための支援機能がハードウェアに組み込まれているか)と、その WinCE デバイスの OAL が、POWER_STATE_OFF に対する KernelLibIoControl() をサポートしているかにかかっています。

SetSystemPowerState() で POWER_STATE_OFF を使えるかどうかを簡単に調べる方法は、実際にテストコードを書いて動かしてみるか、または、レジストリの HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\State キー配下を調べ、”Flags” キーに POWER_STATE_OFF (0×00020000) を指定したエントリがあるかどうかを確認することだと思います。

POWER_STATE_OFF の指定を使えない場合は、手作業でデバイスの電源を切るしかありません。もちろん、電源を切る前には、デバイスをサスペンド状態にして下さい。サスペンド状態への遷移をアプリケーションから行う場合は、「サスペンド動作」の項で述べたように、keybd_event(VK_OFF, 0, 0, 0); を実行するのが簡単でしょう。OS デザインに Power Management と GWES を組み込んでいない場合は、WinCE のリファレンスの次のページが参考になるでしょう:

・OEM Power Off State
 http://msdn.microsoft.com/en-us/library/aa913544.aspx

このページの説明には、GWES や Power Management が組み込まれていない場合、サスペンドするには、次の呼び出しを実行すればよいと書かれています:

 FileSystemPowerFunction(FSNOTIFY_POWER_OFF);
 PowerOffSystem();

Add comment 2009/07/21 koga

Previous Posts


Categories

Links

Posts by Authors

Recent Posts

Calendar

2021年4月
« 9月    
 123
45678910
11121314151617
18192021222324
252627282930  

Posts by Month

Posts by Category

Meta