Xlib in scm
pkg-config
前回libxcbの説明書URLを紹介した。そこに、コンパイルのやり方が紹介されてた。
gcc -Wall prog.c -o prog `pkg-config --cflags --libs xcb`
とても使い勝手がよさそうなので、少し調べておく。
vbox$ pkg-config --cflags xcb -I/usr/X11R6/include vbox$ pkg-config --cflags xcb --libs -I/usr/X11R6/include -L/usr/X11R6/lib -lxcb -lXau -lXdmcp
頼もしい友だな。どんな物が対象になってるかは、下記で調べられる。
vbox$ pkg-config --list-all : x11-xcb X11 XCB - X Library XCB interface x11 X11 - X Library xau Xau - X authorization file management libary xaw7 Xaw - X Athena Widgets Library, version 7 xcursor Xcursor - X Cursor Library xdamage Xdamage - X Damage Library xdmcp Xdmcp - X Display Manager Control Protocol library xext Xext - Misc X Extension Library :
いきがかり上、X関係の奴を少しリストアップしてみた。勿論Xだけでは無く、他のパッケージにも適用出来る。
vbox$ pkg-config --cflags tk85 --libs -I/usr/local/include/tk8.5 -I/usr/local/include/tcl8.5 -L/usr/local/lib -ltk85 -ltcl85
これらの元は、/usr/local/lib/pkgconfig/とかのdirの中にxxx.pcとか言う名前で、まとめられている。
guile2-xlib
色々ぐぐっていたら、面白いものを発見。guileとXが合体出来るんだそうです。 guile2-xlib
guile-2.2-devを取り敢えず入れた。3系もあるけど、人生、欲張らないのさ。んで、いつものお約束。
sakae@deb:/tmp/guile2-xlib$ ./configure : configure: error: Package requirements (guile-2.0) were not met: No package 'guile-2.0' found Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix. Alternatively, you may set the environment variables GUILE_CFLAGS and GUILE_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details.
そりゃ、2.2を入れたんだから、2.0は有りませんよ。解決方法が提示されてた。丁度旨い具合に、pkg-configを調べたばかりだった。実習せいと言う事だな。
sakae@deb:/usr/lib/pkgconfig$ pkg-config --cflags guile-2.2 --libs -pthread -I/usr/include/guile/2.2 -lguile-2.2 -lgc
後は、この情報を環境変数に割り当ててから、configureすればいいんだな。
sakae@deb:/tmp/guile2-xlib$ GUILE_CFLAGS='-pthread -I/usr/include/guile/2.2' GUILE_LIBS='-lguile-2.2 -lgc' ./configure
無事にMakefileが作成されたんで、sudo make install
xlib.x:131:203: error: 'scm_x_window_event_x__subr' undeclared (first use in this function); did you mean 'scm_x_window_event_x'? 131 | ; (((((SCM *)((scm_t_cell *) (((scm_t_bits) (0? (*(SCM*)0=((scm_x_window_event_x__subr))): (scm_x_window_event_x__subr)))))) [(1)]) = ((scm_subr_objcode_trampoline (2, 1, 0))))); scm_define (scm_x_window_event_x__name, scm_x_window_event_x__subr);; | ^~~~~~~~~~~~~~~~~~~~~~~~~~ | scm_x_window_event_x make[1]: *** [Makefile:382: libguilexlib_la-xlib.lo] Error 1 make: *** [Makefile:789: install] Error 2
見事に失敗、時代は進化してて、古いものは対応出来無いんだね。残念でしたね。
scm-xlib
前回のC言語で書かれたXアプリの叩き台をscm語に翻訳してみる。たっぷりとBUGが含まれている事でしょう。
;; Xlib sample of scm (with BUG) (require 'Xlib) (define d (x:open-display #f)) (define s (x:default-screen d)) (define w (x:create-window d (x:root-window d s) 100 100 256 256 4 (x:screen-black d s) (x:screen-white d s))) (x:map-window d w) (define g (x:create-gc d w 0 0)) (x:window-set! w event-mask (or x:Button-Press-Mask x:Exposure-Mask)) (let loop (e (x:next-event d)) (case (x:event-ref e X-event:type) ((X:BUTTON-PRESS (x:draw-string d w g 100 100 "Hello")))) (loop e))
走らせて、どんな具合か確認する。
vbox$ scm scmXlib.scm ;ERROR: "scmXlib.scm": x:create-window: Wrong type in arg1 #<X display "aa.bb.cc.5:0"> ; in expression: (#@x:create-window #@d (#@x:root-window #@d #@s) 100 10 ... ; in top level environment. ; defined by load: "scmXlib.scm" ;STACK TRACE 1; ((#@cond ((#@not #@0+0) (#@set! *catalog* #f)) ((#@slib:provid ... 2; (#@define w (#@x:create-window #@d (#@x:root-window #@d #@s) 1 ... 3; (#@define ((filesuf #@file) (hss (#@has-suffix? #@file (#@sche ...
一発で動くなんて言う奇跡を信じちゃいけないね。
scm with gdb
OpenBSDのパッケージから入れたscmはgdbからコントロールは勿論出来無い状態になっている。久し振りに自前でコンパイルしてみるか。しばし思考錯誤する。で、gdb付になったよ。
A)cd /usr/ports/lang/scm; make configure して、ソースを取寄せ、展開、パッチ当て、configure まで、実施。ここまでは、普通の手順だ。
B)cd /usr/ports/pobj/scm-5f1/scm; emacs build.scm ワークエリアへ移動。build.scmがMakefileの代わりを努めているので、編集。
(defcommand compile-c-files openbsd (lambda (files parms) (and (batch:try-chopped-command parms "cc" "-g" "-c"
ccの引数に、-g を追加する。
C)こうしておいて、lang/scmの中でmake install 。残念ながらinstall中にstripされちゃうので、ワークエリア中の、scmを/usr/local/bin/ へ、cpしてしまう(ちょっとダサイやり方だけど、楽だからね)。
x:create-window
適当に書下したコードのBUGを潰して行く。その前に、C語の該当する関数を眺めておく
Window XCreateSimpleWindow(display, parent, x, y, width, height, border_width, border, background) Display *display; Window parent; int x, y; unsigned int width, height; unsigned int border_width; unsigned long border; unsigned long background;
そしてこちらは、SCM語のマニュアルからの掲載
-- Function: x:create-window window position size border-width depth class visual field-name value ... : -- Function: x:create-window window position size border-width border background :
同じ関数名で、引数の数が違うものが用意されている。引数の最初の幾つかは、同じ型だが、後ろの方は型が違う。それに、引数の個数も片や可変長、一方は6個に固定だ。器用な事するな。関数名を別にすれば良いのに。ボストンの頭の良い先生にはついていけない。
Breakpoint 1, x_create_window (swin=0x4b5e4180, spos=0x4b5e4178, sargs=0x4b5e3d\ 30) at x.c:1009 1009 int len = ilength(sargs); (gdb) bt 7 #0 x_create_window (swin=0x4b5e4180, spos=0x4b5e4178, sargs=0x4b5e3d30) at x.c\ :1009 #1 0x184e6bf3 in ceval_1 (x=0x4b5e3d60) at eval.c:2745 #2 0x184e686f in evalcar (x=<optimized out>) at eval.c:1354 #3 toplevel_define (xorig=<optimized out>, env=0x4b5e4260) at eval.c:737 #4 ceval_1 (x=<optimized out>) at eval.c:2387 #5 0x184e9d78 in ceval (x=0x4b5e4070, static_env=0x4b5e4260, env=0x4774) at ev\ al.c:2025 #6 0x184dab88 in tryload (filename=0x4b5e43f8, reader=0x4374) at repl.c:1898 (More stack frames follow...)
片方の端末でscmを起動しBUG入りを走らせた。もう一方の端末でemacsを起動してからアタッチした。そして目的関数にBPを置いて走らせた。一応バックトレースしたよ。個数は、BUGとり成就を祈願して7個にした。いやそれなら末広がりで8個がいいとか。。。面倒なら全部出すと12フレームだったよ。
SCM x_create_window(swin, spos, sargs) SCM swin, spos, sargs; { XPoint position, size; unsigned int border_width; Window window; B int len = ilength(sargs); =>ASRTER(NIMP(swin) && OPWINDOWP(swin), swin, ARG1, s_x_create_window); scm2XPoint(!0, spos, &position, (char *)ARG2, s_x_create_window); scm2XPoint(0, CAR(sargs), &size, (char *)ARG3, s_x_create_window); :
引数は3個目からは、リストにまとめてしまう仕様のようだ。そして、最初の引数が正当なものか検査してるんだな。
(gdb) n Warning: Cannot insert breakpoint 0. Cannot access memory at address 0xddcc0212 siglongjmp () at /usr/src/lib/libc/arch/i386/gen/sigsetjmp.S:76 76 movl 4(%esp),%edx # parameter, pointer to env
早速検査に引掛って、強制脱出のため、longjmpのルーチンへ飛び込んだよ。
エラーの内容とソースコードを見比べると、第一引数はwindow型のようだ。半信半疑で当初のコード中のdisplayを省略してみたぞ。
;ERROR: "scmXlib.scm": x:create-window: Wrong type in arg2 100 ; in expression: (#@x:create-window (#@x:root-window #@d #@s) 100 100 25 ...
第一引数は満足したみたい。ここにはdisplay要素が含まれているから大丈夫なんだな。その代わり第二引数がおかしいとな。position型が欲しいのにただの数値だと言っている。
positionなら、X座標とY座標の組だろう。多分LISP系なら、リストなりベクターにまとめてしまえだろうね。サイズも窓の幅と高さの組だろう。
;ERROR: "scmXlib.scm": Wrong type to apply: 100 ; in expression: (100 100)
ああ、急いでいて、やっちまっただ。リストの第一引数は関数名。それがいやなら、クォートせよ。データとして扱ってねだ。
これで、第一の難関は突破したぞ。少し、コードを味わっておく。
ASRTER(NIMP(swin) && OPWINDOWP(swin), swin, ARG1, s_x_create_window); scm2XPoint(!0, spos, &position, (char *)ARG2, s_x_create_window); scm2XPoint(0, CAR(sargs), &size, (char *)ARG3, s_x_create_window); sargs = CDR(sargs); GET_NEXT_INT(border_width, sargs, ARG4, s_x_create_window); if (4==len) { unsigned long border; unsigned long background; GET_NEXT_INT(border, sargs, ARG5, s_x_create_window); GET_NEXT_INT(background, sargs, ARGn, s_x_create_window); window = XCreateSimpleWindow(XWINDISPLAY(swin), XWINDOW(swin), position.x, position.y, /* initial placement */ size.x, size.y, border_width, border, background); /* pixel values */ } else { : window = XCreateWindow(XWINDISPLAY(swin), XWINDOW(swin), position.x, position.y, /* initial placement */ size.x, size.y, border_width, depth, class, XVISUAL(svis), valuemask, &attributes);
なんだ、内部的に、XCreateSimpleWindowを使ってるじゃん。これが先生の頭の中身か。ソースが有るとソースかと納得出来る事が多いな。純粋なXでは、第一引数がDisplay型で第二引数がWindow型のparentだけど、scmでは、parentから分解して充当してる。ケチケチ作戦の発動だな。
enable event
scmを走らせると、引数に不備が有ると指摘してくれるんで、単なるもぐら叩きの連続だ。上記のように gdb + source + 多少の想像力で何とかなる。
何とかならないのは、前回の叩き台で出て来た、XSelectInputと言う関数。scmには、そんな関数は無い。無くても何とかなってるって事は裏道が有るに違いない。目を皿のようにしてマニュアルを眺めたよ。そう、元関数のやってる事を理解した上でね。
私の解釈では、許可するイベントを設定してるんだな。それにしては、ButtonPressMaskのMaskって何だ。用語の使い方を間違っていないか。素直なオイラーならButtonPressEnableにするぞ。まあ、これも頭の良いMITの方々が考えた事だから。。。
で、代替え品を発見した。
-- Function: x:window-set! window field-name value ... Changes the components specified by FIELD-NAMEs for the specified WINDOW. The restrictions are the same as for `x:create-window'. The order in which components are verified and altered is server dependent.
これが使えそう。
(x:window-set! w event-mask (or x:Button-Press-Mask x:Exposure-Mask))
event-maskは、当初適当に書いておいたもので、正式には、x:CW-Event-Mask が正解。ここまでは、scmがちゃんと指摘してくれたので楽勝。問題は次のorの指定。
後にちゃんと動くようになって、ここに逆戻りしちゃったのだ。曰く、orの第一引数で指定したものしか、enableにならないんだ。
x11.scm
(define x:Button-Press-Mask 4) (define x:Exposure-Mask 32768)
こんな風に定義されてるんで、orに適用してみる。
> (or x:Button-Press-Mask x:Exposure-Mask) 4 > (or x:Exposure-Mask x:Button-Press-Mask) 32768
ちゃんと機能と現象が一致した。解決策は、bit-or とかロジカルorだろうけど、どうもそんなの無いっぽい。足し算で我慢するか。取り敢えず、これが第二の難関ですかね。細かい事を言うと、整数のBIT幅を確認しておかないと、後に不思議なBUGになる鴨。
落とし穴な case
最後のMainLoop相当を、scheme流に実装した。叩き台では、whileを使ってたけどね。そんなのscmに無いのさ。
で、思ったように反応しない。こういうのが一番困る。まだエラーが出てくれた方が256倍は楽と思うぞ。苦肉の策として、ちゃんとループを回っているか? イベントは届いているかを確認する為、print文(schemeの場合display文だけど)を埋め込んだ。
(let loop ((e (x:next-event d))) (display e)(display (x:event-ref e X-event:type))(display "\n") ; for debug (case (x:event-ref e X-event:type) ((x:Expose) (display "expose\n")) ((x:Button-Press) (x:draw-string w g '(100 100) "BUTTON") (display "button pressed\n"))) (loop (x:next-event d)))
下記の結果が、コンソールに出てきた。
#<X event: ButtonPress>4 #<X event: Expose>12 #<X event: ButtonPress>4
窓の中でマウス・クリックすると4が出力。窓枠をもってズリズリすると12が出力された。でも窓中にはBUTTON表示されず。アレレのレレだ。摩訶ふしぎだ。caseが機能していない。
こうなると、疑うべきは、caseの使いかただな。gaucheのInfoを参照してみた。scmのマニュアルでは、R5RSは、載っていないんだ。そんなの常識でしょって態度。だから改めて常識を確認しましょって事だ。
-- Special Form: case key clause1 clause2 ... [R7RS+ base][SRFI-87] KEYは任意の式です。CLAUSEは以下の形式でなけれ ばなりません。 ((DATUM ...) EXPR EXPR2 ...) ((DATUM ...) => PROC) ここで、各DATUMはSchemeオブジェクトの外部表現であり、全てのDATUMは 異なっていなければなりません。最後のCLAUSEには次の形式を持つelse節 が許されます。 (else EXPR EXPR2 ...) (else => PROC) まずKEYが評価され、その結果がそれぞれのDATUMと比較されます。 KEYの 値と‘eqv?’(see 等価参照)を使って一致するDATUM が見つかれば、対 応するEXPRが順に評価され、その最後の値が‘case’の 値となります。 ‘=>’を含む節はSRFI-87で定義されています。これらの節では、 KEYの結果 がPROCに渡され、その結果が‘case’の値となります。
実を言うと、caseは初体験です。最初はifとかcondを使うんだろうなと、昔のscm本を見ていたら、caseが紹介されたので、取り入れてみた次第。このcaseって、FORTRANに有る、計算型gotoじゃなかろうか、なんてふと思ったぞ。
さらに心配だったので、gauche作者のshiroさんに尋ねてみた(gauche本を三田んだ)。そしたら、注意が載ってたぞ。DATUMは即値の必要があります。ですから変数では指定出来ませんですって。言われてみればそうだな。素早く飛んでけだから、いちいち評価なんてしていられない。
x11.scm
(define x:Button-Press 4) (define x:Expose 12)
上記を参考に、即値に変更。
#<X event: ButtonPress>4 button pressed #<X event: Expose>12 expose #<X event: ButtonPress>4 button pressed
そしたら、ちゃんと動いた。取り敢えず移植完了ですかね。
完動なスクリプト
;; Xlib sample code for scm(scheme) (require 'Xlib) (define d (x:open-display #f)) (define s (x:default-screen d)) (define w (x:create-window (x:root-window d s) '(100 100) '(256 256) 4 (x:screen-black d s) (x:screen-white d s))) (x:map-window w) (define g (x:create-gc w)) (x:window-set! w x:CW-Event-Mask (+ x:Exposure-Mask x:Button-Press-Mask)) (let loop ((e (x:next-event d))) ;; (display e)(display (x:event-ref e X-event:type))(display "\n") ; debug (case (x:event-ref e X-event:type) ((12) (display "expose\n")) ((4) (x:draw-string w g '(100 100) "BUTTON") (display "button pressed\n"))) (loop (x:next-event d)))
見ての通り、ウィジット(部品)をブレッド・ボードに展開しただけの作りです。本当なら、(x:open-display #f)の結果のd等は、トップレベルに出し放なしにしないで、それなりの関数内に押し込めるのが筋。でも、あえてそうはしない。部品取り用ですから。
昨今、半導体不足の折、破棄された製品のボードから、部品を取り出して売ると言うのが横行してるらしい。時流に乘ってみた訳だな。
それから、DEBUG文は、もしもの為に残しているよ。
TODO
上記を走らせて、終了はどうする? 出て来た窓の削除をすれば、取り敢えずおk。
XIO: fatal IO error 0 (No error: 0) on X server "localhost:10.0" after 12 requests (11 known processed) with 0 events remaining.
こんな恨み言を出して終了してくれる。余り気分がいいとは言えないな。キーボードからq を入力したら、正しい手順で終了させる。ついでに、それ以外のキー入力は、窓内に表示させるぐらいは、発展問題として、やってみるか。
まて、それより、 江戸の敵を長崎で討つ が、先かな。
from guile xlib sample
先頭の方に出てきたguileのそれ。何か参考資料は無かと一生懸命に探していた時に見付けたもの。scmには、例が少くて、もう苦しいのよ。
あえてscmに手を出す人なら、マニュアルとソースが有れば十分でしょだからな。世の中の誰かは、多分やってるだろうけど、そういう人は、WEBに載せるなんて面倒事はしないんだろうね。
下記は、これ、どうだ凄いだろうの宣伝文だ。
Bring up an X window: (define d (x-open-display!)) (define w (x-create-window! d)) (x-map-window! w) (x-flush! d) Draw a line and a rectangle in it: (define gc (x-default-gc d)) (x-draw-line! w gc 100 100 200 200) (x-flush! d) (x-draw-rectangle! w gc 200 200 50 50) (x-flush! d)
これ幸いとばかり、guileの敵をscmで討つ、かな。それは次回に回そう。もう少し別な事を書いておく。
event mask
どんなイベントをイネーブルにするかってのを、xxxMaskて風に指定する。用語を間違っていると思うけど、どうよ。
新型コロナから身を守る為、マスクを着用しましょう。ウィルスを拡散しないようにマスクをしましょうのごとく、隠すってニュアンスが普通だと思うぞ。
それが全く、逆の意味で使われている。さあさあ、たぶんこうだったんじゃなかろうか劇場の開幕です。
昔はXサーバーがクライアントに伝えるイベントが少かった。だったら、サーバーは全てのイベントを余す事なく伝えるよ。だから、クライアント側では、不要なイベントをマスクして、伝達不要って宣言してね。
時が過ぎ、あちこちで使われ出すと、色々なイベントの要求が続出した。普通のクライアントには不要なイベントも多数ある。それらを、いちいちマスクするなんて非常に大変。
ええい、面倒だ。ソース上の名前はそのままで、論理を反転しちゃえって暴挙に出た。何たって、Xを開発してる所は、象牙の塔なMITですから。偉い先生には逆らえません。 その証拠。
x11.scm(X11/X.h)
(define x:No-Event-Mask 0) (define x:Key-Press-Mask 1) (define x:Key-Release-Mask 2) (define x:Button-Press-Mask 4) : (define x:Colormap-Change-Mask 8388608) (define x:Owner-Grab-Button-Mask 16777216)
ねっ、解るでしょ。最初の方は皆馴染があると思うけど、最後の方なんて、想像もつかないようなイベントですよ(オイラーの推測です、真にうけないように)。
.gdbinit
scm,code,eval,car,cdr,disp なんて言う楽しそうなgdbスクリプトが用意されてる。 分かりやすい例で、多数の引数はリストにまとめられる。そのリストの内容を、特別コマンドで表示させてみる。
Breakpoint 1, x_create_window (swin=0x6cf96318, spos=0x6cf91a50, sargs=0x6cf91980) at x.c:1009 1009 int len = ilength(sargs); (gdb) disp sargs
第三引数も、gdb上ではただの数値。でも、scmを熟知したコマンドに託すと、
> ((256 256) 4 0 16777215)
結果は、scmを起動してる端末上に表示される。これらなかなか便利ですよ。 これが、.gdbinit中で、どう実現されてるか?
define disp call scm_iprin1($arg0, sys_protects[2], 0) echo \n end
ここで使われている関数を追っていくと、自然とscmの中に潜り込めるな。.gdbinitもお宝だなあ。なお、gdb上から、.gdbinitを使うには、$HOME/.gdbinit
vbox$ cat .gdbinit add-auto-load-safe-path /usr/ports/pobj/scm-5f1/scm/.gdbinit
こんな用意が必要です。