アセンブリ言語とCで遊ぶ
x86-64のアセンブリ言語とCの呼び出し規約のお勉強のため、アセンブリ言語で書いた関数をCから呼び出してみました。 アセンブルにはnasm、Cのコンパイルにはgccを使っています。
Cのプログラムは以下の通り。 appという関数を実行して結果をprintfするだけです。
// main.c #include <stdio.h> int app(int x, int y); int main(void) { int ret = app(10, 20); printf("%d\n", ret); return 0; }
appの実装は以下の通り。appの2つの引数と100とを足し合わせた結果を返すだけです。 呼び出し規約の勉強のためわざと冗長な書き方をしています。
;; app.asm bits 64 global app section .text app: ;; 呼び出し時のrbpを保存 (すぐ下でrspの値を保持するのに使用) push rbp ;; 呼び出し時のrspを保存 mov rbp, rsp push rdi push rsi push 100 call add ;; push rdi等で変更されたrspを呼び出し時の値に戻す mov rsp, rbp ;; rspの保存に使われたrbpを呼び出し時の値に戻す pop rbp ret add: push rbp mov rbp, rsp ;; 「ret先のアドレス」と「push rbp」の16バイト分以降に引数がある mov rax, [rbp+16] add rax, [rbp+24] add rax, [rbp+32] mov rsp, rbp pop rbp ret
以下のようにコンパイル、リンクして実行できます。
$ nasm -f elf64 -o app.o app.asm $ gcc main.c app.o $ ./a.out 130
SBCLの実装を追う ~car, cdr, cons~
なんとなく興味があったのでdisassembleを使ってSBCLのcar、cdr、consの中を追ってみました。 CPUのアーキテクチャはx64です。
carとcdrをdisassembleしてみます。
CL-USER> (disassemble #'car) ; disassembly for CAR ; Size: 12 bytes. Origin: #x52A3B48B ; CAR ; 8B: 488B56F9 MOV RDX, [RSI-7] ; 8F: 488BE5 MOV RSP, RBP ; 92: F8 CLC ; 93: 5D POP RBP ; 94: C3 RET ; 95: CC10 INT3 16 ; Invalid argument count trap NIL CL-USER> (disassemble #'cdr) ; disassembly for CDR ; Size: 12 bytes. Origin: #x52A6E77B ; CDR ; 7B: 488B5601 MOV RDX, [RSI+1] ; 7F: 488BE5 MOV RSP, RBP ; 82: F8 CLC ; 83: 5D POP RBP ; 84: C3 RET ; 85: CC10 INT3 16 ; Invalid argument count trap NIL
C言語によるcarやcdrの実装を呼び出しているのかと思ったのですが、そういうわけではなさそうです。 もしそうならCALLやJMPがdisassembleの結果に現れるはずです。
まだ分からないのでcadrもdisassembleしてみます。 cadrはcdrを取った後でcarを取る関数で上で見た関数の組み合わせです。何か分かるかもしれません。
CL-USER> (disassemble #'cadr) ; disassembly for CADR ; Size: 26 bytes. Origin: #x52D0F4AB ; CADR ; AB: 488B7701 MOV RSI, [RDI+1] ; AF: 8D46F9 LEA EAX, [RSI-7] ; B2: A80F TEST AL, 15 ; B4: 750A JNE L0 ; B6: 488B56F9 MOV RDX, [RSI-7] ; BA: 488BE5 MOV RSP, RBP ; BD: F8 CLC ; BE: 5D POP RBP ; BF: C3 RET ; C0: L0: CC4C INT3 76 ; OBJECT-NOT-LIST-ERROR ; C2: 18 BYTE #X18 ; RSI(d) ; C3: CC10 INT3 16 ; Invalid argument count trap NIL
3つの結果を眺めると、RSI-7, RSI+1, RDI+1, RSI-7など似たようなパーツが浮かび上がってきます。 これで何か分かるかもしれないので、cadrの結果を、car、cdrの結果を見ながら追っていきます。
cadrでは最初にcdrを実行します。
cadrをcdrを比べると、cadrではMOV RSI, [RDI+1]
を実行しており、
cdrではMOV RDX, [RSI+1]
を実行しています。
双方とも、レジスタの値に1足した先にあるメモリの値を取得しています。これがcdrなのでしょうか。
cadrはcdrの結果からさらにcarを取ります。
disassembleの結果によると、次はLEA
、TEST
を実行しています。
しかし、これはJNE
のためのものという感じがして、carを実行しているとは思えません。
その後のMOV RDX, [RSI-7]
がcarでしょうか。
carのdisassemble結果にあるMOV RDX, [RSI-7]
と同じです。
cadrのRSIには[RDI+1]が入っていて、これはcdrと思われる前段処理の結果です。
これらでcarとcdrを実行していると考えるとつじつまが合いそうです。
まとめると、以下のような仮設を立てられます。
- carではRSIにコンスセルのアドレスが入っている
- cdrではRSIにコンスセルのアドレスが入っている
- cadrではRDIにコンスセルへのアドレスが入っている
- コンスセルのアドレス+1から8バイトがcar部分のアドレス
- コンスセルのアドレス-7から8バイトがcdr部分のアドレス
この仮説をもとに、以下のような設定でcadrの動きを追ってみます。
メモリ: 200 207 208 215 | | | | | | | | v v v v +---------+---------+ | xxx | 407 | +---------+---------+ 400 407 408 415 | | | | | | | | v v v v +---------+---------+ | yyy | zzz | +---------+---------+ レジスタ: RDI = 207
- RDIにcadrの引数のアドレス(= 207)が入っている
MOV RSI, [RDI+1]
が実行される- 207 + 1 = 208から8バイトの値は407
- 407がRSIに読み込まれる
MOV RDX, [RSI-7]
が実行される- 407 - 7 = 400から8バイトの値はyyy
- yyyがRDXに読み込まれる
と、このような動きになるようです。
読み飛ばしていたLEA
、TEST
を見てみます。
上気の例だとRSI-7 = 400がEAXに読み込まれ、15 = 0x0Fと論理積が取られます。
結果は0なので無事car部分が実行されます。
論理積がもし非ゼロならJNE LO
が実行され、LO
ラベル部分の命令が実行されます。
ここにはOBJECT-NOT-LIST-ERRORというコメントがあるので、引数がリストでなかったときのエラー処理ということでしょうか。
アドレスは通常8の倍数になるようにアライメントされていると思われます。 8の倍数はアドレス、8の倍数でない値は数値を意味し、下位4ビットはタグになっているとかでしょうか。
とりあえず、cadrを通して、car、cdrの動きを分かったように思います。 +1と-7でちょうど64ビット違うのもつじつまがあいそうですし。
consもdisassembleしてみます。
CL-USER> (disassemble #'cons) ; disassembly for CONS ; Size: 58 bytes. Origin: #x52BF09AF ; CONS ; AF: 4D896D28 MOV [R13+40], R13 ; thread.pseudo-atomic-bits ; B3: 498B5558 MOV RDX, [R13+88] ; thread.cons-tlab ; B7: 488D4210 LEA RAX, [RDX+16] ; BB: 493B4560 CMP RAX, [R13+96] ; BF: 771E JNBE L2 ; C1: 49894558 MOV [R13+88], RAX ; thread.cons-tlab ; C5: L0: 488932 MOV [RDX], RSI ; C8: 48897A08 MOV [RDX+8], RDI ; CC: 80CA07 OR DL, 7 ; CF: 4D316D28 XOR [R13+40], R13 ; thread.pseudo-atomic-bits ; D3: 7402 JEQ L1 ; D5: CC09 INT3 9 ; pending interrupt trap ; D7: L1: 488BE5 MOV RSP, RBP ; DA: F8 CLC ; DB: 5D POP RBP ; DC: C3 RET ; DD: CC10 INT3 16 ; Invalid argument count trap ; DF: L2: 6A10 PUSH 16 ; E1: E86AFAE0FF CALL #x52A00450 ; SB-VM::LIST-ALLOC-TRAMP ; E6: 5A POP RDX ; E7: EBDC JMP L0 NIL
L2
ラベル部分にCALL
があり、SB-VM::LIST-ALLOC-TRAMP
とコメントされています。
つまりこれがconsにおけるメモリ確保の部分だと思われます。
CALL
の後はPOP RDX
で、確保した領域のアドレスをRDXに納めていそうです。
最後にJMP LO
でLO
に飛びます。
LO
ではMOV [RDX], RSI
とMOV [RDX+8], RDI
を実行しています。
確保した領域の0~7バイト目にRSIを、8~15バイト目にRDIを入れています。
OR DL, 7
でRDXの下位3ビットに1を立ててます。つまり200は207になります。
このような割り当ての仕方は、cadrで見たcar、cdrのアクセスと整合しています。
おそらくRSIがcarでRDIがcdrなのでしょう。
というわけで、SBCLのcar、cdr、consを中を追ってみた、でした。 処理系の中を覗いてみるのは楽しいですね。
複数プロセッサーの設定だとVirtualBoxがうまく動かない問題への対処
VirtualBoxで複数のプロセッサーを使う設定してもうまく動かないということがしばらく続いていたのですがようやく決着がついたので忘れないようにメモしておきます。
以下がうまく動いた環境です。
普段の開発はVMで、ここでEmacsなりSBCLなりを動かしています。 さらに、web系の開発をするときやdebファイルを作るときなどはVM上のdockerを使っています。 なのでとりあえずGuest OSでdockerを動かすまでがゴールです。
dockerなのですが、VMのプロセッサー数が1なら問題なく動きました。 しかしプロセッサーが複数ある場合、現時点で最新版のVirtualBox 6.1.34では途中で「watchdog: BUG: soft lockup」などと表示されてフリーズしました。 VirtualBoxのフォーラムを見てみると、このエラーについてはどうやら6.1.32から発生しているようでした。 6.1.30ではうまく動いていたとのことだったので6.1.34を一度アンインストールして6.1.30をインストールしました。
しかし6.1.30で新規にVMを作ると、今度はGuest OSの起動時にエラーが出てVMが起動しなくなりました。 原因はどうやらWindowsの機能と干渉しているためのようです。 6.1.26だと動いていたとのことなので、6.1.30をアンインストールして6.1.26をインストールしました。
6.1.26ではdockerも無事動きました。
今回VirtualBoxのフォーラムを見ながら思ったのですが、Windows hostのVirtualBoxはWindowsの影響をもろに受けていて開発するのが大変そうでした。 WindowsはUbuntuをHyper-Vで動かすんじゃなくてWSL2を使えって言ってるし、 VirtualBoxのUIは割と気に入っているので、VirtualBoxを使っていきたいのですが…。
Windows上でeclを使ってCommon Lispの関数をC++から実行する
はじめに
Windows上でeclを使ってCommon Lispの関数をC++から実行する方法を紹介します。 eclを使えばC++からCommon Lispを実行することができるため、 エントリポイントはプラットフォームが提供するC++のDSLで実装するしかなさそうなんだけど、 メインの処理はスムーズに開発できるCommon Lispで書きたい、ということが可能になります。 このeclの使い方はlinux限定かなと勝手に思い込んでいたのですが、実はWindowsでも使えるようでした。
実行環境
eclを使うにあたってまずはeclをビルドして環境を構築する必要があります。 今回はCドライブの直下にlispフォルダを作って以下のような構成でeclを実行します。
C:/lisp/ + ecl-build/ // eclのソースコード、ツール + ecl-21.2.1/ // eclのソースコード + yasm-1.3.0-win64.exe + ecl/ // eclの実行ファイル、ランタイム + print-factorial/ // C++から実行するCommon Lispのシステム + print-factorial.lisp + print-factorial.asd + print-factorial-lib/ // Common Lispから生成したlibファイルを置く場所 + print-factorial-entry/ // MSVCのプロジェクト (C++のエントリポイント)
eclのビルドとインストール
参考: https://ecl.common-lisp.dev/static/manual/Building-ECL.html
Visual Studio 2022をインストールする
- 2022でなくてもいいかもしれないですが、少なくともx64 Native Tools Command Promptは必要です
yasm-1.3.0-win64.exeをダウンロードする
- 上述したように
C:/lisp/ecl-build
下にダウンロードします
x64 Native Tools Command Prompt for VS 2022を起動する
- 「スタートメニュー > Visual Studio 2022 > x64 Native Tools Command Prompt for VS 2022」にあります
msvcディレクトリに移動する
cd C:/lisp/ecl-build/ecl-21.2.1/msvc
yasmへのパスを通す
set path=C:\lisp\ecl-build;%path%
ビルドする
nmake ECL_CMP=1 ECL_ASDF=1 ECL_WIN64=1 GMP_TYPE=AMD64
インストールする
nmake install prefix=C:\lisp\ecl
quicklispをダウンロードしてインストールする
- どこにダウンロードしてもいいです
サンプルシステムの作成
C++から実行するシステムを作ります。
;; print-factorial.asd (asdf:defsystem :print-factorial :components ((:file "print-factorial")) :depends-on (:alexandria))
;; print-factorial.lisp (defpackage :print-factorial (:use :cl) (:export :print-factorial)) (in-package :print-factorial) (defun print-factorial (n) (print (alexandria:factorial n)) (force-output))
Common Lispのコードからlibファイルの作成
eclを起動する
- x64 Native Tools Command Prompt for VS 2022を起動します
- このコマンドプロンプトでないと
asdf:make-build
時に「 指定されたファイルが見つかりません。」エラーが出ます。 - おそらく、64bitビルドしたlibファイルを読み込めることができないのだと思われます。
- このコマンドプロンプトでないと
cd c:\lisp\
しますasdf:make-build
をすると一時ファイルが現在のフォルダに作られるため、一時ファイルを問題なく作れるフォルダに移動します
- コマンドプロンプトから
c:\lisp\ecl\ecl
を実行します
libファイルを生成する
- eclのREPLから以下を実行します
;; print-factorialの読み込み (push "C:/lisp/print-factorial/" asdf:*central-registry*) (ql:quickload :print-factorial) ;; asdf:make-buildのための事前準備 (require 'cmp) ;; libファイルの生成 (asdf:make-build :print-factorial :type :static-library :monolithic t :move-here "C:\\lisp\\print-factorial-lib" :init-name "init_print_factorial")
eclのドキュメントにはasdf:make-build
をすればlibファイルが生成されるとありますが、実際はその前に(require 'cmp)
しないとファイルが生成されません。
参考: https://stackoverflow.com/questions/55086558/what-is-the-correct-way-to-compile-a-file-using-embeddable-common-lisp
asdf:make-build
を実行すると以下のようなログが出力され、ファイルが生成されます。
> (asdf:make-build :print-factorial :type :static-library :monolithic t :move-here "C:\\lisp\\print-factorial-lib" :init-name "init_print_factorial") ;;; ;;; Compiling C:/lisp/print-factorial/print-factorial.lisp. ;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=0 ;;; ;;; End of Pass 1. ;;; Finished compiling C:/lisp/print-factorial/print-factorial.lisp. ;;; (#P"C:/lisp/print-factorial-lib/print-factorial--all-systems.lib")
MSVCでプロジェクトを作成し、eclで生成したlibファイルと一緒にビルド
MSVCプロジェクトを作成する。
ソリューションエクスプローラー > プロパティで「構成: すべての構成」「プラットフォーム: x64」を選択し、以下のように設定する。
C:\lisp\print-factorial-lib C:\lisp\ecl
- 構成プロパティ > リンカー > 入力 > 追加の依存ファイルに以下を追加
print-factorial--all-systems.lib
以外のファイルはeclフォルダ下にあるlibファイルで、dir /b C:\lisp\ecl\*.lib
で得られます
print-factorial--all-systems.lib asdf.lib cmp.lib deflate.lib defsystem.lib ecl-cdb.lib ecl-curl.lib ecl-help.lib ecl-quicklisp.lib ecl.lib package-locks.lib profile.lib ql-minitar.lib rt.lib sb-bsd-sockets.lib sockets.lib
ソースコードを書く。
#include <ecl/ecl.h> extern "C" { void init_print_factorial(cl_object); } int main() { _wputenv_s(L"ECLDIR", L"C:\\lisp\\ecl\\"); const char* ECL_STRING = "ecl"; char ecl[sizeof(ECL_STRING)]; strncpy_s(ecl, ECL_STRING, sizeof(ecl)); char* argv[] = { ecl }; cl_boot(1, argv); ecl_init_module(NULL, init_print_factorial); cl_funcall(2, cl_eval(c_string_to_object("'print-factorial:print-factorial")), MAKE_FIXNUM(10)); cl_shutdown(); return 0; }
このようにMSVCのプロジェクトを作成してビルドすると、Windowsの実行ファイルが生成されるはずです。
C:\lisp\ecl
フォルダに移動して (ecl.dllを読み込ませるため) 実行ファイルを実行すると、無事結果が表示されます。
c:\lisp\ecl> C:\lisp\print-factorial-entry\x64\Debug\print-factorial-entry.exe 3628800
おわりに
Windows上でeclを使ってCommon Lispの関数をC++から実行する方法を紹介しました。 Common LispのコードとWindowsの関数とを組み合わせることができるので、いろいろできることが広がりそうです。
Common Lispで動くcurses (cl-charms)で日本語を出力する
cl-charmsのAPIをそのまま使うと日本語の文字列は文字化けして出力されます。 対処の方法を調べるのに時間がかかったのでここにメモ。
手元で試した限りだと、cl-charmsの処理に入る前に(cl-setlocale:set-all-to-native)
を実行しておけば大丈夫そうでした。
localeを設定すれば大丈夫というのは検索すれば出てくるのですが、Common Lispからどうやればいいのかは分からず、結局lemのコードを読んでいて見つけました。(ありがとうございます!)
以下、サンプルコードです。
charms-ja.lisp
という名前で保存し、ros -s cl-charms -s cl-setlocale -l charms-ja.lisp -e '(charms-ja:main)' -q
と打てば実行できると思います。
(defpackage :charms-ja (:use :cl) (:export :main)) (in-package :charms-ja) (defun main () (cl-setlocale:set-all-to-native) (charms:with-curses () (charms:disable-echoing) (charms:enable-raw-input :interpret-control-characters t) (charms:enable-non-blocking-mode charms:*standard-window*) (loop for c = (charms:get-char charms:*standard-window* :ignore-error t) do (progn (charms:clear-window charms:*standard-window*) (charms:write-string-at-point charms:*standard-window* "こんにちは世界" 0 0) (charms:refresh-window charms:*standard-window*) (case c ((#\q #\Q) (return)))))))
IMEと確定結果の扱い
はじめに
かな漢字変換の結果を確定した時に確定した漢字を記憶しておき、次回以降の変換時に優先的に出力するのは、IMEのよくある機能です。 今回、この機能の劣化版を個人開発のIMEでも作ってみました。 まだ全然使い物にはならないものなのですが、 機能の作りの方針に関しては今後の開発にも参考にできそうに思ったのでメモしておきます。
既存のつくり
stateful-imeというオブジェクトがあり、ユーザーとの対話を担当しています。 stateful-imeは、state + statelessな機能というつくりになっています。 詳しくは以下にあります。 mhkoji.hatenablog.com
採用した方針
既存のつくりを素直に拡張する方針を採用し、次のように変更しました。 確定結果はhistoryと名付けました (今思うと適当な名前です)。 historyも状態の一つと言えそうに思い、stateのメンバとしました。
stateful-imeの役割は基本的には状態を保持・更新するだけであり、機能自体はstateless側の役割です。 今回は、確定した変換結果もstateful-imeに返却するようにstateless側を修正しました。 stateful-imeは確定した結果をstateless側から受け取ると、その確定結果でもって保持しているhistoryを更新します。
詳しい処理は下のようになります。
キー入力が現在の状態で確定するものだった場合、
senn.win.im.process-input:execute
が確定した変換情報 (committed-segments
) を返すので、
それでhistoryを更新します。
(defun process-input (stateful-ime key) (with-accessors ((history state-history) (input-state state-input-state) (input-mode state-input-mode)) (stateful-ime-state stateful-ime) (let ((result (senn.win.im.process-input:execute stateful-ime input-state input-mode key))) (destructuring-bind (can-process view &key state committed-segments) result ;; update application state (when state (setf input-state state)) (when committed-segments (dolist (seg committed-segments) (history-put history (senn.segment:segment-pron seg) (senn.segment:segment-current-form seg)))) ...
input-stateの更新とhistoryの更新はどちらもstateの更新ということで兄弟のような関係になっています。
次にeffected-imeを定義しました。 effectedにはstateに影響を受けるという意味を持たせているつもりです。
(defclass effected-ime (senn.im:ime) ((kkc :initarg :kkc :reader effected-ime-kkc)))
メンバのkkcはviterbiアルゴリズムによるかな漢字変換を実行するためのオブジェクトです。
imeのレイヤーではsenn.im:convert
メソッドがひらがな列をかな漢字混じり列に変換する機能を提供します。
effected-imeに対してsenn.im:convert
メソッドが呼び出されると、
viterbiによるかな漢字変換 (senn.im.kkc:convert
) 結果を得た後、
状態を取り出し (stateful-ime-state
)、状態のhistoryを参照して上書きします(history-apply
)。
(defmethod senn.im:convert ((ime effected-ime) (pron string) &key 1st-boundary-index) (with-accessors ((kkc effected-ime-kkc) (state stateful-ime-state)) ime (let ((segs (senn.im.kkc:convert kkc pron :1st-boundary-index 1st-boundary-index))) (history-apply (state-history state) segs)))))
effected-imeは状態を参照するだけで、状態自体を保持するようにはしませんでした。これはなんとなくです。 状態の保持・参照を同じオブジェクトに定義する必要はないかなとなんとなく思って分離しました。
状態の扱いの定義は分離された一方、実行時には全部入りのオブジェクトが必要です。 なのでstateful-imeとeffected-imeをミックインし、状態を保持・参照・更新できるクラスを定義します。 今後はこのオブジェクトがユーザーとの対話を担当します。
(defclass stateful-effected-ime (stateful-ime effected-ime) ())
採用しなかった他の方針
上述したように、sennのかな漢字変換はviterbiアルゴリズムというよく知られた方法を利用しています。
history-apply
をこのviterbiレイヤーに持って来るように修正しても今回の機能を実現することはできるのですが、
この方針は採用しませんでした。
viterbiレイヤーには言語モデルやviterbiアルゴリズムなど、一般的に知られた処理が属しています。
一方、以前の変換を利用するのは、よく知られてはいるものの、
以前の変換の定義については一般的には知られていないため、senn独自のものになります。
実際、今回の実装ではこの以前の変換を定義しているのは、上で述べたようにhistory
になっており、
これは一般的に知られていない、今後変更されそうな、今この瞬間のsenn独自のものです。
したがって、viterbiレイヤーは一般的に知られた処理のためのレイヤーとしてそのままにしておき、
今回の処理はsenn独自のレイヤーに持っていくことにしました。
おわりに
採用した方針と採用しなかった方針、採用した方針における状態の扱い方について書いてみました。 今後の機能づくりの参考になればと思います。
SBCLでUnix-domain socketを使う
はじめに
Common Lispでsocketプログラミングをするには通常usocketを使うのですが、 usocketはUnix-domain socketに対応してなさそうです。 そこで、SBCLでUnix-domain socketを使う方法について調べました。 また、簡単なechoサーバーも書いてみました。
local-socket
SBCLのマニュアル によると、Unix-domain socketにはlocal-socketクラスを使うようです。 また、local-abstract-socketというクラスもあり、こちらを使うと抽象ソケットを作ることができるようです。
これらのクラスからlistenするソケットを次のように生成できます。
(defun socket-listen (socket-name &key use-abstract) (let ((socket (make-instance (if use-abstract 'sb-bsd-sockets:local-abstract-socket 'sb-bsd-sockets:local-socket) :type :stream))) (handler-case (progn (sb-bsd-sockets:socket-bind socket socket-name) (sb-bsd-sockets:socket-listen socket 100) socket) (sb-bsd-sockets:address-in-use-error () nil))))
生成したソケットはsb-bsd-sockets:socket-accept
で接続を待ち受けることができます。
接続する側のソケットも同様にlocal-socketを使います。
(defun connect-to (socket-name &key use-abstract) (let ((socket (make-instance (if use-abstract 'sb-bsd-sockets:local-abstract-socket 'sb-bsd-sockets:local-socket) :type :stream))) (sb-bsd-sockets:socket-connect socket socket-name) socket))
echoサーバー
下は、echoサーバーの例です。
(start-server)
して接続を待ち受けた後、別ターミナルで(connect-and-send)
をすると、サーバーに文字列を送っていることが確認できます。
(ql:quickload :bordeaux-threads) ;;; server側 (defun handle-socket (socket) (let ((stream (sb-bsd-sockets:socket-make-stream socket :input t :output t :buffering :full))) (bt:make-thread (lambda () (format t "Connected") (unwind-protect (loop for line = (read-line stream nil nil) while line do (progn (print line) (write-line line stream) (force-output stream))) (sb-bsd-sockets:socket-close socket)) (format t "Disconnected"))))) (defun start-server () (let ((threads nil) (listen-socket (socket-listen "/tmp/server"))) (when listen-socket (unwind-protect (loop do (let* ((socket (sb-bsd-sockets:socket-accept listen-socket)) (thread (handle-socket socket))) (push thread threads))) (mapc #'bt:destroy-thread threads) (sb-bsd-sockets:socket-close listen-socket))))) ;;; client側 (defun connect-and-send () (let ((socket (connect-to "/tmp/server"))) (unwind-protect (let ((stream (sb-bsd-sockets:socket-make-stream socket :input t :output t :buffering :full))) (write-line "Hello" stream) (force-output stream) (print (read-line stream)) (write-line "World" stream) (force-output stream) (print (read-line stream)) (values)) (sb-bsd-sockets:socket-close socket))))