読者です 読者をやめる 読者になる 読者になる

パイプでメモリをストリームにする

メモリ上にあるデータをあたかもファイルのように読みたいというのを以前に考えて、C++ のストリームクラスとして定義したことがある。

しかし、これはもちろん C++ でなければ使えない。 もう少し低水準の API を用いて実現できないかと考えたとき、 Windows API のパイプはほぼファイルと同じインターフェイスで使えることを思い出した。 シークが出来ないという制限はついてしまうが、それを除けば見掛け上はファイルと同じ形で処理できるのではないかと考え、実装してみた。

// memstream.h
HANDLE CreateMemoryFile(const LPVOID data, const DWORD data_size);
// memstream.c
#include <windows.h>
#include <process.h>
#include "memstream.h"

struct writer_args {
  HANDLE hWritePipe;
  LPVOID data;
  DWORD data_size;
};

static const int perround = 1024*64;

static void writer(void* args) {
  struct writer_args* wargs = args;
  char* data = wargs->data;
  DWORD data_size = wargs->data_size;
  HANDLE hWritePipe = wargs->hWritePipe;
  free(wargs);
  DWORD written_size;
  DWORD writing_size;
  
  for(DWORD rest=data_size; rest>0; data+=written_size, rest-=written_size) {
    writing_size = rest<perround ? rest : perround;
    if(!WriteFile(hWritePipe, data, writing_size, &written_size, NULL)) break;
  }

  CloseHandle(hWritePipe);
  _endthread();
}

HANDLE CreateMemoryFile(const LPVOID data, const DWORD data_size) {
  HANDLE rp, wp;
  BOOL result = CreatePipe(&rp, &wp, NULL, 0);
  if(!result) return INVALID_HANDLE_VALUE;
  struct writer_args* wargs = malloc(sizeof(struct writer_args));
  wargs->hWritePipe = wp;
  wargs->data = data;
  wargs->data_size = data_size;
  _beginthread(writer, 0, wargs);
  return rp;
}

これは以下のような要領で使うことが出来る。

#include <windows.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include "memstream.h"

int main(void) {
  char* sample_str="Hello, this is file mapped memory sample";
  HANDLE mfh = CreateMemoryFile(sample_str, strlen(sample_str));
  int fd = _open_osfhandle((int)mfh, _O_RDONLY);
  FILE *fp = _fdopen(fd, "r");
  int ch;
  while((ch=getc(fp)) != EOF) putchar(ch);
  fclose(fp);
  return 0;
}

ウェブ上を検索してみるとスレッドを使わずに一気にパイプに書き込んでしまう方法を紹介しているものもある。 しかし、データ量が充分に小さいときを除いてはパイプのバッファサイズを越えてしまう可能性がある。 そのとき、少しづつでもパイプから読み出してデータを消費するのならばいずれは書込みも完了するのだが、シングルスレッドでは永遠に待つようになってしまう。 CreatePipe API の第四引数でパイプのバッファサイズを指定しても、あくまでヒントとして活用されるに過ぎず、その大きさのバッファを確保することを保証していないということに注意して欲しい。 パイプのバッファサイズ (としてヒントを与えた数値) より少ないデータ量しか書込まないからロックしないとは言い切れず、シングルスレッドで確実にロックしないようにする方法は (MSDN における CreatePipe に関する記述の範囲内では) 読み取れなかった。 そんなわけで、パイプを書き込む側はスレッドにするのが安全であると判断して上記のように実装した次第である。

Document ID: ecd99cc5c21bd06439374c829b044f4b

Chez Scheme でコンソールに日本語を出力する

プログラミング言語 Scheme の処理系のひとつである Chez Scheme仕様 (R6RS) が定める三種類の符号での入出力をサポートしています。

更に、独自拡張として iconv による変換も可能ですので、ほとんどどんな文字コードでも使えると思ってもいいでしょう。 もちろん、日本語の文字コードとしてよく使われる Shift_JIS やら CP932 やらにも対応しています。

さて、 R6RS では標準入出力用のポートにどんな文字コード変換器が結びついているか、あるいは結びついていないかは未定義ですが、 Chez Scheme では UTF-8 を採用しています。 しかし、 Windows の標準出力に UTF-8 のテキストを流し込んでもコンソール上では化けてしまいます。 それを解決するために CP932 に変換してからの出力を試みている事例を見掛けました。

ですが、これでは CP932 の文字セットに含まれない文字の情報が消えてしまいます。

WindowsUnicode 対応に積極的で、文字列のやりとりが必要な API はほぼ全て Unicode 版が存在しますし、コンソール関連の API についてもまたそうであるのですが、互換性の制約からか、標準出力経由でコンソールに表示する文字は CP932 と解釈してしまうようになっているのであって、コンソール関連の API を直接使えば Unicode のままで利用できます。

そこで、 Chez SchemeForeign Interface を用いて Unicode の文字をコンソールに表示するライブラリを作ってみました。 カスタムポートとして定義しており、このポートに対して出力することでコンソールに表示されます。 標準出力をリダイレクトしていてもコンソールに出力します。

(library (console-port)
  (export open-console-output-port)
  (import (chezscheme))

  (define dummy (begin (load-shared-object "kernel32.dll") 1))

  (define-ftype handle void*)

  (define open-existing 3)

  (define create-file
    (foreign-procedure __stdcall "CreateFileW"
      (wstring unsigned-32 unsigned-32 void* unsigned-32 unsigned-32 void*)
      void*))

  (define file-share-write 2)

  (define generic-write #x40000000)
  
  (define (get-active-console-buffer)
    (create-file "CONOUT$" generic-write file-share-write 0 open-existing 0 0))

  (define write-console
    (foreign-procedure __stdcall "WriteConsoleW"
      (void* wstring unsigned-32 u32* void*)
      boolean))

  (define (open-console-output-port)
    (let ((output-handle (get-active-console-buffer))
          (vsize (make-bytevector 4)))
      (define (write-to-console string start count)
        (let ((str (substring string start (+ start count))))
          (write-console output-handle str count vsize 0)
          count))
      (make-custom-textual-output-port "console" write-to-console #f #f #f)))
  )

使い方としては、 open-console-output-port 手続きが返すポートに書き込むだけです。

(import (rnrs)
        (console-port))

(let ((port (open-console-output-port)))
  (display "あいうえお\nかきくけこ\n" port)
  (display "♘♞♙♕♟♝♜♗♛♚♖♔" port)
  (flush-output-port port)) ;; フラッシュを忘れずに!

f:id:SaitoAtsushi:20170110051212p:plain

CP932 の範囲外の文字もきちんと表示できています。 ただし、コンソールに適用しているフォントがグリフを持っていない場合もあるので Unicode にある文字を全て確実に表示できるというわけではありません。

Document ID: fb260a71b629c76209cdc797c43c9bdd

ゲーム「ラビリンスの彼方」の戦闘システムが良かった

ニンテンドー 3DS のゲーム「ラビリンスの彼方」を購入した。 店頭で見てなんとなく表題が良いと感じて買ったのであって、どのようなゲームなのかは箱に書いてある以上の情報を調べなかったのだが、実際やってみると良くできているのだ。 襲ってくる敵を倒しながら迷宮の塔をのぼっていくゲームで、戦闘システムはジャンケンを元にした三竦みの属性を上手く活用している。

ウェブ上で評判を調べてみると戦闘が面倒くさいという意見もある。 しかし、むしろこのゲームは戦闘以外に見るべきものはあまりない。 迷宮にはそれほど凝った仕組みもなくただ延々と進んでいくだけである。 物語は迷宮を進ませる理由付け程度にしかない。 強いて言えば「跳ね橋」「鍵」「隠し扉」といっや要素はある。 跳ね橋は一方向からしか降ろすことが出来ないが、一旦降ろすと降りっぱなしであり、拠点との行き来を少し短かい距離で出来たりする。 錠で守られた扉は別のどこかで鍵を入手してからでないと開けられない。 隠し扉は一見して壁に見える場所を調べると通路だったりする。 隠し扉はそれほど隠れていなくて、割とわかりやすい。

戦闘システムについてであるが、この作品世界では「グーテシウム」「チョキロム」「パーティリウム」の属性を敵味方全員が持っているのが特徴である。 明らかにジャンケンをモチーフにしている。 ここではわかりやすいように「グー」「チョキ」「パー」と呼ぶことにしよう。 グーがチョキに勝ち、チョキがパーに勝ち、パーがグーに勝つのはジャンケンと同じだ。 つまり、たとえばチョキの属性を持つ者にグーの属性を持つ者が攻撃すれば威力が倍増する。 同じ属性であれば普通程度にダメージを与えることができ、相手側の方が強い属性であれば半分のダメージしか与えることができない。

では、なるべく相手の弱点で攻撃すればいいのだろうかといえばそうではない。 弱点を攻撃した (あるいはされた) ときにはダメージ分のエネルギーがその場に放出される。 たとえばチョキの属性を持つ者がグーの属性を持つ者に攻撃するとグーの属性のエネルギーがその場に放出される。 放出されたエネルギーはその属性の者の攻撃ターンに吸収されてダメージが回復する。 つまり、せっかくダメージを与えてもその後の攻撃順序によっては取り戻されてしまうことが有り得るのだ。 逆にダメージを受けても取り返せる可能性がある。 つまり、攻撃順序を調整するのが重要なのだ。 更に言えばこのゲームでの攻撃順序は戦闘終了時の状態が次の先頭に持ち越される。 同じ敵が連続して現れても自分の仲間たちの攻撃順序が違ってしまうので同じようには出来ない。 常によく考えることを強制されるので戦闘が単調にならないようになっている。

では、攻撃順序の調整はどうやるのかというと、攻撃の強さで選択する。 弱い攻撃なら次に攻撃するまでの時間は短かく、強い攻撃なら長くなる。 キャラを鍛えるとより強い攻撃を選択できるようになるが、常にめいっぱい強い攻撃をすればよいというわけではないということだ。

ゲームの分類としては、公式ページにはRPGと書いてあるが、レベルを上げて物理で殴るということが通用しない思考力を問われる戦闘システムはパズルゲームとも言えるだろう。

Document ID: 7200f15d5fbe5311d23d741ce63e380f

Gauche-epub

電子書籍ファイルフォーマットのひとつである ePub の生成を Scheme 処理系 Gauche から行う拡張パッケージ Gauche-epub を公開した。

ePub は html 形式の文章やスタイルシートや画像にいくつかのメタ情報を併せて ZIP アーカイブ形式でまとめた形になっていて、 Gauche-epub は ZIP アーカイブを操作するために Gauche-zip-archive パッケージを利用するので事前にこちらも導入しておく必要がある。 現時点での開発版 (git リポジトリの HEAD) の Gauche を想定しているので、古い Gauche だと動かない部分があるかもしれない。

今の Gauche-epub はまだあまり使い込んでおらず、これからかなり変更を加える可能性がある。 故にドキュメントも用意できていない。 使い方を把握するには同梱している用例を見るのが手っ取り早いと思う。 以前に「文章構造」という表題で書いた構想を元にしている。

Gauche-epub はメタ情報の生成を自動化するものであって、与えられたコンテンツ、すなわち html が ePub として正しいかといったようなことはアプリケーション側で管理する必要があることには留意すべきだ。 アプリケーションが生成した ePubepubcheck といったようなバリデータで確認するのは良い習慣だと思う。

Document ID: 042b5647c17d408eb2a6524d353e7c3b

短縮記法の活用

Common Lisp でのリーダーマクロの興味深い利用例の紹介を見た。

#:g1: リーダーマクロでシンボルの略記をする

プログラミング言語 SchemeLISP 系言語といえども Common Lisp でいうところのリーダーマクロに相当するものはない。 しかし、いくつかの短縮記法は提供されていて、読み込みの段階で対応する名前に変換される。 R5RS や R7RS では以下のよっつがそうだ。

短縮記法 対応する名前
' quote
` quasiquote
, unquote
,@ unquote-splicing

それに加えて R6RS では以下の短縮記法も用意されている。

短縮記法 対応する名前
#' syntax
#` quasisyntax
#, unsyntax
#,@ unsyntax-splicing

これらは単なる短縮記法であるので、たとえば quote の意味を置換えてしまえば ' の意味も変えられる。 新しい記法を追加することは出来なくても、既存の記法の意味を変えることは出来るのだ。

私は以前に quasisyntax を置換える試みをしたことがある。

文字列補間 - 主題のない日記

使いどころを慎重に考えないと混乱の元になりそうだが、コードゴルフくらいには使えるかもしれない。

Document ID: 2882acdda16167cb2246cea8dd2b1ed0

文字列ポートを使おう

プログラミング言語 Scheme において少し気になる string-append の使い方の事例を見た。

これは、私が以前に append が気になるとして記事にした事例とよく似ている。 図を描くのが面倒なのであらためて解説はしないが、空間的にも速度的にも本来なら O(n) のオーダーで出来るはずの処理を O(n2) にしているという意味で同じである。 要するに無駄な中間状態を作っているのだ。 (注意:処理系が文字列の実装に rope などを使っている場合はそんなに差は生じない。 ここでは素朴な処理系を前提としているが、一見して非効率なコードに見えても処理系によっては効率的な場合もある。)

R5RS の範囲内で平均的に性能が良い方法はおそらく結合すべき文字列をリストにまとめてから一気に string-append で結合する方法だと思う。 (末尾再帰にするともっといいかもしれない。)

(define (string-join-with-infix-and-newline string-list delim)
  (apply string-append
         (if (null? string-list)
             '()
             (letrec ((a (lambda(x) (if (null? x) '("\n") (cons delim (b x)))))
                      (b (lambda(x) (cons (car x) (a (cdr x))))))
               (cons (car string-list) (a (cdr string-list)))))))

さて、ここからが本題の文字列ポートの紹介である。 もし処理系が文字列ポートをサポートしているならそれが使える。 文字列ポートは SRFI-6 (Basic String Ports) として提案され、 Script-Fu を含む多くの処理系で採用されている。 R6RS や R7RS では仕様自体に取り込まれた。 (R6RS の文字列ポートは SRFI-6 とは使い方が異なることに気をつけること。)

上述のコードを文字列ポートを使って書き換えるとこのようになる。

(define (string-join-with-infix-and-newline string-list delim)
  (if (null? string-list)
      ""
      (let ((port (open-output-string)))
        (display (car string-list) port)
        (for-each (lambda(x) (display delim port) (display x port))
                  (cdr string-list))
        (newline port)
        (get-output-string port))))

あたかもファイルに出力するのと同じような書き方で文字列ポートに蓄積して、最後に文字列として取り出すという操作で文字列を構築する。 I/O と共通の操作で文字列構築ができるというのはとても使い勝手がよいし、一般的には性能もよい。 Scheme で文字列の処理をするには文字列ポートはなくてはならないものだと私は感じている。

Document ID: 11a193e8d0f408bb591f09f17be3c315

フックポイントとしての総称関数

プログラミング言語 Scheme の仕様にはオブジェクト指向的な支援が含まれない。 しかし、独自の拡張として持っている場合もある。 あるいは後付けでオブジェクト指向的な機能を追加するライブラリもある。 オブジェクト指向というのも様々な種類があるが、 Scheme で使われることが多いのは Common Lisp に含まれるオブジェクトシステム (CLOS) を真似たものであることが多い。 もちろん前提となる言語が違うのであるから、 Scheme の、あるいは処理系の都合に合わせて改変が加えられてはいる。

Scheme 処理系のひとつである Gauche もまた CLOS 風のオブジェクトシステムを持っていて様々に活用されている。 Gauche を使っていて特に頻繁に使われる総称関数 (Scheme の用語では「関数」よりも「手続き」が使われるが、 CLOS の習慣に倣ってかドキュメントには「総称関数」という用語が用いられている) は ref だろうか。 ref は複数の要素をまとめるような型のオブジェクトに対して同じような使い勝手で適用でき、要素を抜き出すものだ。

(ref '(a b c d) 2)
(ref '#(e f g h) 3)
(ref "ijkl" 1)

このように ref はリスト、ベクタ、文字列に使える。 その他にもハッシュテーブルやツリーマップなどにも対応している。 必要であればメソッドを追加して新しい型に対応させることも出来る。

ref の場合は同じような意味を持つ手続きをひとつの名前にまとめたというだけのものだが、アプリケーションに機能を追加するためのフックポイントとしても活用できる。 たとえば先日に紹介した Gauche-zip-archive という拡張において現状では zip-add-entry だけは総称関数として提供していて、これはアーカイブにエントリを追加するにあたって前後に処理を追加したい場合を想定しているからだ。 例として、エントリの名前に必ず拡張子 .txt を追加するというような処理を入れたい場合にはこう書ける。

(use zip-archive)

(define-class <extended-zip> (<output-zip-archive>)
  ())

(define-method zip-add-entry
    ((archive <extended-zip>) (name <string>) (content <string>))
  (next-method archive (string-append name ".txt") content))

(let ((archive (make <extended-zip> :name "test.zip")))
  (zip-add-entry archive "entry1" "hoge-huga-hige")
  (zip-close archive))

このようなカスタマイズ方法は、 Gauche で書かれている Wiki システムである WiLiKi でも活用されている。 私はそれを見て Gauche-zip-archive にも導入してみた次第である。 現時点では ZIP フォーマットのごく基本的な部分にしか対応していないライブラリではあるが、メソッドの追加という形で ZIP フォーマットの様々な拡張に対応できる可能性を提供したつもりだ。 (自分ではやりたくないのでという本音。)

Document ID: 21bf69adf19ce4d9fa17f6c7ad405d68