nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

バイナリストリームの実装

nptでstring-streambyteバージョンを作りました。

Common Lispでは、文字列をストリームに対応させた input-stream, output-streamというものが存在しますが、 characterではなく(unsigned-byte 8)に対応したという話です。

あらかじめ言っておくと、npt以外のCommon Lispでは、 flexi-streamsという便利なライブラリを使うことで実現できます。
この辺りをちゃんと説明して行き、 nptとどう違っているのかを示していきます。

まずflexi-streamsのページを見て例文を見てみます。

FLEXI-STREAMS - Flexible bivalent streams for Common Lisp
http://edicl.github.io/flexi-streams/

例文の一部

(defun bar (pathspec)
  "With a flexi stream."
  (with-open-file (out pathspec
                       :direction :output
                       :if-exists :supersede
                       :external-format '(:latin-1 :eol-style :lf))
    (setq out (make-flexi-stream out :external-format :utf-8))
    (write-line "ÄÖÜ1" out)
    (setf (flexi-stream-external-format out) '(:latin-1 :eol-style :lf))
    (write-line "ÄÖÜ2" out) 
    (write-byte #xeb out)
    (write-sequence #(#xa3 #xa4 #xa5) out)
    (setf (flexi-stream-external-format out) :ucs-2be)
    (write-line "ÄÖÜ3" out)))

write-linewrite-byteが混在しています。
すごいっすね。
このようなことがまともにできるのは、 たぶん処理系がstreamclosでオーバーラップしてカスタマイズ できるようになっているからでしょう。

Gray Streamってやつがそれなんだと思います。
nptでもぜひ実装したい気持ちはあるんですけど、 とりあえずはテストが先です。
まともなCommon Lispになったら考えます。
mopも拡張したいけど後回し!
今回string-streambyteバージョンを作成したのは テストをやりやすくしたいがためだったのです。

ここでflexi-streamsに話題を当てます。
flexi-streamsとは、おそらく開発の動機が ストリームに焦点を当てることから始まっているのだと思います。
しかし私が今回作成したのはファイルに焦点を当てています。
私がやりたかったことはファイルのシミュレートと言ったところでしょうか。
そこで、対応する型はsequenceではなく(unsigned-byte 8)のみになります。
なぜってファイルが大抵8bitbyte形式だからです。

とりあえず例文を示します。
"Hello"というファイルを作成したつもりになってみます。

;;  npt専用のコード
(use-package 'npt-system)

(make-memory-input-stream #(#x48 #x65 #x6C #x6C #x6F)))
-> #<STREAM MEMORY-INPUT #x801288a50>

このMEMORY-INPUTというのが、STRING-INPUTbyte版です。
型は(unsinged-byte 8)であり、read-byteで読み込むことができます。

;;  npt専用のコード
(use-package 'npt-system)

(defun read-byte-string (x)
  (let ((v (read-byte x nil nil)))
    (when v
      (cons (code-char v) (read-byte-string x)))))

(with-open-stream (x (make-memory-input-stream #(#x48 #x65 #x6C #x6C #x6F)))
  (coerce (read-byte-string x) 'string))
-> "Hello"

これくらいならflexi-streamsでも余裕で実現できます。

;;  sbclで実行
(require 'asdf)
(asdf:load-system 'flexi-streams)
(defpackage work (:use cl flexi-streams))
(in-package work)

(defun read-byte-string (x)
  (let ((v (read-byte x nil nil)))
    (when v
      (cons (code-char v) (read-byte-string x)))))

(with-open-stream (x (make-in-memory-input-stream #(#x48 #x65 #x6C #x6C #x6F)))
  (coerce (read-byte-string x) 'string))
-> "Hello"

うーん、おもしろい。
本当にすごいなflexi-streams
どうでもいいこと言うとflexi-streamって最後のsを付け忘れるんですよね。

話を戻しますけど、 nptのmemory-streamはストリームだけではなく、 ファイルそのものとして扱うことができます。
だからopenfilespecに突っ込むことができます。

;;  npt専用のコード
(use-package 'npt-system)

(with-open-stream (file (make-memory-input-stream #(#x48 #x65 #x6C #x6C #x6F)))
  (with-open-file (stream file)
    (read-line stream)))
-> "Hello", T

分かりますでしょうか。
with-open-fileのファイル名の所に、 #p"Hello.txt"とかではなく、memory-streamを突っ込んでいます。
完全な規約違反です。
でもこうしたかったんだもん。
こういう事ってflexi-streamsでもできるんでしょうか、ちょっと調べてないです。

つまり、ファイルを物理デバイス任せではなく、 メモリ内で完結できるようにしたわけです。
デバッグして思ったのですけど、結構楽しいですよ。
実装ではファイルストリームを操作する低レベルの所から memory-streamへ制御をかすめ取るみたいなことをしています。
割と便利なので、 もしあなたが今現在コンパイラやらインタープリタやらを実装している最中なら、 早い段階からこいつみたいなファイル・エミュレーターの ストリームを実装するよう検討してみたらどうでしょうか。

素直にファイル使えや!って意見もあると思いますけど、 実際テスト地獄に身を投じてみると、 速度やら手軽さやらでメモリを使いたくなります。

出力のoutput-streamにも対応しています。

;;  npt専用のコード
(with-open-stream (file (make-memory-output-stream))
  (with-open-file (stream file :direction :output :external-format 'utf-16le-bom)
    (format stream "あいう"))
  (get-output-stream-memory file))
-> #(#xFF #xFE #x42 #x30 #x44 #x30 #x46 #x30)

この例では、あいうという日本語の文字を、 UnicodeのUTF-16LE BOM付きで表したときに どのようなバイト列になるかを示してくれています。
nptのmemory-streamopenを乗っ取ることができているため、 このようなことができるのです。

さらに言うと、I/Oにも対応しています。

;;  npt専用のコード
(with-open-stream (file (make-memory-io-stream))
  (with-open-file (stream file :direction :io)
    (format stream "ZZZZZZZZ")
    (file-position stream :start)
    (format stream "ABCD")
    (file-position stream :start)
    (read-line stream)))
-> "ABCDZZZZ", T

自分はプログラミングをしてきた期間が結構長いと思っていますが、 双方向ストリームをまともに扱ったのは今回が初めてです。
当然、普通のfile-streamにもバグ満載でした。
一応は全部直したつもりですけど、もう少しテストしてみたいと思っています。

【追記】 関数仕様を作成しました。
Lisp関数仕様 - システム関数 - nptclのブログ

さて、今回はファイルストリームのめちゃくちゃ低レベルな操作をかすめ取って memory-streamを実現したという内容でした。
では一体どれくらいのAPIを操作したのでしょうか?
opencloseは当然該当するとして、それ以外のAPIを下記に示します。

read -> read関数、ReadFile関数
write -> write関数、WriteFile関数
flush -> flush関数、FlushFileBuffers関数
read_ready -> select関数
file_length -> fstat関数、GetFileSizeEx関数
file_position -> lseek関数、SetFilePointerEx関数
file_position_start -> lseek関数、SetFilePointerEx関数
file_position_end -> lseek関数、SetFilePointerEx関数
file_position_set -> lseek関数、SetFilePointerEx関数

まとめてみて思ったのですが案外少ないですね。
FreeBSD, Linux, Windowsの3つの環境を考慮して ファイルの入出力をカバーしたいのであれば、 最低でも上記の関数を実装すればいいということになります。
まあ3つと言ってもFreeBSDLinuxは完全に一致していますけどね。