ghostscript探検
ghostscript観光準備
前回の雪観測で、gsのソースの性格が少しは分かってきたように思う。より広範囲に効率よく回れるように、gdbとタイアップする事にした。
configureしたらgccが選ばれて、こんなログが出て来た。これってオイラー仕様の設定? -g3なんて正しくオイラー用だ。
checking supported compiler flags... -O2 -DNDEBUG -Wall -Wstrict-prototypes -Wundef -Wmissing-declarations -Wmissing-prototypes -Wwrite-strings -fno-strict-aliasing -Werror=declaration-after-statement -fno-builtin -fno-common -Werror=return-type -gdwarf-2 -g3 -O0 -fvisibility=hidden ...done.
安全の為、 Makefileを確認したら、ぬか喜びは否定されていたんで、下記のようにdebug道を選んだよ。
CFLAGS_STANDARD= -gdwarf-2 -g3 -O0 #CFLAGS_STANDARD= -O2 -DNDEBUG CFLAGS_DEBUG= -gdwarf-2 -g3 -O0
待つ事約5分で用意が出来た。出来る事なら、emacsのps-modeから起動するgsに、このdebug道が通ったgsを使いたい。どうする? 3秒考えて、PATHの頭にこのgsを持って来る事にした。 幸い、インストールしなくても動いたので良かったわい。
74553 ?? I 0:00.95 /home/sakae/ghostscript-9.53.3/bin/gs -r72 -sPAPERSIZE=a4 28993 p2 S+ 0:01.86 emacs snowflak.ps (emacs-27.1)
ps -a では、起動してるgsが見えなかったので、ぷち焦った。emacsがgsの制御権を持ってるから、出てこないのだな。
gsの入り口
動いてるgsにアタッチメントしてみる。
#0 _thread_sys_read () at /tmp/-:3 #1 0x085fde16 in _libc_read_cancel (fd=0, buf=0x643591b4, nbytes=1024) at /usr/src/lib/libc/sys/w_read.c:27 #2 0x17e108a0 in gp_stdin_read (buf=0x643591b4 "\n/tmp/ps-run-2t5mMQ) run\n", len=1024, interactive=1, f=0x285c35c8 <__sF>) at ./base/gp_stdia.c:29 #3 0x183ca026 in s_stdin_read_process (st=0x643590b4, ignore_pr=0xcf7fab20, pw=0x64359114, last=0) at ./psi/ziodevsc.c:113 #4 0x17f4e906 in sreadbuf (s=0x643590b4, pbuf=0x64359114) at ./base/stream.c:823 :
22フレーム目でやっと最上位になった。ちょいと活動中の働きを見てみるか。
(gdb) b zrand Breakpoint 1 at 0x183cafa6: file ./psi/zmath.c, line 205. (gdb) c Continuing. Breakpoint 1, zrand (i_ctx_p=0x7d1de03c) at ./psi/zmath.c:205 205 os_ptr op = osp; (gdb) bt #0 zrand (i_ctx_p=0x7d1de03c) at ./psi/zmath.c:205 #1 0x183ad2dc in interp (pi_ctx_p=0x48b7be8c, pref=0xcf7fbdec, perror_object=0\ x48b7be74) at ./psi/interp.c:1410 #2 0x183aac9b in gs_call_interp (pi_ctx_p=0x48b7be8c, pref=0xcf7fbdec, user_er\ rors=0, pexit_code=0xcf7fbee0, perror_object=0x48b7be74) at ./psi/interp.c:520 #3 0x183aaa75 in gs_interpret (pi_ctx_p=0x48b7be8c, pref=0xcf7fbdec, user_erro\ rs=0, pexit_code=0xcf7fbee0, perror_object=0x48b7be74) at ./psi/interp.c:477 #4 0x1839bc3f in gs_main_interpret (minst=0x48b7be18, pref=0xcf7fbdec, user_er\ rors=0, pexit_code=0xcf7fbee0, perror_object=0x48b7be74) at ./psi/imain.c:257 #5 0x1839d3bd in gs_main_run_string_continue (minst=0x48b7be18, str=0x17c60dec\ <start_string> "systemdict /start get exec\n", length=27, user_errors=0, pexit\ _code=0xcf7fbee0, perror_object=0x48b7be74) at ./psi/imain.c:908 #6 0x1839d25a in gs_main_run_string_with_length (minst=0x48b7be18, str=0x17c60\ dec <start_string> "systemdict /start get exec\n", length=27, user_errors=0, pe\ xit_code=0xcf7fbee0, perror_object=0x48b7be74) at ./psi/imain.c:866 #7 0x183a4354 in psapi_run_string_with_length (ctx=0x4e6cdf18, str=0x17c60dec \ <start_string> "systemdict /start get exec\n", length=27, user_errors=0, pexit_\ code=0xcf7fbee0) at ./psi/psapi.c:430 #8 0x184312c9 in gsapi_run_string (instance=0x4e6cdf18, str=0x17c60dec <start_\ string> "systemdict /start get exec\n", user_errors=0, pexit_code=0xcf7fbee0) a\ t ./psi/iapi.c:295 #9 0x17e0eb09 in main (argc=3, argv=0xcf7fbf54) at ./psi/gs.c:120
乱数発生のアルゴリズムは、アドビのそれに習いましたって書いてるけど、アドビはソースを公開してるのかしらん?
/* * We use an algorithm from CACM 31 no. 10, pp. 1192-1201, * October 1988. According to a posting by Ed Taft on * comp.lang.postscript, Level 2 (Adobe) PostScript interpreters * use this algorithm too: * x[n+1] = (16807 * x[n]) mod (2^31 - 1)
repeat
repeatってどうなってる? 機械的に zrepeat にBP置いて走らせると
/* <int> <proc> repeat - */ static int repeat_continue(i_ctx_t *); int zrepeat(i_ctx_t *i_ctx_p) { B => os_ptr op = osp; check_proc(*op); check_type(op[-1], t_integer); if (op[-1].value.intval < 0) return_error(gs_error_rangecheck); :
冒頭にforth風の案内が出てて分かりやすい。リピート回数に負の数は取れないって、当たり前だけど、重要なチェックをしてるな。
勿論、負数はエラーだ。
GS<4>-3 {=} repeat Error: /rangecheck in --repeat-- Operand stack: --nostringval-- 1 2 3 -3 --nostringval-- :
それは置いておいて、本筋のop。ユーザーが使うstack。整数とか浮動小数とか文字列とか配列とか関数まで置ける。lispで言うcons相当だな。ちなみみ、ダンプしてみると
(gdb) p *op $3 = { tas = { type_attrs = 1768, _pad = 0, rsize = 1 }, value = { intval = 6330397992, boolval = 13608, realval = 6.82162798e+34, saveid = 2035430696, : pfile = 0x79523528, pdevice = 0x79523528, pstruct = 0x79523528, dummy = 6330397992 } }
このように膨大なエレメントが裏に控えていた。値そのものが置いてある訳じゃなくて、ポインターが置かれるのね。それぞれのエレメントの説明を見るのが、理解への早道かな。
check_proc
とか check_type
で、TOPが関数、その下側が整数か調べているはずだ。けど、gdbでBPを置けないよ。ひょっとしてマクロかな?
(gdb) info macro check_proc The symbol `check_proc' has no definition as a C/C++ preprocessor macro at /tmp/ghostscript-9.53.3/./psi/zcontrol.c:464
引っかかってこないぞ。後はgrepと格闘するしか無いんだろうね。 opecheck.h
#define check_proc(rf)\ BEGIN if ( !r_is_proc(&rf) ) return_error(check_proc_failed(&rf)); END
この定義中の r_is_pro
もマクロになってて、手に負えんぞ。後は感覚で行くしかないな。
と、ここまで32Bit版のOpenBSDでやってた。ポインター等の表示が少ないから好みなんよ。でも作成されたMakefileのdebugの所が、明らかにマクロを扱えるようになってる。これはきっとgccだかgdbの役不足だろう。debian(64Bit)に切り替えてみる。
(gdb) info macro check_proc Defined at /tmp/ghostscript-9.53.3/./psi/opcheck.h:43 included at /tmp/ghostscript-9.53.3/./psi/oper.h:26 included at /tmp/ghostscript-9.53.3/./psi/zcontrol.c:21 #define check_proc(rf) BEGIN if ( !r_is_proc(&rf) ) return_error(check_proc_fai\ led(&rf)); END (gdb) macro expand check_proc(*op) expands to: do { if ( !(((&*op)->tas.type_attrs & ((((1 << 6) - (4)) << 8) + (0\ x40+0x80))) == (((t_array) << 8) + (0x40+0x80))) ) return (check_proc_failed(&*\ op)); } while (0)
今度はちゃんと展開されて、頭が痛くなるような式が出て来た。これも解読が大変だわい。
何でこんなマクロを使うの? 普通のC語の関数だと、呼び出しにオーバーヘッドが有るからだろう。C語用のスタックに引数を積み、戻りアドレスを積み、関数の中では、ローカル変数の確保とかやってたら、遅くてたまらん。
頭の良い人が、C語の関数ぽく書けるマクロを用意した。そしてそれを使えば、あくまで関数を呼び出したように見えるけど、一切C語のスタックが使われる事は無い、だな。
rotate
今度は回転を調べてみる。
/* <angle> rotate - */ /* <angle> <matrix> rotate <matrix> */
こんな案内が有った。回転させたいマトリックスも指定出来ると言う二重性が有るのね。で、最終的には、こちらに飛んできた。sincos恐い!!
/* Rotate a matrix, possibly in place. The angle is in degrees. */ int gs_matrix_rotate(const gs_matrix * pm, double ang, gs_matrix * pmr) => double mxx, mxy; gs_sincos_t sincos; gs_sincos_degrees(ang, &sincos); mxx = pm->xx, mxy = pm->xy; pmr->xx = sincos.cos * mxx + sincos.sin * pm->yx; pmr->xy = sincos.cos * mxy + sincos.sin * pm->yy; pmr->yx = sincos.cos * pm->yx - sincos.sin * mxx; pmr->yy = sincos.cos * pm->yy - sincos.sin * mxy; :
(gdb) p *pm $2 = {xx = 1, xy = 0, yx = 0, yy = -1, tx = 140.333328, ty = 701.666687} (gdb) p sincos $3 = {sin = 0.86602540378443871, cos = -0.49999999999999978, orthogonal = 0}
第一引数はconstが付いているから、入力側だな。回転させた結果をpmrに入れてくれろとな。 そして、回転角度は、120度を指定したんだけど、合ってますかね?
(gdb) p *pmr $3 = {xx = 0.5, xy = -0.866025388, yx = -0.866025388, yy = -0.5, tx = 140.33332\ 8, ty = 701.666687}
これが回転結果だ。
回転ってグラフィックの世界では、 回転行列 と言うやつで表されるはず。これも合ってますかね。
GS>45 [1 1 2 2 3 3] rotate GS<1>pstack [0.707106769 0.707106769 -0.707106769 0.707106769 0.0 0.0]
与えるmatrixには関係なく、回転結果が得られるのかな? 説明では、原点を中心に回転するってなってた。
/Times-Roman findfont 10 scalefont setfont /Courier findfont [ 80 0 0 180 0 0 ] makefont setfont 300 300 moveto 45 rotate (Hello) show
こんなのを実行すると、確かに傾いた文字が表示される。横80で縦180のサイズ。
%!PS 300 300 translate 2 2 scale /tree { 1 sub dup dup 0 gt newpath 0 0 moveto 0 70 lineto stroke { gsave 0 20 translate 20 rotate -0.7 0.7 scale tree grestore gsave 0 40 translate -18 rotate -0.8 0.8 scale tree grestore } {pop pop} ifelse } def 10 tree showpage
再帰的な樹だそうです。20度の回転や原点移動、拡大/縮小のコマンドも使われている。ちょいとお遊びでした。
90度のローテーションを何度かやって、値を確認した。
(gdb) p *pm $19 = {xx = 1, xy = 0, yx = 0, yy = -1, tx = 300, ty = 542} $20 = {xx = 0, xy = -1, yx = -1, yy = -0, tx = 300, ty = 542} $21 = {xx = -1, xy = -0, yx = -0, yy = 1, tx = 300, ty = 542} $22 = {xx = -0, xy = 1, yx = 1, yy = 0, tx = 300, ty = 542} $23 = {xx = 1, xy = 0, yx = 0, yy = -1, tx = 300, ty = 542}
4回繰り返すと元に戻った。gsが内部に保持してる、単位矩形みたいな物なのかな。この中に入る色々な図形の座標は、この係数を掛けて求めていると想像。
だとしたら、図形の拡大や縮小のデータも、ここに反映されているのではなかろうか? こんなスケールを設定してから、ローテーションしてみる。
2 0.5 scale
X軸方向には2倍に拡大、Y軸方向には0.5倍に拡大(それって圧縮だ)と言う定義だ。
(gdb) p *pm $24 = {xx = 2, xy = 0, yx = 0, yy = -0.5, tx = 300, ty = 542} $25 = {xx = 0, xy = -0.5, yx = -2, yy = -0, tx = 300, ty = 542}
どうやら予想的中だな。
pathbbox
今度は、リアル紙サイズを取り出すやつだ。
GS>clippath pathbbox GS<4>pstack 842.0 595.0 0.0 0.0
clippathを最初に発行しておかないとエラーになる。紙の左底辺と右城辺の座標が返ってくる。すなわち、紙のポイントサイズになる。
push(1); make_false(op); => code = z1pathbbox(i_ctx_p); if (code < 0) { pop(1); /* remove the Boolean */ } return code;
push(1)でスタックに1データ分を確保。そこにfalseを書き込む。そして本チャンのやつが来て、codeが負なら失敗。popで1データを棄てる。他の関数を見ても、同じ作りになってる。
/* <bool> .pathbbox <llx> <lly> <urx> <ury> */ static int z1pathbbox(i_ctx_t *i_ctx_p) :
で、追って行ったら、こんなの見つかったけど、登録されていないのでユーザーは使えなかった。隠しコマンドってか、システムコマンドって沢山有るんだろうね。
(gdb) p fbox.p $30 = {x = 0, y = 0} (gdb) p fbox.q $31 = {x = 152320, y = 215552}
で、これが大本のデータだったよ。
todo
ここまで、軽くghostscriptの探検をやってきた。まだまだ入り口に右往左往してるに過ぎない。
これから見るべき所として、strokeとかfillとかの本当に描く部分がどうなっているかとか、defとかの辞書登録はどうか? 軽くzdefにBPを置いてみたけど、ヒットしない。それを言うなら、pstackとかエラー処理等も、担当場所をまだ発見出来ていない。
それより、
debian:psi$ ls i*.c | wc 31 31 282 debian:psi$ ls i*.h | wc 81 81 755
大事なデータ構造はヘッダーファイルに定義されてるはずだ。が、それを使うソース数より圧倒的に多い。上で見たように、マクロがバンバン定義されてるせいだろう。これらをかき分けて探検するのは、容易ではなさそう。と言う事で、暫く作戦を練る事にする。
まあ、ghostscriptは絵が描けるforth。だから、少し方向を変えてforthの構造でも当たってみるかな。それも、forthとlispは非常に相性が良いと思うので、lispで書かれたforthに注目してみる。
try forth
以前にちょっと取り上げた、 Easy-ISLisp のサンプルにforthが置いてある。ちょっと走らせてみる。
debian:eisl$ ./eisl -l example/forth.lsp Easy-ISLisp Ver1.74 > (forth) Forth in ISLisp ? 1 2 3 4 5 .s <5> (1 2 3 4 5) ? * .s <4> (1 2 3 20)
stackはリストで表現されているんですね。
? : myfunc + * ; ? myfunc .s <2> (1 46) ? bye May the Force be with you T >
ghostscriptのdef相当も、ちゃんと実装されていました。これはもう、楽しむしかないな。 鬼滅の刃の決めセリフ(なんだっけ?)みたいな挨拶が出て来た。 そう、この言葉が似合うのはlispとそれをひっくり返したforthだけです。前置と後置の違いしか無いからね。pythonみたいな中置じゃ、つまらん!
本当のForceはマクロに有り。マクロの本場はC語じゃなくてLispだ。マクロ本と言えばポール・グレアム氏の書いた On Lisp だ。OpenBSDとviをこよなく愛するダグ・ホイト氏も彼の信者で、時に宗教本と揶揄される LET OVER LAMBDA なんてのを著している(オイラーも似たようなものだな)。
彼によるとemacsはeditorじゃなくて、それ自体を弄んでしまうので、きっぱりとviだそうだ。viの簡潔なキーバインドが好み。そして % コマンドさえ有れば幸せらしい。%って、カッコに対するコッカ(あるいは、その逆)に、カーソルを移動するコマンドだ。これだけでS式のコピーとか移動って簡単に出来たかなあ? vimなら太っているから出来るかも知れないけど、彼はOpenBSDに備え付けのnvi派だからなあ。今度じっくり試してみるか。
このLOL本の一部が読めるようになってる。
https://letoverlambda.com/index.cl/errata
sbcl使いには、原著に上げられているコードでは不都合が有るので、その訂正版と言う形で、コードが公開されているんだ。オイラーはclisp(著者も推してる)で試してみるか。
ob$ clisp : Welcome to GNU CLISP 2.49 (2010-07-07) <http://clisp.cons.org/> [1]> (load "lol-orig.lisp") ;; Loading file lol-orig.lisp ... ;; Loaded file lol-orig.lisp T [2]> (defvar mine (new-forth)) MINE [3]> (go-forth mine 5 dup * print) 25 NIL
この後、進化させて行くんだけど、摘まみ読みなんで、これぐらいにしておく。