XIMのプロトコルの説明
以前、X Window Systemのクライアント用ライブラリ(XlibやXCB相当)を作ったのですが、日本語入力に対応していなかったので、IM(インプットメソッド)が利用できるように追加でライブラリを作りました。その時、IMサーバーとのやり取りの仕方が理解しにくいものだったため、まとめてみました(前提としてXプロトコルの知識が必要です)。IMサーバーとのやり取りはXのコアプロトコルとは別に仕様が定められています。
参考記事
概要
登場人物
- クライアント(通信はクライアント用ライブラリで行う)
- Xサーバー
- IMサーバー(IBusやFcitxなど)
クライアントとIMサーバーがXサーバーを介して通信を行います。
方式
- バックエンド方式・フロントエンド方式、
- スタティックイベントフロー、ダイナミックイベントフロー
- オンデマンド同期方式、全同期方式
色々な方式の組み合わせがありますが、今回はバックエンド方式×スタティックイベントフロー×オンデマンド同期方式の組み合わせのみ実装しました。また、入力中の文字列をどう表示するかも色々ありますが、一番簡単なルートウィンドウ型で実装しています。
通信手順
接続前の準備
-
ルートウィンドウのプロパティ「XIM_SERVERS」を取得する。(GetProperty)
その中にIMサーバーのアトムのリストが入っている。(※アトム≒ID、4バイトの数値)
(アトム名は@server=fcitxなど) -
1で得られたIMサーバーのセレクションの所有者を習得する。(GetSelectionOwner)
IMサーバーのウィンドウIDが得られる。 -
IMサーバーのウィンドウに接続用のメッセージを送信する(SendEventでClientMessageイベントを送信)
ClientMessageの内容項目 設定値 format 32 window IMサーバーのウィンドウID type 「_XIM_XCONNECT」(アトム) data クライアントが通信に使うウィンドウのID、メジャートランスポートバージョン、マイナートランスポートバージョン この接続メッセージを送ると、IMサーバーから返答があり(発生したClientMessageイベントのうちtypeが「_XIM_XCONNECT」のもの)、そのデータにIMサーバーが通信に使うウィンドウのIDが含まれるので、以後はこのウィンドウに対してメッセージを送ることになります。
今回はトランスポートバージョンはメジャーバージョンが0、マイナーバージョンが0で実装しました。これは、IMサーバーとの通信はその内容がクライアントメッセージで送れるサイズに収まっている場合(20バイト以内)は、クライアントメッセージでデータを送り、収まらない場合はウィンドウのプロパティにデータを設定し、そのプロパティのアトムをクライアントメッセージで送信するということを意味します。
■クライントメッセージでデータを送信する場合
クライアントメッセージの設定項目 設定値 format 8 window IMサーバーが通信に使うウィンドウ type 「_XIM_PROTOCOL」(アトム) ■プロパティでデータを送信する場合
プロパティの設定(ChangeProperty)項目 設定値 mode Append window IMサーバーが通信に使うウィンドウ property インターンしたアトム(1000個ぐらい用意して使いまわし) type 「XA_STRING」 (事前に定義されたアトムで値としては31) format 8 クライアントメッセージの設定
項目 設定値 format 32 window IMサーバーが通信に使うウィンドウ type 「_XIM_PROTOCOL」 data プロパティに設定したデータの長さ(バイト数)、プロパティを識別するアトム
接続〜
- XIM_OPENメッセージを送り、XIM_OPEN_REPLYメッセージを受け取る。
バイトオーダーやプロトコルのバージョンを指定 - XIM_OPENメッセージを送り、XIM_OPEN_REPLYメッセージを受け取る
XIM_OPEN_REPLYでインプットメソッドの設定可能な属性、インプットコンテキスト(IC)の設定可能な属性を知ることができる - XIM_CREATE_ICメッセージを送り、XIM_CREATE_IC_REPLYメッセージを受け取る
- XIM_SET_IC_FOCUSメッセージを送る(送らなくても動作する?)
イベントフィルタリング
XIM_OPENメッセージを送ったあとにXIM_SET_EVENT_MASKメッセージがIMサーバーから送られてくるので(返答ではない)、forward-event-maskにでフラグが立っているイベント(多分KeyPressとKeyRelease)をXIM_FORWARD_EVENTメッセージでIMサーバーに送信するようにします。また、synchronous-event-maskでフラグが立っているイベントはXIM_FORWARD_EVENTメッセージを同期フラグを立てて送信する必要があります。
※今回は同期フラグ立てない場合を前提として実装
XIM_FORWARD_EVENTメッセージを送ると、IMサーバーが送ったイベントを処理しない場合はIMサーバーからもXIM_FORWARD_EVENTメッセージが送られます。このメッセージに対してXIM_SYNC_REPLYメッセージを送って同期を取る必要があります。同期メッセージを送らないと、その後イベントを送っても処理されません。インプットメソッドをONにするキーを送ると、そのイベントからはIMサーバーで処理されるようになります。入力が確定すると、IMサーバーからXIM_COMMITメッセージが送られてきます。この中に入力された文字列が入っています。エンコーディングは試した限りではFcitxではUTF-8(最初と最後にエスケープシーケンス「Esc % G」、「ESC % @」あり。それぞれUTF-8文字集合を選択、デフォルトの文字集合を選択を意味する)、IBusがISO-2022-JP (JISコード)でした。
接続のクローズ
- XIM_CLOSEメッセージを送り、XIM_CLOSE_REPLYメッセージを受け取る
- XIM_DISCONNECTメッセージを送り、XIM_DISCONNECT_REPLYを送る
作ったもの
今回作ったライブラリは以下にあります。
動かす手順
- Common Lispの処理系をインストール(SBCL推奨)
- 以下のリポジトリを~/common-lispにクローン
https://github.com/yoshida2koji/struct-plus.git
https://github.com/yoshida2koji/xclhb.git
https://github.com/yoshida2koji/xclhb-xim.git - Common Lispを起動し、以下のサンプルコードを実行
(asdf:load-system :xclhb-xim)
(defpackage :xim-sample
(:use :cl)
(:local-nicknames (:x :xclhb)))
(in-package :xim-sample)
(defun xim-sample ()
(declare (optimize debug))
(x:with-connected-client (client)
(let ((window (x:allocate-resource-id client))
(wm-protocols-atom (xclhb-xim::intern-atom-sync client "WM_PROTOCOLS"))
(wm-delete-window-atom (xclhb-xim::intern-atom-sync client "WM_DELETE_WINDOW"))
(quit-p))
;; ウィンドウ作成
(x:create-window client 0 window (xclhb-xim::get-root client)
0 0 100 100 0 0 0
(x:make-mask x:+cw--back-pixel+
x:+cw--event-mask+)
0 0 0 0 0 0 0 0 0 0 0
(x:make-mask x:+event-mask--key-press+
x:+event-mask--key-release+)
0 0 0)
;; ウィンドウが閉じられた際にクライアントメッセージが送られるように設定
(x:change-property client 0 window wm-protocols-atom x:+atom--atom+ 32 1
(x:card32->card8-vector wm-delete-window-atom))
(let ((xim (xclhb-xim:create-xim client window
;; 入力確定時の処理
(lambda (s) (format t "~a~%" s))
;; クライアントメッセージのハンドラー(XIM用意外の)
(lambda (e)
(when (eql wm-protocols-atom (x:client-message-event-type e))
(setf quit-p t))))))
(xclhb-xim:set-filter-event-handler xim
(lambda (e) e)
x:+key-press-event+
(lambda (e)
(format t "key press ~a~%" (x:key-press-event-detail e))))
(xclhb-xim:set-filter-event-handler xim
(lambda (e) e)
x:+key-release-event+
(lambda (e)
(format t "key release ~a~%" (x:key-release-event-detail e))))
(x:map-window client window)
(x:flush client)
(xclhb-xim::loop-while client (lambda () (not quit-p)))
(xclhb-xim:destroy-xim xim)))))
(xim-sample)
うまく動くと、このようなウィンドウが表示されて、入力した内容が標準出力に出力されます。
Discussion