WWWWARD > .NET Claimwork 3.0

少し前から気になっていました。Movable Type 4.22で再構築がうまくできない事が。

と言っても、再構築がすべて失敗する訳ではなく、一つ一つ調べてみると、再構築できないのは月別アーカイブだけです。しかも質が悪い事に、何かのエラーを出力してくれる訳でもなく、記事40件分の再構築に成功した(らしい)後、残りの3件がどうしても再構築できないのか、数秒間隔で延々と再読込を続けます。

Googleで探してみましたが、どうもこの現象に遭遇している人は他にはいないのか、ヒットしません。しかし近い所で「再構築に失敗する場合、一度に再構築する記事の数を減らすと直る(事がある)」という情報があったので(どこが出所なのか判らないので特定のリンクはしませんが、Movable Type 再構築 失敗 で検索してみるとこんな感じ)、Movable Typeのアプリケーションディレクトリを%MTAppDir%とした場合の%MTAppDir%/lib/MT/Core.pmの321行目、EntriesPerRebuildの初期値を10に減らしてみました。

# %MTAppDir%/lib/MT/Core.pm line 321
# 変更前
'EntriesPerRebuild' => { default => 40, },
# 変更後
'EntriesPerRebuild' => { default => 10, },

10件の記事しか再構築できなくなってしまいました

まあでも影響している事は間違いあるまい。そんな訳で今度はEntriesPerRebuildを使ってgrepしていると、%MTAppDir%/lib/MT/CMS/Blog.pmの676行目や728行目に気になる単語が。

# %MTAppDir%/lib/MT/CMS/Blog.pm line 676, 728
Limit => $app->config->EntriesPerRebuild,

Limit? Blog.pmを覗いてみると、これはサブルーチンrebuildを呼び出す際の引数の一つのようです。

# %MTAppDir%/lib/MT/CMS/Blog.pm line 671-678
# 変更前
$app->rebuild(
  BlogID => $blog_id,
  ArchiveType => $type,
  NoIndexes => 1,
  Offset => $offset,
  Limit => $app->config->EntriesPerRebuild,
  FilterCallback => $cb,
) or return $app->publish_error();

一回分の再構築で処理される記事の数が制限されているのでしょうか。どう扱われているのか追跡してみましたが、%MTAppDir/lib/MT/WeblogPublisher.pmのサブルーチンrebuildの中、MT::Entry->load_iterという呼び出しの所で吸収されてしまいました(これがどこに書いてあるのかよく判らない)。

色々試してみてただ一つ判ったのは、再構築が一回だけで終われば、(月別アーカイブ以外も含めて)すべての再構築がうまくいく事です。つまり、全記事数以上の値をLimitに与えてやれば、一応、成功します。

# %MTAppDir%/lib/MT/CMS/Blog.pm line 671-678
# 変更後
$app->rebuild(
  BlogID => $blog_id,
  ArchiveType => $type,
  NoIndexes => 1,
  Offset => $offset,
  Limit => $total,
  FilterCallback => $cb,
) or return $app->publish_error();

但し、いくらEntriesPerRebuildを小さくしても進捗状況は見られません。でもまあ大した害じゃないので許せる範囲内でしょう。

これはバグなんでしょうか、それとも個人ライセンス(無償)使うならこれくらいの制限は自分で外せって事なんでしょうか。バグである事を祈ります。

迷走してハンドルをCrowned ClownからE+Xに変えました。以下、言い訳。

  1. 歴史を詳しく知っている訳ではないけれど、インターネットは歴史的に実名を使って情報を交換する為のネットワークだったはず。必ずしも実名を使う必要性は無かっただろうけど、研究者が情報を交換するのに自分が誰だか判らないようにしていたのでは、円滑な研究はできそうもない(責任や栄誉の所在も判らないし)。
  2. そういった慣習が連綿と受け継がれてきた為か、あるいはその他の理由に因るものか、W3Cの勧告文書なんかも、編集者はみんな実名で記名している。
  3. 翻訳版を作って公開している人達も、殆どは実名のようだ。
  4. 合わせた方が失礼が無いかな、と思って合わせた。
  5. ハンドル使ってた意味無いじゃん。
  6. ついでに言うと、思いついた時は語呂の良さしか考えていなかったけれど、crownという単語は英語圏では意味が良すぎる。
  7. しかも、どっかの漫画でそんな名前の何かが出て来るらしい(だいぶいいかげん)。
  8. ハンドル使うのやめよっか。
  9. それでもいいけど、なんかあった方が良さそうだ。
  10. そんなに実名から乖離しないで、奇を衒いすぎないで、かつ少しひねってあるくらいのものがいいな。
  11. 名字を見ていると、最初の文字が E, +, X の組み合わせで書ける事に気がついた。
  12. 得意じゃないけど数学は好きだし、何だかネイピア数(と言うらしい)eと変数xの和みたいで面白いじゃん。
  13. なんだかやっぱり奇を衒ってしまっているような気もするけど無視する。
  14. 多分変えた所で誰も迷惑しないだろうから、今の内に変えちゃえ。

XSLT 1.0の言う所の結果ツリーフラグメントを使わないように、XSLT文書を書き直しました。今まで結果ツリーフラグメントをノードセットに再評価してxsl:for-eachをかけていた所はすべて、デリミタで分けられた文字列を再帰で順番に処理するようになりました。どちらがより負荷が大きいのか判りませんが、何れにせよこの程度なら大したものではなかったようです。

さて、このように書き直した事で、IE 6.0以降やSablotronだけでなく、libxsltやFirefox 2.0以降でも変換できるようになった為、Firefox 2.0以降の場合も変換をクライアントに委譲する事にしました。そう言えばOperaもdocument関数が実装されるんだったな、と思って調べてみた所、Opera 9.50から正式に実装されていたようです。これならOperaでも丸投げできる、と思っていた所、案の定残念な事にOperaではdocument関数がうまく機能していないようです。

はて、こんな事でOpera 9.50のChangelogAdded support for XSLT document() function.などと載せるものだろうか......と思い、少々実験した所、次の事が判りました。

XSLT文書と同階層のelements.xmlに対して、document( 'elements.xml' )
問題無く読めます。
XSLT文書の親階層のelements.xmlに対して、document( '../elements.xml' )
問題ありません。
XSLT文書の親階層のdataディレクトリの中のelements.xmlに対して、document( '../data/elements.xml' )
読めません
/reference/xhtml11/dataのelements.xmlに対して、document( '/reference/xhtml11/data/elements.xml' )
勿論読めません

......何この不思議実装。Operaの開発チームの人達は、スタイルシートとデータを別のディレクトリに置くって事を誰一人しなかったのか。それともこんな事が起こるのは僕の所だけ?

document関数が失敗する事によって変換すべてが失敗する事はありませんが、document関数が空のノードセットしか返してくれないので、結局document関数は使えません。これはdocument関数の振る舞いがおかしいと言うよりは、単にフルパスの解決に失敗しているだけのように思えるので、もしバグであれば簡単に直りそうですが......どこに言えばいいんだろ。ちょっと探してみるか。

追記: レポートの為にもう少し詳しく現象を調べようとした所、どうやらdocument関数の戻り値が必要になった時点でGETリクエストは送っている模様。しかし戻り値は空のまま。というより、XSLTプロセッサ自体がその時々で挙動が変わったりして(何に依存しているのかはもう調べるの面倒)、不安定。ひょっとしたら根の深い問題なのかも知れない。やっぱりOperaは保留。

今手許で使えそうなXSLTプロセッサ。XSLT 2.0をサポートするXSLTプロセッサは、Java VM上で動くSaxonだけしか今の所無いらしいので(知らないだけかも)、多分すべてXSLT 1.0に準拠しているはず。従って、xsl:paramxsl:variable要素の内容はResult Tree Fragmentsになります(ここではノードセットと結果ツリーフラグメントで書きました)。

で、例によって、XSLT 1.0では結果ツリーフラグメントに対して文字列操作しか許されないんですが、WWWWARDで使っているXSLT文書では結果ツリーフラグメントをノードセットに評価し直すという(XSLT 1.0勧告には無い、各実装の独自拡張の)機能を使っているんですね。別にこれはそういう機能がある事を知ってから使い始めた訳ではなく、「こう書けると綺麗だ」と思って書いたら使えて、あとで確認してみると独自拡張だったという......(あんまりよろしくない経緯)。それはともかく、IEに搭載されているXSLTプロセッサや、PHP 4のSablotronではこれが使えます。しかし、libxsltでは使えない。無論XSLT 1.0でこれをするのは邪道なので使えないのがあるべき姿なのかも知れませんけど......。

現在s26.xrea.comサーバに入っているPHPのバージョンは4.4.8だそうです。Sablotronも使えるようになっています。よって今は問題無いんですが、PHP4のサポートは2007-12-31に終わっています。何れPHP5が取って代わるでしょうが、そのPHP5はSablotronを標準装備していません(PECLに移されたって書いてあるけど、見つからない)。代わりにlibxsltが手厚くサポートされているようですが、前述の通り、libxsltはこの「結果ツリーフラグメントのノードセットとしての再評価」を許していないようです(そういう事をしようとすると、それより深い所の変換がすべてキャンセルされる)。

行くべき道は......

  • ほっとく。これは楽で良い。......じゃなくて。
  • XSLT文書を書き直す。XSLT 1.0の機能のみをサポートするプロセッサで変換できるように。元々結果ツリーフラグメントの再評価なんてやりだしたのは、綺麗に書けるからであって、必要不可欠だったからではなかった、はず......(うろ覚え)。尤も単なる文字列処理だけでやろうとすると、若干トリッキーになるか、再帰が深くなるかしそうだけど、許容範囲かな。
  • XSLT 2.0プロセッサ全盛時代の到来を待つ。テンポラリツリーならノードセット操作も問題無し。唯一の問題は、それがいつ来るのか。ひょっとしてもう来てたりして(時事に疎い)。......無いか。夢か。

という訳で、今の内に書き直すべきでしょうか。PHP5+libxsltで変換できると、ローカルマシンでFirefoxを使って変換結果確認ができるので若干幸せなんですよね。若干。

さあ、それは良いとしても......libxsltって、xsl:templatepriority属性は無視なんですかね? てっきりlibxsltではxsl:importが使えないと思っていた時(今さっき)に、xsl:includeを使ってなんとかしようと思ったら、priorityが違っても"duplicate name!"って怒られたのですが......いや、これはさっきまでテンプレートルールの衝突の解決方法を知らなかったなんちゃってXSLTユーザの苦情じゃないですね、はい。

公開から随分時間が経ったようですが、Movable Type 4を導入してみました。

以前はMovable Type 3を入れており、当然バージョン4が出たのも知っていたのですが、無償の個人ライセンスがバージョン4になってもある事を知らず、今までずっとバージョン3を使っていたのです(探し方が悪かったのか、あるいは公開当初は有料ライセンスの差別化の為に用意されていなかったのか)。

インストール時に"Can't locate object method "lowercase_jcode" via package "MT::I18N::default" (perhaps you forgot to load "MT::I18N::default"?)at lib/MT/I18N/default.pm line 110."などと言われて動かなかったトラブルはhttp://www.movabletype.jp/faq/cant-locate-object-method-lowe.htmlで解消され、ドキュメントに書いてあった手順も大体踏みましたが、まだまだ慣れません。むしろMovable Type 3にも慣れていたとは言い難いような。とりあえずあちこち触るしかないですね。

面倒くさいMovable Type 4へのバージョンアップをしようと思い立った理由は、4.2以降に付いたらしいコミュニティ機能(掲示板など)に帰せられる所が大きかったりします。AntiSpamなんちゃらが付いているMovable Typeなら、世の掲示板のように荒れない掲示板がお手軽に準備できるんじゃないかな、と思いまして。

何で掲示板なんかを欲しいと思ったかというと、現在水面下で妄想中の(まだ全然具体的なものはできあがってませんが)企画で必要になりそうだった為です。その企画の方向性はWWWとは殆ど関係無かったりしますが......別アカウントは取れそうにないしなあ。しかし50MBは少ないなあ。

ちょっとアカウント名(w4ard)とサイト名(WWWWARD)の幅を狭めすぎたかと後悔しています。

先日、ちょっとしたツールを作る必要があって、折角だからと思ってWPFで作ってみました。WPFの概要については、++C++; // 未確認飛行物体 C - Windows Presentation Foundation 概要(WPF)辺りを見ると簡潔で判りやすいのではないでしょうか。使ってみた感じでは、Direct Xを汎用化したというよりも、GDIを拡張・汎用化・高速化した、といった感じのライブラリです。Visual Studio 2008以降でGUIアプリケーションを作るなら、今までのSystem.Windows.Formsを使う前に検討する価値はあると思います。

ま、前置きはさておき、WPFでメニューを作る時に戸惑って手間取ったので、ちょっと覚え書き。半ば自分用なので、前提知識の細々とした説明はしません。++C++; // 未確認飛行物体 C - .NET Framework 3.0で全体像を掴んだら、MSDNライブラリのWPFの項、Windows Presentation Foundationを必要に応じて参照すると良いでしょう。今回の話は、主にここのコマンド実行の概要に書いてある話の要約です。

メニュー

作ったツールにもメニューを付けようと思いました。メニューを作る方法自体はすぐ見つかります(何しろツールボックスにも入ってますから)。例えば、まっさらなウィンドウにドックパネルを作って、その一番上にメニューを付けるには、次のようなXAML文書を作ります。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" />
        <Separator />
        <MenuItem Header="他の形式で形式で入力(_I)" />
        <Separator />
        <MenuItem Header="終了(_X)" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

ファイルポップアップメニューの中に、三つのアイテムがあるだけの単純なメニューです。なお、このXAMLソースは、CommandSample名前空間のMainWindowウィンドウに適用する事が想定されています(1行目のx:Class属性指定)。

単純なメニューコマンド処理

これだけではメニューは表示されるだけで何ら役に立ちません。このメニューを機能させる一番簡単で単純な(しかし使われない?)方法は、メニューアイテムのClickイベントを処理する事です。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" Click="FileNew" />
        <Separator />
        <MenuItem Header="他の形式で入力(_I)" Click="FileImport" />
        <Separator />
        <MenuItem Header="終了(_X)" Click="FileExit" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

上のように、Click属性指定でイベントハンドラの名前を値にすれば、クリックされた時にそのイベントハンドラが呼び出されるようになります。

// MainWindow.xaml.cs
using System.Windows;
// ……

namespace CommandSample {
  public partial class MainWindow : Window {
    // ……
    private void FileNew( object sender, RoutedEventArgs e ) {
      // 新規作成の処理
    }
    private void FileImport( object sender, RoutedEventArgs e ) {
      // 別形式入力の処理
    }
    private void FileExit( object sender, RoutedEventArgs e ) {
      // 終了の処理
    }
    // ……
  }
}

イベントハンドラはRoutedEventHandler型です。EventHandlerではEventArgsを第二引数に取っていた所を、RoutedEventArgsを第二引数に取る点が違います。でも、殆ど同じですね。

これでもまあ動くのですが、どうやらWPFで推奨されるメニューコマンド処理手法はこれではないようです。WPFでは、メニューから実行されるコマンドというのはメニューに固有の概念ではなく、メニューというものは、もっと一般化された「コマンド」を呼び出す一つの手段であると解釈されるようです。

コマンド実行の概念

コマンド実行の概要に拠れば、コマンド実行には四つの主要な概念があるんだとか。

  • コマンドは、実行されるアクションです。
  • コマンド ソースは、コマンドを呼び出すオブジェクトです。
  • コマンドの対象は、コマンドが実行されているオブジェクトです。
  • コマンド バインディングは、コマンド ロジックをコマンドに割り当てるオブジェクトです。

コマンドは、先程挙げた例だと、「新規作成する」「他の形式で入力する」「終了する」といった、抽象化された操作です。このコマンドは必ずしもメニューから実行されるものではありません。メニューや、キージェスチャ(キーボードショートカット……例えばCtrl+Nとか)、それにボタンから呼び出されたりします。こういった、コマンドを呼び出すものをコマンドソースと言うようです。コマンドの対象というのは、まあ文字通りですが、そのコマンドを何に対して行うのかを示します。「終了する」なんてのは必ずアプリケーション全体対象ですから気にする必要はありませんが、例えば「コピー」操作なんかは対象を決めないとやりようがありませんね。そんな訳で、コマンドの対象はXAMLの中で明示的に示す事もできます。特に指定しなければ(今回は指定しません)、フォーカスのあるコントロールになります。コマンドバインディングは、コマンドとイベントハンドラとの結びつきの事です。後述します。

コマンド

コマンドは、前述の通り抽象化された操作です。特定のアプリケーションや、実際に行われる処理が何であるかなどに依存しない概念です。この為、一般的なコマンド群というのは、WPFで既に定義されています。その一つがSystem.Windows.Input.ApplicationCommandsです。ここには、NewとかOpenとかSaveとか、何かのデータオブジェクトを扱うようなアプリケーションなら、その大半に存在するような操作のコマンドが定義されています。さっきの例では「新規作成する」というコマンドがありました。これにはApplicationCommands.Newがぴったりです。とりあえずこれを「新規作成」メニューアイテムのコマンドとします。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" Command="New" />
        <Separator />
        <MenuItem Header="他の形式で入力(_I)" />
        <Separator />
        <MenuItem Header="終了(_X)" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

ApplicationCommandsなどのWPFクラスのメンバは、文字列で書いても勝手に変換してくれるので、すっきり書く事ができます。

コマンドバインディング

繰り返しますがコマンドは最初から特定の処理と結びついている訳ではありません。メニューアイテムが選択されてコマンドが呼ばれても、そのままでは何も実行されません。コマンドバインディングは、コマンドと特定のイベントハンドラとの結びつきを記述します。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <Window.CommandBindings>
    <CommandBinding Command="New" Executed="FileNewCommandExecuted"
      CanExecute="FileCommandsCanExecute" />
  </Window.CommandBindings>
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" Command="New" />
        <Separator />
        <MenuItem Header="他の形式で入力(_I)" />
        <Separator />
        <MenuItem Header="終了(_X)" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

Commandにはこのバインディングで結びつけるコマンドを、Executedにはコマンドが実行された時のイベントハンドラを、CanExecuteにはコマンドの実行可否を判断するイベントハンドラを指定します。CanExecuteは必要なければ無くてもいいようです。

// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Input;
// ……

namespace CommandSample {
  public partial class MainWindow : Window {
    // ……
    private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 新規作成の処理
    }
    private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
      e.CanExecute = true;
      return;
    }
    // ……
  }
}

イベントハンドラは上のように記述します。FileCommandsCanExecuteはファイルコマンドの実行可否を判断するものですが、ファイルコマンドはすべていつでも実行可能なので、e.CanExecuteにはtrueを入れて返します。

ここまですると、きちんとメニューから新規作成コマンドを実行できるようになります。

カスタムコマンド

当然ながら、ApplicationCommandsには無い(更に、EditCommandsなどにも無い)コマンドが考えられます。これまでの例では、「他の形式で入力」などがそうです。「終了する」コマンドも、「(ファイルを)閉じる」コマンド(ApplicationCommands.Close)とは微妙にニュアンスが異なる気がします。

自分でコマンドを作るのも、実は簡単です。適当なクラスにRoutedCommandのスタティックインスタンスを作って、それをApplicationCommandsなどのクラスのコマンドの代わりに使えばいいのです。

// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Input;
// ……

namespace CommandSample {
  public partial class MainWindow : Window {
    public static RoutedCommand FileImportCommand = new RoutedCommand();
    public static RoutedCommand FileExitCommand = new RoutedCommand();
    // ……
    private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 新規作成の処理
    }
    private void FileImportCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 別形式入力の処理
    }
    private void FileImportCommandCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
      // 状況に応じてe.CanExecuteにtrue/falseを入れる
    }
    private void FileExitCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 終了の処理
    }
    private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
      e.CanExecute = true;
      return;
    }
    // ……
  }
}

XAMLソースの記述も殆ど同じように変えるだけです。ただ、デフォルトの名前空間はWPFのものなので、今回コマンドを作ったクラスの名前空間を参照できるようにする必要があります。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cs="clr-namespace:CommandSample"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <Window.CommandBindings>
    <CommandBinding Command="New" Executed="FileNewCommandExecuted"
      CanExecute="FileCommandsCanExecute" />
    <CommandBinding Command="{x:Static cs:MainWindow.FileImportCommand}"
      Executed="FileImportCommandExecuted" CanExecute="FileImportCommandCanExecute" />
    <CommandBinding Command="{x:Static cs:MainWindow.FileExitCommand}"
      Executed="FileExitCommandExecuted" CanExecute="FileCommandsCanExecute" />
  </Window.CommandBindings>
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" Command="New" />
        <Separator />
        <MenuItem Header="他の形式で入力(_I)"
          Command="{x:Static cs:MainWindow.FileImportCommand}" />
        <Separator />
        <MenuItem Header="終了(_X)" Command="{x:Static cs:MainWindow.FileExitCommand}" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

これで他のコマンドも動くようになります。

キージェスチャでコマンド呼び出し

コマンドを呼び出せるのは、前述の通りメニューアイテムだけではありません。ここでは一般的によく使われるであろうキージェスチャにも対応したいと思います。

尤も、ApplicationCommandsのコマンドには、既定のインプットジェスチャがあったりして、ここまでの記述だけで、新規作成コマンドはCtrl+Nで呼び出せるはずです。しかし、カスタムコマンドである「他の形式で入力」コマンドもキージェスチャだけで呼び出せるようにしたいと思うかも知れません(思わないとは思いますが、まあ「ファイルを開く」コマンドはもう既定のキージェスチャがついてしまっているので、説明用です)。これは入力バインディングの一つとしてキーバインディングを定義する事で実現されます。

<!-- MainWindow.xaml -->
<Window x:Class="CommandSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cs="clr-namespace:CommandSample"
  Title="コマンドのサンプル" WindowStartupLocation="CenterScreen">
  <Window.CommandBindings>
    <CommandBinding Command="New" Executed="FileNewCommandExecuted"
      CanExecute="FileCommandsCanExecute" />
    <CommandBinding Command="{x:Static cs:MainWindow.FileImportCommand}"
      Executed="FileImportCommandExecuted" CanExecute="FileImportCommandCanExecute" />
    <CommandBinding Command="{x:Static cs:MainWindow.FileExitCommand}"
      Executed="FileExitCommandExecuted" CanExecute="FileCommandsCanExecute" />
  </Window.CommandBindings>
  <Window.InputBindings>
    <KeyBinding Key="I" Modifiers="Control"
      Command="{x:Static cs:MainWindow.FileImportCommand}" />
  </Window.InputBindings>
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ファイル(_F)">
        <MenuItem Header="新規作成(_N)" Command="New" />
        <Separator />
        <MenuItem Header="他の形式で入力(_I)" InputGestureText="Ctrl+I"
          Command="{x:Static cs:MainWindow.FileImportCommand}" />
        <Separator />
        <MenuItem Header="終了(_X)" Command="{x:Static cs:MainWindow.FileExitCommand}" />
      </MenuItem>
    </Menu>
  </DockPanel>
</Window>

プログラムコード中からコマンド呼び出し

プログラムコードの中からも、コマンドを呼び出したい事があります。例えば、ファイルを開いたり、他の形式で入力を受け付ける場合には、その前にまず状態を初期化しておきたいでしょう。この時FileNewCommandExecutedと全く同じ内容を書くのはやりたくありません。

コマンドは、メニューやボタンが無くても、プログラム中から直に呼び出せます。コマンドのインスタンスのExecuteメソッドを呼べばいいだけです。

// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Input;
// ……

namespace CommandSample {
  public partial class MainWindow : Window {
    public static RoutedCommand FileImportCommand = new RoutedCommand();
    public static RoutedCommand FileExitCommand = new RoutedCommand();
    // ……
    private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 新規作成の処理
    }
    private void FileImportCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      ApplicationCommands.New.Execute( null, this );
      // 別形式入力の処理
    }
    private void FileImportCommandCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
      // 状況に応じてe.CanExecuteにtrue/falseを入れる
    }
    private void FileExitCommandExecuted( object sender, ExecutedRoutedEventArgs e ) {
      // 終了の処理
    }
    private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
      e.CanExecute = true;
      return;
    }
    // ……
  }
}

RoutedCommand.Executeの第一引数は、任意のパラメータ(object型)です。このパラメータはイベントハンドラの引数eParameterメンバとしてイベントハンドラに渡されるので、必要なら適当なクラスのインスタンスを渡す事で呼び出し元と呼び出し先との間でデータのやりとりができます。

mpg123はコンソールで走るMPEGオーディオデコーダですが、その構成要素にlibmpg123というのがあります。今回はこいつを.NETからプラットフォーム呼び出しして使えるように、Win32 DLLを作ってみます。と言うよりは、自分で作ってみた時に困ったり躓いた点のメモです。


実は既に用意されている

ダウンロードページにlibmpg123 Win32 DLLっていうのが親切にも置いてある。が、MinGWでの使用を想定して作られたものなので、今回はこれでは不十分。面倒だけど自分で作る。

コンパイラ

結論から言うと、VC++では無理っぽい。やろうとしても、環境特有のヘッダとか定数とか型とか設定とか嫌と言うほど出てきて、やる気にならない、と言うか、無理。お勧め通りMinGWのgccを使う事にする。

MinGWのインストール

MinGWとMSYSをインストールする。MinGW + MSYS による Win32 な OS での開発が参考になる。gccが動くかどうかテストする時、Vistaだとcc1.exeが見つからないって言われるけど、適当にパス通せば良い。例えば、/etc/fstabに

C:/MinGW/libexec/gcc/mingw32/3.4.5 /mingwlib

を追加しておいて、/etc/profileに

export PATH="$PATH:/mingwlib"

を書いておくとか。ここまでしたら、MSYSのシェルでgccがちゃんと動くようになる。

libmpg123のダウンロード

SourceForgeから最新版を落としてくる。ここではそれを解凍して、できたディレクトリの内容を~/mpg123の中にコピーしたとして話を進める。

呼び出し規約の変更

なんだかやっぱりデフォルトの呼び出し規約はstdcallじゃないっぽい(cdeclでもないらしい? 未確認)ので、プラットフォーム呼び出しする関数と、少なくともアンマネージドコードから呼び出されるコールバック関数については__stdcallを付けて回る。具体的には、~/mpg123/src/libmpg123/mpg123.h.inのプロトタイプ宣言と、同ディレクトリのmpg123.cの関数本体の定義を探し回って__stdcallを書き加えていく。mpg123_replace_readerを使う場合は、それの引数(勿論r_readとr_lseekだけ)に加えて、reader.hのreader_data構造体のメンバ、r_read、r_lseek、read、lseek、それとreaders.cのposix_readとposix_lseekもstdcallを使うようにしておく。

隠れた曲者CCALIGN

詳細は後述するが、(MinGW上のgccでのみDLLを利用するのでない限り)CCALIGNを1にしてはいけない。~/mpg123/src/config.hはconfigureさんが作ってしまうので、彼自身を編集する。~/mpg123/configureの、14767行目辺りのccalign="yes"ccalign="no"に変更する。その下のresult: yesもresult: noにしておいた方が気持ちいいかも。修正後はその近辺は次のようになる。

ccalign="unknown"
if test $ccalign = "unknown"; then
  { echo "$as_me:$LINENO: checking __attribute__((aligned(16)))" >&5
echo $ECHO_N "checking __attribute__((aligned(16)))... $ECHO_C" >&6; }
  ccalign="no"
  echo '__attribute__((aligned(16))) float var;' > conftest.c
  if $CC -c -o conftest.o conftest.c; then
    ccalign="no"
    { echo "$as_me:$LINENO: result: no" >&5
echo "${ECHO_T}yes" >&6; }
  else
    { echo "$as_me:$LINENO: result: no" >&5
echo "${ECHO_T}no" >&6; }
  fi
  rm -f conftest.o conftest.c
fi
if test $ccalign = "yes"; then

cat >>confdefs.h <<\_ACEOF
#define CCALIGN 1
_ACEOF

fi

これでconfig.hに#define CCALIGN 1は現れなくなる。

make libmpg123.dll

ここまで来たら、あとはmakeするだけ。~/mpg123/makedll.shを実行すれば、時間はかなり(冗談じゃない程)かかるものの一発でDLLを作成してくれる。デバッグ版を作りたい場合は--enablge-debug=yesをオプションとして付けるといい。makeが終わったら、Hintsにあるようにstrip --strip-unneeded libmpg123-0.dllしておくと軽量化できる。そういう訳で、DLLは~/mpg123/libmpg123-0.dllとして得られるのだけれど……0って何だ。


おまけ : デバッグ版

gccでデバッグ版DLLを作っても、VC++やVisual Studioじゃデバッグできない、それじゃ--enable-debug=yesしても意味無いじゃん。それはそうなんだけど、DEBUGが定義されていると、libmpg123は標準エラー出力に色々と進捗状況を吐き出してくれる。何も無いよりは随分ましだろう。尤もプラットフォーム呼び出しをする場合標準エラー出力はどっかに飛んでいってしまう(のか、どうだか?)ので、そのままではこれも意味が無い。更に、.NETのSystem.Console.SetErrorでリダイレクトっぽい事をしようとしてみても、してくれない。

それじゃあlibmpg123側でリダイレクトしてしまえ、と言う事で、libmpg123.cのmpg123_initの頭でこうやっておく。

#ifdef DEBUG
  freopen( "./stderr.log", "w", stderr );
#endif

律儀にfreopenの戻り値をグローバル変数か何かに取っておいて、mpg123_exitでfcloseしてもいい。ちなみに、--enable-debug=yesオプション付きでmakeしたDLLは、stripするとかなり痩せる。どうせgdbは使えないんだし、痩せさせてしまえ。

そのおまけ : stderrオートフラッシュ

stderrはバッファリングされなくても、再割り当てしたファイルストリームはバッファリングされてしまって、AccessViolationExceptionとかで正常にfcloseされなかった時にstderr.logに何も入ってなかった、じゃ悲しいので、面倒だけど毎回バッファをフラッシュしてやる事にする。幸いlibmpg123のエラー出力は~/mpg123/src/libmpg123/debug.hにまとまっているので、そのマクロ集を次のように変えてやればいい。

#define debug(s) do{ fprintf(stderr, "[" __FILE__ ":%i] debug: " s "\n", __LINE__);\
	fflush( stderr ); }while(0)
#define debug1(s, a) do{ fprintf(stderr, "[" __FILE__ ":%i] debug: " s "\n", __LINE__, a);\
	fflush( stderr ); }while(0)
/* ...... */

fflushを付けてブロックにしただけ。do-whileもくっついてるけど、これはereturnとの付き合いの結果。

CCALIGN?

CCALIGNを1にしておくと、適宜アライメントをしてくれる。gccはアライメントを指定する修飾子に__attribute__((aligned(x)))というのがあるようで、これを使えば変数をxバイト境界に乗っけてくれるようだ。libmpg123のmakeでは、CCALIGNが1と定義されていれば、この修飾子を使ってアライメントが必要な変数の位置を調整する。

そんなものが使われているという事は使わないといけないようなものがあるからで、例えばSSE命令がメモリにアクセスする場合、普通はメモリ上のデータは16バイト境界に乗っていないといけない。らしい。libmpg123も最適化デコーダの一つにSSEデコーダを持っていて、mpg123_newのdecoderに"sse"を渡せば使ってくれるし、NULLを渡した場合でも、利用可能な範囲内でSSEデコーダが一番効率がいいと思えば暗黙裏に使ってくれる。

そんな訳で、gccは__attribute__((aligned(x)))が使えるので、放っておけば良さそうなものだけれど、実は問題がある。どうも、gccで作ったDLLをgccから利用した場合はアライメントは保たれたままのようなのだが、gccで作ったDLLのアライメント情報は、他の環境では理解されないようなのだ。VC++然り、CLR然り。この辺って処理系依存なのか。VC++にも似たような__declspec(align(x))という修飾子があるけど、Microsoft Specificだ。で、運が悪いと、__attribute__((aligned(16)))で修飾されたデータのアドレスが16バイト境界からずれて(実際、AccessViolationExceptionが投げられていた時には4バイトずれていた)、movapsが駄々をこねる……。

いや、そう、実は駄々をこねるのはmovapsだけであって、libmpg123では、他のSSE命令はメモリに直接アクセスしないように書かれている。CCALIGNがオプショナルである事が何よりの証拠。CCALIGNが定義されない場合は、アライメントを行う代わりに、16バイト境界に乗っていなくても文句を言わないmovupsを使ってくれる。パフォーマンスは多少落ちるかも知れないけれど、_alligned_mallocを使って動的に領域をlibmpg123の外から与えたりするのはちょっと暴挙に過ぎるし、何より一番簡単だろう。

放置しすぎて、このまま放置を続けた方が本来あるべき姿のような気もするけど、折角書く気になったのだから捨て置くのももったいない。という訳で何か最近気づいた事を適当に書いてみる。

最近ようやくやる気が復活してきたので、例の(?)DirectXのラッパライブラリを見直している。Managed DirectXってなんかどうも捨てられてるっぽい(November 2007以降ではSDKからManaged用のドキュメントが削除されるとか......XNAってのが主流なのか?)し、既存のラッパライブラリをC++ .NETからいじった方が色々と楽そうな気もするけど、無下にするのも忍びないし、ま、一回くらい自力でラッパライブラリ作る経験があったって罰は当たらないだろうという事で継続する事にする。しかし、企画した時から2年以上経っているんだなあ。無為に過ごすと時って凄く速く過ぎ去るもので。少年老いやすく学成りがたしとはよく言ったものだ。


ま、それはともかく。Microsoft.DirectX.Direct3D.Textureのコンストラクタでストリームから画像を読み込むと、とんでもなく時間がかかりませんか?

Device device;
Stream source;

this.texture = new Texture( device, source, Format.Unknown, Pool.Managed );

みたいな事をやると。いや勿論、いつでも遅いという訳では無いのです。ただ、これやると、デバッガでデバッグしようとすると破壊的に遅くなる。物的には何も破壊しないけれど、デバッグ付きで実行しようとする気を決定的なまでに殺いでくれる。このコンストラクタ一回呼び出しただけで数秒かかるんじゃやってられない。

コード見直すに当たって、これも解消できたらいいなあと思って代替手段を探していた時、そう言えばTextureLoaderっていうのもあったなあと思って、それっぽく書き直してみた。

Device device;
Stream source;

// 実際にはFilter.Linearが使えるかどうかCaps見てやんないと駄目だろうけど
this.texture = TextureLoader.FromStream( device, source, 0, 0, 1,
  Usage.None, Format.Unknown, Pool.Managed, Filter.Linear, Filter.Linear, 0 );

あっさり改善されました。

......。

ひょっとしてTextureコンストラクタってMicrosoftの罠?

TextureLoaderは名前空間こそDirect3Dだけど、Direct3DXのアセンブリに入ってるのでどれか一つ参照に追加しないとならない。まあ一番古いのを使えばいいか。


今度はMP3に浮気中。MP3って、Ogg VorbisやFLACみたいにリファレンスエンコーダ/デコーダが無かったから敬遠していたけど、MP3ファイルを一々WAVEやOgg Vorbisに直してから使うの面倒くさいなあ、と思い始めて、いっその事対応する予定と相成りました。問題はどのデコーダを使うか......。エンコーダならLAMEが有名らしい事は知ってるんだけど、デコーダは何が有名なのかな。mpg123? メモリ上のデータをデコードするmpg123_decodeの仕様は気に入らないけど、代わりにread/seekを置き換えるmpg123_replace_reader使えば何とか綺麗にできそうだ。

XML 1.1 (第二版)のついでに、XML 1.0 (第四版)もおまけで作ってみました。と言っても大部分の文章はXML 1.1 (第二版)と同じです。変更箇所が無いか逐一確認しながらコピー&ペーストだけだったのですが……これがなかなか辛い。おまけのつもりが結構大変に。のべ作業時間は多分数日もありませんが三ヶ月弱もかかってますね。あなおそろしや。

XML 1.0とXML 1.1の違いは仕様書にもまとめられています。XML 1.1での変更点とその原理的説明がそれです。ここを押さえておけば、概念を掴むだけならどっちを読んでも大丈夫そうです。整形式性の定義が違うので、実装者は生成規則を改めて見直す必要があるのでしょうが、文書の作成者には殆ど影響がありませんね。

さて、この方面からは暫く離れましょう。以前言っていたDirectXのラッパライブラリも、ライブラリだけ作ってると段々やる気が失せていくのでこれもパス。と言う訳で、昨日から前々から作る予定だったシューティングゲームの骨組みを作っています。C++で途中まで組んだものを今回はC#で書くのですが、ハードディスクが去年の春吹っ飛んだお陰で以前のソースコードは残っていません。尤も、C#と.NET Frameworkのお陰でコーディングは大分楽になるはずです。現在、C# 2.0で導入されたyield returnを利用して、同期されたマルチタスク処理を簡単に(かつシングルスレッドで)行う機構を実装中。こういうのコルーチンって言うんですね。

複数のスレッドが動いていると、どのような順番でそれぞれの処理が実行されるかという事は、予測が困難なものですね。

先日、DirectSound周りの処理を改良していた時、スレッドを二つ作り、メインスレッドでは次のような処理を行わせていました(話を単純にする為にある程度簡略化しています)。

// メインスレッド(通常の優先度)で
private void PlayButton_Click( object sender, EventArgs e ) {
  // ……
  // OggVorbisDecoderは、ISoundDecoderを実装するクラス
  this.sound = new StreamingSound(
    new OggVorbisDecoder( new FileStream( this.sounds[ 0 ] ) )
  );
  for ( int i = 1 ; i < sounds ; i++ )
    // StreamingSound.AddFollowingDecoderは、
    // 部分ループ再生の為に後続のデコーダを追加するメソッド
    this.sound.AddFollowingDecoder(
      new OggVorbisDecoder( new FileStream( this.sounds[ i ] ) )
    );
  // それなりに長い処理(空行抜きで15行分程度)
  this.sound.Play();
  // ……
}

// メインスレッドから呼び出される、StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// メインスレッドから呼び出される、StreamingSoundのメソッド
public void Play() {
  // this.bufferは、Microsoft.DirectX.DirectSound.SecondaryBuffer
  if ( this.buffer.Status.Playing )
    return;

  // this.decoderLockは、単なるobjectのインスタンス
  lock ( this.decoderLock )
    this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

そして、セカンダリバッファの内容を更新するRefreshingThreadでは、次のようにしていました。

// バックグラウンドスレッド(通常の優先度)で
private void RefreshingThread() {
  // 変数宣言(代入含めて、5つ程)……
  // まだPlayが呼び出されては困る(ゴミが再生される恐れがある)
  lock ( this.decoderLock )
    // バッファの前半にPCMデータを書き込む
    this.WriteHalfBuffer( 0, 0 );
  // 準備完了、もうPlayを呼び出しても大丈夫
  // ……
}

lock ( this.decoderLock )というのは、クリティカルセクションを作る為に導入したものです。lock文のブロックの中に何れかのスレッドが進入すると、他のスレッドでは、現在クリティカルセクションに進入しているスレッドがそのオブジェクトの所有権を手放さない限り(つまりlock文を抜けない限り)、同じオブジェクトを使ってlock文の中に入る事ができなくなります。ここでは僕は、二つのスレッドの優先度が同じで、RefreshingThreadをthis.refreshingThread.Start()で開始させた時からの作業量はどう見てもバックグラウンドスレッドの方が少ないので、WriteHalfBufferが先に呼ばれるものと思っていました。実際Debugビルドではそうだったようで、再生してみても、サウンドの頭から綺麗に再生されていました。

ところが、Releaseビルドで作られた実行ファイルでは、どうも初回の再生だけ汚くなるのです。ノイズが聞こえる訳ではないですが、どうも頭の部分が切れている。初回の再生(初めて「再生」ボタンを押した時)と二回目以降の再生で違うのは、StreamingSoundを作り直すかどうかだけ。つまり、上記のソースコードで一番上の辺りの処理が怪しいのです。色々調べてみましたが、ふと気になって、StreamingSound.Playの中と、RefreshingThreadのWriteHalfBufferの前後で、System.DateTime.Now.Ticksを使って、処理が実行された時間を調べてみました。StreamWriterによって書き出されたログを見てみると……案の定、Playが先に呼ばれています。少し後れてRefreshingThreadのWriteHalfBufferが実行されていたようです。そりゃー、頭が切れる訳ですね。再生開始時点では何もバッファに書き込まれていない訳ですから。雑音が聞こえなかったのは運が良かったのでしょう。

さて、ではどう解消すべきか。lock文は、クリティカルセクションを作る事はできますが、スレッドの処理の実行順序を支配する事はできません。ミューテックス(System.Threading.Mutex)も同様ですし、モニタ(System.Threading.Monitor)はlock文の実体なので、書き換えた所で意味がありません。また、Releaseビルドでは問題が起こるけれどもDebugビルドでは起こらないから、といってReleaseビルドの実行ファイルを使わないようにするのも面白くありません。それに、Debugビルドだって、きちんと動いているように見えるだけで、その動作は「たまたま」上手く行っているのでしょう。確実に実行順序を支配する機構が欲しい所です。

これに打って付けなのは、イベントでしょう。と言ってもC#の言語機構のeventキーワードやdelegateを使う方ではなく、Win32 APIにもあるような、スレッドの同期を実現するイベントの方です。.NET Frameworkでは、System.Threading.AutoResetEventやSystem.Threading.ManualResetEventがそれに当たります。今回は特にマニュアルリセットが必要とされるものでもないので、AutoResetEventを使って同期してみました。主な変更は次の3箇所です。

// StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  // 最初は非シグナル状態にするので、falseを渡す
  this.decoderReady = new AutoResetEvent( false );
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// StreamingSound.RefreshingThread
private void RefreshingThread() {
  // 変数宣言……
  // バッファの前半にPCMデータを書き込む
  this.WriteHalfBuffer( 0, 0 );
  this.decoderReady.Set();
  // ……
}

// StreamingSound.Play
public void Play() {
  if ( this.buffer.Status.Playing )
    return;

  this.decoderReady.WaitOne();
  this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

イベントには、ミューテックスなどと同じように、シグナル状態と非シグナル状態という二つの状態があります。シグナル状態ではWaitOneなどのメソッドは成功し、次に進む事ができます。非シグナル状態ではWaitOneなどのメソッドは成功する事は無く、次に進む事はできません(無理矢理進んだりしても、同期に失敗しているのですから望む結果は得られないでしょう)。シグナル状態には、コンストラクタでtrueを渡す、Setメソッドを呼び出す、などの操作をする事で変化させる事ができます。非シグナル状態には、コンストラクタでfalseを渡す、Resetメソッドを呼び出す、WaitOneなどの待機関数を成功させる、などの操作で変化させられます。

StreamingSound.Playで呼び出しているWaitOne関数は引数がありません。引数無しでこの関数を呼び出すと、そのイベントがシグナル状態になるまで無限に待機します。コンストラクタではこのイベントは非シグナル状態にされているので、バックグラウンドスレッドのRefreshingThreadがSetしない限り、メインスレッドはサウンドの再生を開始しないという訳です。

……久し振りにプログラミングに関する覚え書きを書きました。