flexi-streams用法简介

每过一段时间总会燃起一种用Common Lisp(下文简称CL)来写Web应用的冲动,继而就会开始感慨在CL的生态圈中居然没有一款好用的Web框架。尽管放狗搜索“common lisp web framework”可以找到一些——例如Caveman2,以及在Cliki中记录的一些其它框架。然后使用过其中一部分的人就会知道,大部分用起来的体验都不咋地。

在业界摸爬打滚了一小段时光(从业几年姑且可以这么说吧)后,感觉制作一款专门用于编写JSON-in-JSON-out的Web应用的Web框架应该是一个不错的点子——反正大家都是发出application/json的请求期望application/json的响应,于是乎就撸起袖子自己干了。不过完全从零开始编写起是不现实的,于是乎选择了一个“平台”来作为基础。这个平台就是Clack

Clack会负责屏蔽下层的Web Server的差异,它只需要我提供一个函数给它作为来访的HTTP请求的“handler”即可,然后在这个handler中我就可以为所欲为啦。Clack在收到HTTP请求后,会把HTTP请求中的一些信息组织为一个列表类型的值传递给这个handler。在这个handler中,我只需要综合运用CAR、CDR之类的奇怪名字的函数就可以拿到自己需要的东西了——当然了,鉴于这个列表是个plist,用CL提供的DESTRUCTURING-BIND就可以很方便地提取啦。

在这个plist中,就有一个叫做:RAW-BODY的p,它的值是一个“流”——是的,就是那种文件流的流!但它又不是一个路边随处可见的妖艳贱货的流,而是一个来自FLEXI-STREAMS这个包(指CL中的package)的流。FLEXI-STREAMS是一个提供流操作的库,鉴于我没有看过Gray streams相关的内容,就不在这里瞎逼逼误导读者了。总而言之,我必须找到一个办法可以从一个FLEXI-STREAMS提供的输入流类型的值中读出一些东西来。

其实这个办法很简单,就是用CL原生提供的读取流的函数即可——比如READ-SEQUENCE这样的函数。不过我得验证一下不是,为此,我需要有办法可以构造出一个FLEXI-STREAMS流类型的值出来。FLEXI-STREAMS提供了一个叫做MAKE-FLEXI-STREAM的函数,显然这个就是我所需要调用的最后一个函数了。从它的描述来看,它需要一个CL中的原生流来作为第一个参数才行。为此,我试了一下下面的代码

1
2
3
4
5
(let ((text "Hello, world!"))
(with-input-from-string (s text)
(let ((buffer (make-array (length text)))
(fs (flexi-streams:make-flexi-stream s)))
(read-sequence buffer fs))))

遗憾的是,运行上面的代码会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
The value
#\H
is not of type
(UNSIGNED-BYTE 8)
when setting an element of (ARRAY (UNSIGNED-BYTE 8))
[Condition of type TYPE-ERROR]

Restarts:
0: [RETRY] Retry SLIME interactive evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [ABORT] abort thread (#<THREAD "worker" RUNNING {1007766D53}>)

Backtrace:
0: ((SB-VM::OPTIMIZED-DATA-VECTOR-SET (UNSIGNED-BYTE 8)) #<unavailable argument> #<unavailable argument> #<unavailable argument>)
1: (SB-IMPL:ANSI-STREAM-READ-SEQUENCE #(0 0 0 0 0 0 ...) #<SB-IMPL::STRING-INPUT-STREAM {1007975973}> 0 13)
2: (READ-SEQUENCE #(0 0 0 0 0 0 ...) #<SB-IMPL::STRING-INPUT-STREAM {1007975973}> :START 0 :END 13)
3: ((FLET FLEXI-STREAMS::FILL-BUFFER :IN FLEXI-STREAMS::READ-SEQUENCE*) 13)
4: ((:METHOD FLEXI-STREAMS::READ-SEQUENCE* (FLEXI-STREAMS::FLEXI-LATIN-1-FORMAT T T T T)) #<FLEXI-STREAMS::FLEXI-LATIN-1-FORMAT (:ISO-8859-1 :EOL-STYLE :LF) {1007975B43}> #<FLEXI-STREAMS:FLEXI-INPUT-STRE..
5: ((:METHOD STREAM-READ-SEQUENCE (TRIVIAL-GRAY-STREAMS:FUNDAMENTAL-INPUT-STREAM T)) #<FLEXI-STREAMS:FLEXI-INPUT-STREAM {1007975C73}> #(0 0 0 0 0 0 ...) 0 NIL) [fast-method]
6: (READ-SEQUENCE #(0 0 0 0 0 0 ...) #<FLEXI-STREAMS:FLEXI-INPUT-STREAM {1007975C73}> :START 0 :END NIL)
7: ((LAMBDA ()))
8: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET ((TEXT "Hello, world!")) (WITH-INPUT-FROM-STRING (S TEXT) (LET # #))) #<NULL-LEXENV>)
9: (EVAL (LET ((TEXT "Hello, world!")) (WITH-INPUT-FROM-STRING (S TEXT) (LET # #))))
10: ((LAMBDA NIL :IN SWANK:INTERACTIVE-EVAL))
--more--

既然在调用READ-SEQUENCE的时候出状况了,不妨试试下面的代码

1
2
3
4
(let ((text "Hello, world!"))
(with-input-from-string (s text)
(let ((fs (flexi-streams:make-flexi-stream s)))
(read-char fs))))

再次令人遗憾的,它会抛出另一个状况(CL中的condition啦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#<SB-IMPL::STRING-INPUT-STREAM {10020A7243}> is not a binary input stream.
[Condition of type SIMPLE-TYPE-ERROR]

Restarts:
0: [RETRY] Retry SLIME interactive evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [ABORT] abort thread (#<THREAD "worker" RUNNING {1001F6E813}>)

Backtrace:
0: (SB-KERNEL:ILL-BIN #<SB-IMPL::STRING-INPUT-STREAM {10020A7243}>)
1: (READ-BYTE #<SB-IMPL::STRING-INPUT-STREAM {10020A7243}> NIL NIL)
2: ((:METHOD FLEXI-STREAMS::READ-BYTE* (FLEXI-STREAMS:FLEXI-INPUT-STREAM)) #<FLEXI-STREAMS:FLEXI-INPUT-STREAM {10020A74C3}>) [fast-method]
3: ((:METHOD FLEXI-STREAMS::OCTETS-TO-CHAR-CODE (FLEXI-STREAMS::FLEXI-LATIN-1-FORMAT T)) #<FLEXI-STREAMS::FLEXI-LATIN-1-FORMAT (:ISO-8859-1 :EOL-STYLE :LF) {10020A7393}> #<unavailable argument>) [fast-me..
4: ((:METHOD STREAM-READ-CHAR (FLEXI-STREAMS:FLEXI-INPUT-STREAM)) #<FLEXI-STREAMS:FLEXI-INPUT-STREAM {10020A74C3}>) [fast-method]
5: (READ-CHAR #<FLEXI-STREAMS:FLEXI-INPUT-STREAM {10020A74C3}> T NIL #<unused argument>)
6: ((LAMBDA ()))
7: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET ((TEXT "Hello, world!")) (WITH-INPUT-FROM-STRING (S TEXT) (LET # #))) #<NULL-LEXENV>)
8: (EVAL (LET ((TEXT "Hello, world!")) (WITH-INPUT-FROM-STRING (S TEXT) (LET # #))))
9: ((LAMBDA NIL :IN SWANK:INTERACTIVE-EVAL))
--more--

看来从一开始提供给MAKE-FLEXI-STREAM函数的参数就应当是一个“二进制”的流才对。为此,我需要借助FLEXI-STREAMS自身的力量——调用它的STRING-TO-OCTETS函数。使用这个函数,可以将一个字符串转换为某种编码下的字节数组,例如下面的代码

1
(flexi-streams:string-to-octets "Hello") ;#(72 101 108 108 111)

得到一串“octet”后,还需要将其转换为“流”才行。再次借助FLEXI-STREAMS的力量,调用它的MAKE-IN-MEMORY-INPUT-STREAM函数,然后将这个函数调用的返回值作为MAKE-FLEXI-STREAM的第一个参数即可,最终的代码如下

1
2
3
4
5
6
7
(let* ((text "Hello, world!")           ; 原始文本
(octets (flexi-streams:string-to-octets text)) ; 使用flexi-streams转换为字节数组,因为下一个函数只接受这种类型的参数
(memory-input (flexi-streams:make-in-memory-input-stream octets)) ; 同样先转换为内存中的流,因为下一个函数只接受这种类型的参数
(flexi-stream (flexi-streams:make-flexi-stream memory-input)) ; 终于可以得到一个真正的flexi-stream了
(buffer (make-array (flexi-streams:octet-length text)))) ; 这里其实用字节的长度还是用字符的长度(flexi-streams:char-length)都没差
(read-sequence buffer flexi-stream) ; 可以像处理CL中的流那样处理flexi-stream
(print (coerce buffer 'string))) ; 把字节数组拼成字符串再输出比较好看

全文完