🐡

XIMのプロトコルの説明

2025/01/20に公開

以前、X Window Systemのクライアント用ライブラリ(XlibやXCB相当)を作ったのですが、日本語入力に対応していなかったので、IM(インプットメソッド)が利用できるように追加でライブラリを作りました。その時、IMサーバーとのやり取りの仕方が理解しにくいものだったため、まとめてみました(前提としてXプロトコルの知識が必要です)。IMサーバーとのやり取りはXのコアプロトコルとは別に仕様が定められています。
https://www.x.org/releases/X11R7.6/doc/libX11/specs/XIM/xim.html

参考記事
https://qiita.com/ai56go/items/63abe54f2504ecc940cd

概要

登場人物

  • クライアント(通信はクライアント用ライブラリで行う)
  • Xサーバー
  • IMサーバー(IBusやFcitxなど)

クライアントとIMサーバーがXサーバーを介して通信を行います。

方式

  • バックエンド方式・フロントエンド方式、
  • スタティックイベントフロー、ダイナミックイベントフロー
  • オンデマンド同期方式、全同期方式

色々な方式の組み合わせがありますが、今回はバックエンド方式×スタティックイベントフロー×オンデマンド同期方式の組み合わせのみ実装しました。また、入力中の文字列をどう表示するかも色々ありますが、一番簡単なルートウィンドウ型で実装しています。

通信手順

接続前の準備

  1. ルートウィンドウのプロパティ「XIM_SERVERS」を取得する。(GetProperty)
    その中にIMサーバーのアトムのリストが入っている。(※アトム≒ID、4バイトの数値)
    (アトム名は@server=fcitxなど)

  2. 1で得られたIMサーバーのセレクションの所有者を習得する。(GetSelectionOwner)
    IMサーバーのウィンドウIDが得られる。

  3. 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 プロパティに設定したデータの長さ(バイト数)、プロパティを識別するアトム

接続〜

  1. XIM_OPENメッセージを送り、XIM_OPEN_REPLYメッセージを受け取る。
    バイトオーダーやプロトコルのバージョンを指定
  2. XIM_OPENメッセージを送り、XIM_OPEN_REPLYメッセージを受け取る
    XIM_OPEN_REPLYでインプットメソッドの設定可能な属性、インプットコンテキスト(IC)の設定可能な属性を知ることができる
  3. XIM_CREATE_ICメッセージを送り、XIM_CREATE_IC_REPLYメッセージを受け取る
  4. 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コード)でした。

接続のクローズ

  1. XIM_CLOSEメッセージを送り、XIM_CLOSE_REPLYメッセージを受け取る
  2. XIM_DISCONNECTメッセージを送り、XIM_DISCONNECT_REPLYを送る

作ったもの

今回作ったライブラリは以下にあります。
https://github.com/yoshida2koji/xclhb-xim

動かす手順

  1. Common Lispの処理系をインストール(SBCL推奨)
  2. 以下のリポジトリを~/common-lispにクローン
    https://github.com/yoshida2koji/struct-plus.git
    https://github.com/yoshida2koji/xclhb.git
    https://github.com/yoshida2koji/xclhb-xim.git
  3. 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