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

この後、進化させて行くんだけど、摘まみ読みなんで、これぐらいにしておく。


This year's Index

Home