chez scheme with ffi

『文系AI人材になる』なんて本を読んだ。統計・プログラム知識は不要ってサブタイトルが打ってあった。

そうか、統計とプログラミングが壁な訳ね。それにも関わらずAIしたい、いややらないと世の中から取り残されてしまうって強迫観念が有るんだな。確かにAIが入ってくればホワイトカラーは職を失い食がなくなってしまいますからね。 切実な問題でしょう。

今まではAIを作るって事で理系が幅を利かせていたけど、もうそんな時代は終わり。これからは、AIを単に使う時代に入ったと筆者は言う。

AIの難しい所は丸暗記で分かった積りになれ。AIの特技は大きく分けて4つに分類される。人間とAIがどう関わるかと言うと、人間の能力増強型と、もう一つは人間の代行をするものになるとな。で、計8分類でAIを理解するのが早道らしい。

Pythonを操ってAIしましょなんてのは、もう時代遅れなのさ。これからはAIで何をやらせるって言う企画力の時代だと言う。そうなると文系の出番って訳。

8分類の例として出てたのは、癌の画像診断。AI特技の分類分野を使って、ぼんくら医師の能力増強。もう一つは、AIの会話特技を使って、サポートセンターのオペレーター代行。

そこらにいる事務職のおねーちゃん(昔言葉で言うと、BGとかOLね -- って、おじさん色丸出し)が、困っている事を見つけて、それをAIに肩代わりさせるんだそうです。そう、提案制度で提案章をたくさんもらうような人が有利。8つの分類がアイデア発想の元になりますよ。

なんでも、最も嫌われている業務は「一般的なデータ入力」らしいです。こういう不満をAIに肩代わりさせましょ。 データ入力作業なんてのは非人間的な作業ですよ。そんなのAIとタイアップしたOCRの仕事でしょ、手書き文字認識なんて、もっともAIが得意とする分野。

某メーカーはサポセンだけで2000人も雇っているとか。でも、定着率が低い。ユーザーのストレスの発散場。話相手のいない寂しい老人の憩の場、そんなのに付き合っていたら、ストレスが乗り移ってしまいますからね。尻尾を巻いて逃げ出すのが大半でしょう。 そういう職場にはAIによる音声認識、自動応答がうってつけ。

未だに機械学習でデープランニングなんて記事を出してる雑誌は、読まなくてもいいからね。この本を読むと、AIの適用45事例なんてのが紹介されてる。ヒント満載ですよ。

こういう書き込みは、あおりだってAIに認識され、即リジェクトされるんだろうな。恐いよAI。

chez with ffi

guileも秋て来たので今度は、chez schemeでFFIを試してみる。まずは説明書って事で、Chapter 4. Foreign Interfaceなんてのを見ると、サンプルが載ってた。 こういうのは、コピペしてから、色々改変してみるのが良い。そこから新たな知見が得られるだろう。科学業界も人の成果を論文で確かめ、そこから独創的な研究をするってのが定番ですから。何も躊躇する事は無い。

サンプルでは、C語で小さなライブラリィーを提示(csocket.c)。それをchez側から利用出来るようにまとめておき(socket.ss)、それらを使って実行って流れだ(example.scm)。

(base) sakae@debian:ffi$ cat example.scm
;;Sample session.

(system "cc -fPIC -shared -o csocket.so  csocket.c")
(load "socket.ss")

(define client-pid)
(define client-socket)
(define hoge (make-string 32))
   :
(define put     ; procedure to send data to client
  (lambda (x)
    (let ([s (format "~s~%" x)])
      (c-write client-socket s (string-length s)))
    (void)))

(define get     ; procedure to read data from client
  (let ([buff (make-string 32)])
    (lambda ()
      (let ([n (c-read client-socket buff (string-length buff))])
        (printf "client:~%~a~%server:~%" (substring buff 0 n))))))

(get)
(put '(let ([x 3]) x))
(get)
(terminate-process client-pid)
; (exit)

紙面がもったいないので中抜きしちゃったけど、こいつを走らせれば、ちゃんと結果が出て来るはず。(Webに載ってるのからは、若干変更してるけどね)

(base) sakae@debian:ffi$ scheme example.scm
Chez Scheme Version 9.5.3
Copyright 1984-2019 Cisco Systems, Inc.

Exception: incorrect number of arguments to #<procedure c-read>

早速エラーの返礼。しかも落ちている所が、引数の数が合わないですって。例では3引数になってるけど、ライブラリィーとchezへの繋ぎスクリプトでは、4引数になってる。

      (let ([n (c-read client-socket buff 0 (string-length buff))])

c-read, c-write に、引数を追加した。これで実行すると

Exception in c-read: invalid foreign-procedure argument "\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;\x0;"

今度は、こんなエラーが発生。

> (put '(let ([x 3]) x))
Exception in c-write: invalid foreign-procedure argument "(let ((x 3)) x)\n"
Type (debug) to enter the debugger.

putを単独で試してみるも、第二引数に相当する所で、嫌われているって事だな。 そんじゃ、第一引数はintって宣言してあるので、ここを他にものに変えると、どうなる?

> (c-read "HERE" hoge 0 5)
Exception in c-read: invalid foreign-procedure argument "HERE"
> (listen 2 "FOO")
Exception in listen: invalid foreign-procedure argument "FOO"

listenは、第二引数にもintを要求してるけど、間違った引数を提示してるね。

すると、c-readとかの第二引数のタイプが間違っているな。デフォルトでは、u8* になってるけど、stringに変えてみる。そしたらハングアップしたよ。こういうのに最強と思われる void* に変更しても、やはりお気にめさない。

make-bytevector 系に変えたら、argmentエラーは無くなったけど、今度はハングする。

> (define hoge (make-bytevector 32))
> (c-read client-socket hoge 0 (bytevector-length hoge))

もうお手上げ!!

他の道を行く

ここで詰まっていてもしょうがないので、上記のページを流し読み。既にあるライブラリィーを使う例が出てたので試してみた。

(load-shared-object "libc.so.6")
(load-shared-object "libm.so.6")

(define mygetenv
  (foreign-procedure "getenv" (string) string))

(define mysin
  (foreign-procedure "sin" (double) double))

(define j0
  (foreign-procedure "j0" (double) double))

(define E (exp 1))
(define PI (* (acos 0.0) 2))

libcとlibmを使ってみましょって例。getenvもsinもデフォルトで提供されてたので、わざわざ別の名前で定義した。ベッセル関数は、利用者が少ないので、未定義だったから、そのままscheme側でも同じ名称にした。

おまけで、有名なpiとかeはずばりと定義はされていないので、派生形として求めてみた。ここで使ってるacosはcosの逆関数だ。0.0 = cos(x) で、xを求めると、それは π/2 == 90度ってのを使ってる。 asin(1.0)でも、同様に求められる。

注目はgetenvの定義の仕方かな。man getenv すると

       char *getenv(const char *name);

入力も出力もchar*型だよと言う事。scheme世界ではstring型。今はscheme世界で考えているんで、素直な指定。libc内のgetenvを呼び出す時に、C語世界に合わせてくれるって寸法だな。 これが分かれば、取り合えず十分かな。

> (sin 1)
0.8414709848078965
> (mysin 1)
Exception in mysin: invalid foreign-procedure argument 1
Type (debug) to enter the debugger.
> (mysin 1.0)
0.8414709848078965

それから、デフォのsinは入力が整数でも受け付けるけど、自分で定義したやつは、きっちりと型を要求してる。

memcpy

guileにも出てきたんだけど、memcpyを書くのがプチ流行みたい。 ftype-pointer and foreign-procedure: counter-intuitiveに、悩みと共に例が出てた。消えるといけないので引用させてもらう。

(import (chezscheme))
(load-shared-object "libc.so.6")
(define memcpy/void* (foreign-procedure "memcpy" (void* void* size_t) void*))

(define sz (ftype-sizeof uptr))
(define a-addr (foreign-alloc sz))
(define b-addr (foreign-alloc sz))
(define a (make-ftype-pointer uptr a-addr))
(define b (make-ftype-pointer uptr b-addr))

(ftype-set! uptr () a #xdeadbeef)
(memcpy/void* b-addr a-addr sz)
(pretty-print (number->string (ftype-ref uptr () b) 16))

(foreign-free a-addr)
(foreign-free b-addr)

結構手間取るね。オイラーもこんなのを毎度書かないといけないと思うと、うんざりしますよ。 でも、これが試練なんですかね。

chez scheme tcp example

更に例を求めてうろうろしてたら、素敵な奴を発見。 自前でC語のTCP通信機能を書き、それをライブラリィー化。chezのアプリでそれを使って、IRCの通信をやるっぽい。

chez-scheme-tcp-example

オイラーはIRCなんて知らないので、httpでページを取得するようにmainを定義。

(define (main)
  (define socket (check 'dial (c-dial "localhost" 8080)))
  ;(c-set-read-timeout socket 300)
  (write-string socket "GET / HTTP/1.1\n\n")
  (do ([msg (read-string socket) (read-string socket)])
      ((eof-object? msg) (display "DONE!\n"))
    (display msg))
  (close-socket socket))

httpサーバーはpythonのモジュールに依頼。

alias server='python3 -m http.server 8080'

これを走らせて、httpサーバーを立ち上げてから、クライアントを実行

(base) sakae@debian:chez-scheme-tcp-example$ scheme main.ss
Chez Scheme Version 9.5.3
Copyright 1984-2019 Cisco Systems, Inc.

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.7.6
Date: Tue, 04 Feb 2020 20:59:16 GMT
Content-type: text/html; charset=utf-8
Content-Length: 465

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="csocket.c">csocket.c</a></li>
<li><a href="csocket.so">csocket.so</a></li>
<li><a href="main.ss">main.ss</a></li>
<li><a href="Makefile">Makefile</a></li>
</ul>
<hr>
</body>
</html>
DONE!
>

どんな事になってるか、きちんと把握して血肉にしような。ありがとう有志の方。

いよいよ本丸か

上で出て来た例は、C語の引数が割合単純なものだった。本格的なライブラリィーだと、引数は、構造体になってるのが普通だ。だから、この構造体のやり取り方法を是非とも習得する必要が有る。

難しいのは後回しにして、勝手知ったる gettimeofday あたりがいいかな。guileでも出て来たしね。これをchezに移植したらどうなるって所から始めるかな。

#include <stdio.h>
#include <sys/time.h>

int main(int argc, char **argv) {
      struct timeval tv;
      gettimeofday(&tv, NULL);
      printf("%ld %06lu\n", tv.tv_sec, tv.tv_usec);
      return 0;
}

C語だとこんな感じ。main側で、結果を受け取る箱(構造体)を用意して、そのアドレスを 引数にして関数を呼ぶ。すると箱の中に結果が詰まって帰ってくる。

(base) sakae@debian:tmp$ ./a.out
1580852572 469088
(base) sakae@debian:tmp$ echo 1580852572 | awk '{ print strftime("%c", $1); }'
Wed 05 Feb 2020 06:42:52 AM JST

BSD系だとunix時間(通称epoc time)を、人間が読めるフォーマットに変換するには date -r が使えるけど、Linuxは何故か面倒だ。

これがgettimeofdayのmanからの情報。タイムゾーンは現在使われていない盲腸みたいな物なのでNULLを渡す。時刻情報が入るやつは、2つのパートに分かれている。time_t,susecond_tなんて型になってるけど、long(64Bit)が実体になる。昔はこれが32Bitだったものだから、2038年問題に怯えたものさ。今は、そんな心配は無用。でも新たな悩みが、、、gettimeofdayは、これからは非推奨ですってさ。こうやって、プログラマーの食い扶持を確保しておりますとな。 Linuxカーネル5.6、32ビット版で2038年問題への対応が行われる

SYNOPSIS
       #include <sys/time.h>

       int gettimeofday(struct timeval *tv, struct timezone *tz);

           struct timeval {
               time_t      tv_sec;     /* seconds */
               suseconds_t tv_usec;    /* microseconds */
           };

上でtimevalのそれぞれの要素はlongって言っちゃったけど、正式にはどうやって調べる? 動くソースが有るなら、cppで展開して結果を拾えば良い。typedefで別名が与えられている事が分かる。

(base) [sakae@c8 tmp]$ cc -E t.c | grep suseconds_t
typedef long int __suseconds_t;
  __suseconds_t tv_usec;
typedef __suseconds_t suseconds_t;

そんな都合良く動くソースが無いよと言う時は、下記のようにする手もある。

(base) [sakae@c8 tmp]$ echo '#include <sys/time.h>' >z.c
(base) [sakae@c8 tmp]$ cc -E z.c | grep suseconds_t
typedef long int __suseconds_t;
  __suseconds_t tv_usec;
typedef __suseconds_t suseconds_t;

Ftypes: Structured foreign types 開発者による難解な記事?

;; myt -- gettimeofday

(load-shared-object "libc.so.6")

(define-ftype timeval
  (struct
   [tv_sec  long]
   [tv_usec long]))

(define tv
  (make-ftype-pointer timeval
                      (foreign-alloc (ftype-sizeof timeval))))

(define my-f
  (foreign-procedure "gettimeofday" ((* timeval) void*) int))


;(define (gettimeofday)  (my-f tv 0))
;(gettimeofday)
;(ftype-ref timeval (tv_sec) tv)

(define (gettimeofday)
  (my-f tv 0)
  (cons (ftype-ref timeval (tv_sec) tv) (ftype-ref timeval (tv_usec) tv)))

ちょっと苦労したけど、見よう見まねで何とか出来た。

define-ftypeを使って、timevalの構造体を宣言。次にforeign-allocを使ってtimevalのエリアを確保し、そのアドレスのポインターを得る。そいつにtvって名前を付けておく。

my-fは、C語のgettimeofdayの雛型宣言。NULLはvoid*にするのね。そして、コメントにしてる3行は、C語流の使い方例。関数を定義して、それを呼んで、後で箱の中身を取り出す例だ。

これでは使い勝手が悪いので、guile流にgettimeofdayを定義してみた。結果はcons対にして返す形式。下記は実行例。

debian:tmp$ scheme -q myt.ss
(gettimeofday)
(1580940316 . 87964)
debian:tmp$ echo 1580940316 | awk '{ print strftime("%c", $1); }'
Thu 06 Feb 2020 07:05:16 AM JST

どうやら上手くいってるようだ。

scheme@(guile-user)> (gettimeofday)
$1 = (1580941144 . 421776)
debian:tmp$ echo 1580941144 | awk '{ print strftime("%c", $1); }'
Thu 06 Feb 2020 07:19:04 AM JST

それから注意事項を一つ。flreign-allocで取られる領域はschemeの管理するエリア外との事。 まあ、OSから見れば、Schemeからの要求だろうと、それ以外からの要求だろうと同じ事。 要求が来た順番に割り当てて行くのだろう。

って事は、foreign-allocで要求した物がずっとそこに居座ってしまって、schemeからの追加要求で得たものが、前からあるものと分断される可能性が有る。マニュアルでは、使い終わったら、速やかに開放するように勧めていたよ。

2038問題?

上記の実行は、debian(32Bit)なんだけど、2038が心配(人によっては8050問題の方が深刻なのは認めます)なので、確認する。

debian:tmp$ uname -a
Linux debian 4.19.0-6-686 #1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11) i686 GNU/Linux
debian:tmp$ echo '#include <sys/time.h>' >z.c
debian:tmp$ cc -E z.c | grep suseconds_t
__extension__ typedef long int __suseconds_t;
  __suseconds_t tv_usec;
typedef __suseconds_t suseconds_t;

正体は、long int だそうです。結局32Bitって事?

> (ftype-sizeof timeval)
8
> (ftype-sizeof void*)
4

longなメンバーを2つ収納してても8byteって事は、1つ32Bit(4byte)って事っすね。ポインターも同様に32Bitでした。

このままOSがバージョンアップされないで使っていると、2038年の1月以降は問題が生じる可能性がありますって事だな。後、余命14年と心得よ。まあ、それまでにHDDとかがお陀仏するだろうけどね。

比較の為にCentOS(64Bit)で確認

> (ftype-sizeof timeval)
16
> (ftype-sizeof void*)
8

軽々2038年問題はクリアーしてます。

guile vs chez

気のせいか、同じお題ではguileの方が簡単そう。guileのそれを再掲する。

(use-modules (system foreign))

(define gettimeofday
  (let ((f (pointer->procedure
            int
            (dynamic-func "gettimeofday" (dynamic-link))
            (list '* '*)))
        (tv-type (list long long)))
    (lambda ()
      (let* ((timeval (make-c-struct tv-type (list 0 0)))
             (ret (f timeval %null-pointer)))
        (if (zero? ret)
            (apply values (parse-c-struct timeval tv-type))
            (error "gettimeofday returned an error" ret))))))

timevalの構造体からtvを作り出すのが、guileの方が簡単に書けているように思う。対してchezの方はC語をそのまま変換してるように見受けられるから。

構造体のメンバーに構造体が含まれているような場合、chezの方は宣言量が多くなる感じがする。でも、決まりはC語に直結してるんで、間違いは少ないだろう。

若しchezで書くとしたら、構造体の宣言やそれを使った関数のプロトタイプ宣言は、C語のヘッダーファイル宜しく、別ファイルにするだろうね。そして本体側のファイルからは、includeならぬloadで取り込むのが良さそう。

懸念が一つある。flreign-allocで確保したやつの解法問題。上の例では、tvを一気に作成して しまったけど、これでは解放出来ない。下記のように、分解して宣言する必要が有りそうだ。

> (define aa (foreign-alloc (ftype-sizeof timeval)))
> aa
38114128
> (define bb (make-ftype-pointer timeval aa))
> bb
#<ftype-pointer timeval 38114128>

恐ろしい事をやってみる。

> (foreign-free aa)
> aa
38114128
> bb
#<ftype-pointer timeval 38114128>
> (foreign-free aa)
free(): double free detected in tcache 2
Aborted (core dumped)

解放しても、ポインターがそのままってのは、C語では重大問題だった。更に問題なのは二重解放、致命的だよ。

だからGCなんてのが発明されたのに、また持ち込んじゃったよ。これはもう、宿命なんですかね?

ぶつぶつ言いつつ結局、慣れの問題になるのかな? 一つ問題が有るとすれば、chez scheme は、BSD系には提供されていない事。linuxにロックインされるのが癪に障るな。

need check

libsoundio Chez Scheme wrapper. なんだか大変そうです

Is there any high quality 3rd libraries for Chez? 速くサポートしてください

互換レイヤを書く によるとpffi すなわちポータブル版のFFIが有るそうです

Chez Scheme で Foreign Interface を使ってみた やる人は、昔からやってる

SchemeCrossReferenceこれは有り難いです。