2013年3月15日
Firefox上のFlash Playerで、保護モードが有効でもIMEが利用できるようにする方法
初回投稿日時: 2013年03月15日20時33分51秒
最終更新日時: 2013年03月19日14時26分00秒
カテゴリ: Firefox Flash Mozilla Core plugin Windows
固定リンク: id=2013031500
SNS:
(list )
最初に断わっておきますが、これはユーザがどうこうする話ではなくて、Web開発者向けの話です。ただ、利用しているサイトでこの問題で困っていて、保護モードを切るのに抵抗がある方は、このエントリ をそのサイトの運営者の方に報告してください。ひょっとすると対策をとってくれるかもしれません。
ではまず、この問題のおさらいですが、Windows版のFlash PlayerはFirefox上で動く場合にのみ、保護モードという、内部処理を権限が制限されたプロセスで動かすモードを有効にして動いてることは、これまで何度かここに書いてきました。このモードの最大の問題は、WindowsのIMEというのは、利用しているアプリの一部としてロードされ、動作するため、制限されたプロセス内でロードされてしまうと、変換エンジンが別プロセスの場合、そのプロセスと通信できなかったり(Google日本語入力のケース)、かな入力を行うためのカナロックを自動的にかけることができなかったり(Google日本語入力以外と、ATOK 2013で『自動カナロックの詳細設定』ダイアログで『ATOKの内部状態を優先する』にチェックを入れていない場合)、といった、IME自体にまで動作制限がかかってしまうことがその原因でした(この件に関しては、Windows版Flash Player 11.3と各IMEの動作確認表 (もちろん非公式) も参照してください)。
ユーザ側での対抗策として、これまでは保護モードを無効にすることを紹介していました が、今回、テスト中にたまたま見つけた、Webサイト側でとれる対策を紹介します。その対策とは実にシンプルで、param
要素で、wmode
をopaque
か、transparent
に指定し、Flash Playerをwindowlessモード で動かすこと です(windowedモードと、windowlessモードの違いについては、Windows版Firefoxのwindowed modeとwindowless modeそれぞれのプラグインの構造 を参照してください)。
以下、実例です。Flashコンテンツを作るツールを持っていないので、bugzilla-jpに添付されていたswfファイルを拝借してきました。実際に、Webコンテンツを作られている方は、wmode
を指定するparam
要素を挿入するかどうかは、UA名にFirefoxが含まれているかどうか 、Windows版かどうか 、この二点を確認した上で挿入した方が良いでしょう 。一般的に、windowlessモードは各ブラウザの実装のバラツキが大きいためです。
wmode指定なし (windowedモード)
wmode opaque (windowlessモード)
wmode transparent (windowlessモード)
なぜこれで、IMEに関する互換問題が一掃されるのか、その理由は簡単です。
windowedモード では、Flash Player自身がネイティブウインドウをFirefoxの上に保護モードで作り、このウインドウがフォーカスを持ち、入力されたメッセージを保護モードで処理します。このため、IMEは保護モード下で動作し、上述の制限を受けることになります。
これに対し、windowlessモード では、Flash Playerのプロセスは自分自身でウインドウを生成しません。Flashの内容が、Firefoxのウインドウ上に、直接描画されるモードだからです。このため、IMEのイベントを受け取るのはFirefox自身のウインドウになります。これはつまり、IMEは保護モードの外、Firefoxのプロセス内で動作します。このため、通常の権限でIMEが動作することになります。Firefoxは、受け取ったIMEのイベントを、内部処理専用のDOMイベントを利用して、フォーカスを持ったwindowlessプラグインのプロセスへ渡します。これにより、IMEがFlash上でも使えるのです。
ただし、一つだけ、このwindowlessモードには欠点があります。それは、on-the-spotでIME経由で入力できないということです。つまり、未確定文字列が、点滅するキャレットの位置に描画されません。これは、Windowsでは、アプリ自身が未確定文字列を描画する場合や、候補ウインドウ位置を指定する時にはWindowsのAPIを通じて、動作中のIMEのコンテキストにアクセスしなければいけないのですが、別プロセスのコンテキストにアクセスすることはセキュリティ上、できないため、このようなことになります。
この件に関しては、そのうちに、プラグインの描画領域のすぐ下にでも表示されてマシになるように、対策はとりたいとは考えています。
2013年3月27日
GeckoのXP部分のソースコードは、nsIWidget
の様々なメソッドを使って、ネイティブのIMEにリクエストを出したり、通知したりしていますが、今までは、それぞれのためにメソッドを追加する、という形をとっていました。しかし、これでは、ただでさえ巨大化しているnsIWidget
をますます肥大化させる可能性があり、メンテナンスの際にもインターフェースの変更が必要な分、super reviewの手間がかかります。これでは、非常に効率が悪いので、今回、整理を行いました。
新たに、nsIWidget::NotifyIME(mozilla::widget::NotificationToIME aNotification)
というAPIを追加しました。aNotification
で、リクエストの内容や、通知内容を指定します。
NOTIFY_IME_OF_CURSOR_POS_CHANGED
エディタに未確定文字列が無い場合に、nsIEditorIMESupport::forceCompositionEnd()
が呼ばれた場合にのみ、呼び出されます。これは、従来のnsIWidget::ResetInputState()
の本来の機能を置き換えます。
REQUEST_TO_COMMIT_COMPOSITION
エディタに未確定文字列がある場合に、これを強制確定する際に呼び出されます。nsIWidget::ResetInputState()
のモダンな実装を置き換えます。
REQUEST_TO_CANCEL_COMPOSITION
エディタに未確定文字列がある場合に、これを破棄する際に呼び出されます。nsIWidget::CancelComposition()
を置き換えます。
NOTIFY_IME_OF_FOCUS
エディタがフォーカスを得た場合に呼び出されます。nsIWidget::OnIMEFocusChange(true)
を置き換えます。
NOTIFY_IME_OF_BLUR
エディタがフォーカスを失った場合に呼び出されます。nsIWidget::OnIMEFocusChange(false)
を置き換えます。
NOTIFY_IME_OF_SELECTION_CHANGE
エディタ内のキャレット位置、もしくは選択範囲が変更された場合に呼び出されます。nsIWidget::OnIMESelectionChange()
を置き換えます。
REQUEST
というネーミングから分かるように、実際に、これらが動作するかは、ネイティブIMEのAPIに依存します。
置き換えられたメソッドは全て、削除され、すっきりとしています。
また、新しいメソッド名にあわせて、nsIWidget::OnIMETextChange(uint32_t aStart, uint32_t aOldEnd, uint32_t aNewEnd)
は、nsIWidget::NotifyIMEOfTextChange(uint32_t aStart, uint32_t aOldEnd, uint32_t aNewEnd)
にリネームしています。
TSFのドキュメントロックが、仕様通りに動かないのでデザインを大幅に見直しました。
TSFは、TIP (IME)がアプリにアクセスを行う際に、ITextStoreACP::RequestLock()
を呼び出し、アプリ自身や、他の何らかのテキストサービスが、そのコンテンツを変更できないようにロックを行ってから処理を開始します。
ITextStoreACP
の実装側(GeckoではnsTextStore
)は、ロックを受け入れることができるなら、ITextStoreACP::RequestLock()
内でITextStoreACPSink::OnLockGranted()
を呼び出します。TIPは、この、OnLockGranted()
が呼び出されている間が、ロックが実際に実行されている、という状況になりますので、アプリ側から見ると、OnLockGranted()
を呼び出している最中に、文字列を変更するメソッドが呼ばれたり、キャレット位置を変更するメソッドが呼ばれたり、コンテンツの内容を調べるメソッドが呼ばれたり、コンテンツ中の特定の文字の画面座標を調べるメソッドが呼ばれたりします。
従来のnsTextStore
は、各種メソッドがTSFから呼び出された時に、逐一、DOMイベントを発行して、エディタの内容に反映し、最新のコンテンツの情報を調べて返す、という実装でした。つまり、OnLockGranted()
の最中に、DOMイベントが発生するため、そのイベントハンドラから、エディタの内容や、フォーカスの移動といった、何らかの変更が可能で、実際にはロックができていない状態でした。このため、実際にそのような状況が発生すると、TIPが混乱してしまうということがありました。
今回の修正で、nsTextStore
は以下のように修正されました。
まず、未確定文字列の変更等、ドキュメントの変更に関するアクションが呼ばれた場合、アクションという形で記録だけを行い、OnLockGranted()
から処理が戻ってきてから、ため込んだアクションから、DOMイベントを一気に発行するようになりました。
そして、最新のコンテンツの情報は、アクションがペンディング状態になっている時には取得できませんので、ロック後、最初に、最新のコンテンツ情報や、選択位置の情報をキャッシュし、各アクションが記録される際に、このキャッシュしたコンテンツのみを更新して、TSFにはこの情報をもとに応答する、という形をとっています。
また、コンテンツの任意の文字の位置情報だけはどうしようもありませんので、レイアウトがまだ完了していない、というエラーを発行するようにし、記録されたアクションを全て実行した直後に、TSFに対して、レイアウトの計算が終了した旨を伝えるようにしています。
今回の修正により、まず、コンテンツの情報は、一回のロックにつき、一回しか取得しにいかなくなりましたので、CPUパワーの必要な、クエリイベントを利用を大幅に削減しました。これにより、軽快に動作するようになっています。
また、選択位置の変更と、文字列の変更の合間でクエリが行われた場合に対応するための中途半端な、コンテンツ情報のキャッシュコードをごっそりと削除することができました。
そして余分なtext
イベントを発行して、未確定文字列がちらついてしまうのを阻止するために、最後に発行したtext
イベントを記録し、比較するコードの削除もできました。
これにより、かなり、壊れにくい、安定した、読みやすいコードに生まれ変わったと言えます。
ただし、この修正が入った今でも、RequestLock()
中にDOMイベントが発生することになりますので、そのハンドラにより、何らかの変更が行われた場合に、TSFにそれを通知する手段がありません。これはまた、別のバグで対応予定ですが、XP側に相当量の変更が必要なので、今年中に取りかかれるかすら、見通しが立っていません。
高解像度スクロールに対応した、一部ノートPCのタッチパッドでは、スクロールが非常に遅く、解決策としてGoogle Chromeに逃げ出してるユーザが多いぞ、というバグです。
D3EのWheelEventの実装時に、当然、システムのスクロールスピードがカスタマイズされていない場合に、ルートのスクロール速度だけ倍にする、という機能の部分にも手を入れたのですが、その変更時に、高解像度スクロールに対応した環境では、カーソルの加速に対応しており、この機能が逆に邪魔をしてしまう、という判断から、高解像度スクロールに対応していない環境でのみ、速度を倍にする処理を呼び出していました。
しかし実際には、高解像度スクロールに対応はしていても、ノートPCのタッチパッドだと、カーソルの加速には対応していない環境が多いようで、システム設定の倍の量、スクロールしてしまうGoogle Chromeに比べて「遅い」ブラウザだと感じる人が多数居るようでした。
今回の修正で、D3E WheelEventの実装前の状態になっていることになりますので、これによるregressionは無いとは思うのですが、システム設定を無視するこの機能が不要な方は、mousewheel.system_scroll_override_on_root_content.enabled
をfalse
に変更してください。
行儀の悪いアプリの方が評価される、この状況、どうにかならんものかといつも思うんですけどね。
WindowsのIMEのAPIセットが、IMMからTSFへの移行するにあたり、TSFにネイティブ対応していないアプリのために、IMM-TSFのエミュレーション機能である、CUASと呼ばれるシステムが、通常のアプリの場合は動作しています。これは、メッセージをフックし、様々な処理を自動で行い、必要があればIMMのメッセージを発行したりしています。
ITfMessagePump
は、PeekMessageW()
メソッドを持っていて、これを利用すると、通常のPeekMessageW()
よりは生のメッセージキューからメッセージを取得し、処理を行うことができます。
TSF対応アプリが、従来からあるPeekMessageW()
APIを利用してメッセージを取得した際に、CUASが先にメッセージをTSFに渡してしまい、もし、キーイベントが処理されてしまった場合、アプリはキーイベントの発生自体が分からなくなっていることに、テスト中に気付きました。
これでは、D3Eの仕様書で定義されている、keydown
イベントと、keyup
イベントを、未確定文字列がある場合でも発行する、という仕様に従うことができないので、その準備として今回の修正を行いました。
これにより、TSFモードの場合、PeekMessageW()
の呼び出しが、virtual callになってしまうので、かなりパフォーマンスが悪くなっているのではないかと思われます。色々と検討してみたんですが、virtual callを阻止して、直接実装されているメソッドを呼び出す方法が思いつきませんでしたので、今のところ、必要悪なパフォーマンス低下になっています(IMMモードでは、従来通り、CのPeekMessageW()
を呼び出すので、パフォーマンスに変化はありません)。
また、この修正により、CUASが自動でキーイベントをTSFに渡してくれなくなりますので、ITfKeystrokeMgr
を利用して、nsTextStore
自身がTSFにキーイベントを通知するように修正しています。
Metroアプリ版のFirefoxでは、IMMは利用できないので、TSFモードで動作しますが、Metro版用のnsIWidget
の実装である、MetroWidget
がnsIWidget::GetIMEUpdatePreference()
の実装を忘れていたため、IMEを利用して日本語を入力しようとすると、常に、フォーカスをあわせた際のキャレット位置に文字が挿入される、という状態になっていました。
この原因は、nsIWidget::GetIMEUpdatePreference()
が適切に実装されていないため、nsIWidget::NotifyIME(NOTIFICATION_TO_SELECTION_CHANGE)
や、nsIWidget::NotifyIMEOfTextChange()
が一切呼び出されず、TIPにキャレット位置や、テキストが変化したことを通知できていない、ということでした。
それにしても、このレベルのバグがあったことから、日本人は全然、Metro版Firefoxをテストしていないのが露呈しちゃってますね(そもそもMetro版の完成度低くて使えないってのはありますが)。
Windows版Geckoのメッセージループは、開発者には有名な話ですが、独自のメッセージ取得順序の最適化を行っています。本来は、SendMessage()
で送信されたメッセージ、PostMessage()
で送信されたメッセージ、SendInput()
等で発生した入力に関するメッセージ、といった感じになっているのですが、Geckoは10年以上前から、入力処理を最優先で処理するようになっていました。
その理由は、古いFlash Playerが、描画の更新のために、WM_USER + 1
というメッセージを大量にメッセージキューに投げてくるため、入力イベントの処理がスムーズにいかない、というものがあったようですが、現在のFlash Playerではこの問題は無くなっています。
また、このメッセージの最適化により、ATOKをオンにした直後の入力イベントが、ATOKが処理する前に、Geckoに取得され、最初の何文字かが半角英数のまま入力されてしまう、というバグが、TSFモードでは再発していました。
こういった理由から、もはやこのメッセージ順序の最適化を行い続ける理由はないだろう、ということで、私のハック史上、最も影響範囲の広大な、この修正を行うことになりました。
現在、Mozilla 22向けのmozilla-centralにパッチは投入されていますが、これほどリスキーな変更をNightlyで一週間しかテストしないのは危険なので、Mozilla 22がAuroraになった時点で、ひとまず、Auroraからバックアウトすることを予定しています。そのため、最初にこの修正が入るリリースビルドは23になると思われます。
この処理の副作用として、メッセージの取得順序最適化の際に大量に呼び出していたPeekMessageW()
の呼び出しが大幅に削減されていますので、特にvirtual callでこのAPIを呼ばなくてはいけないTSFモードで、パフォーマンスが改善されています。
test_imestate.html
というchromeの自動テストが、isnot()
と書くべき所を、typoにより、is_not()
と書いてしまっている、というバグです。
現在、mochitest-chromeはJavascriptにエラーがあっても、そのままテストが完走してしまうんだとか。
で、単に、この名前だけを修正しても、エラーが出てしまうので、修正依頼が、原因作った私のところへ来ていました。
幸い、エラーはテストのバグであり、Gecko側のバグではありませんでしたので、テストのみを修正しました。