ものがたり(旧)

atsushieno.hatenablog.com に続く

XIMとnative interopにハマるの巻

最近仕方なくWindows Formsの日本語入力まわりの実装をやっている。今までと全然違う領域だ。マルチバイト入力なんてうちの仲間は誰もやっていないので*1、結局一番近いところにいるのが僕ということになってしまった。

で、Windows FormsのXplatUIX11はX11ベースで実装されているので、[DllImport("libX11")] で何とかしたい。となるとやはりgtk-immoduleではなくXIMということになってしまう。外野からiiimfとかには対応しないのかと有言の圧力を受けつつ(謎)、XIMである。iiimfは、誰かやってちょうだい…w

XIMクライアントなんてそんなに難しくないはずだと思うのだけど、まだ出来ていない現在まででもいろいろ罠にハマったので、その恨みつらみを書いてみる。

そもそもXIMを有効にするまで:

  • XFilterEvent()にハマる。XplatUIX11では、キーボード入力のXIM/XICインスタンスがRootWindow1つについてのみ生成されるので、XFilterEvent()をこのRootWindowに対して呼び出す必要がある。
    • この方式については、ウィンドウごとに入力コンテキストを持たせるオプションがほしいという要望もあって、今後どうなるかは分からない。
    • clientWindowにXSetICFocus()すればclientWindowで拾えるものだと思っていたのだけど、これは出来なかった(あるいは、まだ成功していない)
  • XIM/XICのあるウィンドウがXMapWindow()で描画されていないと、変換ウィンドウが表示されない。atokx3(あるいはiiimxか)では今でもこの問題があって、まだ表示できない(scim-anthyだと機能している)。RootWindowを描画してしまうと、全てのwinformsアプリでゴミ窓が出てしまうので、これはできない。
  • XplatUIX11の内部でキーボードのshift stateが必要になる部分があって、その状態管理をXplatUIX11自身で行っているため、これがXIM有効時のXFilterEvent()と相性が悪く、衝突がまだ解決できていない。atokx3だとまだ大文字が入力できない(!)
    • KeyPressのフィルタリングとKeyReleaseのフィルタリングがミスマッチなのもハマりどころかもしれない。
  • XIMのイベント処理はFIFOではなくLIFOであるらしい(Xのイベント処理はキューなのだけど)。この関係で、キーボードイベントの処理とXPending()/XNextEvent()の呼び出しタイミングによっては、kmflなんかが上手く動かなくなることがあるらしい。kmflの人が詳解している。

XCreateIC()とXVaCreateNestedList()まわりでもハマる:

  • 可変長引数の扱いに困る。__arglistを使えばいけるはず、と思ったのだが、どうもちゃんと実装されていないようだ。仕方ないので、これらはたくさんオーバーロードする。
  • XNPreeditPositionやXNPreeditCallbacksの時は、XVaCreateNestedList()を使う必要があるが、コレがハマる。まず項目名部分にstringを渡してはいけない。理由は後で詳しく説明する。
  • XIMCallbackは関数ポインタを含むので、ここにはdelegateを渡してやることになるが、アンマネージドコードにマーシャリングされる時、これは単なるポインタに変換されてしまう。マネージドオブジェクトであるdelegateはいつGCが移動してしまうか分からないので、delegateオブジェクトは固定されていなければならない。これにはGCHandle.Alloc()を使う。
    • XIMCallback構造体に[NonSerialized]でGCHandleのフィールドを追加したら落ちるようになって、しばらく理由が分からなかったのだけど、コードの途中ではなく末尾に追加したら落ちなくなった。LayoutKind.Sequentialでシリアライズするようにしているのに、これはいかがなものか…多分monoのバグだと思うけど。

XVaCreateNestedList()にstringを渡してはいけない理由は、DllImportした関数の呼び出し時に、stringがどのようにマーシャリングされるかという仕組みにある。CLIのstringは変更されてはならないから、アンマネージドコードの呼び出し前に、monoランタイム側で一時的に内容をコピーして、そのポインタを渡してやることになる*2

XVaNestedListがどう実装されるかはXの実装によるが、X.Orgx11 7.3におけるXVaCreateNestedList()の実装では、これは単なる名前へのポインタと値へのポインタの並びになっている。

さて、アンマネージドコードが終了したとき、マネージドコードから渡されたstringに対応する一時文字列は消えてしまう。monoはそのメモリ領域をいちいちfreeしたりはしないが、次のマーシャリングがあると、その領域を再利用する。この時何が起こるかというと、XVaCreateNestedList()で返されたXVaNestedListの中で示される名前へのポインタは、もう正しい名前を指さなくなってしまうということだ*3

ではどうすればいいかというと、stringをGCHandleで固定してしまうこと…ではなく*4、Marshal.AllocHGlobalAnsi()などを使って固定文字列のバッファを生成して、それをIntPtrとして渡せば良い。

…とりあえずこんなところだろうか。マーシャリングまわりでいろいろややこしい問題にハマったのは初めてなので、いろいろ戸惑った(というか戸惑っている)。

*1:まあ、数えるのであればDuncanはそうだ。

*2:逆に、もしstringの引数がrefであれば、戻り値の文字列は場合によっては新しく生成されることにもなるが、そのインスタンス生成もアンマネージドコードの戻り値からランタイムがコピーして返している。

*3:gdbで眺めてみれば、XVaNestedListの項目名へのポインタが示す文字列が、次のマーシャリングのタイミングで変わることだろう。

*4:CLI側の文字列の内部バッファへのポインタが固定されていても、マーシャリングの一時バッファには無関係である。