週刊mosh - 0.2.7の変更サマリ / 非同期I/Oと同期I/O

というわけで、0.2.7をmilestoneにしていた変更が全て完了した。GUI関連を非同期I/O上に実現することにしたので、今回もバグフィックスがメインのリリースになりそう。
もっとも、ドキュメンテーションの調整や、nmosh側の不要なライブラリを消すといった細かい作業がまだ残っている。

主な変更点(0.2.6→0.2.7)

  • FFI: pointer-ref-*の互換性の無い仕様変更
  • NetBSDのサポート
  • Wine1.3以降のサポート(nmoshのみ)
  • MonaOSサポートの統合
  • out-of-treeビルドのより完全なサポート(autotoolsビルド)
  • (mosh process)にgetpidが追加された
  • SocketのTLS/SSL化をサポート。InsecureWindows上はサポートなし(intentionally - GMPとOpenSSLがライセンス的に互換性が無い*1ため。)
  • いくつかの実験的なライブラリの追加(templete, facebook)
  • nmoshの出力がカラーになった(コンソールのみ。NMOSH_CLICOLOR環境変数をenableに設定する)
  • × Windows上のpsyntax-moshの廃止(Cygwinは従来通りサポート)
  • × VisualStudio 2008サポートの終了
  • × Windows上のautotoolsビルドの廃止(Cygwinは従来通り可能)
  • 多くのバグフィックス

非同期I/Oでの同期I/O

nmoshの非同期I/Oでは、同期I/Oを次のように書ける。
ここで書いているprocess->stdout+stderrは、プロセスを起動し、結果とstdout、stderrの3値を返す。

(define-syntax io-dispatch-sync
  (syntax-rules ()
    ((_ resume form0 ...)
     (call/cc (lambda (resume)
                (let () form0 ...)
                (io-dispatch-loop))))))

(define (process->stdout+stderr . args)
  (io-dispatch-sync return
                    (launch! (exec . args)
                             ((finish stdout stderr) return))))

要するに、

  1. 非同期I/Oオペレーションを起動する(launch!)
  2. スレッドのデフォルトキューを待ち合わせる(io-dispatch-loop)
  3. 必要な処理が終わったらcall/ccでつくった継続を使って脱出(return)

mosh(というかR6RS)は、call/ccでつくった継続に複数の値を渡すことで多値を返すことができる。
moshのcall/ccはあまり軽い処理では無いが、この手の同期I/O手続きは基本的に便宜的に提供されるもので、パフォーマンスを気にするなら非同期I/Oを使えば良いということで割り切っている。
ただ、schemeのportは非常に頻繁に使われるので、全部のportをこの形の同期I/Oにするのは好ましくないかもしれない。たとえば、バッファに貯める時だけ同期I/Oを起動するように配慮する必要が有りそう。
デザイン上微妙だと思っているのは、nmoshの非同期I/Oは基本的に"システムのバッファは可能なかぎり早くユーザ側に引き取る"という思想を持っている点。これはかなりのケースで正しいが、メッセージ型のネットワークI/Oでは問題が起こる可能性がある。(N byte受信して初めてデータをコピーするという形にしたほうが効率的なケースがある)
あと、Gaucheのselect/selectorと違って、queue(Gaucheだとselector)に対して能動的にハンドラを追加する方法が無い。一応、ハンドラの中からハンドラを起動することはできるので、

(define total-bytes 0) ;; total I/O bytes
(define sockets '())
(listen-tcp! server-port
             (service 9999)
             (shutdown (^_ (exit 0)))
             (accept
               (^[socket name]
                 (set! sockets (cons socket sockets))
                 (stream-tcp!
                   (socket socket)
                   (user-data (gensym))
                   ((recv user-data buf count)
                    (display (list 'RECV-FROM: (socketname->string/numeric name)
                                   'SOCKET-ID: user-data
                                   buf))(newline)
                    (set! total-bytes (+ total-bytes count))
                    (when (> total-bytes 10000) ;; 全ソケットのI/Oバイトが合計10000バイトを超えたら、全部shutdown
                      (socket-shutdown server-port)
                      (for-each socket-shutdown sockets)))))))
(io-dispatch-loop)

(↑のコードではuser-dataを渡しているが、今の(nmosh io)では渡せない。必要性が微妙なため。)
のように書くことで、listen socketのハンドラから、実際の通信ソケットハンドラを起動することはできる。

*1:MinGW上のGMPはデフォルトで静的リンクになるため、GPLv3の制約を受ける