🐴

Common Lisp でX Window Systemのクライアント用ライブラリを作成

2023/01/09に公開

はじめに

名前はxclhb(X Common Lisp Hobby Binding)です。あくまでおもちゃです。
https://github.com/yoshida2koji/xclhb
作成した動機としては、

  • 同様のライブラリにCLXがあるが、正直使いにくい(自分のX Window Systemへの理解不足)。
  • できるだけ何もしない小さいライブラリがほしい(コールバック形式にしたのはやりすぎた気もする)。

といったところです。

参考にしたもの

X Window Systemとは

主にUnix系のOSでGUIを作成するために使われる。WindowsやMacとは違い、OSに統合されていない。サーバーとクライアントに分かれている。サーバーはクライアントからの要求に従ってウィンドウを作成し表示したり、マウスやキーボードの入力をクライアントに通知したりする。
クライアントとサーバー間の通信は、共有メモリ(Unixドメインソケット)、またはTCPによって行われる。

X Window Systemを利用するには以下の準備が必要

  1. クライアントがXサーバーとの通信用のソケットを作成
  2. クライアントが接続設定(バイトオーダーやプロトコルのバージョンなど)をソケットに書き込む
  3. Xサーバーが書き込んだ、これからリクエストをするにあたって必要な情報を読み込む

これ以降はクライアントはリクエストの書き込み、Xサーバーによって書き込まれたリプライ(リクエストに対する)、イベント、エラーの読み込みを行いながらGUIを構築する。

X Window Systemで規定されているパケットの種類

  • リクエスト
    クライアントからサーバーに送られる。ウィンドウの作成や描画などのリクエスト。

    名前 オフセット バイト数 備考
    メジャーオペコード 0 1
    データ 1 1 リクエストの種類に応じてあったりなかったり
    リクエストのサイズ 2 2 リクエスト全体のバイト数 / 4

    これ以降はリクエストに応じたデータが続いていく。

  • エラー
    サーバーからクライアントに送られる。リクエストが不正だった場合にエラーが発生する。

    名前 オフセット バイト数 備考
    種類 0 1 固定で「0」
    エラーコード 1 1
    対応するリクエストの連番 2 2
    データ 4 4 問題のあったリソースIDなどが設定される
    マイナーオペコード 8 2 不正なリクエストのマイナーオペコード(拡張プロトコルのみ)
    メジャーオペコード 10 1 不正なリクエストのメジャーオペコード
    未使用項目 11 21 エラーはすべて32バイト
  • リプライ
    サーバーからクライアントに送られる。リクエストに対するリプライ。

    名前 オフセット バイト数 備考
    種類 0 1 固定で「1」
    データ 1 1 リクエスの種類に応じてあったりなかったり
    対応するリクエストの連番 2 2
    リプライのサイズ 4 4 (リプライ全体のバイト数 - 32バイト) / 4

    これ以降はリクエストに応じたデータが続いていく。

  • イベント
    サーバーからクライアントに送られる。ユーザーの入力などをイベントとして通知する。

    名前 オフセット バイト数 備考
    イベントコード 0 1
    最後にされたリクエストの連番 2 2

    これ以降はイベントに応じたデータが続いていく。

エラー、リプライ、イベントは最低でも32バイト。
リクエストの連番は1始まり。サーバーから帰ってくる連番は下位2バイトということに注意。

ウィンドウを表示するまでに何をやっているかをサンプルを元に解説

https://github.com/yoshida2koji/xclhb-samples
このサンプルは背景が青のウィンドウを3秒間表示するだけのプログラム。

;; xはxclhbのローカルニックネーム
(defun show-window (&optional host)
  (x:with-connected-client (client host)
    (let ((root (x:screen-root (elt (x:setup-roots (x:client-server-information client)) 0)))
          (wid (x:allocate-resource-id client)))
      (x:create-window client ; client
                           0 ; depth 0 is same as th parent
                           wid ; wid
                           root ; parent
                           0 ; x changed by window manager
                           0 ; y changed by window manager
                           800 ; width
                           600 ; height
                           0 ; border-width
                           0 ; class 0 is same as the parent
                           0 ; visual 0 is same as the parent
                           (x:make-mask x:+cw--back-pixel+) ; value-mask
                           ;; the following arguments are used when the collesponding
                           ;; bits of the value-mask are set.
                           0 ; background-pixmap
                           #x0000ff ; background-pixel blue
                           0 ; border-pixmap
                           0 ; border-pixel
                           0 ; bit-gravity
                           0 ; win-gravity
                           0 ; backing-store
                           0 ; backing-planes
                           0 ; backing-pixel
                           0 ; override-redirect
                           0 ; save-under
                           0 ; event-mask
                           0 ; do-not-propogate-mask
                           0 ; colormap
                           0 ; cursor
                           )
      (x:map-window client wid)
      (x:flush client)
      (sleep 3))))

with-connected-clientマクロ

展開すると以下のようになる

(multiple-value-bind (client x::err)
    (x::x-connect host)
  (when x::err (error "connection error ~a" x::err))
  (unwind-protect
      (progn
       (let ((root
              (x:screen-root
               (elt (x:setup-roots (x:client-server-information client)) 0)))
             (wid (x:allocate-resource-id client)))
         (x:create-window client 0 wid root 0 0 800 600 0 0 0
                          (x:make-mask x:+cw--back-pixel+) 0 255 0 0 0 0 0 0 0
                          0 0 0 0 0 0)
         (x:map-window client wid)
         (x:flush client)
         (sleep 3)))
    (x:x-close client)))

x-connect関数によりXサーバーと接続を行い、bodyの処理が終わったら接続を終了する、よくあるマクロ

x-connect関数

(defun x-connect (&optional host)
  (let ((stream (make-x-stream host))) ; 1
    ;; request
    (destructuring-bind (auth-name auth-data) (get-auth-info) ; 2
      (let* ((setup-request (make-setup-request :byte-order #x42 ; msb ; 3
                                                :protocol-major-version 11
                                                :protocol-minor-version 0
                                                :authorization-protocol-name-len (length auth-name)
                                                :authorization-protocol-data-len (length auth-data)
                                                :authorization-protocol-name auth-name
                                                :authorization-protocol-data auth-data))
             (buf (make-buffer (%setup-request-length setup-request))))
        (write-setup-request buf (make-offset) setup-request) ; 4
        (write-sequence buf stream)) ; 5
      (finish-output stream))
    (multiple-value-bind (response status) (read-setup-response stream) ; 6
      (if (= status 1)
          (with-setup (resource-id-base resource-id-mask) response
            (make-client :stream stream
                         :server-information response
                         :resource-id-base resource-id-base
                         :resource-id-byte-spec (make-byte-spec-for-resource-id resource-id-mask)))
          (values nil response)))))
  1. make-x-stream関数によりXサーバーと通信するためのソケットを作成する。
    hostが指定されている場合はTCPを、指定されていない場合はUnixドメインソケットを使用する。この部分はCLXを参考に作成。

  2. get-auth-info関数で認証情報を取得する。この部分もCLXを参考に作成。

  3. にmake-setup-request関数でsetup-request構造体を作成する。

  4. write-setup-request関数でその内容をバイト配列へ書き込む。(このあたりの読み込み・書き込み用関数はXCBのXMLファイルを元にマクロを使用して作成)

  5. バイト配列の内容をソケットに書き込むことで実際のXサーバーへのリクエストが行われる。

  6. リクエストを書き込んだらXサーバーからのレスポンスが書き込まれるので、read-setup-response関数によりレスポンスを読み込む

read-setup-response関数

(defun read-setup-response (stream)
  (let ((header-buffer (make-buffer 8)))
    (read-sequence header-buffer stream)
    (let* ((status (read-card8 header-buffer 0))
           (length (read-card16 header-buffer 6))
           (buffer (make-buffer (+ 8 (* 4 length)))))
      (dotimes (i (length header-buffer))
        (setf (aref buffer i) (aref header-buffer i)))
      (read-sequence buffer stream :start (length header-buffer))
      (values (funcall (ecase status
                         (0 #'read-setup-failed)
                         (1 #'read-setup)
                         (2 #'read-setup-authenticate))
                       buffer (make-offset))
              status))))

レスポンスの最初の1バイト目は接続が成功か失敗かを表す。成功と失敗でレスポンスの内容も長さも異なるので、1バイト目の値に応じてレスポンス読み込み用の関数を切り替えて読み込みを行う。

x-connect関数に戻り、レスポンスの1バイト目が成功の場合はclient構造体(ソケットやXサーバーの情報をまとめたもの)を作成し関数の返り値とする。

with-connected-clientマクロに渡したボディ部分

(let ((root
       (x:screen-root
        (elt (x:setup-roots (x:client-server-information client)) 0)))
      (wid (x:allocate-resource-id client)))
  (x:create-window client 0 wid root 0 0 800 600 0 0 0
                   (x:make-mask x:+cw--back-pixel+) 0 255 0 0 0 0 0 0 0
                   0 0 0 0 0 0)
  (x:map-window client wid)
  (x:flush client)
  (sleep 3))

ウィンドウを作成する前に親ウィンドウのリソースIDの取得と作成するウィンドウのリソースIDの割当を行う。親ウィンドウのリソースIDは接続設定のレスポンスに含まれるスクリーンのルートウィンドウのものを使用する。

create-window関数でウィンドウ作成リクエストを行う。
この例ではウィンドウの左上の座標が(0, 0)、ウィンドウのサイズが800*600、背景色が青となるように設定している。ただしウィンドウの左上の座標についてはウィンドウマネージャーによって変更される。
ウィンドウを作成しただけではウィンドウは表示されないのでmap-window関数によりウィンドウを配置させる。

以上で画面にウィンドウが表示される。

おわりに

リプライ、イベント、エラー周りをどう処理するかだったり、XMLの定義から構造体や関数を作成するところだったりは気が向いたら書こうと思います。

Discussion