call chezscheme from rust

chez-sys for 32bit

gitでリリースされてる奴は64bit用。それをかまわず32bitなマシンで実行してエラーを楽しむ(変態だな)。

error[E0308]: mismatched types
 --> src/main.rs:7:42
  |
7 |         Sfixnum_value(Scall2(ti, Sfixnum(x), Sfixnum(y)))
  |                                          ^ expected `i32`, found `i64`
  |

どうやら32bit用のchezでは、整数の扱いが32bitみたい。あれ? 巨大整数にswitchしてくれないのかな。数字に五月蝿いschemeの特徴だと思うんだけど。

sakae@deb:/tmp/chez-sys$ cargo r          
Compiling chez-sys v0.1.0 (/tmp/chez-sys)
error: linking with `cc` failed: exit status: 1                                   |
  = note: "cc" "-m32" "/tmp/chez-sys/target/debug/deps ...
  = note: /usr/bin/ld: /tmp/chez-sys/target/debug/deps/chez_sys-154e0aee2832fb79.3zkps90p5d2qr35f.rcgu.o: in function `chez_sys::myadd':
          /tmp/chez-sys/src/main.rs:6: undefined reference to `Sstring_to_symbol'
  = help: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
  = note: use the `-l` flag to specify native libraries to link
  = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-libkindname)

The links field これが関係してくるのかな? やっぱり、オリジナル版で、雑音が無い環境でやるのがよかろう。

それはいいけど、rust-gdbも余り頼りにならないし、どうしたものか? use dbg!()

strace

問題は、scheme.boot等がロード出来無いって亊なんで、straceで所要のファイルがオープン出来ているか、確認すればいいな。これ、今回の場合にだけ使えそうな、特殊相対性理論です。どんな場合でも使える、一般相対性理論は、アインシュタインも完成するまでに10年を要しているから、先の亊は考えない。

chez-sys main版に対抗して、C言語版を作っておいたので、それで予備実験 @32bit

sakae@deb:/tmp/t$ strace ./a.out |& grep open
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/i386-linux-gnu/libm.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
  :
openat(AT_FDCWD, "./petite.boot", O_RDONLY) = 3
openat(AT_FDCWD, "./scheme.boot", O_RDONLY) = 4

うん、相対チェックには、もってこいだな。

いよいよ前回の最後にやった、アプリ版の実行。

sakae@pen:/tmp/chez-sys/target/debug$ strace ./chez-sys |& grep open
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libuuid.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/tmp/t/petite.boot/tmp/t/scheme.boot\nS_generic_invoke return", O_RDONLY) = -1 ENOTDIR (Not a directory)
write(2, "cannot open boot file /tmp/t/pet"..., 83cannot open boot file /tmp/t/petite.boot/tmp/t/scheme.boot

確かに、こんなファイルは無いなと納得。それでいいのか? どうも、ファイル名を表す文字列の表現に難があるっぽい。鬼門だな。

repair

SDDに負担をかけない様、作業はMEMORY DISKにしてる/tmp上で行っている。作業が済んだら、それをSDDに保管って流れ。そんなルーチンを取っているものだから、main.rsとlib.rsが同居するというcargoの想定外の状況が生れた。

その結果がわけわかめなエラーになった。皆さんもくれぐれも注意しましょう。まあ、cargoが気を効かせて、–libなの? それとも–binなのって、聞いてきてくれるのが望ましいんだけどね。そこまでは親切ではない。

で、Cの文字列問題を解決して、再び走らせてみると、

sakae@pen:/tmp/chez-sys$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/chez-sys`
Exception: invalid memory reference.  Some debugging context lost

こんどは、いかにも間口が広いエラーになった。こういう場合はどうする? もう一歩一歩やるしかないだろう。後ろの方をコメントにして、一つづつ実行範囲を広げていった。

そしたら、最後のprintlnを有効にすると、上記のエラーになる事が確認出来た。それで呼び出している関数を眺めたら、Cの文字列問題が隠れている事を発見。

同じようなミスをしてないか、確認すべきだったね。いわゆる、ゴキブリを一匹見つけたら、他にも十匹は隠れているよって教訓だ。実世界の教訓をソフトウェアの世界にも是非持ち込むべき。

result

mod bindings;
pub use bindings::*;
use std::ffi::CString;

fn myadd(x: i64, y: i64) -> i64 {
    let ss = CString::new("+").unwrap();
    unsafe {
        let ti = Stop_level_value(Sstring_to_symbol(ss.as_ptr()));
        Sfixnum_value(Scall2(ti, Sfixnum(x), Sfixnum(y)))
    }
}

fn main() {
    let ps = CString::new("/usr/lib/csv9.5.6/a6le/petite.boot").unwrap();
    let ss = CString::new("/usr/lib/csv9.5.6/a6le/scheme.boot").unwrap();
    unsafe {
        Sscheme_init(None);
        Sregister_boot_file(ps.as_ptr());
        Sregister_boot_file(ss.as_ptr());
        Sbuild_heap(std::ptr::null::<i8>(), None);
        println!("{}", myadd(123456, 654321));
    }
}

これが完成図だ。FFIでC言語側とやり取りする場合、文字列はC言語用で用意しなければならない。それが定石のCString::new("xxx").unwrap()て奴。"xxx"は、この時点でrust言語上に文字列だけど、上記を実行すると、C言語用になる。

使う時は、大概文字列のポインターを要求されるので、C言語文字列のインスタンスを引数(って表現でいいのかな?)にして、 as_ptr() を呼び出せばよい。

両者を結合してC言語の関数の引数上で表現する亊も可能らしいけど、種々の問題を孕んでいるので、止めるべきと高名なハッカーがおっしゃっていた。

sakae@pen:/tmp/chez-sys$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/chez-sys`
777777

実行結果は、あっけないぐらい、つまらないものだ。わざわざ載せるのが憚れられる。

sakae@pen:/tmp/chez-sys/target/debug$ strace ./chez-sys |& grep open
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
  :
openat(AT_FDCWD, "/usr/lib/csv9.5.6/a6le/petite.boot", O_RDONLY) = 3
openat(AT_FDCWD, "/usr/lib/csv9.5.6/a6le/scheme.boot", O_RDONLY) = 4

一応straceの結果を載せておく。ファイルディスクリプターの3番が開放されていないけど、これって、後で使うからそのままにしてるの? 変な所に気が付く、困ったちゃんであります。

なお、当初

Sbuild_heap(0 as *const i8, None);

てしてたのを、上記のように普通では思い付かないように書き換えたのは、度々登場してる、家庭教師 clippy君による。binding.rsにも提案が有ったけど、今回は載せないでおく。

mail

上記のように、自助努力で解決したけど、実は二股かけていたんだ。chez-sysの作者さんに、お助けメールをだしてた。ええ、勿論、翻訳ソフトで翻訳させたやつね。

返事がきたので、参考に載せておく。

Hi Sakae,

The chez-sys crate is very low-level, it's the same as interacting directly 
with the C-interface. However, because you're trying to do it via Rust the 
situation actually becomes harder not easier.

The output or your main hints at the answer:

cannot open boot file /tmp/t/petite.boot/tmp/t/scheme.boot

The real problem, though, is that the strings you're giving the C-interface 
are not null-terminated (because Rust strings are not null-terminated). 
The point of `chez-rs` is to provide a high-level interface as well that hides
all these annoying complications, but I have not had time to finish it. 
To that point, I would not recommend you use the crate at all at this time.

Best,
Andrew

>
> Please let me know if you have any hints.

> https://hamesspam.sakura.ne.jp/hes2022/220217.html#org37ea29d

質問はつたない英語より、rust言語で行う方が、確実だ。好き者同士の共通語ですからね。

敗因分析

昔のってか戦前の軍部は、戦争に負けた時の敗因分析をきちんとやらなかったらしい。ただ、精神論で、突撃突撃、勝つまでは我慢我慢ですじゃ、その時点で負けが決定してますよ。

オイラーも多分にこの傾向があるね。

コンパイラーがエラーを捉えて、こうしたらいいよって提案してくれるものだから、顔色を伺って、コンパイルが成功する亊だけを目標にやってきた。

丁度、東大に合格する亊だけが、人生のあがりと思ってる諸君みたいにね(そうじゃ無い志ある諸君がいたらごめんなさい)。親が高学歴だと特にそうだろう。親の言い付けをきちんと守る良い子ちゃんロボット。おっと、言い過ぎたか。

根本原理が分っていなかったんだね。自戒しましょ!!!!

まあ、新たなソースを探検するときは、何をおいてもヘッダーファイルを見ておけってのが、すっかり抜け落ちていた。

scheme.h

何はなくとも、これから始まる。これが無ければchezさえもコンパイル出来無い、超重要なファイル。基本設計図に相当する。

有り難い亊に、 scheme.h を解析した方がおられる。

このファイルをちら見してたら、自動生成されてるので、お手を触れないようになって注意が出てた。そして、32/64bitで差がどれぐらいあるか気になった。

--- i3le/scheme.h       2022-02-20 14:30:18.771668656 +0900
+++ a6le/scheme.h       2022-02-20 14:30:18.755668470 +0900

-#define Sfixnump(x) (((uptr)(x)&0x3)==0x0)
+#define Sfixnump(x) (((uptr)(x)&0x7)==0x0)

-#define Sfixnum_value(x) ((iptr)(x)/4)
+#define Sfixnum_value(x) ((iptr)(x)/8)

-#define Scdr(x) (*((ptr *)((uptr)(x)+11)))
+#define Scdr(x) (*((ptr *)((uptr)(x)+15)))

この他にも多数、差異がある。同一のものは使えないって、あたり前の結論でした。

bindings.rs

そして、上記のscheme.hをrustから利用出来るように準備したものが、bindings.rsだ。 順番として、scheme.hを見てからbindings.rsが、正統なコース図だろう。

predicateってマクロで、綺麗にtagの処理をしてるね。scheme.hとは雲泥の差だ。scheme大好きな人の面目が十分に発揮されてるな。

Accessorsの分類の中にcar,cdrがあるってのは、納得。他にvectorとかは、まだrust側に持ってこれていない(該当部分がコメントになってる)。

Constructorsの分類の中にconsが入ってるのも納得。この分類の中にtrue/falseが有るのは頷けるけどnilもあるな。何故? schemeには必要無いよな。と、考えてたら、C言語の亊も頭に置いておいて下さいって、影の声が聞こえてきたよ。

上の方で頭を悩ましていたやつは

EXPORT void Sregister_boot_file PROTO((const char *)); // from scheme.h
extern "C" {
    pub fn Sregister_boot_file(arg1: *const raw::c_char);
}

こんな表現になってた。最初にどちらかのファイルを見ていれば、あんな大失敗をすることは無かったろうに。

で、約60個程の外部手続が登録されてた。

in kernel

kernel.o or libchez.a は、chezschemeのRuntimeになる。いや、最下層の部分。gaucheでは、可搬製を考慮して仮想マシンになってるけど、こちらでは剥き出しの石(amd64とかi386)で動くようになってる。だから速い。

そのマシンに命令を与えるのが、約60個の手続呼出だ。いわゆるシステムコールだな。

Sregister_boot_file の定義場所を探してみる。調べるのはコンパイルの残骸が置いてある場所がよい。ノイズが無いからね。

sakae@pen:/tmp/ChezScheme/a6le/c$ grep Sregister_boot_file *.c
main.c:        Sregister_boot_file(argv[n]);
scheme.c:extern void Sregister_boot_file(name) const char *name; {
scheme.c:  check_boot_file_state("Sregister_boot_file");
scheme.c:extern void Sregister_boot_file_fd(name, fd) const char *name; int fd; {
scheme.c:  check_boot_file_state("Sregister_boot_file_fd");

つらつらとコードを見ていくと、マジック文字列が有るよとかマシンタイプが埋め込まれているとよかが分る。分かり易い例で、

sakae@pen:/tmp/ChezScheme/a6le/boot/a6le$ hd  petite.boot | head -2
00000000  00 00 00 00 63 68 65 7a  49 15 0c 16 28 29 24 80  |....chezI...()$.|
00000010  2a 0b 01 00 4c 09 18 ca  e4 e4 de e4 5a d2 dc ec  |*...L.......Z...|

最初の8文字がマジック。こんなのOS付属のファイルの種別判定には引っ掛らないだろう。謎のフォーマットって亊で、ただのdataとしか表示出来無い(ようにしている)。 それから、10個のこの種のファイルを読み込めるとか、秘密を暴くって楽しいな。

おっとschemeのコードに出会った時の儀式。consがどうなってるか? 答はalloc.cに有り。

ptr Scons(car, cdr) ptr car, cdr; {
    ptr tc = get_thread_context();
    ptr p;

    thread_find_room(tc, type_pair, size_pair, p);
    INITCAR(p) = car;
    INITCDR(p) = cdr;
    return p;
}

どうやら、その時のthreadの状況を把握して、そこから必要な場所を確保してるようだ。深入りすると際限がなくなりそうなので、この辺で止めておく。

それからちょっと気になった亊、scheme.hに***LOCKってのがdefineされてる。これが*.cの中で使われていないけど、これって過去の残骸?

Sbuild_heap てのを実行して、初めてヒープが確保されて、登録しておいたbootファイルがロードされるのね。 ユーザー・マニュアルにそう説明されてた。ああ、それでファイル・ディスクリプターをそのまま(ファイルをクローズしない)にしてるんだね。コードを読むと疑問が氷解していくので、気持が好いな。

s

chezの核心部分は /usr/lib/csv9.5.6/a6le/ 等に出来るけど、いわゆる*.ssはどこにもみえない。困ったものだ。なんたって、scheme.bootみたいなファイルにマシン御で格納されてるからね。どうしても見たいなら、ソースを開く必要がある。

mkheader.ssで、scheme.hを自動作成してた。後はじっくり見ていくしかないだろう。

常々、どうやって*.ssをmake時にコンパイルしてるか疑問だった。じっくりログを眺めてみたぞ。

make all
echo '(reset-handler abort)'\
               :
             '(generate-inspector-information #f)'\
             '(subset-mode (quote system))'\
             '(compile-file "cmacros.ss" "cmacros.so")'\
             | ../bin/i3le/scheme -q
compiling cmacros.ss with output to cmacros.so

chezでコンパイルする時の条件やらをパイプで、出来合いのchezschemeに渡しているね。

*.ssの中で、Sconsとかのkernel側呼出はみつからなかった。どうなってるの?

ちょいと有名な手続をみてみる。例えば、これ。 5_2.ss

(define-who reverse
  (lambda (ls)
    ($list-length ls who)
    (do ([ls ls (cdr ls)] [a '() (cons (car ls) a)])
        ((null? ls) a))))

普通のconsで、何の変哲もない。内心はSconsとかを期待してたんだけど。はて、困ったぞと。

得意のマクロを使ってマシン語を作る時に、変換してるのかなあ(淡い想像だけど)。

こうなったらgdb君の登場かな?

run chez under gdb

tar弾を展開してconfigureした後、ちょいと修整

sakae@deb:/tmp/csv9.5.6/i3le/c$ head Mf-config
CC=gcc
CPPFLAGS="-g"

make runの技法を盗んで、インストールしないで実行。

sakae@deb:/tmp/csv9.5.6$ export SCHEMEHEAPDIRS=i3le/boot/i3le
sakae@deb:/tmp/csv9.5.6$ gdb i3le/bin/i3le/scheme
 :
Reading symbols from i3le/bin/i3le/scheme...
(gdb) b main
Breakpoint 1 at 0x77f0: file main.c, line 86.
(gdb) r
Starting program: /tmp/csv9.5.6/i3le/bin/i3le/scheme

Breakpoint 1, main (argc=1, argv=0xbffff5d4) at main.c:86
86        const char *execpath = argv[0];
(gdb) n
104       if (strcmp(Skernel_version(), VERSION) != 0) {
(gdb)
109       Sscheme_init(ABNORMAL_EXIT);

replが動き出してしまうと、C-c で、gdbに入れないなあ。取り敢えず起動しておいて、後からgdbをアタッチするかな。

(gdb)
#0  0xb7f19559 in __kernel_vsyscall ()
#1  0xb7cc8f97 in __GI___libc_read (fd=0, buf=0xbf87be6b, nbytes=1)
    at ../sysdeps/unix/sysv/linux/read.c:26
#2  0x0044eedf in s_ee_read_char (blockp=1) at expeditor.c:691
#3  0xb66af9ad in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

replで待っててくれるかと思ったら、備付のeditorの中だった。

(gdb) b Scons
Breakpoint 1 at 0x422de0: file alloc.c, line 431.
(gdb) c
Continuing.

Breakpoint 1, Scons (car=0x3b4, cdr=0xb6705ba7) at alloc.c:431
431         thread_find_room(tc, type_pair, size_pair, p);
(gdb) bt
#0  Scons (car=0x3b4, cdr=0xb6705ba7) at alloc.c:431
#1  0x00446fcf in S_clock_gettime (typeno=3) at stats.c:330
#2  0xb66b21d5 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

repl側で、(cons 1 2)とかしたら、確かに止ってくれた。けど、contすると、何回も止ったぞ。どれがオイラーの要求した奴じゃ? じっくり見て池じゃなくて、深かい沼か。

素直にSconsにBPを設定してから起動すると、こんな感じになった。まだeditorも起動していない状態。ってか、それに向けての準備作業中であります。

(gdb) bt
#0  Scons (car=0xb7b8f697, cdr=0x42e750 <load_shared_object>) at alloc.c:431
#1  0x0042ebdc in Sforeign_symbol (v=0x42e750 <load_shared_object>,
    s=0x48c0ac "(cs)load_shared_object") at foreign.c:84
#2  Sforeign_symbol (s=0x48c0ac "(cs)load_shared_object",
    v=0x42e750 <load_shared_object>) at foreign.c:161
#3  0x0042efba in S_foreign_init () at foreign.c:320
#4  0x00437ce2 in main_init () at scheme.c:77
#5  Sbuild_heap (kernel=0xbffff71e "/tmp/csv9.5.6/i3le/bin/i3le/scheme",
    custom_init=0x0) at scheme.c:1120
#6  0x00408138 in main (argc=1, argv=0xbffff5d4) at main.c:292

これは見所沢山あるなと思って、emacsからgdbを起動したら、

cannot find compatible scheme.boot in search path
  "i3le/boot/i3le"

って言われた。環境変数で設定した SCHEMEHEAPDIRS=i3le/boot/i3le は、引き継がれないのか。

(gdb) r -b /tmp/csv9.5.6/i3le/boot/i3le/petite.boot

これで取り敢えず観光出来るようになった。で、上流へ遡っていったら、 scheme.c/main_init に、/* create dependency for linker */ こんなコメントを発見。

カーネルにとっては、petite.bootなんのは、外からやってくるモジュールの扱いなんだな。それで、結合はリンカー任せって亊だから準備しとくとな。

    S_segment_init();
    S_alloc_init();
    S_thread_init();
    S_intern_init();
    S_gc_init();
    S_number_init();
    S_schsig_init();
    S_new_io_init();
    S_print_init();
    S_stats_init();
=>  S_foreign_init();
    S_prim_init();
    S_prim5_init();
    S_fasl_init();
    S_machine_init();
    S_flushcache_init(); /* must come after S_machine_init(); */
#ifdef FEATURE_EXPEDITOR
    S_expeditor_init();
#endif /* FEATURE_EXPEDITOR */

最後にeditorのイニシャライズをしてるんだな。

asm

s/Mf-baseで色々とコンパイル時の状況を設定出来る亊を、以前に軽く調べておいた。で、つらつら見てたら

# compile determines the entry point for compilng files
# another useful value for this is compile-with-asm, defined in debug.ss
compile = compile-with-asm

こんな亊も出来るよと。これはもう、やってみる鹿。で、その成果を、prim.asmで確認してみる。

(define cons (lambda (x y) (cons x y)))
(define car (lambda (p) (#2%car p)))
(define cdr (lambda (p) (#2%cdr p)))

Uum … 何処まで行ってもconsはconsなんだなと思っていたら、ファイルの後ろの方に、

cons:
entry.9966:
0:       cmpi           (imm 2), %ac0
3:       bne            lf.8094(38)
dcl.9967:
5:       mov            (disp 4 %sfp), %td
8:       mov            $ap, %ts
:
23:      bcs            Lget-room.8093(11)
ej.8092:
25:      mov            %xp, %ac0
27:      mov            %esi, (disp 7 %ac0)
30:      mov            %td, (disp 11 %ac0)
33:      jmp            (disp 0 %sfp)
Lget-room.8093:
36:      bsr            (literal 33 (library-code #(libspec get-room 168)))
41:      bra            ej.8092(-18)
lf.8094:
43:      bra            (literal 33 (library-code #(libspec doargerr 166)))
48:      <end cons>

car:
entry.9968:
0:       cmpi           (imm 1), %ac0
3:       bne            lf.8096(21)
dcl.9969:
5:       mov            %esi, %td
7:       andi           (imm 7), %td
10:      cmpi           (imm 1), %td
13:      bne            Lerr.8095(6)
lt.9970:
15:      mov            (disp 7 %esi), %ac0
18:      jmp            (disp 0 %sfp)
Lerr.8095:
21:      bra            (literal 33 (library-code #(libspec car 50178)))
lf.8096:
26:      bra            (literal 33 (library-code #(libspec doargerr 166)))
31:      <end car>

こんなコードが掲載されてた。最初のcmpiは、引数の数が正しいかのチェックだろう。literalの部分は、エラー表示で使われるのだろう。後の真ん中部分は、残念ながら朧である。

今迄、chezschemeは何度も観光しようとチャレンジしたけど、技量不足で征服出来無かった。オイラーにとっての、いわば聖域。今回は、rustの支援を受けて、ここまで到達出来た。

rustて凄いパワーを持ってるな。って、それは話が違うだろうに。

again chez for 32bit

冒頭であげた32bitで動かない問題。無理するなって亊で、

fn myadd(x: i32, y: i32) -> i32 {
    let ss = CString::new("+").unwrap();

こんな修整を施したら、動いた。但し、bindings.rsは、相変わらず64Bit用のリナックスなんで、凝った亊をすると牙を矛く亊でしょう。原理が分っていれば、やみくもの怖がる亊は無い。

other lisp