バイナリストリームの実装
nptでstring-stream
のbyte
バージョンを作りました。
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-line
とwrite-byte
が混在しています。
すごいっすね。
このようなことがまともにできるのは、
たぶん処理系がstream
をclos
でオーバーラップしてカスタマイズ
できるようになっているからでしょう。
Gray Streamってやつがそれなんだと思います。
nptでもぜひ実装したい気持ちはあるんですけど、
とりあえずはテストが先です。
まともなCommon Lispになったら考えます。
mopも拡張したいけど後回し!
今回string-stream
のbyte
バージョンを作成したのは
テストをやりやすくしたいがためだったのです。
ここでflexi-streams
に話題を当てます。
flexi-streams
とは、おそらく開発の動機が
ストリームに焦点を当てることから始まっているのだと思います。
しかし私が今回作成したのはファイルに焦点を当てています。
私がやりたかったことはファイルのシミュレートと言ったところでしょうか。
そこで、対応する型はsequence
ではなく(unsigned-byte 8)
のみになります。
なぜってファイルが大抵8bit
のbyte
形式だからです。
とりあえず例文を示します。
"Hello"
というファイルを作成したつもりになってみます。
;; npt専用のコード (use-package 'npt-system) (make-memory-input-stream #(#x48 #x65 #x6C #x6C #x6F))) -> #<STREAM MEMORY-INPUT #x801288a50>
このMEMORY-INPUT
というのが、STRING-INPUT
のbyte
版です。
型は(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
はストリームだけではなく、
ファイルそのものとして扱うことができます。
だからopen
のfilespec
に突っ込むことができます。
;; 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-stream
がopen
を乗っ取ることができているため、
このようなことができるのです。
さらに言うと、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を操作したのでしょうか?
open
とclose
は当然該当するとして、それ以外の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つと言ってもFreeBSDとLinuxは完全に一致していますけどね。