アセンブリ言語と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の結果によると、次はLEATESTを実行しています。 しかし、これは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に読み込まれる

と、このような動きになるようです。

読み飛ばしていたLEATESTを見てみます。 上気の例だと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 LOLOに飛びます。

LOではMOV [RDX], RSIMOV [RDX+8], RDIを実行しています。 確保した領域の0~7バイト目にRSIを、8~15バイト目にRDIを入れています。 OR DL, 7RDXの下位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のVirtualBoxWindowsの影響をもろに受けていて開発するのが大変そうでした。 WindowsUbuntuを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++のエントリポイント)

lispフォルダ

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/C++ > 全般 > 追加のインクルード ディレクトリにC:\lisp\eclを追加

  • 構成プロパティ > リンカー > 全般 > 追加のライブラリ ディレクトリに以下を追加

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に対応してなさそうです。 そこで、SBCLUnix-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))))