<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://makiuchi-d.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="http://makiuchi-d.github.io/" rel="alternate" type="text/html" /><updated>2026-02-26T16:59:14+00:00</updated><id>http://makiuchi-d.github.io/feed.xml</id><title type="html">MakKi (makki_d)</title><entry><title type="html">乱数はどこから来るか―TinyGo RP2040の場合</title><link href="http://makiuchi-d.github.io/2025/12/07/tinygo-rand.ja.html" rel="alternate" type="text/html" title="乱数はどこから来るか―TinyGo RP2040の場合" /><published>2025-12-07T00:00:00+00:00</published><updated>2025-12-07T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2025/12/07/tinygo-rand.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2025/12/07/tinygo-rand.ja.html"><![CDATA[<p style="background-color:lightcyan;border-left:0.3em solid cyan;padding:0.5em">
<strong>ℹ️</strong>
この記事は <a href="https://qiita.com/advent-calendar/2025/tinygo">TinyGo Advent Calendar 2025</a> 7日目の記事です。
</p>

<p><a href="https://tinygo-keeb.org/">TinyGo Keeb Tour</a>で作成する
キーボード/マクロパッド「<a href="https://github.com/tinygo-keeb/workshop?tab=readme-ov-file#%E9%96%8B%E7%99%BA%E5%AF%BE%E8%B1%A1">zero-kb02</a>」には
<a href="https://www.raspberrypi.com/products/rp2040/">RP2040</a>というマイコンが載っていて、
<a href="https://tinygo.org/">TinyGo</a>を使ってGo言語で書いたプログラムを動かすことができます。
このキーボードで遊べる簡単なゲームを実装している時、<a href="https://pkg.go.dev/math/rand"><code class="language-plaintext highlighter-rouge">math/rand</code></a>のグローバルな乱数が毎回固定なことに気づきました。</p>

<p>Go言語では1.20以降、グローバルな乱数のシードは起動時にデフォルトでランダムに設定されるので、毎回違う乱数列になるはずです。
TinyGoでは多くの標準パッケージはGo本体のものをそのまま利用していて、<code class="language-plaintext highlighter-rouge">math/rand</code>もGo本体と同じコードが使われています。</p>

<p>ではなぜ毎回同じ乱数列になってしまうのか、乱数はどこからやって来るのか追いかけてみました。
なお、参照しているバージョンは次の通りです。</p>
<ul>
  <li>Go: v1.25.5</li>
  <li>TinyGo: v0.39.0</li>
</ul>

<h2 id="mathrandを読む"><code class="language-plaintext highlighter-rouge">math/rand</code>を読む</h2>

<p>グローバルな乱数を返す関数は何種類かありますが、内部では同じ乱数生成器（random number generator）が使われています。
たとえば<code class="language-plaintext highlighter-rouge">rand.Init()</code>は次のようになっています</p>

<p>▼<a href="https://cs.opensource.google/go/go/+/go1.25.5:src/math/rand/rand.go;l=448-449">src/math/rand/rand.go:448-449</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Int returns a non-negative pseudo-random int from the default [Source].</span>
<span class="k">func</span> <span class="n">Int</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span> <span class="k">return</span> <span class="n">globalRand</span><span class="p">()</span><span class="o">.</span><span class="n">Int</span><span class="p">()</span> <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">globalRand()</code>は<code class="language-plaintext highlighter-rouge">*rand.Rand</code>を返す関数で、
<code class="language-plaintext highlighter-rouge">atomic.Pointer[Rand]</code>を使って1回初期化した<code class="language-plaintext highlighter-rouge">*Rand</code>が使い回されています。</p>

<p>▼<a href="https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/math/rand/rand.go;l=319-350">src/math/rand/rand.go:319-350</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// globalRand returns the generator to use for the top-level convenience</span>
<span class="c">// functions.</span>
<span class="k">func</span> <span class="n">globalRand</span><span class="p">()</span> <span class="o">*</span><span class="n">Rand</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">r</span> <span class="o">:=</span> <span class="n">globalRandGenerator</span><span class="o">.</span><span class="n">Load</span><span class="p">();</span> <span class="n">r</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">r</span>
	<span class="p">}</span>

	<span class="c">// This is the first call. Initialize based on GODEBUG.</span>
	<span class="k">var</span> <span class="n">r</span> <span class="o">*</span><span class="n">Rand</span>
	<span class="k">if</span> <span class="n">randautoseed</span><span class="o">.</span><span class="n">Value</span><span class="p">()</span> <span class="o">==</span> <span class="s">"0"</span> <span class="p">{</span>
		<span class="n">randautoseed</span><span class="o">.</span><span class="n">IncNonDefault</span><span class="p">()</span>
		<span class="n">r</span> <span class="o">=</span> <span class="n">New</span><span class="p">(</span><span class="nb">new</span><span class="p">(</span><span class="n">lockedSource</span><span class="p">))</span>
		<span class="n">r</span><span class="o">.</span><span class="n">Seed</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">r</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">Rand</span><span class="p">{</span>
			<span class="n">src</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">runtimeSource</span><span class="p">{},</span>
			<span class="n">s64</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">runtimeSource</span><span class="p">{},</span>
		<span class="p">}</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="o">!</span><span class="n">globalRandGenerator</span><span class="o">.</span><span class="n">CompareAndSwap</span><span class="p">(</span><span class="no">nil</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span> <span class="p">{</span>
		<span class="c">// Two different goroutines called some top-level</span>
		<span class="c">// function at the same time. While the results in</span>
		<span class="c">// that case are unpredictable, if we just use r here,</span>
		<span class="c">// and we are using a seed, we will most likely return</span>
		<span class="c">// the same value for both calls. That doesn't seem ideal.</span>
		<span class="c">// Just use the first one to get in.</span>
		<span class="k">return</span> <span class="n">globalRandGenerator</span><span class="o">.</span><span class="n">Load</span><span class="p">()</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">r</span>
<span class="p">}</span>
</code></pre></div></div>

<p>若干長いですが、要するにこの<code class="language-plaintext highlighter-rouge">*Rand</code>が本体です。</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>		<span class="n">r</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">Rand</span><span class="p">{</span>
			<span class="n">src</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">runtimeSource</span><span class="p">{},</span>
			<span class="n">s64</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">runtimeSource</span><span class="p">{},</span>
		<span class="p">}</span>
</code></pre></div></div>

<p>そして、<code class="language-plaintext highlighter-rouge">*Rand</code>の<code class="language-plaintext highlighter-rouge">Int()</code>は次のようになっていて、<code class="language-plaintext highlighter-rouge">Rand.src.Int63()</code>を呼び出しています。
▼<a href="https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/math/rand/rand.go;l=95-116">src/rand/rand.go:95-116</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.</span>
<span class="k">func</span> <span class="p">(</span><span class="n">r</span> <span class="o">*</span><span class="n">Rand</span><span class="p">)</span> <span class="n">Int63</span><span class="p">()</span> <span class="kt">int64</span> <span class="p">{</span> <span class="k">return</span> <span class="n">r</span><span class="o">.</span><span class="n">src</span><span class="o">.</span><span class="n">Int63</span><span class="p">()</span> <span class="p">}</span>

<span class="p">(</span><span class="n">略</span><span class="p">)</span>

<span class="c">// Int returns a non-negative pseudo-random int.</span>
<span class="k">func</span> <span class="p">(</span><span class="n">r</span> <span class="o">*</span><span class="n">Rand</span><span class="p">)</span> <span class="n">Int</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span>
	<span class="n">u</span> <span class="o">:=</span> <span class="kt">uint</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Int63</span><span class="p">())</span>
	<span class="k">return</span> <span class="kt">int</span><span class="p">(</span><span class="n">u</span> <span class="o">&lt;&lt;</span> <span class="m">1</span> <span class="o">&gt;&gt;</span> <span class="m">1</span><span class="p">)</span> <span class="c">// clear sign bit if int == int32</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Rand.src</code>は<code class="language-plaintext highlighter-rouge">runtimeSource</code>型で、その<code class="language-plaintext highlighter-rouge">Int63()</code>は<code class="language-plaintext highlighter-rouge">runtime_rand()</code>を呼び出しています。</p>

<p>▼<a href="https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/math/rand/rand.go;l=362-364">src/math/rand/rand.go:362-364</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="o">*</span><span class="n">runtimeSource</span><span class="p">)</span> <span class="n">Int63</span><span class="p">()</span> <span class="kt">int64</span> <span class="p">{</span>
	<span class="k">return</span> <span class="kt">int64</span><span class="p">(</span><span class="n">runtime_rand</span><span class="p">()</span> <span class="o">&amp;</span> <span class="n">rngMask</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">runtime_rand()</code>(は次のように定義されていて、<code class="language-plaintext highlighter-rouge">go:linkname</code>で<code class="language-plaintext highlighter-rouge">runtime</code>パッケージの<code class="language-plaintext highlighter-rouge">rand()</code>が実体となっています。</p>

<p>▼<a href="https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/math/rand/rand.go;l=352-353">src/math/rand/rand.go:352-353</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">//go:linkname runtime_rand runtime.rand</span>
<span class="k">func</span> <span class="n">runtime_rand</span><span class="p">()</span> <span class="kt">uint64</span>
</code></pre></div></div>

<p>TinyGoは各マイコンボード用に<code class="language-plaintext highlighter-rouge">runtime</code>を独自実装しているので、どうやら<code class="language-plaintext highlighter-rouge">runtime</code>に乱数が固定になる原因があるようです。</p>

<h2 id="tinygoのruntimeを読む">TinyGoの<code class="language-plaintext highlighter-rouge">runtime</code>を読む</h2>

<p>TinyGoの<code class="language-plaintext highlighter-rouge">runtime.rand()</code>は<code class="language-plaintext highlighter-rouge">algorithm.go</code>に定義されています。
<code class="language-plaintext highlighter-rouge">hardwareRand()</code>を呼び出してみて、<code class="language-plaintext highlighter-rouge">!ok</code>なら<code class="language-plaintext highlighter-rouge">fastrand64()</code>にフォールバックするコードになっています。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/runtime/algorithm.go#L70-L81">tinygo/src/runtime/algorithm.go:71-81</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Function that's called from various packages starting with Go 1.22.</span>
<span class="k">func</span> <span class="n">rand</span><span class="p">()</span> <span class="kt">uint64</span> <span class="p">{</span>
	<span class="c">// Return a random number from hardware, falling back to software if</span>
	<span class="c">// unavailable.</span>
	<span class="n">n</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">hardwareRand</span><span class="p">()</span>
	<span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
		<span class="c">// Fallback to static random number.</span>
		<span class="c">// Not great, but we can't do much better than this.</span>
		<span class="n">n</span> <span class="o">=</span> <span class="n">fastrand64</span><span class="p">()</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">n</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hardwareRand()</code>の定義は複数あり、ターゲットごとに設定されたビルドタグで実装が切り替わるようになっています。
zero-kb02用にビルドする時のターゲットは<code class="language-plaintext highlighter-rouge">waveshare-rp2040-zero</code>なので、<code class="language-plaintext highlighter-rouge">tinygo info</code>コマンドでビルドタグを確認します。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>tinygo info <span class="nt">--target</span><span class="o">=</span>waveshare-rp2040-zero
LLVM triple:       thumbv6m-unknown-unknown-eabi
GOOS:              linux
GOARCH:            arm
build tags:        cortexm baremetal linux arm rp2040 rp waveshare_rp2040_zero tinygo purego osusergo math_big_pure_go gc.conservative scheduler.cores serial.usb
garbage collector: conservative
scheduler:         cores
</code></pre></div></div>

<p>このビルドタグに合致する<code class="language-plaintext highlighter-rouge">hardwareRand()</code>が実装されたファイルは<code class="language-plaintext highlighter-rouge">rand_norng.go</code>です。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/runtime/rand_norng.go"><code class="language-plaintext highlighter-rouge">tinygo/src/runtime/rand_norng.go</code></a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">//go:build baremetal &amp;&amp; !(nrf || (stm32 &amp;&amp; !(stm32f103 || stm32l0x1)) || (sam &amp;&amp; atsamd51) || (sam &amp;&amp; atsame5x) || esp32c3 || tkey || (tinygo.riscv32 &amp;&amp; virt))</span>

<span class="k">package</span> <span class="n">runtime</span>

<span class="k">func</span> <span class="n">hardwareRand</span><span class="p">()</span> <span class="p">(</span><span class="n">n</span> <span class="kt">uint64</span><span class="p">,</span> <span class="n">ok</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="no">false</span> <span class="c">// no RNG available</span>
<span class="p">}</span>
</code></pre></div></div>

<p>ここで固定値の<code class="language-plaintext highlighter-rouge">0, false</code>が返されるので、<code class="language-plaintext highlighter-rouge">runtime.rand()</code>では<code class="language-plaintext highlighter-rouge">fastrand64()</code>にフォールバックされることになります。
<code class="language-plaintext highlighter-rouge">fastrand64()</code>は<code class="language-plaintext highlighter-rouge">algorithm.go</code>に定義されています。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/runtime/algorithm.go#L44-L50">tinygo/src/runtime/algorithm.go:44-50</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// This function is used by hash/maphash.</span>
<span class="c">// This function isn't required anymore since Go 1.22, so should be removed once</span>
<span class="c">// that becomes the minimum requirement.</span>
<span class="k">func</span> <span class="n">fastrand64</span><span class="p">()</span> <span class="kt">uint64</span> <span class="p">{</span>
	<span class="n">xorshift64State</span> <span class="o">=</span> <span class="n">xorshiftMult64</span><span class="p">(</span><span class="n">xorshift64State</span><span class="p">)</span>
	<span class="k">return</span> <span class="n">xorshift64State</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Go本体では1.22以降、疑似乱数生成アルゴリズムとしてChaCha8が使われているのですが、
TinyGoはより軽量なXorshiftなんですね。</p>

<p><code class="language-plaintext highlighter-rouge">xorshift64State</code>の初期値がどうなっているかというと、<code class="language-plaintext highlighter-rouge">hardwareRand()</code>を使って初期化しています。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/runtime/algorithm.go#L26-L30">tinygo/src/runtime/algorithm.go:26-30</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">initRand</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">r</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">hardwareRand</span><span class="p">()</span>
	<span class="n">xorshift64State</span> <span class="o">=</span> <span class="kt">uint64</span><span class="p">(</span><span class="n">r</span> <span class="o">|</span> <span class="m">1</span><span class="p">)</span> <span class="c">// protect against 0</span>
	<span class="n">xorshift32State</span> <span class="o">=</span> <span class="kt">uint32</span><span class="p">(</span><span class="n">xorshift64State</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>そうです。
ここで返された固定値<code class="language-plaintext highlighter-rouge">0</code>を使って毎回<code class="language-plaintext highlighter-rouge">1</code>が設定されるので、いつでも同じ乱数列になっていたのでした。</p>

<p><strong>12/20 追記</strong>:
この部分間違っていました。v0.39.0までは、RP2040では<code class="language-plaintext highlighter-rouge">initRand()</code>は呼ばれていませんでした。
変数の宣言の初期値<code class="language-plaintext highlighter-rouge">1</code>がそのまま使われていいました。</p>

<h2 id="tinygoのcryptorandを読む">TinyGoの<code class="language-plaintext highlighter-rouge">crypto/rand</code>を読む</h2>

<p><code class="language-plaintext highlighter-rouge">math/rand</code>の乱数が毎回同じ乱数列になる謎は解けました。
ところで、Goの乱数生成器は<code class="language-plaintext highlighter-rouge">math/rand</code>の他に<code class="language-plaintext highlighter-rouge">crypto/rand</code>もあります。
TinyGoは<code class="language-plaintext highlighter-rouge">crypto</code>パッケージを独自に実装しているので、こちらも見てみます。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/crypto/rand/rand.go">tinygo/src/crypto/rand/rand.go</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Copyright 2010 The Go Authors. All rights reserved.</span>
<span class="c">// Use of this source code is governed by a BSD-style</span>
<span class="c">// license that can be found in the LICENSE file.</span>

<span class="c">// Package rand implements a cryptographically secure</span>
<span class="c">// random number generator.</span>
<span class="k">package</span> <span class="n">rand</span>

<span class="k">import</span> <span class="s">"io"</span>

<span class="c">// Reader is a global, shared instance of a cryptographically</span>
<span class="c">// secure random number generator.</span>
<span class="k">var</span> <span class="n">Reader</span> <span class="n">io</span><span class="o">.</span><span class="n">Reader</span>

<span class="c">// Read is a helper function that calls Reader.Read using io.ReadFull.</span>
<span class="c">// On return, n == len(b) if and only if err == nil.</span>
<span class="k">func</span> <span class="n">Read</span><span class="p">(</span><span class="n">b</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">Reader</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="s">"no rng"</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadFull</span><span class="p">(</span><span class="n">Reader</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Read()</code>は<code class="language-plaintext highlighter-rouge">crypto.Reader</code>から読み取るだけの実装なのですが、RP2040向けのコードではこの<code class="language-plaintext highlighter-rouge">Reader</code>を初期化するコードがありません！
このため、最初の<code class="language-plaintext highlighter-rouge">nil</code>チェックで<code class="language-plaintext highlighter-rouge">panic</code>します。</p>

<h2 id="rp2040の乱数生成器">RP2040の乱数生成器</h2>

<p>RP2040は乱数生成器を持っていて、TinyGoからも<code class="language-plaintext highlighter-rouge">machine</code>パッケージの<code class="language-plaintext highlighter-rouge">GetRNG()</code>を通して利用できます。</p>

<p>▼<a href="https://github.com/tinygo-org/tinygo/blob/v0.39.0/src/machine/machine_rp2_rng.go#L14-L41">tinygo/src/machine/machine_rp2_rng.go:14-41</a></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// GetRNG returns 32 bits of semi-random data based on ring oscillator.</span>
<span class="c">//</span>
<span class="c">// Unlike some other implementations of GetRNG, these random numbers are not</span>
<span class="c">// cryptographically secure and must not be used for cryptographic operations</span>
<span class="c">// (nonces, etc).</span>
<span class="k">func</span> <span class="n">GetRNG</span><span class="p">()</span> <span class="p">(</span><span class="kt">uint32</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">val</span> <span class="kt">uint32</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">4</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">val</span> <span class="o">=</span> <span class="p">(</span><span class="n">val</span> <span class="o">&lt;&lt;</span> <span class="m">8</span><span class="p">)</span> <span class="o">|</span> <span class="kt">uint32</span><span class="p">(</span><span class="n">roscRandByte</span><span class="p">())</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">val</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="k">var</span> <span class="n">randomByte</span> <span class="kt">uint8</span>

<span class="k">func</span> <span class="n">roscRandByte</span><span class="p">()</span> <span class="kt">uint8</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">poly</span> <span class="kt">uint8</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numberOfCycles</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">randomByte</span><span class="o">&amp;</span><span class="m">0x80</span> <span class="o">!=</span> <span class="m">0</span> <span class="p">{</span>
			<span class="n">poly</span> <span class="o">=</span> <span class="m">0x35</span>
		<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
			<span class="n">poly</span> <span class="o">=</span> <span class="m">0</span>
		<span class="p">}</span>
		<span class="n">randomByte</span> <span class="o">=</span> <span class="p">((</span><span class="n">randomByte</span> <span class="o">&lt;&lt;</span> <span class="m">1</span><span class="p">)</span> <span class="o">|</span> <span class="kt">uint8</span><span class="p">(</span><span class="n">rp</span><span class="o">.</span><span class="n">ROSC</span><span class="o">.</span><span class="n">GetRANDOMBIT</span><span class="p">())</span> <span class="o">^</span> <span class="n">poly</span><span class="p">)</span>
		<span class="c">// TODO: delay a little because the random bit is a little slow</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">randomByte</span>
<span class="p">}</span>
</code></pre></div></div>

<p><a href="https://pip-assets.raspberrypi.com/categories/814-rp2040/documents/RP-008371-DS-1-rp2040-datasheet.pdf?disposition=inline">RP2040のデータシート</a>にあるとおり、
ROSC（リング・オシレータ）の<code class="language-plaintext highlighter-rouge">RANDOMBIT</code>レジスタから1ビットずつ取り出す実装になっています。</p>

<p>今のところはこの関数を使えば、毎回異なる乱数列を取り出すことができます。
ただし、この乱数生成器は暗号論的にセキュアではないので、利用用途には注意が必要です。</p>

<h2 id="まとめ">まとめ</h2>

<p>RP2040で毎回同じ乱数列になってしまう原因を求めて、TinyGoの乱数生成器のコードを追いかけてきました。
結論として、<code class="language-plaintext highlighter-rouge">runtime/rand_norng.go</code>で定義されている<code class="language-plaintext highlighter-rouge">hardwareRand()</code>が固定値<code class="language-plaintext highlighter-rouge">0</code>を返し、それによって乱数の初期値も固定されているのが原因でした。</p>

<p>一方で、RP2040の持つ乱数生成器は<code class="language-plaintext highlighter-rouge">machine.GetRNG()</code>で利用可能で、毎回異なる値が取得できます。</p>

<p>実は<code class="language-plaintext highlighter-rouge">runtime/rand_hwrng.go</code>には、この<code class="language-plaintext highlighter-rouge">machine.GetRNG()</code>を使った<code class="language-plaintext highlighter-rouge">hardwareRand()</code>が実装されています。
なので、ビルドタグを調整するだけで<code class="language-plaintext highlighter-rouge">math/rand</code>も毎回異なる乱数列にできたりします。
これについて近々Pull Requestを出そうと思っています。</p>

<p><strong>12/20 追記</strong>:
無事Pull Requestが取り込まれ、v0.40.1がリリースされました。
RP2040でも<code class="language-plaintext highlighter-rouge">hardwareRand()</code>が<code class="language-plaintext highlighter-rouge">machine.GetRNG()</code>を呼び出す形になりました。</p>

<h2 id="引用ソースコードライセンス">引用ソースコードライセンス</h2>

<h3 id="go">Go</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Copyright 2009 The Go Authors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</code></pre></div></div>

<h3 id="tinygo">TinyGo</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved.

TinyGo includes portions of the Go standard library.
Copyright (c) 2009-2024 The Go Authors. All rights reserved.

TinyGo includes portions of LLVM, which is under the Apache License v2.0 with
LLVM Exceptions. See https://llvm.org/LICENSE.txt for license information.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</code></pre></div></div>]]></content><author><name></name></author><summary type="html"><![CDATA[ℹ️ この記事は TinyGo Advent Calendar 2025 7日目の記事です。]]></summary></entry><entry><title type="html">GopherJSからWebAssemblyへ: Go-TypeScript連携の再構築 (KLabTechBook Vol. 15)</title><link href="http://makiuchi-d.github.io/2025/11/15/klabtechbook15-gopherjs-wasm.ja.html" rel="alternate" type="text/html" title="GopherJSからWebAssemblyへ: Go-TypeScript連携の再構築 (KLabTechBook Vol. 15)" /><published>2025-11-15T00:00:00+00:00</published><updated>2025-11-15T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2025/11/15/klabtechbook15-gopherjs-wasm.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2025/11/15/klabtechbook15-gopherjs-wasm.ja.html"><![CDATA[<p>この記事は2024年11月2日から開催された<a href="https://techbookfest.org/event/tbf18">技術書典18</a>にて頒布した「<a href="https://techbookfest.org/product/xmtJPdPuamKDgrnmkek9pn">KLabTechBook Vol. 15</a>」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf19">技術書典19</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/aVbpmUVUwehW3Mym5rYrzN">KLabTechBook Vol.16</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2025/tbf19.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
あわせてごらんください。</p>

<p><a href="https://techbookfest.org/product/aVbpmUVUwehW3Mym5rYrzN"><img src="/images/2025-11-15/ktbv16.png" width="40%" alt="KLabTechBook Vol.16" /></a></p>

<hr />

<p>KLabのリアルタイム通信システム「<a href="https://github.com/KLab/wsnet2">WSNet2</a>」はGo言語で開発していますが、
付属の簡易ダッシュボードはTypeScript（TS）で実装しています。
このTSコードからGoで実装した機能を利用するために、<a href="https://github.com/gopherjs/gopherjs"><strong>GopherJS</strong></a>を利用していました。</p>

<p>この章では、このGopherJSで直面した問題と <strong>WebAssembly（Wasm）</strong> に置き換えることになった経緯、そして具体的な移行作業について紹介します。</p>

<h2 id="ことの発端">ことの発端</h2>

<h3 id="wsnet2の技術構成">WSNet2の技術構成</h3>

<p>KLabではオンライン対戦や協力プレイのためのリアルタイム通信システム「WSNet2」を開発・運用しています。
WSNet2のサーバーは、大量の同時接続を効率的に処理するため、平行処理が得意なGo言語で実装しています。</p>

<p>WSNet2には、部屋の情報などを閲覧するための簡易ダッシュボードも付属しており、
こちらはフロントエンド・バックエンドともにTypeScript（TS）で開発しています。
ここで一つ課題がありました。
WSNet2では部屋の情報をネットワークで送受信するために独自のシリアライズフォーマットを採用しており<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>、
ダッシュボードでこの情報を表示するためにはデシリアライズ処理が必要です。</p>

<p>このデシリアライザはWSNet2サーバー用のGoの実装がすでに存在します。
そこで、同じロジックをTSで再実装するのではなく、<strong>GopherJS</strong> を利用することにしました。
GopherJSはGoのコードをJavaScript（JS）にトランスパイルするツールです。
これを使うことで、Goで記述されたデシリアライザをダッシュボードのバックエンドのTSコードから利用できるようにしました。</p>

<h3 id="gopherjsとgoのバージョン問題">GopherJSとGoのバージョン問題</h3>

<p>このように便利に使っていたGopherJSでしたが、ある時、問題に直面しました。
WSNet2のサーバーで利用しているライブラリとGoコンパイラをアップデートしたところ、
ダッシュボード側でのGopherJSによるトランスパイルが失敗するようになってしまいました。</p>

<p>GopherJSがサポートするGoのバージョンは厳密に決まっています。
このとき利用していたGopherJSは「1.19.0 beta1 for Go 1.19.13」でした。
しかし、アップデートした一部のライブラリがGo 1.20以降の新しいバージョンを要求していたため、
GopherJSによるトランスパイルができなくなってしまいました。</p>

<p>このままでは、WSNet2サーバー自体の更新も滞ってしまいます。
デシリアライザをTSで再実装することも考えましたが、
ちょうどその頃、Goの公式コンパイラがWasmのサポートを強化しているという話を思い出しました。
WasmであればJSから利用できるので、Goで実装したデシリアライザをWasmにコンパイルすれば、TSからも直接呼び出すことができるはずです。
ということで、これまでGopherJSが担っていた役割を、このGo公式のWasm機能で置き換えることにしました。</p>

<h2 id="goのwasmについて">GoのWasmについて</h2>

<h3 id="wasmとは">Wasmとは</h3>

<p>Wasmは、ブラウザ上でプログラムを高速に実行するためのバイナリフォーマットで、JSを補完するものとして開発されました。
現在ではブラウザだけに留まらず、Node.jsのようなJSランタイムやwasmtimeといったネイティブWasmランタイムなどでも利用できます。</p>

<p>また、Wasmのフォーマットは標準化されており、C/C++、Rust、Goなど多様なプログラミング言語からコンパイルできます。
さらに、特定のCPUやOSに依存せず動作することから、ポータブルなアプリケーションフォーマットとしても注目されつつあります。</p>

<h3 id="wasmのビルドjstsからの利用">Wasmのビルド、JS/TSからの利用</h3>

<p>まず、Goのコード<code class="language-plaintext highlighter-rouge">hello.go</code>を用意します（リスト1）。</p>

<p>▼リスト1: hello.go</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="s">"syscall/js"</span>

<span class="c">// hello : JSに公開する関数</span>
<span class="k">func</span> <span class="n">hello</span><span class="p">(</span><span class="n">this</span> <span class="n">js</span><span class="o">.</span><span class="n">Value</span><span class="p">,</span> <span class="n">args</span> <span class="p">[]</span><span class="n">js</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span> <span class="n">any</span> <span class="p">{</span>
	<span class="k">return</span> <span class="s">"Hello, "</span> <span class="o">+</span> <span class="n">args</span><span class="p">[</span><span class="m">0</span><span class="p">]</span><span class="o">.</span><span class="n">String</span><span class="p">()</span> <span class="o">+</span> <span class="s">"!"</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="c">// JSのglobalThisにhello関数をセット</span>
	<span class="n">js</span><span class="o">.</span><span class="n">Global</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"hello"</span><span class="p">,</span> <span class="n">js</span><span class="o">.</span><span class="n">FuncOf</span><span class="p">(</span><span class="n">hello</span><span class="p">))</span>

	<span class="o">&lt;-</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{})(</span><span class="no">nil</span><span class="p">)</span> <span class="c">// main関数を終了させない</span>
<span class="p">}</span>
</code></pre></div></div>

<p>このコードでは、<code class="language-plaintext highlighter-rouge">hello</code>関数をJSから呼び出せるように<code class="language-plaintext highlighter-rouge">main</code>関数内で<code class="language-plaintext highlighter-rouge">globalThis</code>オブジェクトにセットしています。
<code class="language-plaintext highlighter-rouge">main</code>関数が終了するとエクスポートした関数も利用できなくなるため、nilチャネルを使って永久にブロックしています。</p>

<p>Go 1.24からは<code class="language-plaintext highlighter-rouge">go:wasmexport</code>ディレクティブによっても関数を公開できるようになりました。
しかし引数や戻り値で利用できる型が限られており、WSNet2のデシリアライザのような複雑なデータ構造を扱うには適さないため、
ここでは従来の<code class="language-plaintext highlighter-rouge">js.FuncOf</code>を使う方法のみ解説します。
興味のある方はぜひ調べてみてください。</p>

<p>GoでWasmをビルドするには、環境変数でターゲットのOSを<code class="language-plaintext highlighter-rouge">js</code>、アーキテクチャを<code class="language-plaintext highlighter-rouge">wasm</code>のように指定します。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GOOS</span><span class="o">=</span>js <span class="nv">GOARCH</span><span class="o">=</span>wasm go build <span class="nt">-o</span> hello.wasm hello.go
</code></pre></div></div>

<p>このコマンドでは<code class="language-plaintext highlighter-rouge">hello.wasm</code>という名前でWasmのバイナリを出力しています。
これをNode.jsから利用するには、WebAssembly APIとGoが提供する<code class="language-plaintext highlighter-rouge">wasm_exec.js</code><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>を用いてロード・実行します（リスト2）。</p>

<p>▼リスト2: hello.js</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">./wasm_exec.js</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:fs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:path</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// wasm_exec.jsで提供されるGoランタイムなどの初期化</span>
<span class="kd">const</span> <span class="nx">go</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Go</span><span class="p">();</span>

<span class="c1">// hello.wasmをロード</span>
<span class="kd">const</span> <span class="nx">dir</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">dirname</span><span class="p">(</span><span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">).</span><span class="nx">pathname</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">wasm</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">WebAssembly</span><span class="p">.</span><span class="nx">instantiate</span><span class="p">(</span>
    <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="dl">"</span><span class="s2">hello.wasm</span><span class="dl">"</span><span class="p">)),</span> <span class="nx">go</span><span class="p">.</span><span class="nx">importObject</span><span class="p">);</span>

<span class="c1">// hello.wasm内のmain関数を実行</span>
<span class="nx">go</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">wasm</span><span class="p">.</span><span class="nx">instance</span><span class="p">);</span>

<span class="c1">// hello.wasmでglobalThisにセットしたhello関数を呼び出す</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">globalThis</span><span class="p">.</span><span class="nx">hello</span><span class="p">(</span><span class="dl">"</span><span class="s2">world</span><span class="dl">"</span><span class="p">));</span>
</code></pre></div></div>

<p>この<code class="language-plaintext highlighter-rouge">hello.js</code>と先程ビルドした<code class="language-plaintext highlighter-rouge">hello.wasm</code>、Goのコンパイラに付属している<code class="language-plaintext highlighter-rouge">wasm_exec.js</code>を同じディレクトリに置き、
Node.js<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>で実行すると次のようにhello関数の実行結果が得られます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node hello.js
Hello, world!
</code></pre></div></div>

<p>このように、GoでWasmバイナリをビルドし、wasm_exec.jsとWebAssembly APIを利用して、JSやTSからGoで実装した関数を呼び出せます。</p>

<h3 id="gopherjsとの違い">GopherJSとの違い</h3>

<p>GopherJSもWasmも、GoのコードをJS/TS環境で実行するという目的は同様ですが、そのアプローチと特性は大きく異なります。</p>

<h4 id="トランスパイル-vs-コンパイル">トランスパイル vs コンパイル</h4>

<p>GopherJSはGoのソースコードを等価なJSコードにトランスパイルします。つまり、最終的に実行されるのはJSのコードです。
一方Wasmでは、GoのソースコードはWebAssemblyバイナリにコンパイルされます。実行時には実行環境のWasmエンジンがこのバイナリを解釈・実行します。</p>

<p>どちらの場合もGoのランタイム実装やそれをエミュレートするコードが含まれるため、ファイルサイズはある程度大きくなります。
ただ、Wasmの場合はコンパイルオプションによる最適化やTinyGoのような小さなバイナリを生成できるコンパイラを使うことでサイズを削減できる余地があります。</p>

<p>またパフォーマンス面では、計算の効率だけであれば事前に最適化されやすいWasmのほうが有利になる傾向があります。
しかしWasmでは、JSとWasm間でデータをやり取りする際に、境界をまたぐためのコンテキストスイッチや値の変換のオーバーヘッドが生じます。
そのため、GoとJSの間で頻繁にやりとりが発生する場合は、境界を超える必要のないGopherJSのほうが有利になることもあります。</p>

<h4 id="goとjsの連携">GoとJSの連携</h4>

<p>GopherJSでは、GoのコードからのJSのオブジェクトや関数へのアクセスは<a href="https://pkg.go.dev/github.com/gopherjs/gopherjs/js">GopherJS独自の<code class="language-plaintext highlighter-rouge">js</code>パッケージ</a>を通して行います。
また、プリミティブな型だけでなくGoの<code class="language-plaintext highlighter-rouge">struct</code>型、<code class="language-plaintext highlighter-rouge">map</code>や<code class="language-plaintext highlighter-rouge">slice</code>といった複合型も、
JSのオブジェクトや配列と相互変換されるように見せてくれるため、そのまま扱えます。
加えて、ユーザー定義の<code class="language-plaintext highlighter-rouge">struct</code>とメソッドも<code class="language-plaintext highlighter-rouge">js.MakeWrapper</code>関数を利用することで簡単にJSからも利用できます。</p>

<p>一方Wasmでは、標準の<code class="language-plaintext highlighter-rouge">syscall/js</code>パッケージを通してJSのグローバルオブジェクトや関数にアクセスします。
JSの値はGo側では<code class="language-plaintext highlighter-rouge">js.Value</code>型として受け渡されるため、利用するときに開発者は明示的に変換処理を呼び出すことになります。
特に<code class="language-plaintext highlighter-rouge">slice</code>や<code class="language-plaintext highlighter-rouge">map</code>、ユーザー定義型の場合は、明示的にコピーや<code class="language-plaintext highlighter-rouge">map[string]any</code>型への詰め替えを行い、JSのオブジェクトに変換するような実装が必要です。</p>

<h4 id="モジュールシステム">モジュールシステム</h4>

<p>JSからGoの実装を利用するには、GopherJSの生成したJSファイルを<code class="language-plaintext highlighter-rouge">require</code>したり、WasmファイルをWebAssembly APIでロードする必要があります。</p>

<p>JSで他のJSファイルをモジュールとして読み込む方法は、Node.jsで伝統的に使われてきた <strong>CommonJS（CJS）</strong> と、
新たにブラウザでの利用も考慮して非同期処理に対応し標準規格としても策定されている <strong>ES Modules（ESM）</strong> の2つの形式があります。</p>

<p>GopherJSは元々Node.jsをターゲットにしていたこともありCJS形式です。
CJSの<code class="language-plaintext highlighter-rouge">require</code>は同期的な処理が前提となっており、GopherJSの出力したJSファイルも読み込んだらすぐに使えるようになります。</p>

<p>一方で、WasmをロードするためのWebAssembly APIは基本的に非同期処理として提供されています。
GopherJSのときと同じような使い方、つまりWasmをロードするJSファイルを読み込んですぐ使えるようにするためには、
トップレベルでawaitを使ってロードが完了するのを待つのが簡単な方法です。
トップレベルawaitを使うためにはESMにする必要があります。</p>

<p>ここでひとつ問題があります。
ESM側でCJSのモジュールを読み込むのは簡単ですが、逆にCJS側でESMのモジュールを読み込むのは困難です。
今回GopherJSからWasmに移行したいダッシュボードはCJSで作られていました。
このため、WasmをロードするモジュールをESMにしたいがために、プロジェクト全体をESMにしなければなりませんでした。
この変更作業の詳細についても、後ほど紹介します。</p>

<h2 id="gopherjsからwasmへの移行">GopherJSからWasmへの移行</h2>

<p>前置きが長くなりましたが、ここからはGopherJSからWasmへの移行にあたって、具体的にどのような変更を行ったのか紹介します。
実際のWSNet2リポジトリのPullRequestもあわせてご覧ください。</p>

<ul>
  <li><a href="https://github.com/KLab/wsnet2/pull/91">https://github.com/KLab/wsnet2/pull/91</a></li>
</ul>

<h3 id="goのコードの変更">Goのコードの変更</h3>

<p>GopherJSやWasmでビルドするためのGoのコードでは、
WSNet2のダッシュボードで利用する<code class="language-plaintext highlighter-rouge">binary.UnmarshalRecursive</code>関数をJS側にエクスポートします。
この関数の型はバイト列を受け取り、ネストされたオブジェクトにデシリアライズするものです。</p>

<h4 id="gopherjs向けの実装">GopherJS向けの実装</h4>

<p>GopherJSでは関数の引数や戻り値はシンプルな<code class="language-plaintext highlighter-rouge">struct</code>や文字列キーの<code class="language-plaintext highlighter-rouge">map</code>であれば、
それらがネストされていてもそのまま対応する形のJSのオブジェクトに変換されます。
このため、Node.jsで利用できるようにするにはリスト3のように<code class="language-plaintext highlighter-rouge">main</code>関数の中で
<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数をCJSのモジュールとしてエクスポートするだけでよく、非常に簡単です。</p>

<p>▼リスト3: GopherJSのためのGoの実装（main.go）</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"github.com/gopherjs/gopherjs/js"</span>
	<span class="s">"wsnet2/binary"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ex</span> <span class="o">:=</span> <span class="n">js</span><span class="o">.</span><span class="n">Module</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"exports"</span><span class="p">)</span>
	<span class="n">ex</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"UnmarshalRecursive"</span><span class="p">,</span> <span class="n">binary</span><span class="o">.</span><span class="n">UnmarshalRecursive</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>これをGopherJSで次のようにビルドし、生成された<code class="language-plaintext highlighter-rouge">binary.js</code>をダッシュボードのソースの中にコピーして利用します。
GopherJSの対応するGoよりも新しいGoを使用していることが普通でしょうから、
<code class="language-plaintext highlighter-rouge">GOPHERJS_GOROOT</code>環境変数で対応するバージョンのパスを指定する必要があります。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GOPHERJS_GOROOT</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>go1.19.13 <span class="nb">env </span>GOROOT<span class="si">)</span><span class="s2">"</span> gopherjs build <span class="nt">-o</span> binary.js main.go
</code></pre></div></div>

<h4 id="wasm向けの実装">Wasm向けの実装</h4>

<p>WasmでJSに公開できる関数の引数は<code class="language-plaintext highlighter-rouge">js.Value</code>型なので、
値を取り出し適切な型に変換してから<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>に渡すラッパー関数を定義して、こちらを公開します。
また戻り値は<code class="language-plaintext highlighter-rouge">any</code>型ですが、実際に返せるのはプリミティブな型やそれらを含む<code class="language-plaintext highlighter-rouge">map</code>や<code class="language-plaintext highlighter-rouge">slice</code>などに限られます。
独自型を含むような値を直接返すことができません。</p>

<p>一方、<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数でデシリアライズした値は独自型を含むネストした形になっています。
そのような値をJS側に返すには、再帰的に一つ一つ<code class="language-plaintext highlighter-rouge">map</code>に詰め直すことが必要な場面です。
ただ幸い、この値はもともとJSONへシリアライズできるような実装になっていました。
このため、ラッパー関数では値をまるごとJSONに変換して文字列として返し、JS側でデコードして独自型を含む値に戻すようにしました。</p>

<p>▼リスト4: WasmのためのGoの実装（main.go）</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"encoding/json"</span>
	<span class="s">"syscall/js"</span>
	<span class="s">"wsnet2/binary"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">js</span><span class="o">.</span><span class="n">Global</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"binary"</span><span class="p">,</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
		<span class="s">"UnmarshalRecursive"</span><span class="o">:</span> <span class="n">js</span><span class="o">.</span><span class="n">FuncOf</span><span class="p">(</span><span class="n">unmarshalRecursive</span><span class="p">),</span>
	<span class="p">})</span>
	<span class="o">&lt;-</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{})(</span><span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// unmarshalRecursive unmarshals binary formatted custom props.</span>
<span class="c">// binary.UnmarshalRecursive(arg number[]): { val: string, err: string }</span>
<span class="k">func</span> <span class="n">unmarshalRecursive</span><span class="p">(</span><span class="n">this</span> <span class="n">js</span><span class="o">.</span><span class="n">Value</span><span class="p">,</span> <span class="n">args</span> <span class="p">[]</span><span class="n">js</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span> <span class="p">(</span><span class="n">ret</span> <span class="n">any</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span> <span class="c">// panicしたとき、errorを取り出してretに詰めて返す</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="nb">recover</span><span class="p">();</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">ret</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
				<span class="s">"val"</span><span class="o">:</span> <span class="s">""</span><span class="p">,</span>
				<span class="s">"err"</span><span class="o">:</span> <span class="s">"UnmarshalRecursive: "</span> <span class="o">+</span>
					<span class="n">err</span><span class="o">.</span><span class="p">(</span><span class="kt">error</span><span class="p">)</span><span class="o">.</span><span class="n">Error</span><span class="p">(),</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}()</span>

	<span class="c">// 引数を[]byteに詰め直す</span>
	<span class="n">arg</span> <span class="o">:=</span> <span class="n">args</span><span class="p">[</span><span class="m">0</span><span class="p">]</span>
	<span class="nb">len</span> <span class="o">:=</span> <span class="n">arg</span><span class="o">.</span><span class="n">Length</span><span class="p">()</span>
	<span class="n">b</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="nb">len</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="nb">len</span> <span class="p">{</span>
		<span class="n">v</span> <span class="o">:=</span> <span class="n">arg</span><span class="o">.</span><span class="n">Index</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="o">.</span><span class="n">Int</span><span class="p">()</span> <span class="c">// can be panic</span>
		<span class="k">if</span> <span class="n">v</span> <span class="o">&gt;</span> <span class="m">255</span> <span class="p">{</span>
			<span class="nb">panic</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"arg[%v]=%v &gt; 255"</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">v</span><span class="p">))</span>
		<span class="p">}</span>
		<span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="kt">byte</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// デシリアライズ処理本体</span>
	<span class="n">v</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">binary</span><span class="o">.</span><span class="n">UnmarshalRecursive</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>


	<span class="c">// JSONに変換</span>
	<span class="n">u</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// 空のerrorと合わせて返す</span>
	<span class="k">return</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
		<span class="s">"val"</span><span class="o">:</span> <span class="kt">string</span><span class="p">(</span><span class="n">u</span><span class="p">),</span>
		<span class="s">"err"</span><span class="o">:</span> <span class="s">""</span><span class="p">,</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">main</code>関数では、グローバルオブジェクトに<code class="language-plaintext highlighter-rouge">binary</code>というオブジェクトをセットし、
その中に公開したい関数を詰め込むようにしました。
もし公開したい関数が増えたとしても衝突のリスクを軽減できます。</p>

<p>次に定義している<code class="language-plaintext highlighter-rouge">unmarshalRecursive</code>関数が、型変換などを担うラッパー関数です。
<code class="language-plaintext highlighter-rouge">js.Value</code>型の引数から、配列の各要素の数値を取り出して<code class="language-plaintext highlighter-rouge">[]byte</code>に詰め直しています。
実は標準ライブラリの<code class="language-plaintext highlighter-rouge">js.CopyBytesToGo</code>関数を使えばよかったことに後から気づきましたが、処理自体は同等のものです。</p>

<p>引数を詰め直したら<code class="language-plaintext highlighter-rouge">binary.UnmarshalRecursive</code>関数でデシリアライズし、JSONに変換して返します。
これらの処理ではエラーが発生しうるので、戻り値にはデシリアライズ結果とともにエラーも文字列として含めておき、呼び出し側でハンドリングできるようにしました。</p>

<p>この<code class="language-plaintext highlighter-rouge">main.go</code>を次のように<code class="language-plaintext highlighter-rouge">binary.wasm</code>にビルドし、<code class="language-plaintext highlighter-rouge">wasm_exec.js</code>と一緒にダッシュボードのソースの中にコピーして利用します。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GOOS</span><span class="o">=</span>js <span class="nv">GOARCH</span><span class="o">=</span>wasm go build <span class="nt">-o</span> binary.wasm main.go
</code></pre></div></div>

<h3 id="tsコードの改修">TSコードの改修</h3>

<h4 id="gopherjs向けの実装-1">GopherJS向けの実装</h4>

<p>GopherJSの生成したJSはそのままCJSのモジュールとしてJS/TSから<code class="language-plaintext highlighter-rouge">require</code>でき、
<code class="language-plaintext highlighter-rouge">export</code>された<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数をそのまま呼べるようになります。</p>

<p>▼リスト5: JS/TSからの利用部分の抜粋</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GopherJSで生成したJSを読み込む</span>
<span class="k">import</span> <span class="nx">binary</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">../plugins/binary.js</span><span class="dl">"</span><span class="p">);</span>

<span class="p">...</span>

<span class="nx">binary</span><span class="p">.</span><span class="nx">UnmarshalRecursive</span><span class="p">(</span><span class="nx">room</span><span class="p">.</span><span class="nx">getPublicProps_asU8</span><span class="p">());</span>
</code></pre></div></div>

<p>生成された<code class="language-plaintext highlighter-rouge">binary.js</code>に対するTS用の型定義はリスト6のように書きます。
<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>はデシリアライズされたオブジェクトと<code class="language-plaintext highlighter-rouge">error</code>の2つの値を返す関数なので、
2値をまとめた<code class="language-plaintext highlighter-rouge">Unmarshal</code>型を戻り値の型として定義しています。</p>

<p>▼リスト6: GopherJS生成JS用の型定義（binary.d.ts）</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">declare</span> <span class="nx">namespace</span> <span class="nx">binary</span> <span class="p">{</span>
  <span class="k">export</span> <span class="nx">type</span> <span class="nx">Unmarshaled</span> <span class="o">=</span> <span class="p">[</span><span class="nx">unknown</span><span class="p">,</span> <span class="nx">object</span> <span class="o">|</span> <span class="kc">null</span><span class="p">];</span>
  <span class="k">export</span> <span class="kd">function</span> <span class="nx">UnmarshalRecursive</span><span class="p">(</span><span class="nx">src</span><span class="p">:</span> <span class="nb">Uint8Array</span><span class="p">):</span> <span class="nx">Unmarshaled</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="o">=</span> <span class="nx">binary</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="wasmのロード処理とラッパー関数">Wasmのロード処理とラッパー関数</h4>

<p>Wasmをロードするbinary.jsを作成し、GopherJSの場合と同様にこのファイルを<code class="language-plaintext highlighter-rouge">import</code>すれば関数を呼び出せるようにします。
これにより、<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数の利用箇所の変更を最小限にできます。</p>

<p>▼リスト7: Wasmをロードするbinary.js</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">./wasm_exec.js</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:fs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:path</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">dir</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">dirname</span><span class="p">(</span><span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">).</span><span class="nx">pathname</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">go</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Go</span><span class="p">();</span>

<span class="c1">// 同じディレクトリにあるbinary.wasmをロード、実行する</span>
<span class="kd">const</span> <span class="nx">wasm</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">WebAssembly</span><span class="p">.</span><span class="nx">instantiate</span><span class="p">(</span>
    <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="dl">"</span><span class="s2">binary.wasm</span><span class="dl">"</span><span class="p">)),</span> <span class="nx">go</span><span class="p">.</span><span class="nx">importObject</span><span class="p">);</span>
<span class="nx">go</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">wasm</span><span class="p">.</span><span class="nx">instance</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">binary</span> <span class="o">=</span> <span class="nx">globalThis</span><span class="p">.</span><span class="nx">binary</span><span class="p">;</span>

<span class="c1">// モジュールがexportする関数。JSONの展開やエラーハンドリングも行う</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">UnmarshalRecursive</span><span class="p">(</span><span class="nx">src</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nx">binary</span><span class="p">.</span><span class="nx">UnmarshalRecursive</span><span class="p">(</span><span class="nx">src</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">ret</span><span class="p">.</span><span class="nx">err</span> <span class="o">!=</span> <span class="dl">""</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="kc">null</span><span class="p">,</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">err</span><span class="p">]</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="p">[</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">ret</span><span class="p">.</span><span class="nx">val</span><span class="p">),</span> <span class="kc">null</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Goの実装でも紹介したとおり、Wasmの公開する<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数はJSON文字列にしたオブジェクトとエラー文字列の組を返します。
このため、この関数を直接<code class="language-plaintext highlighter-rouge">export</code>するのではなく、JSONのデコードやエラーハンドリングをするラッパー関数を用意しました。</p>

<p>このラッパー関数の型はGopherJSの場合と合わせてあるので、利用側の変更を減らせます。
TS用の型定義ファイルもリスト8のように書き換えます。</p>

<p>▼リスト8: Wasm用の型定義（binary.d.ts）</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="nx">declare</span> <span class="nx">type</span> <span class="nx">Unmarshaled</span> <span class="o">=</span> <span class="p">[</span><span class="nx">unknown</span><span class="p">,</span> <span class="nx">object</span> <span class="o">|</span> <span class="kc">null</span><span class="p">];</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">UnmarshalRecursive</span><span class="p">(</span><span class="nx">src</span><span class="p">:</span> <span class="nx">UintArray</span><span class="p">):</span> <span class="nx">Unmarshaled</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="インポート処理の変更">インポート処理の変更</h4>

<p>Wasm用の<code class="language-plaintext highlighter-rouge">binary.js</code>ではトップレベル<code class="language-plaintext highlighter-rouge">await</code>を使用しているため、ESM形式となります。
このため利用側のTSコードでは、CJS形式の<code class="language-plaintext highlighter-rouge">require</code>を使った<code class="language-plaintext highlighter-rouge">import</code>文からESM形式の<code class="language-plaintext highlighter-rouge">import</code>文に書き換える必要があります。
この変更のdiffをリスト9に示します。</p>

<p>▼リスト9: 利用側のimport文の変更</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- import binary = require("../plugins/binary.js");
</span><span class="gi">+ import * as binary from "../plugins/binary.js";
</span></code></pre></div></div>

<p>GopherJSの場合は<code class="language-plaintext highlighter-rouge">require</code>が返すモジュールオブジェクトを<code class="language-plaintext highlighter-rouge">binary</code>という名前に拘束していたので、
<code class="language-plaintext highlighter-rouge">binary.UnmarshalRecursive(...)</code>という形でデシリアライズ関数を呼び出していました。
Wasm用の<code class="language-plaintext highlighter-rouge">binary.js</code>では<code class="language-plaintext highlighter-rouge">UnmarshalRecursive</code>関数を個別に<code class="language-plaintext highlighter-rouge">export</code>しているので、
<code class="language-plaintext highlighter-rouge">import * as binary</code>のように名前空間インポートして名前を<code class="language-plaintext highlighter-rouge">binary</code>とすることで、同様の呼び出し方が可能になります。</p>

<h4 id="esm-対応">ESM 対応</h4>

<p>これまでダッシュボードのバックエンドはCJS形式で実装されていました。
しかし、モジュールシステムの節でも触れましたが、CJS形式からESM形式のモジュールをインポートするのは困難なため、
プロジェクト全体をESM形式に変更することにしました。</p>

<p>まずNode.jsの設定<code class="language-plaintext highlighter-rouge">package.json</code>で<code class="language-plaintext highlighter-rouge">"type"</code>を<code class="language-plaintext highlighter-rouge">"module"</code>に変更します。
TSの設定ファイル<code class="language-plaintext highlighter-rouge">tsconfig.json</code>についても、
モジュールシステムをESM形式とし、トップレベル<code class="language-plaintext highlighter-rouge">await</code>を利用しているため、
<code class="language-plaintext highlighter-rouge">"taraget"</code>と<code class="language-plaintext highlighter-rouge">"module"</code>をそれぞれ<code class="language-plaintext highlighter-rouge">"es2022"</code>、<code class="language-plaintext highlighter-rouge">"esnext"</code>に変更します。</p>

<p>使用しているライブラリやツールもESM対応のものに差し替える必要があります。
ダッシュボードからWSNet2のサーバーへの通信にはProtocol BuffersとgRPCを利用していますが、
コード生成ツールを<code class="language-plaintext highlighter-rouge">grpc_tools_node_protoc_ts</code>と<a href="https://www.npmjs.com/package/grpc_tools_node_protoc_ts"><code class="language-plaintext highlighter-rouge">protoc-gen-ts</code></a>から、
ESM対応の<a href="https://www.npmjs.com/package/@bufbuild/protoc-gen-es"><code class="language-plaintext highlighter-rouge">protoc-gen-es</code></a>を使うように変更しました。
またgRPCのクライアントライブラリも<a href="https://www.npmjs.com/package/@grpc/grpc-js"><code class="language-plaintext highlighter-rouge">grpc-js</code></a>から
<a href="https://www.npmjs.com/package/@connectrpc/connect"><code class="language-plaintext highlighter-rouge">connectrpc</code></a>に変更しました。
これにより、生成される型やメソッド、gRPCの呼び出し方が変わってしまうので、利用箇所を修正しました。</p>

<p>さらに、既存のCJS形式の<code class="language-plaintext highlighter-rouge">import</code>文を一つ一つ直していきます。
拡張子<code class="language-plaintext highlighter-rouge">.js</code>や<code class="language-plaintext highlighter-rouge">index.js</code>を省略しているところは全て省略せずに書き足します。
この修正はプロジェクトのほぼ全てのファイルに必要でした。</p>

<p>また<code class="language-plaintext highlighter-rouge">nexus-prisma</code>のようなCJS形式のモジュールは、リスト10の差分のように
一度全体をインポートしたあと必要なオブジェクトを取り出す形に書き換えます。</p>

<p>▼リスト10: nexus-prismaからのimport文の書き換え</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- import { room } from "nexus-prisma";
</span><span class="gi">+ import np from "nexus-prisma";
+ const { room } = np;
</span></code></pre></div></div>

<p>TSビルド時のエラーを手がかりに、このような修正をひたすら行いました。
これらの修正によって、無事GopherJSからWasmへの移行をすることができました。</p>

<h2 id="成果と考察">成果と考察</h2>

<p>この章では、WSNet2のダッシュボードにおいてGopherJSを使ってGoのコードを利用していた部分を、
Wasmを使う形に置き換えた実例を紹介しました。</p>

<p>筆者がJSやWasmについて詳しくなかったこともあり、
当初想像していたよりもかなり大掛かりな改修となってしまいました。
特にJSのモジュールシステムの違いには戸惑いましたし、
その違いに伴いライブラリの差し替えも必要だとは思いもよりませんでした。
もしかするとデシリアライザをTSで再実装するほうが早かったかもしれません。</p>

<p>しかしながら、TSの型チェックにもかなり助けられ、なんとか移行することができました。
やはり型システムは偉大です。
元々型を明示していなかったPHPやPythonでも最近は型を書くことが推奨されているのにも納得です。</p>

<p>この移行を通してGopherJSとWasmの違いに触れてきましたが、
GopherJSはWasmと比べると圧倒的に少ない労力でGoのコードをJSから利用できるものの、
やはり最新のGoへの追従の遅さには不安があります。
一方、Wasmに移行したことでGoの公式コンパイラを使ってビルドできるようになりました。
これでWSNet2サーバーの更新も滞り無く進められるようになり、当初の問題は完全に解消されました。
加えて、ダッシュボードのTSも新しいESM形式になり、モダンなライブラリにも移行できたことで、
今後の改修がやりやすくなるだろうとも感じています。</p>

<p>また、筆者は当初JSやTSをほとんど書いたことがなく、Wasmについて概要程度しか知らない状態でしたが、
今回の移行作業やこの記事の執筆を通してWasmの仕組みや近年の動向、
JSのモジュールシステムの歴史的経緯などを深く学ぶことができました。</p>

<p>WasmやESMは今まさに発展中の技術なので、今後のエコシステムのさらなる充実も期待できます。
GopherJSも便利ではありますが、長期的な視点で見るとWasmへの移行を検討する価値はあるのではないかと思います。
同じような課題に直面している方は多くはないかもしれませんが、そんな開発者の皆様の参考になれば幸いです。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>詳細は KLabTechBook Vol.9「<a href="/2024/06/02/klabtechbook9-wsnet2-serializer.ja.html">オンライン対戦を支える独自シリアライズフォーマット</a>」で紹介しています <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">GOROOT</code>以下の<code class="language-plaintext highlighter-rouge">lib/wasm/wasm_exec.js</code>にあります（Go 1.23以前は<code class="language-plaintext highlighter-rouge">misc/wasm/wasm_exec.js</code>） <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>Node.js v23.11.0で確認しています <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2024年11月2日から開催された技術書典18にて頒布した「KLabTechBook Vol. 15」に掲載したものです。]]></summary></entry><entry><title type="html">XMLプログラミング言語「XSLT」の可能性 (KLabTechBook Vol. 14)</title><link href="http://makiuchi-d.github.io/2025/06/02/klabtechbook14-bfxslt.ja.html" rel="alternate" type="text/html" title="XMLプログラミング言語「XSLT」の可能性 (KLabTechBook Vol. 14)" /><published>2025-06-02T00:00:00+00:00</published><updated>2025-06-02T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2025/06/02/klabtechbook14-bfxslt.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2025/06/02/klabtechbook14-bfxslt.ja.html"><![CDATA[<p>この記事は2024年11月2日から開催された<a href="https://techbookfest.org/event/tbf17">技術書典17</a>にて頒布した「<a href="https://techbookfest.org/product/bZpYWjnBQDRe15rq1JdqqU">KLabTechBook Vol. 14</a>」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf18">技術書典18</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/xmtJPdPuamKDgrnmkek9pn">KLabTechBook Vol.15</a>」および既刊を頒布（電子版無料、紙+電子 500円）しているほか、<a href="https://www.klab.com/jp/blog/tech/2025/tbf18.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。</p>

<p><a href="https://techbookfest.org/product/xmtJPdPuamKDgrnmkek9pn"><img src="/images/2025-06-02/ktbv15.png" width="40%" alt="KLabTechBook Vol.15" /></a></p>

<p>また、6月14日〜15日に開催される<a href="https://fortee.jp/2025fp-matsuri">関数型まつり2025</a>に、
この記事の内容を元にしたセッション「<a href="https://fortee.jp/2025fp-matsuri/proposal/8dcaecb5-4541-4262-a047-3e330a7bcdb8">XSLTで作るBrainfuck処理系――XSLTは関数型言語たり得るか？</a>」で登壇します。
参加される方はぜひ直接のツッコミをお願いします。</p>

<p><a href="https://fortee.jp/2025fp-matsuri/proposal/8dcaecb5-4541-4262-a047-3e330a7bcdb8"><img src="/images/2025-06-02/fp-matsuri-xslt.png" alt="XSLTで作るBrainfuck処理系――XSLTは関数型言語たり得るか？" /></a></p>

<hr />

<h2 id="ことのはじまり">ことのはじまり</h2>

<p>それは、とある勉強会の懇親会でのことでした。
出版業界の方とお話する中で、組版作業でXMLとXSLTを利用することがあると聞きました。
XSLTはXMLを整形するためのテンプレート言語で、それ自体もXMLで記述します。
さらに驚くべきことに、XSLTはテンプレート言語でありながらチューリング完全だといいます。
つまり、XSLTは<strong>XMLのXMLによるXMLのためのプログラミング言語</strong>なのです。</p>

<p>ところで筆者は以前KLabTechBook Vol.1にて、M4というマクロ言語がチューリング完全であることを示しました<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。
そうです、またなのです。確かめたくなってしまったのです。</p>

<p>今回も同様に、XSLTでBrainfuckのインタプリタを実装することで、XSLTがチューリング完全であることを確認したいと思います。</p>

<h2 id="xsltとは">XSLTとは</h2>

<p>XSLT（Extensible Stylesheet Language Transformations）は、主にXMLを整形・変換するためのテンプレート言語です。
XML文書を受け取り、それを他の形式（HTML、テキスト、あるいは別のXML）に変換することができます。</p>

<p>次の例はXSLTでXML文書を表示用HTMLに変換するものです。
リスト1は図書館の蔵書を表すXML文書です。
これにリスト2のように書かれたXSLテンプレートを適用することで、
表示用に整形されたリスト3のHTMLを出力しています。</p>

<p>▼リスト1 入力のXML文書</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;library&gt;</span>
  <span class="nt">&lt;book&gt;</span>
    <span class="nt">&lt;title&gt;</span>Learning XSLT<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;author&gt;</span>John Doe<span class="nt">&lt;/author&gt;</span>
  <span class="nt">&lt;/book&gt;</span>
  <span class="nt">&lt;book&gt;</span>
    <span class="nt">&lt;title&gt;</span>XML in Action<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;author&gt;</span>Jane Smith<span class="nt">&lt;/author&gt;</span>
  <span class="nt">&lt;/book&gt;</span>
<span class="nt">&lt;/library&gt;</span>
</code></pre></div></div>

<p>▼リスト2 XSLテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;xsl:stylesheet</span> <span class="na">version=</span><span class="s">"1.0"</span> <span class="na">xmlns:xsl=</span><span class="s">"http://www.w3.org/1999/XSL/Transform"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;xsl:output</span> <span class="na">method=</span><span class="s">"html"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;html&gt;</span>
      <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;h1&gt;</span>Library Books<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;ul&gt;</span>
          <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"library/book"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/body&gt;</span>
    <span class="nt">&lt;/html&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"book"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;li&gt;</span>
      <span class="nt">&lt;strong&gt;</span>
        <span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"title"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;/strong&gt;</span> by <span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"author"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
<span class="nt">&lt;/xsl:stylesheet&gt;</span>
</code></pre></div></div>

<p>▼リスト3 出力HTML</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html&gt;</span>
  <span class="nt">&lt;body&gt;</span>
    <span class="nt">&lt;h1&gt;</span>Library Books<span class="nt">&lt;/h1&gt;</span>
    <span class="nt">&lt;ul&gt;</span>
      <span class="nt">&lt;li&gt;&lt;strong&gt;</span>Learning XSLT<span class="nt">&lt;/strong&gt;</span> by John Doe<span class="nt">&lt;/li&gt;</span>
      <span class="nt">&lt;li&gt;&lt;strong&gt;</span>XML in Action<span class="nt">&lt;/strong&gt;</span> by Jane Smith<span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;/ul&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>XSLTはテンプレートマッチングを駆使して文書の特定の部分に応じた処理を行います。
この例では、XSLの <code class="language-plaintext highlighter-rouge">&lt;xsl:template match="/"&gt;</code> のテンプレートが文書全体にマッチして呼び出され、
その中の <code class="language-plaintext highlighter-rouge">&lt;xsl:apply-templates select="library/book"/&gt;</code> で、
孫要素の <code class="language-plaintext highlighter-rouge">&lt;book&gt;</code> それぞれに対してマッチするテンプレート <code class="language-plaintext highlighter-rouge">&lt;xsl:template match="book"&gt;</code> が呼び出される形となっています。</p>

<p>XSLTには <code class="language-plaintext highlighter-rouge">xsl:if</code> や <code class="language-plaintext highlighter-rouge">xsl:for-each</code> のような制御構文もありますが、
このようなテンプレート言語が本当にプログラミング言語と言えるのでしょうか？</p>

<h2 id="チューリング完全性について">チューリング完全性について</h2>

<h3 id="チューリング完全とは">チューリング完全とは</h3>

<p>プログラミング言語であるためには、その言語がチューリング完全であることが必要だとよく言われます。</p>

<p>チューリング完全とは、チューリングマシンと同等の計算能力があることを意味します。
チューリングマシンはアラン・チューリングによって考案された仮想の機械で、無限に長いテープ（記憶装置）と、
そこに対してデータを読み書きするヘッドで構成されています。
この機械に、ヘッドの移動や読み書きを指示するプログラムを与えることで動作する計算機です。</p>

<p><img src="/images/2025-06-02/turing-machine.png" alt="チューリングマシンの概念図" /><br />
▲図1 チューリングマシンの概念図</p>

<p>チューリングマシンは非常に単純な構造ですが、これまでに知られているどんな複雑な計算機械でも、
理論上チューリングマシンで再現できることが知られています。
そして計算可能なあらゆるアルゴリズムは、このチューリングマシンでも計算できるとされています<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。</p>

<p>プログラミング言語であるならば、あらゆる計算問題に対応できなくてはなりません。
チューリング完全であれば、どんな計算問題でも計算できることが理論的に保証されるわけです。
これが、プログラミング言語がチューリング完全であることを必要とする理由です。</p>

<h3 id="brainfuckとチューリング完全">Brainfuckとチューリング完全</h3>

<p>ある言語がチューリング完全であることを示す方法のひとつが、すでにチューリング完全であることがわかっている言語の処理系をその言語で実装することです。
チューリング完全な言語で書かれたプログラムをすべて実行できるのであれば、その言語を通せばあらゆる計算を実行できることになり、
チューリング完全であると言えるわけです。</p>

<p>ところで、Brainfuckというプログラミング言語をご存知でしょうか？
この言語は記号のみで記述する、いわゆる難解プログラミング言語のひとつとして知られていますが、
実は非常に有用な特徴を持っています。</p>

<p>▼リスト4 Brainfuckで<code class="language-plaintext highlighter-rouge">Hello, world!</code></p>
<div class="language-brainfuck highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">+++++++++</span><span class="p">[</span><span class="nb">&gt;</span><span class="nf">++++++++</span><span class="nb">&gt;</span><span class="nf">+++++++++++</span><span class="nb">&gt;</span><span class="nf">+++++</span><span class="nb">&lt;&lt;&lt;</span><span class="nf">-</span><span class="p">]</span><span class="nb">&gt;</span><span class="nf">.</span><span class="nb">&gt;</span><span class="nf">++.+++++++..+++.</span><span class="nb">&gt;</span><span class="nf">-.</span><span class="c1">
</span><span class="nf">------------.</span><span class="nb">&lt;</span><span class="nf">++++++++.--------.+++.------.--------.</span><span class="nb">&gt;</span><span class="nf">+.</span><span class="c1">
</span></code></pre></div></div>

<p>Brainfuckの処理系は、1次元のメモリ配列とそのアドレスを示すポインタを1つ持ち、
プログラムはポインタの移動とメモリ上の値の読み書きといった命令の列になっています。</p>

<p><img src="/images/2025-06-02/brainfuck.png" alt="Brainfuck処理系の概念図" /><br />
▲図2 Brainfuck処理系の概念図</p>

<p>ご覧のとおり、Brainfuckの処理系はチューリングマシンとほぼ同一の構成になっていて、チューリング完全であることが自明です。
加えて、命令もわずか8種類のみであり、実装が容易です。
このため、チューリング完全であることを示すために実装するにはうってつけの言語です。</p>

<p>▼表1 Brainfuckの命令一覧</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">記号</th>
      <th style="text-align: left">処理</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&gt;</code></td>
      <td style="text-align: left">ポインタを右に移動</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;</code></td>
      <td style="text-align: left">ポインタを左に移動</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">+</code></td>
      <td style="text-align: left">ポインタの指すメモリの値をインクリメント</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-</code></td>
      <td style="text-align: left">ポインタの指すメモリの値をデクリメント</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">[</code></td>
      <td style="text-align: left">ポインタの指すメモリの値が0なら対応する <code class="language-plaintext highlighter-rouge">]</code> の次にジャンプ（ループ起点）</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">]</code></td>
      <td style="text-align: left">対応する <code class="language-plaintext highlighter-rouge">[</code> にジャンプ（ループ終点）</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">.</code></td>
      <td style="text-align: left">ポインタの指すメモリの値をASCII文字として出力</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">,</code></td>
      <td style="text-align: left">入力を受け取りポインタの指すメモリに書き込む</td>
    </tr>
  </tbody>
</table>

<p>ということで、本題にもどりましょう。
ここからはXSLTでBrainfuckの処理系を実装していきます。</p>

<h2 id="xsltによるbrainfuckインタプリタの実装">XSLTによるBrainfuckインタプリタの実装</h2>

<p>これから紹介するXMLによるBrainfuckインタプリタの実装はGitHubにて公開していますので、適宜参照してみてください。</p>

<ul>
  <li><a href="https://github.com/makiuchi-d/bfxslt">https://github.com/makiuchi-d/bfxslt</a></li>
</ul>

<p>インタプリタ本体は <code class="language-plaintext highlighter-rouge">bf.xsl</code> に実装しています。
この他、 <code class="language-plaintext highlighter-rouge">sample</code> ディレクトリにサンプルプログラムのXMLファイルをいくつか用意しました。</p>

<p>実行はXSLTプロセッサのコマンドやブラウザで行うことができます。
リポジトリのREADMEにも記載していますが、たとえば <code class="language-plaintext highlighter-rouge">xsltproc</code> を使用する場合は次のようにコマンドを実行してください。</p>

<p>▼リスト5 xsltprocでの実行</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>xsltproc bf.xsl sample/hello.xml
Hello, world!
</code></pre></div></div>

<h3 id="xmlによるbrainfuckコードの表現">XMLによるBrainfuckコードの表現</h3>

<p>XSLTでは、入力はXMLでなくてはなりません。
Brainfuckのコードは単純な命令の列なので、これをXMLで表現します。
また、Brainfuckは入力も受け付けるので、これも同じXMLドキュメントにまとめてしまいましょう。
まずルート要素として <code class="language-plaintext highlighter-rouge">&lt;bf&gt;</code> タグを配置し、
その中にBrainfuckのコードの <code class="language-plaintext highlighter-rouge">&lt;code&gt;</code> タグ、入力データの <code class="language-plaintext highlighter-rouge">&lt;input&gt;</code> タグを入れ子にします。</p>

<p>たとえば、入力として「<code class="language-plaintext highlighter-rouge">Aa</code>」を読み込み、それぞれ1ずつインクリメントして
「<code class="language-plaintext highlighter-rouge">Bb</code>」を出力するプログラムはリスト6のようなXMLになります。</p>

<p>▼リスト6 XMLによるBrainfuckコードと入力の表現</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;bf&gt;</span>
  <span class="nt">&lt;input&gt;</span>Aa<span class="nt">&lt;/input&gt;</span>
  <span class="nt">&lt;code&gt;</span>
    <span class="nt">&lt;inc/&gt;&lt;inc/&gt;</span>
    <span class="nt">&lt;loop&gt;</span>
      <span class="nt">&lt;dec/&gt;&lt;right/&gt;&lt;read/&gt;&lt;inc/&gt;&lt;print/&gt;&lt;left/&gt;</span>
      <span class="nt">&lt;end/&gt;</span>
    <span class="nt">&lt;/loop&gt;</span>
  <span class="nt">&lt;/code&gt;</span>
<span class="nt">&lt;/bf&gt;</span>
</code></pre></div></div>

<p>元のBrainfuckのコードは「<code class="language-plaintext highlighter-rouge">++[-&gt;,+.&lt;]</code>」<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>ですが、
この各記号を表2のように定義したXMLタグに置換して記述しています。</p>

<p>▼表2 Brainfuckの命令とタグの対応</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">記号</th>
      <th style="text-align: left">xmlタグ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&gt;</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;right/&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;left/&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">+</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;inc/&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;dec/&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">[...]</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;loop&gt;...&lt;end/&gt;&lt;/loop&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">,</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;read/&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">.</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">&lt;print/&gt;</code></td>
    </tr>
  </tbody>
</table>

<p>ここで、 <code class="language-plaintext highlighter-rouge">]</code> は <code class="language-plaintext highlighter-rouge">&lt;/loop&gt;</code> のようにタグを閉じるだけでなく、直前に必ず <code class="language-plaintext highlighter-rouge">&lt;end/&gt;</code> タグを挿入するようにしました。
この理由は後ほど説明します。</p>

<h3 id="実装の方針">実装の方針</h3>

<p>XSLTでは、特定のXML要素に対応するテンプレートを定義して、それが呼び出されることで処理が進行します。
インタプリタ実装の <code class="language-plaintext highlighter-rouge">bf.xsl</code> にはいくつかテンプレートが定義されていますが、
最初に実行されるのはマッチングパターンが最も具体的な、リスト7のテンプレートです。
このテンプレートは、プログラムのXMLの全体を囲んでいる <code class="language-plaintext highlighter-rouge">bf</code> 要素にマッチして実行されます。</p>

<p>▼リスト7 最初に処理されるテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/bf"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"code/*[1]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"0"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">&gt;&lt;_0&gt;</span>0<span class="nt">&lt;/_0&gt;&lt;/xsl:with-param&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>ここではまず、入力にあたる <code class="language-plaintext highlighter-rouge">&lt;input&gt;</code> 要素を <code class="language-plaintext highlighter-rouge">xsl:variable</code> を使って変数 <code class="language-plaintext highlighter-rouge">input</code> に格納しています。
次に <code class="language-plaintext highlighter-rouge">&lt;code&gt;</code> の中の先頭のBrainfuckの命令にあたる要素に対して <code class="language-plaintext highlighter-rouge">xsl:apply-template</code> でテンプレートを適用しています。
このときテンプレートにはパラメータとして、ポインタを表す <code class="language-plaintext highlighter-rouge">ptr</code> 、メモリを表す <code class="language-plaintext highlighter-rouge">mem</code> 、入力の <code class="language-plaintext highlighter-rouge">input</code> を <code class="language-plaintext highlighter-rouge">&lt;xsl:with-param&gt;</code> で渡します。
 <code class="language-plaintext highlighter-rouge">ptr</code> は数値で初期値は0です。
 <code class="language-plaintext highlighter-rouge">mem</code> はリスト8のような <code class="language-plaintext highlighter-rouge">&lt;_アドレス&gt;</code> 要素の列としました。
新たなアドレスにデータを書き込むたびに要素が増えていく形です。</p>

<p>▼リスト8 メモリの表現</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;_0&gt;</span>0<span class="nt">&lt;/_0&gt;</span>
<span class="nt">&lt;_1&gt;</span>10<span class="nt">&lt;/_1&gt;</span>
<span class="nt">&lt;_2&gt;</span>20<span class="nt">&lt;/_2&gt;</span>
</code></pre></div></div>

<p>ところで、XSLTでは変数は一度定義したら書き換えることができません。
このため、グローバルな変数は使えず、変更を加えた値をパラメータとして次の命令の処理に引き回すことで、状態を更新するようにしています。</p>

<p>各命令のタグのテンプレートはリスト9の形をしています。</p>

<p>▼リスト9 各命令のテンプレートの基本形</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"命令のタグ名"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="c">&lt;!-- 各命令固有の処理 --&gt;</span>

    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">xsl:param</code> で指定されているとおり、パラメータとして <code class="language-plaintext highlighter-rouge">ptr</code> 、 <code class="language-plaintext highlighter-rouge">mem</code> 、 <code class="language-plaintext highlighter-rouge">input</code> を受け取ります。
そして各命令固有の処理をしたあと、次の命令に進むのですが、 <code class="language-plaintext highlighter-rouge">following-sibling::*[1]</code> によって現在処理している要素の次の兄弟要素、
つまり次の命令にあたる要素を指定して <code class="language-plaintext highlighter-rouge">xsl:apply-templates</code> で再帰的にテンプレートを適用します。
こうすることで、次の命令の要素の名前とマッチするテンプレートが選ばれて処理され、これを繰り返すことでプログラムが順次処理されていくことになります。</p>

<h3 id="各命令の実装">各命令の実装</h3>

<p>それでは各命令の処理をするテンプレートを見ていきましょう。</p>

<h4 id="ポインタの移動-right-left-">ポインタの移動： <code class="language-plaintext highlighter-rouge">right, left</code>（<code class="language-plaintext highlighter-rouge">&gt;</code> <code class="language-plaintext highlighter-rouge">&lt;</code>）</h4>

<p>リスト10はポインタを右に移動する <code class="language-plaintext highlighter-rouge">&lt;right/&gt;</code> の処理をするテンプレートです。
次の命令に進む <code class="language-plaintext highlighter-rouge">xsl:apply-templates</code> に渡すパラメータのうち、 <code class="language-plaintext highlighter-rouge">ptr</code> の値を <code class="language-plaintext highlighter-rouge">"$ptr + 1"</code> とすることで、
次の処理ではポインタが移動した状態となります。
他のパラメータの <code class="language-plaintext highlighter-rouge">mem</code> と <code class="language-plaintext highlighter-rouge">input</code> は受け取ったものをそのまま次の命令の処理に渡しています。</p>

<p>▼リスト10 right命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"right"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr + 1"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>ポインタを左に移動する <code class="language-plaintext highlighter-rouge">&lt;left/&gt;</code> は、マッチングパターンを <code class="language-plaintext highlighter-rouge">"left"</code> とし、
次に渡す <code class="language-plaintext highlighter-rouge">ptr</code> の値を <code class="language-plaintext highlighter-rouge">"$ptr - 1"</code> とするだけで実現できます。</p>

<h4 id="メモリの値の加減算-inc-dec--">メモリの値の加減算： <code class="language-plaintext highlighter-rouge">inc, dec</code>（<code class="language-plaintext highlighter-rouge">+</code> <code class="language-plaintext highlighter-rouge">-</code>）</h4>

<p>リスト11は、 <code class="language-plaintext highlighter-rouge">&lt;inc/&gt;</code> の処理、つまり現在のポインタの指すメモリの値を1増加させるテンプレートです。
まず値を取り出し、値を更新したメモリを生成し、次の命令のテンプレートを呼び出すという流になっています。</p>

<p>それぞれの処理がどう実現されているか見ていきましょう。</p>

<p>▼リスト11 inc命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"inc"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"concat('_', $ptr)"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"sum(exsl:node-set($mem)/*[name()=$key])"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:call-template</span> <span class="na">name=</span><span class="s">"write-val"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"$key"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"$val + 1"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;/xsl:call-template&gt;</span>
    <span class="nt">&lt;/xsl:variable&gt;</span>

    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>まず最初に値を取り出す処理として、リスト12の部分で変数 <code class="language-plaintext highlighter-rouge">key</code> と <code class="language-plaintext highlighter-rouge">val</code> を定義しています。</p>

<p>▼リスト12 keyとvalueの定義</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"concat('_', $ptr)"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"sum(exsl:node-set($mem)/*[name()=$key])"</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">key</code> は <code class="language-plaintext highlighter-rouge">'_'</code> とポインタの値を結合したもので、これがポインタの指す、 <code class="language-plaintext highlighter-rouge">mem</code> の中の要素名になります。
 <code class="language-plaintext highlighter-rouge">val</code> の定義では、 <code class="language-plaintext highlighter-rouge">mem</code> の中のノードの集合から、 <code class="language-plaintext highlighter-rouge">key</code> と同じ名前の要素を取り出しています。
要素が存在しなかったときに <code class="language-plaintext highlighter-rouge">0</code> となるように、 <code class="language-plaintext highlighter-rouge">sum</code> 関数を利用しています。
一番最初に <code class="language-plaintext highlighter-rouge">mem</code> の初期値として <code class="language-plaintext highlighter-rouge">&lt;_0&gt;0&lt;/_0&gt;</code> を用意していたのは、 <code class="language-plaintext highlighter-rouge">mem</code> の中身をノードの集合にしておくためです。</p>

<p>続いてこれらの値を使って、値を更新した状態のメモリを新たに作り、変数 <code class="language-plaintext highlighter-rouge">mem</code> を定義します<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
新たなメモリを作る処理は他の命令でも利用するので、リスト13のようにテンプレートで行っています。</p>

<p>▼リスト13 値を書き込んだメモリを生成するテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">name=</span><span class="s">"write-val"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"key"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"val"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:for-each</span> <span class="na">select=</span><span class="s">"exsl:node-set($mem)/*[name()!=$key]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:copy-of</span> <span class="na">select=</span><span class="s">"."</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:for-each&gt;</span>
    <span class="nt">&lt;xsl:element</span> <span class="na">name=</span><span class="s">"{$key}"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"$val"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:element&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>このテンプレートでは、最初に <code class="language-plaintext highlighter-rouge">xsl:for-each</code> を使って、与えられた <code class="language-plaintext highlighter-rouge">mem</code> のうち要素名が <code class="language-plaintext highlighter-rouge">key</code> ではないものをすべて <code class="language-plaintext highlighter-rouge">xsl:copy-of</code> でコピーしています。
その後に続ける形で、 <code class="language-plaintext highlighter-rouge">key</code> の名前の要素を作り、内容を <code class="language-plaintext highlighter-rouge">val</code> に設定しています。
この方法では要素の順序が書き換えるたびに入れ替わってしまうのですが、読み取るときには要素名でアクセスするので問題ありません。</p>

<p><code class="language-plaintext highlighter-rouge">&lt;inc/&gt;</code> の処理では、このテンプレートに渡すパラメータは現在のメモリ <code class="language-plaintext highlighter-rouge">mem</code> と書き込む場所の <code class="language-plaintext highlighter-rouge">key</code> 、そして <code class="language-plaintext highlighter-rouge">val</code> は <code class="language-plaintext highlighter-rouge">"$val + 1"</code> のように1加算した値となっていました。
これによって、 <code class="language-plaintext highlighter-rouge">key</code> の場所の値を <code class="language-plaintext highlighter-rouge">1</code> 増やしたメモリが生成されるわけです。</p>

<p>最後にこの <code class="language-plaintext highlighter-rouge">mem</code> を次の命令のテンプレートに渡せば <code class="language-plaintext highlighter-rouge">&lt;inc/&gt;</code> の処理は完了です。
 <code class="language-plaintext highlighter-rouge">&lt;dec/&gt;</code> も同じ処理の流れで、加算していたところを <code class="language-plaintext highlighter-rouge">"$val - 1"</code> とするだけで実現できます。</p>

<h4 id="ループ-loop-end-">ループ： <code class="language-plaintext highlighter-rouge">loop, end</code>（<code class="language-plaintext highlighter-rouge">[ ]</code>）</h4>

<p>Brainfuckの <code class="language-plaintext highlighter-rouge">[</code> は、ポインタの指すメモリの値が0なら対応する <code class="language-plaintext highlighter-rouge">]</code> の次の命令にジャンプするという命令で、
 <code class="language-plaintext highlighter-rouge">]</code> は対応する <code class="language-plaintext highlighter-rouge">[</code> に戻るという命令です。
つまり、ポインタの指すメモリの値が0でない間 <code class="language-plaintext highlighter-rouge">[</code> と <code class="language-plaintext highlighter-rouge">]</code> の間にある命令が繰り返し実行されます。</p>

<p>リスト14が今回実装した <code class="language-plaintext highlighter-rouge">[</code> に相当する <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> を処理するテンプレートです。
メモリの値を読み取って <code class="language-plaintext highlighter-rouge">val</code> とする部分は <code class="language-plaintext highlighter-rouge">inc</code> の処理と同じです。
その後、 <code class="language-plaintext highlighter-rouge">xsl:choose</code> によって、 <code class="language-plaintext highlighter-rouge">val</code> の値に応じて分岐します。
 <code class="language-plaintext highlighter-rouge">val</code> が0ではないとき、次に実行する要素として <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> の内側の要素の先頭のものを <code class="language-plaintext highlighter-rouge">"*[1]"</code> で指定します。
一方それ以外のときは、 <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> の外にある次の要素を <code class="language-plaintext highlighter-rouge">"following-subling::*[1]</code> で指定します。
このように、 <code class="language-plaintext highlighter-rouge">[</code> と <code class="language-plaintext highlighter-rouge">]</code> の対応関係を <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> のネストで表現しているので、ジャンプ先を簡単に指定できます。</p>

<p>▼リスト14 loop命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"loop"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"concat('_', $ptr)"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"sum(exsl:node-set($mem)/*[name()=$key])"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:choose&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val != 0"</span><span class="nt">&gt;</span>
        <span class="c">&lt;!-- 繰り返す: 子要素の先頭に進む --&gt;</span>
        <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"*[1]"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;/xsl:apply-templates&gt;</span>
      <span class="nt">&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:otherwise&gt;</span>
        <span class="c">&lt;!-- 繰り返し終了: 次の要素に進む --&gt;</span>
        <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;/xsl:apply-templates&gt;</span>
      <span class="nt">&lt;/xsl:otherwise&gt;</span>
    <span class="nt">&lt;/xsl:choose&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>ループの終わりは、 <code class="language-plaintext highlighter-rouge">&lt;end/&gt;</code> を置いてから <code class="language-plaintext highlighter-rouge">&lt;/loop&gt;</code> のように閉じています。
この <code class="language-plaintext highlighter-rouge">&lt;end/&gt;</code> を処理するテンプレートをリスト15に示します。</p>

<p>▼リスト15 end命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"end"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"parent::node()"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>このテンプレートでは、次に処理する要素として親ノード、つまり自身を包んでいる <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> を <code class="language-plaintext highlighter-rouge">"parent::node()"</code> で指定します。
パラメータはここまで引き継がれてきた <code class="language-plaintext highlighter-rouge">ptr</code> 、 <code class="language-plaintext highlighter-rouge">mem</code> 、 <code class="language-plaintext highlighter-rouge">input</code> を渡します。
こうすることで、次に実行される <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> は、最新のメモリ状態を元にジャンプ先を分岐できるようになります。
この <code class="language-plaintext highlighter-rouge">&lt;end/&gt;</code> が無い場合、親の <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> は最初に実行された時点のポインタとメモリの状態しかわからないため、正しく処理を継続できません。
また、この実装では <code class="language-plaintext highlighter-rouge">end</code> を書き忘れてしまうと、親の <code class="language-plaintext highlighter-rouge">&lt;loop&gt;</code> に処理を戻すことができず終了してしまいます。
これが <code class="language-plaintext highlighter-rouge">&lt;end/&gt;</code> を挿入する理由です。</p>

<h4 id="出力-print">出力： <code class="language-plaintext highlighter-rouge">print</code>（<code class="language-plaintext highlighter-rouge">.</code>）</h4>

<p>Brainfuckでの <code class="language-plaintext highlighter-rouge">.</code> は、ポインタの指すメモリの値をASCIIコードとして出力します。
リスト16は <code class="language-plaintext highlighter-rouge">&lt;print/&gt;</code> 命令を実行するテンプレートです。
メモリの値を取得する方法は他の命令と同様に、変数 <code class="language-plaintext highlighter-rouge">val</code> に値を保存します。
その後 <code class="language-plaintext highlighter-rouge">&lt;xsl:choose&gt;</code> と <code class="language-plaintext highlighter-rouge">&lt;xsl:when&gt;</code> で <code class="language-plaintext highlighter-rouge">val</code> の値ごとに分岐してASCIIコードに該当する文字を出力しています。</p>

<p>▼リスト16 print命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"print"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"concat('_', $ptr)"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"sum(exsl:node-set($mem)/*[name()=$key])"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:choose&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=9"</span><span class="nt">&gt;&lt;xsl:text&gt;</span><span class="ni">&amp;#9;</span><span class="nt">&lt;/xsl:text&gt;&lt;/xsl:when&gt;</span> 
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=10"</span><span class="nt">&gt;&lt;xsl:text&gt;</span><span class="ni">&amp;#10;</span><span class="nt">&lt;/xsl:text&gt;&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=13"</span><span class="nt">&gt;&lt;xsl:text&gt;</span><span class="ni">&amp;#13;</span><span class="nt">&lt;/xsl:text&gt;&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=32"</span><span class="nt">&gt;&lt;xsl:text</span>
                          <span class="na">disable-output-escaping=</span><span class="s">"yes"</span><span class="nt">&gt;</span> <span class="nt">&lt;/xsl:text&gt;&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=33"</span><span class="nt">&gt;</span>!<span class="nt">&lt;/xsl:when&gt;</span>
      (中略)
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=125"</span><span class="nt">&gt;</span>}<span class="nt">&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$val=126"</span><span class="nt">&gt;</span>~<span class="nt">&lt;/xsl:when&gt;</span>
      <span class="nt">&lt;xsl:otherwise&gt;</span><span class="ni">&amp;amp;</span>#<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"$val"</span><span class="nt">/&gt;</span>;<span class="nt">&lt;/xsl:otherwise&gt;</span>
    <span class="nt">&lt;/xsl:choose&gt;</span>

    <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
      <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/xsl:apply-templates&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>XSLT2.0以降であればASCIIコードと数値を相互変換する関数も提供されていますが、
今回対象としているのはXSLT1.0なので愚直に値ごとに分岐するようにしました。
また、XSLT1.0では扱える制御文字が <code class="language-plaintext highlighter-rouge">HT(9)</code> 、 <code class="language-plaintext highlighter-rouge">LF(10)</code> 、 <code class="language-plaintext highlighter-rouge">CR(13)</code> に限られているため、
それ以外の制御文字やASCIIコード外の127以降の値は <code class="language-plaintext highlighter-rouge">"&amp;#数値;"</code> という形式の文字列を出力するようにしました。</p>

<h4 id="入力-read">入力： <code class="language-plaintext highlighter-rouge">read</code>（<code class="language-plaintext highlighter-rouge">,</code>）</h4>

<p>Brainfuckの <code class="language-plaintext highlighter-rouge">,</code> 命令は、入力から1文字受け取り、ポインタが指すメモリ位置にそれをASCIIコードとして書き込みます。
リスト17はこの <code class="language-plaintext highlighter-rouge">&lt;read/&gt;</code> 命令を処理するテンプレートです。</p>

<p>今回のXSLTの実装では、入力はコードと同じXMLに含まれていて、 <code class="language-plaintext highlighter-rouge">input</code> パラメータとして引き回されてきました。
この <code class="language-plaintext highlighter-rouge">input</code> から先頭1文字を取り出してメモリに書き込み、取り出した文字を削除した新しい <code class="language-plaintext highlighter-rouge">input</code> を次の命令に渡すのがこのテンプレートの処理の流れです。</p>

<p>まず最初に <code class="language-plaintext highlighter-rouge">input</code> の文字列長を <code class="language-plaintext highlighter-rouge">string-length</code> 関数を使って確認しています。
もし空であれば入力終了を意味するので、ポインタが指す位置に <code class="language-plaintext highlighter-rouge">EOF</code> を表す <code class="language-plaintext highlighter-rouge">255</code> を書き込んだメモリを次のテンプレートに渡します。</p>

<p><code class="language-plaintext highlighter-rouge">input</code> が空でない場合、まず <code class="language-plaintext highlighter-rouge">substring($input, 1, 1)</code> で先頭1文字を取り出し変数 <code class="language-plaintext highlighter-rouge">c</code> に保存します。
そして次の命令に渡す <code class="language-plaintext highlighter-rouge">input</code> も <code class="language-plaintext highlighter-rouge">substring($input, 2)</code> のように2文字目以降を切り出しておきます。</p>

<p>取り出した先頭文字 <code class="language-plaintext highlighter-rouge">c</code> は、出力のときと同じように <code class="language-plaintext highlighter-rouge">&lt;xsl:choose&gt;</code> で文字種ごとに分岐してASCIIコードの数値に変換します。
このとき、扱えない制御文字やASCIIコード外の文字は、すべて <code class="language-plaintext highlighter-rouge">255</code> とすることにします。
この値を書き込んだメモリと、1文字削った <code class="language-plaintext highlighter-rouge">input</code> を引数にして、次の命令のテンプレートを呼び出したら完了です。</p>

<p>▼リスト17 read命令のテンプレート</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"read"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"ptr"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;xsl:param</span> <span class="na">name=</span><span class="s">"input"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"concat('_', $ptr)"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;xsl:choose&gt;</span>
      <span class="c">&lt;!-- inputが空のときは255(EOF)を書き込む --&gt;</span>
      <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"string-length($input)=0"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;xsl:call-template</span> <span class="na">name=</span><span class="s">"write-val"</span><span class="nt">&gt;</span>
              <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
              <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"$key"</span><span class="nt">/&gt;</span>
              <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"255"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;/xsl:call-template&gt;</span>
          <span class="nt">&lt;/xsl:with-param&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;/xsl:apply-templates&gt;</span>
      <span class="nt">&lt;/xsl:when&gt;</span>

      <span class="c">&lt;!-- inputが空でないとき1文字取り出してメモリに書き込む --&gt;</span>
      <span class="nt">&lt;xsl:otherwise&gt;</span>
        <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"c"</span> <span class="na">select=</span><span class="s">"substring($input, 1, 1)"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"substring($input, 2)"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"val"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:choose&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='&amp;#9;'"</span><span class="nt">&gt;</span>9<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='&amp;#10;'"</span><span class="nt">&gt;</span>10<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='&amp;#13;'"</span><span class="nt">&gt;</span>13<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c=' '"</span><span class="nt">&gt;</span>32<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='!'"</span><span class="nt">&gt;</span>33<span class="nt">&lt;/xsl:when&gt;</span>
            (中略)
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='}'"</span><span class="nt">&gt;</span>125<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:when</span> <span class="na">test=</span><span class="s">"$c='~'"</span><span class="nt">&gt;</span>126<span class="nt">&lt;/xsl:when&gt;</span>
            <span class="nt">&lt;xsl:otherwise&gt;</span>255<span class="nt">&lt;/xsl:otherwise&gt;</span>
          <span class="nt">&lt;/xsl:choose&gt;</span>
        <span class="nt">&lt;/xsl:variable&gt;</span>

        <span class="nt">&lt;xsl:variable</span> <span class="na">name=</span><span class="s">"mem"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:call-template</span> <span class="na">name=</span><span class="s">"write-val"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"key"</span> <span class="na">select=</span><span class="s">"$key"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"val"</span> <span class="na">select=</span><span class="s">"$val"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;/xsl:call-template&gt;</span>
        <span class="nt">&lt;/xsl:variable&gt;</span>

        <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"following-sibling::*[1]"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"ptr"</span> <span class="na">select=</span><span class="s">"$ptr"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"mem"</span> <span class="na">select=</span><span class="s">"$mem"</span><span class="nt">/&gt;</span>
          <span class="nt">&lt;xsl:with-param</span> <span class="na">name=</span><span class="s">"input"</span> <span class="na">select=</span><span class="s">"$input"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;/xsl:apply-templates&gt;</span>
      <span class="nt">&lt;/xsl:otherwise&gt;</span>
    <span class="nt">&lt;/xsl:choose&gt;</span>
  <span class="nt">&lt;/xsl:template&gt;</span>
</code></pre></div></div>

<p>以上で、Brainfuckの8種類の命令をすべて実装することができました。
実際に動くかどうかは、ぜひお手元で確認してみてください。</p>

<h2 id="まとめと展望">まとめと展望</h2>

<p>この章では、XSLTによるBrainfuckの処理系の実装例を紹介しました。
実際に実装できたことからXSLTはチューリング完全であり、理論的には任意の計算が可能で、
文書の変換や操作にとどまらない強力なツールになりえる可能性があります。</p>

<p>しかし、これを実用するには多くの課題があります。
実際パフォーマンスも悪く、複雑なプログラム<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>は途中で強制終了してしまうことも珍しくありません。
また文法も、この章を読んでいただいた方なら感じられたと思いますが、人間が読み書きするのに向いておらず、
デバッグやエラーハンドリングの面でも制約しかありません。
くわえて、XSLT 2.0や3.0がW3Cより勧告<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>されてすでに何年も経っているのですが、
主要ブラウザでは未だサポートされておらず、今後の発展は期待しにくいでしょう。</p>

<p>一方でXSLTによるプログラミングでは、パターンマッチングや再帰呼び出しといった宣言型・関数型のパラダイムを強制されることになります。
プログラミング用途としてはまったく実用には向きませんが、文法の厳しさも含めて、奇特な方向けのトレーニングにはうってつけの言語といえるでしょう。
みなさんもぜひXSLTでのプログラミングにチャレンジしてみてください。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="/2023/05/22/klabtechbook1-bfm4.ja.html">テキストマクロプロセッサ「M4」のチューリング完全性について</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>原義的には、計算可能性を定義するためにチューリングマシンが利用されています。 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>単純に「<code class="language-plaintext highlighter-rouge">,+.,+.</code>」のようにも書けますが、ループの例を示すためにあえて複雑にしています <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>XSLTでは同じ名前の変数を同じスコープでは再定義できないのですが、この時点での<code class="language-plaintext highlighter-rouge">mem</code>はテンプレートのパラメータでありスコープが異なるので、同じ名前で定義できます <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>たとえば、リポジトリの <code class="language-plaintext highlighter-rouge">sample/toupper.xml</code> はブラウザや <code class="language-plaintext highlighter-rouge">xsltproc</code> では実行できません。 <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><a href="https://www.w3.org/TR/xslt20/">https://www.w3.org/TR/xslt20/</a>、<a href="https://www.w3.org/TR/xslt-30/">https://www.w3.org/TR/xslt-30/</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2024年11月2日から開催された技術書典17にて頒布した「KLabTechBook Vol. 14」に掲載したものです。]]></summary></entry><entry><title type="html">WSL2のファイル変更監視問題とfspollを作った話</title><link href="http://makiuchi-d.github.io/2025/03/30/arelo-fspoll.ja.html" rel="alternate" type="text/html" title="WSL2のファイル変更監視問題とfspollを作った話" /><published>2025-03-30T00:00:00+00:00</published><updated>2025-03-30T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2025/03/30/arelo-fspoll.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2025/03/30/arelo-fspoll.ja.html"><![CDATA[<p><a href="https://github.com/makiuchi-d/arelo">arelo</a>ではファイルの更新検知に<a href="https://pkg.go.dev/github.com/fsnotify/fsnotify">fsnotify</a>を利用していますが、
WSL2で利用している時にWindows側ファイルシステムのイベントが受け取れない問題がありました。
<a href="https://github.com/microsoft/WSL/issues/4739">この問題はWSL2の制限として知られています</a>。</p>

<p>そこで、このような環境でも動作するようにするために、
ファイルの更新日時などを定期的にチェックして変更を検知するポーリング機能を追加することにしました。</p>

<p>この記事では、ポーリング機能実装時に気をつけていたgoroutineの扱い方について紹介します。</p>

<h2 id="既存の解決策とその問題">既存の解決策とその問題</h2>

<p>areloと同じく自動リロードを提供する<a href="https://github.com/air-verse/air">air</a>は既にポーリング機能を実装していました。
areloでも同様に実装しようとしましたが、airがポーリングに利用している<a href="https://pkg.go.dev/github.com/gohugoio/hugo/watcher/filenotify">fugoのfilenotify</a>は
fsnotifyと互換がありますが、いくつかの点で挙動が異なっていました。</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">filePoller.Close()</code>したあとEvents・Errorsチャネルがcloseされない</li>
  <li>ディレクトリを監視している時、そのディレクトリのchmodイベントが発火しない</li>
  <li>監視対象が削除されたり<code class="language-plaintext highlighter-rouge">filePoller.Remove()</code>を呼んでも監視が止まらない</li>
</ol>

<p>このうち1は、areloではCloseを呼ばないので問題ありません。
2についてもディレクトリのchmodイベントでリロードしたいケースはあまり多くないので許容範囲でしょう。
しかし、3が問題でした。</p>

<p>airやareloではディレクトリを再帰的に監視対象として追加していきます。
そしてfilenotifyでは監視対象を追加するごとにgoroutineを立ち上げていて、それが消えることはありません。
つまり、ディレクトリの作成・削除あるいはリネームを繰り返すたび、どんどんgoroutineが増えていきます。
自動リロードツールを利用するのは主に開発の場面なので、gitのブランチ切り替えでディレクトリの作成・削除が頻発することは普通にありえるでしょう。
これは致命的です。</p>

<p>他に良さそうなライブラリもぱっとは見つからなかったので、じゃあ自作するしかありませんね！
ということで、fsnotifyと互換のファイルポーリングライブラリ <a href="https://pkg.go.dev/github.com/makiuchi-d/arelo/fspoll">github.com/makiuchi-d/arelo/fspoll</a> を実装しました。</p>

<h2 id="goroutineのリーク防止">goroutineのリーク防止</h2>

<p>fspollもfilenotifyと同じく監視対象ごとにgoroutineを立ち上げます。
ですが監視対象の削除やRemove呼び出しでgoroutineを確実に停止するようにしました。
また、Poller.Closeの呼び出し時にはすべての監視goroutineを停止します。</p>

<p>このような親子関係のあるgoroutine制御こそ<a href="https://pkg.go.dev/context">context.Context</a>の出番です。
しかし、fsnotifyには外部からContextを渡すインターフェイスがありません。
Goのベストプラクティスからは外れますが、fspollではPoller構造体初期化時に親となるContextとそれを停止するためのCancelFuncを作成し、そのまま保持するようにしました。</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
	<span class="n">p</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">Poller</span><span class="p">{</span>
		<span class="n">events</span><span class="o">:</span>     <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="n">Event</span><span class="p">,</span> <span class="m">1</span><span class="p">),</span>
		<span class="n">errors</span><span class="o">:</span>     <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">error</span><span class="p">,</span> <span class="m">1</span><span class="p">),</span>
		<span class="n">interval</span><span class="o">:</span>   <span class="n">interval</span><span class="p">,</span>
		<span class="n">ctx</span><span class="o">:</span>        <span class="n">ctx</span><span class="p">,</span>
		<span class="n">cancel</span><span class="o">:</span>     <span class="n">cancel</span><span class="p">,</span>
		<span class="n">cancellers</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">context</span><span class="o">.</span><span class="n">CancelFunc</span><span class="p">),</span>
	<span class="p">}</span>
</code></pre></div></div>

<p>監視goroutineを立ち上げるときは、context.WithCancelで子ContextとCancelFuncを作成します。
このCancelFuncはRemoveする時用にPollerが保持しておきます。
また、監視goroutineが終了するときに確実にCancelFuncも削除します。</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">ctx</span><span class="p">)</span>
	<span class="n">p</span><span class="o">.</span><span class="n">cancellers</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">cancel</span>

	<span class="n">ready</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{})</span>
	<span class="n">p</span><span class="o">.</span><span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">p</span><span class="o">.</span><span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="k">if</span> <span class="n">fi</span><span class="o">.</span><span class="n">IsDir</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">p</span><span class="o">.</span><span class="n">pollingDir</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">fi</span><span class="p">,</span> <span class="n">ready</span><span class="p">)</span>
		<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
			<span class="n">p</span><span class="o">.</span><span class="n">pollingFile</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">fi</span><span class="p">,</span> <span class="n">ready</span><span class="p">)</span>
		<span class="p">}</span>
		<span class="n">cancel</span><span class="p">()</span> <span class="c">// to prevent deadlock: ready might not be closed</span>
		<span class="n">_</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">Remove</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
	<span class="p">}()</span>
</code></pre></div></div>

<p>polling関数の中ではいくつかのチャネルの読み書きを行いますが、その全てで必ずcontext.Doneと合わせてselectし、
Context完了時は速やかにreturnしてgoroutineを終了します。</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">t</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">interval</span><span class="p">)</span>
	<span class="k">for</span> <span class="p">{</span>
		<span class="k">select</span> <span class="p">{</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
			<span class="k">return</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">t</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
		<span class="p">}</span>

		<span class="o">...</span><span class="n">略</span><span class="o">...</span>

		<span class="k">if</span> <span class="n">m</span><span class="p">,</span> <span class="n">s</span> <span class="o">:=</span> <span class="n">fi</span><span class="o">.</span><span class="n">ModTime</span><span class="p">(),</span> <span class="n">fi</span><span class="o">.</span><span class="n">Size</span><span class="p">();</span> <span class="n">m</span> <span class="o">!=</span> <span class="n">modt</span> <span class="o">||</span> <span class="n">s</span> <span class="o">!=</span> <span class="n">size</span> <span class="p">{</span>
			<span class="n">modt</span> <span class="o">=</span> <span class="n">m</span>
			<span class="n">size</span> <span class="o">=</span> <span class="n">s</span>
			<span class="k">if</span> <span class="o">!</span><span class="n">p</span><span class="o">.</span><span class="n">sendEvent</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">Write</span><span class="p">)</span> <span class="p">{</span>
				<span class="k">return</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}</span>
</code></pre></div></div>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">Poller</span><span class="p">)</span> <span class="n">sendEvent</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">name</span> <span class="kt">string</span><span class="p">,</span> <span class="n">op</span> <span class="n">Op</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">isClosed</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">return</span> <span class="no">false</span>
	<span class="p">}</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
		<span class="k">return</span> <span class="no">false</span>
	<span class="k">case</span> <span class="n">p</span><span class="o">.</span><span class="n">events</span> <span class="o">&lt;-</span> <span class="n">Event</span><span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="n">name</span><span class="p">,</span> <span class="n">Op</span><span class="o">:</span> <span class="n">op</span><span class="p">}</span><span class="o">:</span>
		<span class="k">return</span> <span class="no">true</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>チャネルの読み書きは容易に待ち状態になりgoroutineリークの原因になる部分なので、
必ずselectと組み合わせてContext完了時に確実に抜けられるようにしましょう。</p>

<h2 id="チャネルのclose">チャネルのclose</h2>

<p>fsnotifyではWatcher.Close呼び出し後、EventsとErrorsのチャネルがcloseされます。
fspollのPollerもfsnotifyと挙動を合わせてチャネルをcloseするようにしました。</p>

<p>このとき監視goroutineが動いているままcloseしてしまうと、closeしたチャネルに書き込もうとしてpanicする可能性がでてきてしまいます。
これを避けるためにPoller.Close呼び出して親Contextが完了した後、WaitGroupを使って監視goroutineがすべて終了するのを待ってcloseしています。</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="o">&lt;-</span><span class="n">p</span><span class="o">.</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">p</span><span class="o">.</span><span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
		<span class="nb">close</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">events</span><span class="p">)</span>
		<span class="nb">close</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">errors</span><span class="p">)</span>
	<span class="p">}()</span>
</code></pre></div></div>

<h2 id="終わりに">終わりに</h2>

<p>fspollを実装したことで、areloでもWSL2環境でのファイル変更検知ができるようになりました。
fsnotifyとの互換性も高く、goroutineのリークもしないよう気を使った堅牢なものになっています。
この実装の工夫は基本に忠実なやり方なので、ぜひ参考にしてみてください。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[areloではファイルの更新検知にfsnotifyを利用していますが、 WSL2で利用している時にWindows側ファイルシステムのイベントが受け取れない問題がありました。 この問題はWSL2の制限として知られています。]]></summary></entry><entry><title type="html">完全にLinuxのみで確定申告する現時点で一番簡単な方法</title><link href="http://makiuchi-d.github.io/2025/03/22/e-tax-linux.ja.html" rel="alternate" type="text/html" title="完全にLinuxのみで確定申告する現時点で一番簡単な方法" /><published>2025-03-22T00:00:00+00:00</published><updated>2025-03-22T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2025/03/22/e-tax-linux.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2025/03/22/e-tax-linux.ja.html"><![CDATA[<p style="background-color:pink;border-left:0.3em solid red;padding:0.5em">
<strong>⚠ ID・パスワード方式はまもなく使えなくなりそうです</strong><br />
この記事で紹介しているID・パスワード方式に必要なIDとパスワードの新規発行が停止してしまいました。
すでに発行済みの方は令和7年分の申告にはまだつかえるようですが、近いうちに廃止されると思われます。
おとなしくUser-Agentを偽装して国を騙しながら申告しましょう。<br />
<a href="https://www.e-tax.nta.go.jp/topics/2025/topics_20250925.htm">
ID・パスワードの新規発行停止について| 【e-Tax】国税電子申告・納税システム(イータックス)
</a>
</p>

<p>少し前に<a href="https://qiita.com/nanbuwks/items/3ceb0b3f8e15a8aa3dbf">Linuxで確定申告 2024年度版</a>が話題になっていましたが、
私がここ数年Linuxのみ（Kubuntu + Google Chrome）で行っている、一番簡単な確定申告の方法を紹介します。</p>

<p>User-Agent偽装などすることなく、なんならスマホもマイナンバーカードも使うことなく、完全にLinuxのみで電子送信までする方法です。</p>

<p>その方法とは、<strong>「ID・パスワード方式」</strong>を利用することです。</p>

<h2 id="idパスワード方式とは">ID・パスワード方式とは</h2>

<p><a href="https://www.e-tax.nta.go.jp/kojin/idpw.htm">ID・パスワード方式について| 【e-Tax】国税電子申告・納税システム(イータックス)</a></p>

<p>基本的な内容はe-Taxのページを見ていただきたいのですが、
簡単に言えば、あらかじめ発行しておいたIDとパスワードを使って
「確定申告書等作成コーナー」で作成した申告書を直接電子送信する方法です。</p>

<p>マイナカードによる署名が不要となるため、マイナポータルとの連携も不要になり、
そこで障害となっていたUser-Agentによる制限も関係なくなるわけです。</p>

<p>ただし、e-Taxのページには次のように書かれています：</p>

<blockquote>
  <p>ID・パスワード方式は、マイナンバーカード及びICカードリーダライタが普及するまでの暫定的な対応ですので、 マイナンバーカードの取得をご検討ください。</p>
</blockquote>

<p>マイナカードを既に持っていてもID・パスワード方式を利用することはもちろんできます。
今のところ新規のID・パスワードの申請もまだできるようなので、少なくとも今年分は使えるのではないかなと薄く期待しています。</p>

<h2 id="idパスワードの発行">ID・パスワードの発行</h2>

<p>ID・パスワードの発行手順は2種類あります</p>

<ol>
  <li>税務署に行って申請する</li>
  <li>マイナカードを使ってWEBから申請する</li>
</ol>

<p>マイナカードを使った申請はおそらく確定申告と同じ轍を踏むので、税務署に赴いて申請するのがLinuxユーザとしては簡単です。
それにしても、マイナカード等の普及までの暫定措置と言いつつ、マイナカードを使って申請ができるのは不思議ですね。</p>

<p>免許証やマイナカード等の本人確認書類を持って税務署に行き、対面で本人確認をすることでID・パスワードを発行してもらいます。
このとき（私が申請したときは）自分で入力したパスワードがそのまま印刷された紙を渡されて、
職員さんと二人で目視でパスワードをチェックするという、信じがたいフローがあったので注意してください。</p>

<p>こうして発行したID・パスワードは翌年以降もずっと利用できます。
毎年税務署に通うようなことにはならないので安心してください。</p>

<h2 id="idパスワードの利用">ID・パスワードの利用</h2>

<p>e-Taxのページにあるとおり、「確定申告書等作成コーナー」でID・パスワード方式を選択して認証したら、
あとは通常通り確定申告書の項目を入力していくだけで、そのまま何事もなく電子送信できます。
少なくとも私は2019年分以降この方法で電子送信しています。</p>

<h2 id="まとめ">まとめ</h2>

<p>完全にLinuxのみで確定申告する簡単な方法として、ID・パスワード方式を紹介しました。
Linuxで困ることとして確定申告を挙げるのはもうやめましょう。</p>

<p>とはいえ、この方式は一応暫定的な対応らしいので、今後いつサポートが終了するかは不透明です。
そのため両手を上げてのおすすめはできないのですが、現時点での一番簡単な方法だと思います。</p>

<p>そもそもとして、行政が特定企業の製品の使用を強制するのはよくありません。
あらゆるOSでの動作保証までしろとは全く言うつもりはありませんが、
余計な制限を設けず、動作確認済み環境以外での動作は自己責任ということにしてほしいと願います。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[⚠ ID・パスワード方式はまもなく使えなくなりそうです この記事で紹介しているID・パスワード方式に必要なIDとパスワードの新規発行が停止してしまいました。 すでに発行済みの方は令和7年分の申告にはまだつかえるようですが、近いうちに廃止されると思われます。 おとなしくUser-Agentを偽装して国を騙しながら申告しましょう。 ID・パスワードの新規発行停止について| 【e-Tax】国税電子申告・納税システム(イータックス)]]></summary></entry><entry><title type="html">TCP_NODELAYの効果を確かめる (KLabTechBook Vol.13)</title><link href="http://makiuchi-d.github.io/2024/11/16/klabtechbook13-tcp-nodelay.ja.html" rel="alternate" type="text/html" title="TCP_NODELAYの効果を確かめる (KLabTechBook Vol.13)" /><published>2024-11-16T00:00:00+00:00</published><updated>2024-11-16T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2024/11/16/klabtechbook13-tcp-nodelay.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2024/11/16/klabtechbook13-tcp-nodelay.ja.html"><![CDATA[<p>この記事は2024年5月25日から開催された<a href="https://techbookfest.org/event/tbf16">技術書典16</a>にて頒布した「<a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5">KLabTechBook Vol.13</a>」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf17">技術書典17</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/bZpYWjnBQDRe15rq1JdqqU">KLabTechBook Vol.14</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2024/tbf17.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
合わせてごらんください。</p>

<p><a href="https://techbookfest.org/product/bZpYWjnBQDRe15rq1JdqqU"><img src="/images/2024-11-03/ktbv14.jpg" width="40%" alt="KLabTechBook Vol.14" /></a></p>

<hr />

<p>KLabでは独自のリアルタイム通信基盤「<a href="https://github.com/KLab/wsnet2">WSNet2</a>」を開発運用し、OSSとしても公開しています。
ある日、WSNet2のサーバーを海外に建て、手元のクライアントからのメッセージの応答時間を調べていたところ、
想定より妙に長い時間がかかっていることに気づきました。</p>

<p>WSNet2はメッセージの送受信にWebSocketを利用しています<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。
WebSocketはHTTPをベースとしていて、基本的にはTCPでパケットを送り合うことになります。
そしてこのとき見つけた遅延の原因はTCPの機能にありました。</p>

<p>この章では、遅延の原因となったTCPの機能である<strong>Nagleのアルゴリズム</strong>と<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>オプションについて解説し、
実際にどのような動きをするのか確認します。
加えて、Unity特有の事情についても解説します。</p>

<h2 id="tcpの小さなパケット問題">TCPの小さなパケット問題</h2>

<p>TCPではパケットが到達することをプロトコル自体で保証しています。
送信側はパケットのヘッダにシーケンス番号などの情報を付け加えておき、
また受信側は受信したことをACKというメッセージで送信側に通知します。
これらの情報を使って、未到達のパケットを自動で再送するようになっています。</p>

<p>このため、TCPでは1バイトのデータを送るだけでもTCPとIPのヘッダを合わせて40バイト以上送信することになります。
このような小さなパケットを多数送るような状況はtelnetセッションなどでよく発生し、
通信帯域の限られていた1980年代には輻輳崩壊の原因になりうるような無視できないオーバーヘッドだったようです。
この問題を回避する方法として、<a href="https://datatracker.ietf.org/doc/html/rfc896">RFC 896</a>が提案されました。</p>

<h2 id="nagleのアルゴリズムとtcp_nodelay">NagleのアルゴリズムとTCP_NODELAY</h2>

<p>RFC 896で提案された手法は、送信するデータをある程度バッファリングしてひとつのTCPパケットにまとめることで効率化するというものです。
データをまとめるかどうかの決定方法は、RFCの著者の名前をとってNagleのアルゴリズムと呼ばれています。</p>

<p>Nagleのアルゴリズムでは、次の条件を満たすまでデータをバッファリングして、ひとつのパケットにまとめます。</p>

<ol>
  <li>未送信のデータが最大セグメントサイズ<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>を超える</li>
  <li>未受信のACKがなくなる</li>
</ol>

<p>2024年05月12日時点の<a href="https://ja.wikipedia.org/wiki/Nagle%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0">日本語版Wikipedia</a>では条件に「タイムアウトになる」が加えられていますが間違いです。
元のRFCにはタイマーは不要と明記されていますし<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>、少なくともLinuxカーネルの実装にはタイムアウトはありません。</p>

<p>さて、WSNet2で問題になっていたのはサーバーを海外に建てているときでした。
物理的な距離によりレイテンシーが高く、ACKが届くのにも時間がかかっていました。</p>

<p>他にもACKが遅延する要因として、<a href="https://datatracker.ietf.org/doc/html/rfc1122">RFC 1122</a>に記載されているTCP遅延ACKという機能もあります。
これはACKの返答を一時的に遅らせ、複数のACK応答をまとめて返すことでプロトコルのオーバーヘッドを減らすものです。</p>

<p>これらの要因でACKが遅延したために、Nagleのアルゴリズムにより、
ACKが届くまでの間に送信したメッセージはTCPのレイヤーでバッファリングされてしまいました。
このバッファリングされている時間の分、アプリケーションからは応答時間が長くなっているように見えたわけです。</p>

<p>このようなケースに対応するために、Nagleのアルゴリズムを無効にするオプション<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>が用意されており、<code class="language-plaintext highlighter-rouge">setsockopt</code>関数で設定できます。</p>

<h2 id="実験">実験</h2>

<p>それでは実際にNagleのアルゴリズムの働きと<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>の効果を確認してみましょう。</p>

<h3 id="tcpサーバーとネットワーク遅延設定">TCPサーバーとネットワーク遅延設定</h3>

<p>まずはDockerを使って単純なTCPサーバーとネットワーク遅延環境を用意します。
DockerはLinuxなので、tcコマンド（traffic control）で通信遅延のエミュレートが簡単にできます。</p>

<p>最初にTCPサーバー用のコンテナを立ち上げます。
ネットワーク遅延を設定するためには、<code class="language-plaintext highlighter-rouge">--privileged</code>オプションの指定が必要です<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
また、名前を<code class="language-plaintext highlighter-rouge">tcpsv</code>としておきます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--name</span><span class="o">=</span>tcpsv <span class="nt">--privileged</span> alpine
</code></pre></div></div>

<p>コンテナが起動したら<code class="language-plaintext highlighter-rouge">tc</code>コマンドをインストールし、送信パケットを1秒遅延させるように設定します。
<code class="language-plaintext highlighter-rouge">tc</code>コマンドで指定するデバイス<code class="language-plaintext highlighter-rouge">eth0</code>は外部との通信に使われるネットワークインターフェイスです。
このコンテナから外部へ送信するパケットは遅延しますが、受信するものは遅延しないことに注意してください。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apk add iproute2-tc
tc qdisc add dev eth0 root netem delay 1s
</code></pre></div></div>

<p>TCPサーバーは<code class="language-plaintext highlighter-rouge">nc</code>コマンド（netcat）を使うことで簡単に用意できます。
次のように5000番ポートで待ち受けます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-l</span> <span class="nt">-p</span> 5000
</code></pre></div></div>

<h3 id="クライアントの実装">クライアントの実装</h3>

<p>指定サーバーにTCPで1文字ずつ送るプログラムを用意しました。
このソースコードは<a href="https://gist.github.com/makiuchi-d/f748ca25bd4089756faa45fe3af4ced0/raw/main.c">GitHub Gistにも置いてある</a>のでダウンロードしてお使いください。</p>

<p>▼リスト1 main.c</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;netdb.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/socket.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;netinet/tcp.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">&lt;=</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">printf</span><span class="p">(</span><span class="s">"usage: %s &lt;host&gt; &lt;port&gt; [nodelay]</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
		<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">struct</span> <span class="n">addrinfo</span> <span class="n">hint</span><span class="p">,</span> <span class="o">*</span><span class="n">addr</span><span class="p">;</span>
	<span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hint</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">hint</span><span class="p">));</span>
	<span class="n">hint</span><span class="p">.</span><span class="n">ai_family</span> <span class="o">=</span> <span class="n">AF_INET</span><span class="p">;</span>
	<span class="n">hint</span><span class="p">.</span><span class="n">ai_socktype</span> <span class="o">=</span> <span class="n">SOCK_STREAM</span><span class="p">;</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">getaddrinfo</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">hint</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">addr</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">perror</span><span class="p">(</span><span class="s">"getaddrinfo"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="kt">int</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span>
		<span class="n">addr</span><span class="o">-&gt;</span><span class="n">ai_family</span><span class="p">,</span> <span class="n">addr</span><span class="o">-&gt;</span><span class="n">ai_socktype</span><span class="p">,</span> <span class="n">addr</span><span class="o">-&gt;</span><span class="n">ai_protocol</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">sock</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">perror</span><span class="p">(</span><span class="s">"socket"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="cm">/* TCP_NODELAYの設定 */</span>
	<span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="p">(</span><span class="n">argc</span> <span class="o">&gt;</span> <span class="mi">3</span><span class="p">)</span> <span class="o">?</span> <span class="n">atoi</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span> <span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">sock</span><span class="p">,</span> <span class="n">SOL_TCP</span><span class="p">,</span> <span class="n">TCP_NODELAY</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">n</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">n</span><span class="p">))</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">perror</span><span class="p">(</span><span class="s">"setsockopt"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="p">(</span><span class="n">connect</span><span class="p">(</span><span class="n">sock</span><span class="p">,</span> <span class="n">addr</span><span class="o">-&gt;</span><span class="n">ai_addr</span><span class="p">,</span> <span class="n">addr</span><span class="o">-&gt;</span><span class="n">ai_addrlen</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">perror</span><span class="p">(</span><span class="s">"connect"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="cm">/* 1文字ずつ100ms間隔で送信 */</span>
	<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="mi">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
		<span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="sc">'0'</span> <span class="o">+</span> <span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">10</span><span class="p">);</span>
		<span class="n">write</span><span class="p">(</span><span class="n">sock</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">c</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
		<span class="n">usleep</span><span class="p">(</span><span class="mi">100000</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="n">close</span><span class="p">(</span><span class="n">sock</span><span class="p">);</span>
	<span class="n">freeaddrinfo</span><span class="p">(</span><span class="n">addr</span><span class="p">);</span>

	<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>クライアント用のコンテナを立ち上げ、<code class="language-plaintext highlighter-rouge">gcc</code>でコンパイルします。
サーバーコンテナ<code class="language-plaintext highlighter-rouge">tcpsv</code>にアクセスしやすいよう<code class="language-plaintext highlighter-rouge">--link</code>も指定しておきます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--link</span><span class="o">=</span>tcpsv alpine

apk add gcc libc-dev
wget https://gist.github.com/makiuchi-d/f748ca25bd4089756faa45fe3af4ced0/raw/main.c
gcc <span class="nt">-o</span> tcpcl main.c
</code></pre></div></div>

<p>ビルドした<code class="language-plaintext highlighter-rouge">tcpcl</code>コマンドは引数でサーバーとポートを指定して実行します。
サーバーコンテナには<code class="language-plaintext highlighter-rouge">tcpsv</code>という名前でアクセスできます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./tcpcl tcpsv 5000
</code></pre></div></div>

<p>サーバーコンテナの画面に0~9の数字が順に表示されることが確認できるはずです。
クライアントが終了すると<code class="language-plaintext highlighter-rouge">nc</code>も停止するので、サーバーコンテナ側で毎回<code class="language-plaintext highlighter-rouge">nc</code>コマンドを実行しなおしてください<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>。</p>

<h3 id="通信内容の確認">通信内容の確認</h3>

<p>LinuxマシンでDockerを使っている場合は簡単です。
ホストの<code class="language-plaintext highlighter-rouge">docker0</code>インターフェイスを<a href="https://www.wireshark.org/">Wireshark</a>などでキャプチャすることでコンテナ間の通信内容を見ることができます。</p>

<p><code class="language-plaintext highlighter-rouge">tcpcl</code>実行時の通信内容は図1のようになっています。</p>

<p><img src="/images/2024-11-16/cap-nagle.png" alt="Nagleのアルゴリズムの効果" />
▲図1 Nagleのアルゴリズムの効果</p>

<p>サーバーからのACKを受け取ってからデータを送信していて、Data部が<code class="language-plaintext highlighter-rouge">"0123456789"</code>の10文字になっているのがわかります。
また実行中にサーバーの画面をみていると、約10文字ごとに表示が進んでいくのが見て取れると思います。</p>

<p>続いて<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>を有効にしてみます。
<code class="language-plaintext highlighter-rouge">tcpcl</code>の3番目の引数に<code class="language-plaintext highlighter-rouge">1</code>を指定すると有効になります。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./tcpcl tcpsv 5000 1
</code></pre></div></div>

<p>図2のように、サーバーからのACKを待たずに1文字ずつ送信しているのが見て取れます。
サーバーの画面も1文字ずつ順に表示が進んでいくことが分かるはずです。</p>

<p><img src="/images/2024-11-16/cap-nodelay.png" alt="TCP_NODELAYの効果" />
▲図2 <code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>の効果</p>

<p>このように、Nagleのアルゴリズムによって1パケットにデータがまとめられている様子や、
<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>によってそれが無効になっている様子が確認できました。</p>

<h2 id="unity特有の事情">Unity特有の事情</h2>

<p>C#では<code class="language-plaintext highlighter-rouge">Socket</code>クラスの<code class="language-plaintext highlighter-rouge">NoDelay</code>プロパティによって<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>の有効無効を設定できます。
このプロパティをセットすると、内部では最終的に<code class="language-plaintext highlighter-rouge">setsockopt</code>関数がよばれます。</p>

<p>WSNet2のC#クライアント実装では、標準ライブラリの<code class="language-plaintext highlighter-rouge">System.Net.WebSockets.ClientWebSocket</code>を使用しています。
現在公式サポートされているUnityの<a href="https://docs.unity3d.com/ja/2023.2/Manual/dotnetProfileSupport.html">C#ランタイムは.NET Framework 4.8相当</a>で、
とても残念なことに、WebSocket接続時の<code class="language-plaintext highlighter-rouge">NoDelay</code>は<code class="language-plaintext highlighter-rouge">false</code>になっています。
このために、冒頭で言及した応答時間調査のときにはNagleのアルゴリズムが有効になっていて余計な遅延が発生していました。</p>

<p>さらに酷いことに、<code class="language-plaintext highlighter-rouge">ClientWebSocket</code>クラスには内部の<code class="language-plaintext highlighter-rouge">Socket</code>にアクセスする手段がありません。
仕方がないので、WSNet2ではUnityの場合にはリフレクションを使って<code class="language-plaintext highlighter-rouge">Socket</code>を取り出し、<code class="language-plaintext highlighter-rouge">NoDelay</code>プロパティを<a href="https://github.com/KLab/wsnet2/blob/v0.6.1/wsnet2-unity/Assets/WSNet2/Scripts/Core/Connection.cs#L476-L521">設定するようにしました</a>。</p>

<p>.NET 5以降では、依然として<code class="language-plaintext highlighter-rouge">ClientWebSocket</code>から<code class="language-plaintext highlighter-rouge">NoDelay</code>を設定するインターフェイスは存在しないものの、
WebSocket接続時に<code class="language-plaintext highlighter-rouge">NoDelay</code>は<code class="language-plaintext highlighter-rouge">true</code>に設定されるので、Nagleのアルゴリズムによる遅延の心配はありません。</p>

<h2 id="まとめ">まとめ</h2>

<p>ここまで、Nagleのアルゴリズムと<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>オプションについて解説し、実際の動作を確認しました。
ゲームの協力プレイやオンライン対戦では、パケットをまとめることによる帯域の節約よりもリアルタイム性のほうが大切です。
このようなケースでは<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>を設定したほうがよいでしょう。
また、想定以上の遅延が見られたときは<code class="language-plaintext highlighter-rouge">TCP_NODELAY</code>が設定されているか確認してみてください。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>WSNet2のWSはWebSocketの略です <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>TCPの1つのパケットに載せられる最大サイズ <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>原文: <em>This inhibition is to be unconditional; no timers, tests for size of data received, or other conditions are required.</em> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>userns-remapを設定している場合は<code class="language-plaintext highlighter-rouge">--userns=host</code>の指定も必要です。userns-remapについてはKLabTechBook Vol.11「<a href="/2024/06/08/klabtechbook11-docker-userns-remap.ja.html">Dockerを使うなら当然userns-remapしてるよね！</a>」をご覧ください。 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>ncを終了するためにサーバーコンテナ側で何度かエンターキーを入力する必要があることがあります <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2024年5月25日から開催された技術書典16にて頒布した「KLabTechBook Vol.13」に掲載したものです。]]></summary></entry><entry><title type="html">GitHub Actionsをローカル環境で実行する「act」(KLabTechBook Vol.12)</title><link href="http://makiuchi-d.github.io/2024/11/03/klabtechbook12-act.ja.html" rel="alternate" type="text/html" title="GitHub Actionsをローカル環境で実行する「act」(KLabTechBook Vol.12)" /><published>2024-11-03T00:00:00+00:00</published><updated>2024-11-03T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2024/11/03/klabtechbook12-act.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2024/11/03/klabtechbook12-act.ja.html"><![CDATA[<p>この記事は2023年11月11日から開催された<a href="https://techbookfest.org/event/tbf15">技術書典15</a>にて頒布した「<a href="https://techbookfest.org/product/d20GG5Femwp1rTWSveSiHF">KLabTechBook Vol.12</a>」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf17">技術書典17</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/bZpYWjnBQDRe15rq1JdqqU">KLabTechBook Vol.14</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2024/tbf17.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
合わせてごらんください。</p>

<p><a href="https://techbookfest.org/product/bZpYWjnBQDRe15rq1JdqqU"><img src="/images/2024-11-03/ktbv14.jpg" width="40%" alt="KLabTechBook Vol.14" /></a></p>

<hr />

<h2 id="github-actions-と-act">GitHub Actions と act</h2>

<p>GitHub ActionsはGitHubに統合されたCI/CDプラットフォームです。
リポジトリへのPushやPull Requestなどのイベントと関連付けて、自動テストやビルド、デプロイといったさまざまなワークフローを自動実行できます。</p>

<p>しかし、GitHub Actionsのワークフローを起動するには、コードをコミットしてリポジトリにPushしなければなりません。
このため、新しいワークフローを作成するときなど、動作確認のために毎回Pushする必要があり煩雑です。
また、単純なエラー、例えばタイポやコーディングスタイルのミスなどは、リポジトリにPushする前に検出したいところです。</p>

<p>そこで登場するのが「<a href="https://github.com/nektos/act">act</a>」というツールです。
actを使えばGitHub Actionsのワークフローの各ジョブをローカル環境で実行でき、
リポジトリへコミットやPushをすることなく、事前にエラー検知できるようになります。
これにより、大人数での開発プロジェクトで起こりがちなジョブのランナーの枯渇も軽減できるでしょう。</p>

<p>この章では、GitHub Actionsをローカル環境で実行する「act」について、
基本的な使い方と制限事項、便利に使うためのTipsを紹介します。</p>

<h2 id="actのインストール">actのインストール</h2>

<h3 id="dockerの用意">Dockerの用意</h3>

<p>actはGitHub ActionsのジョブのランナーをDockerコンテナによって再現するため、事前にDockerをインストールしておく必要があります。
Docker Desktop、またはその他のDocker環境をセットアップしてください。</p>

<h3 id="actのインストール-1">actのインストール</h3>

<p>actのインストールはパッケージマネージャを使うと簡単です。
macOSやLinuxでよく使われているHomebrew、WindowsのChocolateyやWingetの他、
多くのパッケージマネージャに登録されています<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。
ご自身の環境にあわせて選ぶとよいでしょう。
リスト1とリスト2にHomebrewとChocolateyの例を示します。</p>

<p>▼リスト1 Homebrewによるインストール（Linux、macOS）</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>act
</code></pre></div></div>

<p>▼リスト2 Chocolateyによるインストール（Windows）</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>choco <span class="nb">install </span>act-cli
</code></pre></div></div>

<p>また、actはGo言語で実装されているため、Goの開発環境が整っている場合はリスト3のようにインストールすることもできます。</p>

<p>▼リスト3 Goによるインストール</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go <span class="nb">install </span>github.com/nektos/act@latest
</code></pre></div></div>

<h3 id="コンテナイメージの準備">コンテナイメージの準備</h3>

<p>actがインストールできたのでさっそく動かしたいところですが、その前に使用するコンテナイメージを準備しましょう。
actによって自動生成される設定では若干不都合があるので、リスト4のように設定ファイル<code class="language-plaintext highlighter-rouge">.actrc</code>を用意します<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。</p>

<p>▼リスト4 <code class="language-plaintext highlighter-rouge">.actrc</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest
-P ubuntu-22.04=ghcr.io/catthehacker/ubuntu:act-22.04
-P ubuntu-20.04=ghcr.io/catthehacker/ubuntu:act-20.04
</code></pre></div></div>

<p>このファイルはact実行時に毎回指定するコマンドラインオプションを記載するもので、
<code class="language-plaintext highlighter-rouge">-P</code> (<code class="language-plaintext highlighter-rouge">--platform</code>)オプションでジョブを実行するコンテナのイメージを指定しています。</p>

<p>また、actの実行によってコンテナイメージをダウンロードした場合、その進捗が表示されません。
あらかじめ<code class="language-plaintext highlighter-rouge">docker</code>コマンドでダウンロードしておく方が安心できます。</p>

<p>▼リスト5 dockerコマンドでのイメージのダウンロード</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull ghcr.io/catthehacker/ubuntu:act-latest
docker pull ghcr.io/catthehacker/ubuntu:act-22.04
docker pull ghcr.io/catthehacker/ubuntu:act-20.04
</code></pre></div></div>

<h2 id="actの実行">actの実行</h2>

<h3 id="ジョブの一覧">ジョブの一覧</h3>

<p>それでは、actを使ってみましょう。
最初にジョブ一覧を表示する<code class="language-plaintext highlighter-rouge">-l</code> (<code class="language-plaintext highlighter-rouge">--list</code>)オプションを紹介します。
リポジトリのルートディレクトリで<code class="language-plaintext highlighter-rouge">act -l</code>としてみます。</p>

<p><img src="/images/2024-11-03/act-list.png" alt="ジョブ一覧" />
▲図1 <code class="language-plaintext highlighter-rouge">ジョブ一覧</code></p>

<p>これはKLabのOSS、<a href="https://github.com/KLab/wsnet2">WSNet2</a>のジョブ一覧です。
リポジトリの<code class="language-plaintext highlighter-rouge">.github/workflow</code>以下のyamlファイルを読み取り、一覧化してくれます。</p>

<p>actでジョブを実行するときは、ここに表示されているJob IDやWorkflow file、Eventsの値を指定することになるので、
このコマンドで確認するとよいでしょう。</p>

<h3 id="ジョブの実行">ジョブの実行</h3>

<p><code class="language-plaintext highlighter-rouge">act</code>をパラメータなしで実行すると、すべてのジョブが実行されます。
しかし、これではログが見にくいことに加え、ローカルでの確認ではGitHubのイベントやJob IDを指定して必要なジョブだけ実行したいケースが多いでしょう。</p>

<p>たとえば、WSNet2で.NETアプリのビルドとテストを行う<code class="language-plaintext highlighter-rouge">dotnet</code>ジョブを実行するにはリスト6のようにします。</p>

<p>▼リスト6 actでdotnetジョブを実行</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>act pull_request <span class="nt">-j</span> dotnet <span class="nt">-W</span> .github/workflow/wsnet2-dotnet.yml
</code></pre></div></div>

<p>パラメータの<code class="language-plaintext highlighter-rouge">pull_request</code>でイベントを指定し、<code class="language-plaintext highlighter-rouge">-j</code> (<code class="language-plaintext highlighter-rouge">--job</code>)オプションでジョブを指定しています。
この場合<code class="language-plaintext highlighter-rouge">dotnet</code>ジョブを起動するイベントは<code class="language-plaintext highlighter-rouge">pull_request</code>しか定義されていないため、イベントの指定は省略できます。
また、<code class="language-plaintext highlighter-rouge">-W</code> (<code class="language-plaintext highlighter-rouge">--workflows</code>)オプションは複数のyamlファイルでワークフローを定義しているときに、どのyamlファイルのジョブかを識別するために必要になります。
WSNet2では<code class="language-plaintext highlighter-rouge">wsnet2-dotnet.yml</code>の他に<code class="language-plaintext highlighter-rouge">wsnet2-dashboard.yml</code>、<code class="language-plaintext highlighter-rouge">wsnet2-server.yml</code>が存在するため指定が必要です。</p>

<p>ここで、<code class="language-plaintext highlighter-rouge">dotnet</code>ジョブの中身をリスト7に示します。</p>

<p>▼リスト7 <code class="language-plaintext highlighter-rouge">wsnet2-dotnet.yml</code></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">WSNet2 dotnet ci</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>
    <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">.github/workflows/wsnet2-dotnet.yml'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">wsnet2-dotnet/**'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">wsnet2-unity/**'</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">dotnet</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ubuntu-latest"</span>
    <span class="na">defaults</span><span class="pi">:</span>
      <span class="na">run</span><span class="pi">:</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">wsnet2-dotnet</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup .NET</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-dotnet@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">dotnet-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">6.x"</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dotnet build</span>
        <span class="na">run</span><span class="pi">:</span>  <span class="s">dotnet build WSNet2.sln</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dotnet test</span>
        <span class="na">run</span><span class="pi">:</span>  <span class="s">dotnet test WSNet2.sln</span>
</code></pre></div></div>

<p>まず注目していただきたいのが13行目の<code class="language-plaintext highlighter-rouge">runs-on: "ubuntu-latest"</code>です。
この行でジョブのランナーを指定しています。</p>

<p>ここで、<code class="language-plaintext highlighter-rouge">.actrc</code>に記載した<code class="language-plaintext highlighter-rouge">-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest</code>を思い出してください。
この<code class="language-plaintext highlighter-rouge">-P</code>オプションが、<code class="language-plaintext highlighter-rouge">runs-on</code>に指定された<code class="language-plaintext highlighter-rouge">ubuntu-latest</code>でのジョブの実行に使うコンテナイメージの指定になっています。</p>

<p>こうして指定されたイメージのコンテナを起動したら、actはyamlの17行目以降に指定された各ステップを順に実行していきます。</p>

<p><img src="/images/2024-11-03/act-run-dotnet-1.png" alt="ジョブの実行" />
▲図2 ジョブの実行</p>

<p>actは基本的には<code class="language-plaintext highlighter-rouge">steps</code>に定義されたアクションやコマンドをそのまま実行しますが、<code class="language-plaintext highlighter-rouge">actions/checkout</code>だけは例外です。
デフォルトではローカルのファイルを<code class="language-plaintext highlighter-rouge">docker cp</code>でコンテナ内に転送します<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>。
これによりローカルの変更をリポジトリにコミットやPushしなくても、そのまま実行されるジョブの対象とできるわけです。</p>

<p><img src="/images/2024-11-03/act-run-dotnet-2.png" alt="ジョブの完了" />
▲図3 ジョブの完了</p>

<p>ジョブが完了すると、<code class="language-plaintext highlighter-rouge">Job succeeded</code>のログが表示されてコンテナも終了します。
エラーが発生したときはコンテナが消えずに残るため、アタッチして原因を究明できます。
もしエラー時にもコンテナを削除したい場合は<code class="language-plaintext highlighter-rouge">--rm</code>オプションを指定してください。</p>

<p>このほか詳しい使い方は<a href="https://nektosact.com/">ユーザーガイド</a>をご覧ください。</p>

<h2 id="actの制限">actの制限</h2>

<p>ここまで紹介したように、actはDockerコンテナによってジョブの実行環境を再現します。
GitHubがホストするランナーはUbuntuだけでなくWindowsやmacOSも提供されていますが、
Dockerコンテナは基本的にはLinuxであるため、actではUbuntuのみをサポートします<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
加えて、GitHubが提供するランナーと完全に同じ環境ではないため、動かないアクションもあるかもしれません。</p>

<p>また、サービスコンテナやジョブのタイムアウトなどいくつか<a href="https://nektosact.com/not_supported.html">未実装の機能</a>があります。</p>

<p>サービスコンテナについては、手動でコンテナを起動することで一応対処できます。
ジョブ実行コンテナのネットワークモードはhostになっているため、
サービスコンテナ起動時に<code class="language-plaintext highlighter-rouge">-p</code> (<code class="language-plaintext highlighter-rouge">--publish</code>)オプションでポートをホストに公開しておくことで、
ジョブ実行コンテナからも<code class="language-plaintext highlighter-rouge">localhost</code>の該当ポートでサービスコンテナにアクセスできます。</p>

<h2 id="その他のtips">その他のTips</h2>

<p>ここで、actで使える便利なTipsをいくつか紹介します。</p>

<h3 id="シークレットの受け渡し">シークレットの受け渡し</h3>

<p>GitHub上で保存したシークレットの値や<code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code>は、当然ながらactからは参照できません。
これらの値を必要とするジョブを実行するには、<code class="language-plaintext highlighter-rouge">-s</code> (<code class="language-plaintext highlighter-rouge">--secret</code>}または<code class="language-plaintext highlighter-rouge">--secret-file</code>オプションによって値を受け渡します。</p>

<p>このとき、コマンドラインで<code class="language-plaintext highlighter-rouge">act -s MY_SECRET=value</code>のように値を指定してしまうと、
コマンド履歴ファイルなどにシークレットの値が保存されてしまう可能性があり好ましくありません。
代わりに<code class="language-plaintext highlighter-rouge">act -s MY_SECRET</code>のようにして、コマンドラインでは値を指定せずにセキュアな対話インターフェイスで値を入力する方法が推奨されています<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>。</p>

<h3 id="actでの実行ではスキップする">actでの実行ではスキップする</h3>

<p>actで実行しているときは特定のジョブやステップをスキップしたいこともあるでしょう。
actはジョブを実行するときに環境変数<code class="language-plaintext highlighter-rouge">ACT=true</code>を設定します。
これを利用して、リスト8のようにジョブやステップに<a href="https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idif"><code class="language-plaintext highlighter-rouge">if</code>条件文</a>を書くことでスキップできます。</p>

<p>▼リスト8 actでの実行時はスキップする</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">my-job</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "スキップされない"</span>

      <span class="pi">-</span> <span class="na">if</span><span class="pi">:</span> <span class="s">${{ !env.ACT }}</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">echo "スキップされる"</span>

      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "スキップされない"</span>
</code></pre></div></div>

<h3 id="セルフホステッドランナーへの対応">セルフホステッドランナーへの対応</h3>

<p>GitHub Actionsでは、自身で用意したマシンでジョブを実行するセルフホステッドランナーもサポートされています。
セルフホステッドランナーでジョブを実行するには、<code class="language-plaintext highlighter-rouge">runs-on</code>に<code class="language-plaintext highlighter-rouge">self-hosted</code>を指定します。</p>

<p>actでセルフホステッドランナーに対応するには、まずランナーとなるコンテナイメージを用意します。
そのうえで、act実行時に<code class="language-plaintext highlighter-rouge">-P self-hosted=イメージ名</code> のようにコンテナイメージを指定します。
このとき、actは指定のイメージの最新版がないかオンラインのコンテナリポジトリを確認するのですが、
リポジトリ上にイメージが存在しない場合はエラーになってしまいます。
ローカルにしかないイメージを使うときは<code class="language-plaintext highlighter-rouge">--pull=false</code>オプションを指定して、最新版の確認をスキップする必要があります。</p>

<h3 id="dockerコマンド使用時の注意点"><code class="language-plaintext highlighter-rouge">docker</code>コマンド使用時の注意点</h3>

<p>actで実行されるコンテナ内でも<code class="language-plaintext highlighter-rouge">docker</code>コマンドを使用できます。
ただし、接続するDockerデーモンはコンテナ内ではなくactを実行しているホストで動いているものです<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>。
このため、ポートなどのリソースの競合やバインドマウントするときのパスなどに注意する必要があります。</p>

<h3 id="dockerでuserns-remapを有効にしている場合">Dockerでuserns-remapを有効にしている場合</h3>

<p>前巻 KLabTechBook Vol.11 第1章「<a href="/2024/06/08/klabtechbook11-docker-userns-remap.ja.html">Dockerを使うなら当然userns-remapしてるよね！</a>」で紹介したように、
LinuxでDockerを利用するときは<code class="language-plaintext highlighter-rouge">userns-remap</code>を有効にして一般ユーザーの名前空間でコンテナを動かすことをお勧めしています。</p>

<p>一方、actはコンテナをホストの名前空間で実行することを要求するので、コンテナ起動時に<code class="language-plaintext highlighter-rouge">--userns=host</code>オプションを指定する必要があります。
actでのジョブ実行時のオプションに<code class="language-plaintext highlighter-rouge">--container-options --userns=host</code>を追加することでこれを実現できます。
この設定を<code class="language-plaintext highlighter-rouge">.actrc</code>ファイルに記載しておくとよいでしょう。</p>

<h2 id="おわりに">おわりに</h2>

<p>この章では、GitHub Actionsをローカル環境で実行する「act」というツールについて、基本的な使い方を紹介しました。</p>

<p>GitHub Actionsはそれ自体でも十分すぎるほど強力なツールですが、
actを活用することでより効率的に使用できるでしょう。
ぜひ活用してみてください。</p>

<hr />

<h3 id="コラム-デフォルトのコンテナイメージ">コラム: デフォルトのコンテナイメージ</h3>

<p>コンテナイメージを何も指定せずにジョブを実行しようとすると、
図4のようにデフォルトのコンテナイメージを選択する画面が表示されます。</p>

<p><img src="/images/2024-11-03/act-select-image.png" alt="デフォルトイメージの選択画面" />
▲図4 デフォルトイメージの選択画面</p>

<p>act version 0.2.52において、この表示内容は古く、実際のイメージと乖離しています。</p>

<dl>
  <dt><strong>Large</strong></dt>
  <dd>
    利用されうるほぼすべてのツールがインストールされていますが、サイズは20GBどころか50GB以上あります。
    また現在はUbuntu 20.04と22.04がサポートされています。
  </dd>
  <dt><strong>Medium</strong></dt>
  <dd>
    アクションの起動に必要なツールのみインストールしてサイズは1.5GB以下に抑えられています。ほとんどのアクションが動作します。
  </dd>
  <dt><strong>Micro</strong></dt>
  <dd>
    表示のとおり、アクションを起動するためのNode.jsだけのイメージです。サイズも200MB未満です。
  </dd>
</dl>

<p>これらのイメージはDocker Hubにホストされています。</p>

<table>
  <tbody>
    <tr>
      <td>Large</td>
      <td><code class="language-plaintext highlighter-rouge">catthehacker/ubuntu:full-latest</code> など</td>
    </tr>
    <tr>
      <td>Medium</td>
      <td><code class="language-plaintext highlighter-rouge">catthehacker/ubuntu:act-latest</code> など</td>
    </tr>
    <tr>
      <td>Micro</td>
      <td><code class="language-plaintext highlighter-rouge">node:16-buster-slim</code> など</td>
    </tr>
  </tbody>
</table>

<p>actはジョブ実行時に最新のイメージがあるかを確認して自動的にpullするのですが、
このとき<a href="https://matsuand.github.io/docs.docker.jp.onthefly/docker-hub/download-rate-limit/">Docker Hubのダウンロード率制限</a>に引っかかることがあります。
<code class="language-plaintext highlighter-rouge">catthehacker/ubuntu</code>のイメージと同じものがGitHubコンテナレジストリにも登録されているので、
<code class="language-plaintext highlighter-rouge">ghcr.io/catthehacker/ubuntu</code>のイメージを指定することで、この制限を回避できます。</p>

<hr />
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Homebrew、MacPorts、Chocolatey、Scoop、Winget、AUR、COPR、Nix など <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>LinuxやmacOSでは<code class="language-plaintext highlighter-rouge">$HOME</code>（つまり<code class="language-plaintext highlighter-rouge">/home/ユーザ名/</code>）、Windowsでは<code class="language-plaintext highlighter-rouge">%USERPROFILE%</code>（つまり<code class="language-plaintext highlighter-rouge">C:\USER\ユーザ名\</code>）に配置します <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">-b</code> (<code class="language-plaintext highlighter-rouge">--bind</code>)オプションを指定すると、バインドマウントによってファイルを同期するようにできます。 <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>筆者は試していませんが、実行環境が整っていればWindowsコンテナを使用してWindowsランナーを再現できるかもしれません。 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>このとき環境変数@<tt>{MY_SECRET}が定義されているとそちらが優先されることに注意が必要です。</tt> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p>ホストの<code class="language-plaintext highlighter-rouge">/var/run/docker.sock</code>がコンテナにバインドマウントされています。 <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2023年11月11日から開催された技術書典15にて頒布した「KLabTechBook Vol.12」に掲載したものです。]]></summary></entry><entry><title type="html">Docker を使うなら当然 userns-remap してるよね！ (KLabTechBook Vol.11)</title><link href="http://makiuchi-d.github.io/2024/06/08/klabtechbook11-docker-userns-remap.ja.html" rel="alternate" type="text/html" title="Docker を使うなら当然 userns-remap してるよね！ (KLabTechBook Vol.11)" /><published>2024-06-08T00:00:00+00:00</published><updated>2024-06-08T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2024/06/08/klabtechbook11-docker-userns-remap.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2024/06/08/klabtechbook11-docker-userns-remap.ja.html"><![CDATA[<p>この記事は2023年5月20日から開催された<a href="https://techbookfest.org/event/tbf14">技術書典14</a>にて頒布した「<a href="https://techbookfest.org/product/m6LUwU6LgC1FbEW3NVhw14">KLabTechBook Vol.11</a>」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf16">技術書典16</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5">KLabTechBook Vol.13</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2024/tbf16.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
合わせてごらんください。</p>

<p><a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5"><img src="/images/2024-05-29/ktbv13.jpg" width="40%" alt="KLabTechBook Vol.13" /></a></p>

<hr />

<p>Docker<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>は開発作業において今や無くてはならないツールとなっています。
ところで、コンテナを利用してファイルを生成したとき、
所有者がホストマシン上で<code class="language-plaintext highlighter-rouge">root</code>になってしまい困ったことはありませんか？
この章では<strong>userns-remap</strong><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>という機能を使って、ファイルの所有権問題を解決する方法を紹介します。</p>

<h2 id="dockerとは">Dockerとは</h2>

<p>Dockerは、アプリケーションを「コンテナ」と呼ばれる隔離環境で実行するプラットフォームです。
コンテナの中にアプリケーションの実行に必要なファイルや設定を詰め込むことで、ホストマシンの環境を変更すること無くアプリケーションを実行できます。
またこれらのファイルや設定は「イメージ」という形で保存でき、イメージを共有すれば異なるマシン上でも同一の環境を再現できます。</p>

<p>Dockerの利用場面は常駐するサービスの実行環境だけでなく、単発のアプリケーションを実行するものとしても便利です。
特にファイル生成ツールのようなアプリケーションは、バージョンによって出力が微妙に異なることも少なくありません。
このようなとき、生成ツールをまるごとイメージとして共有しておくと、複数人での作業でもトラブルを避けられます。</p>

<p>ちなみに、このKLabTechBookもpdfの生成にDockerイメージのvvakame/review<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>を利用しています。</p>

<h2 id="ファイルの所有権問題">ファイルの所有権問題</h2>

<p>Dockerでファイル生成する時、ボリュームマウントでホストのディレクトリをコンテナにマウントするのがよくある手法です。
リスト1では、カレントディレクトリを<code class="language-plaintext highlighter-rouge">/work</code>にマウントし、<code class="language-plaintext highlighter-rouge">/work/hello.txt</code>を生成しています。</p>

<p>▼リスト1 単純なファイルの生成</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/work busybox sh <span class="nt">-c</span> <span class="s1">'echo Hello! &gt; /work/hello.txt'</span>
<span class="nv">$ </span><span class="nb">cat </span>hello.txt
Hello!
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-l</span>
total 4
<span class="nt">-rw-r--r--</span> 1 root root 7 Apr 14 18:40 hello.txt
</code></pre></div></div>

<p>ファイルを生成したあと<code class="language-plaintext highlighter-rouge">ls</code>コマンドで調べてみると、所有者が<code class="language-plaintext highlighter-rouge">root</code>になっていました。
これではホストの一般ユーザーでは生成したファイルの編集ができず不便です。
これがファイルの所有権問題です。</p>

<p>DockerはLinuxカーネルの機能を組み合わせることでコンテナを実現しているため、
WindowsやmacOSでDockerを使うには、仮想マシンでLinuxを動かし、その上でDockerを動かすことになります。
これによりパフォーマンス上の問題がある一方で、ファイルの所有権問題は仮想マシンのファイル共有機能によって解決されている場合があります。
このためWebサーバーなどのLinuxマシンで直接Dockerを使おうとしてはじめて所有権問題に悩まされることになった人も多いのではないでしょうか。</p>

<p>Linuxでは、ユーザーは<code class="language-plaintext highlighter-rouge">UID</code>（User ID）という番号で識別されます。
ファイルやディレクトリにも<code class="language-plaintext highlighter-rouge">UID</code>が割り当てられ、所有者を表します<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
一般的に<code class="language-plaintext highlighter-rouge">root</code>の<code class="language-plaintext highlighter-rouge">UID</code>には0が割り当てられていて、特権ユーザーを表します。</p>

<p>Dockerのコンテナ内では、基本的には<code class="language-plaintext highlighter-rouge">root</code>ユーザーがコマンドを実行するため、
リスト1の例では、作成された<code class="language-plaintext highlighter-rouge">hello.txt</code>の所有者として<code class="language-plaintext highlighter-rouge">UID=0</code>が割り当てられました。
<code class="language-plaintext highlighter-rouge">UID=0</code>はホスト上でも<code class="language-plaintext highlighter-rouge">root</code>なので、<code class="language-plaintext highlighter-rouge">hello.txt</code>の所有者は<code class="language-plaintext highlighter-rouge">root</code>になってしまいました。</p>

<p>この問題への対処方法はいくつかやり方がありますが、ここでは<strong>userns-remap</strong>を使ってスマートに解決してみます。</p>

<h2 id="userns-remapとは">userns-remapとは</h2>

<p>デフォルトではDockerコンテナ内の<code class="language-plaintext highlighter-rouge">UID</code>は、ホストの<code class="language-plaintext highlighter-rouge">UID</code>とそのまま対応しています。
このためコンテナ内の<code class="language-plaintext highlighter-rouge">root</code>（<code class="language-plaintext highlighter-rouge">UID=0</code>）が作ったファイルはホスト上でも<code class="language-plaintext highlighter-rouge">root</code>（<code class="language-plaintext highlighter-rouge">UID=0</code>）の所有物でした。
また、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>はホスト上でも<code class="language-plaintext highlighter-rouge">root</code>と同じ権限を持ってしまうので、
万が一コンテナによる隔離が破られてしまった場合に重大なセキュリティリスクとなってしまいます。</p>

<p>userns-remapは、コンテナの<code class="language-plaintext highlighter-rouge">UID</code>をホスト上の別の範囲にマッピングする機能です。
つまり、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>はコンテナ内では<code class="language-plaintext highlighter-rouge">UID=0</code>の特権ユーザーですが、ホスト上では一般ユーザーの<code class="language-plaintext highlighter-rouge">UID</code>が割り振られていることになります。
これにより、コンテナによる隔離が破られてしまっても、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>は一般ユーザーの権限しか持たないため、ホストマシン全体への悪影響を防げます。
また、ホストの<code class="language-plaintext highlighter-rouge">root</code>やマッピング範囲外のユーザーの所有物はコンテナ内では<code class="language-plaintext highlighter-rouge">nobody</code>の所有物となり、
コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>であっても許可されていなければ書き換えられなくなります。</p>

<p>この機能を使ってファイルの所有権問題を解決するためには、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>をホストの自分自身<code class="language-plaintext highlighter-rouge">UID</code>にマッピングすればよいです。
つまり、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>の所有物は、自動的にホスト上では自分自身の所有物になるという寸法です。
それでは、具体的な設定方法を見ていきましょう。</p>

<h2 id="userns-remap-の設定方法">userns-remap の設定方法</h2>

<p>例として、自分自身をログイン名<code class="language-plaintext highlighter-rouge">makki</code>、<code class="language-plaintext highlighter-rouge">UID=1000</code>、<code class="language-plaintext highlighter-rouge">GID=1000</code>として、userns-remapの設定方法を説明します。
ホストマシン上に、<code class="language-plaintext highlighter-rouge">/etc/subuid</code>（リスト2）、<code class="language-plaintext highlighter-rouge">/etc/subgid</code>（リスト3）、
<code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code>（リスト4）の3つのテキストファイルを用意します<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>。
存在しない場合は作成してください。</p>

<p>▼リスト2 /etc/subuid</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>makki:1000:65536
</code></pre></div></div>

<p>▼リスト3 /etc/subgid</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>makki:1000:65536
</code></pre></div></div>

<p>▼リスト4 /etc/docker/daemon.json</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"userns-remap"</span><span class="p">:</span><span class="w"> </span><span class="s2">"makki"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/subuid</code>では、ログインユーザー<code class="language-plaintext highlighter-rouge">makki</code>が<code class="language-plaintext highlighter-rouge">UID</code>1000番から65536個をマッピング先として利用できるようにする設定です。
すなわち、コンテナ内の<code class="language-plaintext highlighter-rouge">UID</code>の0〜65535を、ホストの1000〜66535に対応させるようにできます。
<code class="language-plaintext highlighter-rouge">/etc/subgid</code>も<code class="language-plaintext highlighter-rouge">GID</code>についての同様の設定です。</p>

<p><code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code>で、userns-remapに利用するユーザー名を指定しています。
もしすでに他の設定項目がある場合は、<code class="language-plaintext highlighter-rouge">"userns-remap"</code>の項目をそこに加えます。</p>

<p>これらのファイルを用意したら、Dockerデーモンを再起動してください。
これでuserns-remapが有効になりました。
戻すときは<code class="language-plaintext highlighter-rouge">daemon.json</code>から<code class="language-plaintext highlighter-rouge">"userns-remap"</code>の項目を消して再起動すれば元通りです。</p>

<p>ひとつ注意点として、Dockerのイメージやボリュームなどの保存場所が、
デフォルトの<code class="language-plaintext highlighter-rouge">/var/lib/docker</code>から<code class="language-plaintext highlighter-rouge">/var/lib/docker/1000.1000</code>に変更されます<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>。
このため、これまで使っていたイメージなどをそのまま使いたい場合は自身でエクスポート/インポートする必要があります。</p>

<p>それではさっそく、ファイル生成を試してみましょう。</p>

<p>▼リスト5 もう一度ファイルを生成</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/work busybox sh <span class="nt">-c</span> <span class="s1">'echo Hello! &gt; /work/hello2.txt'</span>
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-l</span>
total 8
<span class="nt">-rw-r--r--</span> 1 makki makki 7 Apr 14 20:36 hello2.txt
<span class="nt">-rw-r--r--</span> 1 root  root  7 Apr 14 18:40 hello.txt
</code></pre></div></div>

<p>コンテナ内で生成された<code class="language-plaintext highlighter-rouge">hello2.txt</code>の所有者が自分になっているので、コンテナ外での編集も問題なくできます。</p>

<h2 id="他の方法との比較">他の方法との比較</h2>

<h3 id="rootlessモード">Rootlessモード</h3>

<p>Rootlessモードは、Dockerのデーモン自体を一般ユーザーで動かすDockerの機能です。
このとき、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>はデーモンを動かしている一般ユーザーにマッピングされます。
つまり、普段作業しているユーザーでRootlessモードのDockerデーモンを動かせば、ファイルの所有権問題は解決できます。</p>

<p>ただし、Rootlessモードでは一部の機能が制限されていて、
たとえばTCP/UDPの1024未満のポートをListenできないなど、普通の開発作業を行う上での利便性が損なわれてしまいます。</p>

<p>一方でuserns-remapでは、Dockerデーモン自体は<code class="language-plaintext highlighter-rouge">root</code>ユーザーで起動しているので、機能的な制限はほとんどありません。</p>

<h3 id="コンテナ内にユーザーを作る">コンテナ内にユーザーを作る</h3>

<p>ホストの作業ユーザーと同じ<code class="language-plaintext highlighter-rouge">UID</code>、<code class="language-plaintext highlighter-rouge">GID</code>のユーザーをコンテナ内に作り、
そのユーザーでファイルを生成すれば、ホスト上でも作業ユーザーの所有物となるはずです。
このやり方はウェブ上で多くの記事を見かけますが、ファイルの所有権問題の解決法としては完全に悪手です。</p>

<p>まず、ホストの作業ユーザーの<code class="language-plaintext highlighter-rouge">UID</code>は環境によって異なる可能性があります。
作業者の<code class="language-plaintext highlighter-rouge">UID</code>が異なっていた場合、生成されたファイルの所有者は別のユーザーになってしまい、
問題が解決できていないばかりか、セキュリティリスクにもなりえます。</p>

<p>これを解決するために、作業ユーザーの<code class="language-plaintext highlighter-rouge">UID</code>に合わせてコンテナのイメージを作り直すこともできますが、
これでは同じ環境を再現するというDockerの利点が失われてしまいます。</p>

<p>以前はコンテナ内に専用ユーザーを作ることをセキュリティの面で推奨する向きもありましたが、
これはuserns-remapやRootlessモードによってほとんど解消されています。
常駐型のサービスを動かすコンテナであれば専用ユーザーをつくるほうがよいケースもありますが、
ファイル生成のような単発のアプリケーションであれば、Dockerの標準的な方法のとおり<code class="language-plaintext highlighter-rouge">root</code>で動作させるのがよいでしょう。</p>

<p>コンテナ内でファイルを生成するユーザーを<code class="language-plaintext highlighter-rouge">root</code>に固定しておけば、
ホストごとに作業ユーザーの<code class="language-plaintext highlighter-rouge">UID</code>が違っていてもuserns-remapでそれぞれのホストに適切なマッピングを設定すればよく、
問題なく同じイメージを共有することができます。</p>

<h2 id="userns-remapの注意点">userns-remapの注意点</h2>

<p>userns-remapでは、コンテナ内の<code class="language-plaintext highlighter-rouge">UID</code>を単純な連番としてホストの<code class="language-plaintext highlighter-rouge">UID</code>に割り当てます。
つまり、コンテナ内の<code class="language-plaintext highlighter-rouge">root</code>をホストの<code class="language-plaintext highlighter-rouge">UID=1000</code>に割り当てた場合、
コンテナ内の<code class="language-plaintext highlighter-rouge">UID=1</code>はホストの<code class="language-plaintext highlighter-rouge">UID=1001</code>に割り当てられてしまいます。</p>

<p>ユーザーが自分のみであれば大きな問題にはなりませんが、ホストマシンを複数のユーザーで共用している場合、
他ユーザーのファイルにコンテナ内からアクセスできてしまいます。
このような環境では別の方法を考えないといけません。</p>

<p>また、userns-remapを有効にすると、一部のリソースへのアクセスが制限されます。
たとえば、GitHub Actionsをローカルで実行する「act<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>」というツールでは、ホストの名前空間のネットワークを必要としますが、
userns-remapが有効な場合、ネットワークの名前空間がホストとは別のものになってしまいアクセスできません。</p>

<p>これを回避するには、docker runコマンドなどに<code class="language-plaintext highlighter-rouge">--userns=host</code>オプションを指定して実行することで、
一時的にuserns-remapを無効にできます。
特にactでは、<code class="language-plaintext highlighter-rouge">~/.actrc</code>ファイルに<code class="language-plaintext highlighter-rouge">--container-options --userns=host</code>と記載しておけば、
内部でのdocker呼び出し時にこのオプションが自動的に付加されるため便利です。</p>

<h2 id="おわりに">おわりに</h2>

<p>LinuxのホストマシンでDockerを使ったときに直面しがちなファイルの所有権問題について、
userns-remapを使った解決方法を紹介しました。
これはセキュリティ面からも有効にしたほうがよいお勧めの機能です。</p>

<p>DockerDesktopが有料化して以降、
WindowsやmacOSではDockerを動かす方法が乱立していて、
迷ったり困ったりすることも多いのではないでしょうか。
ホストがLinuxであれば、公式のパッケージを無料で使えますし<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">8</a></sup>、
パフォーマンス面でも圧倒的に有利です。
また、ホストをLinuxにしてまず直面するであろうファイルの所有権問題は、
ここまで解説したとおりuserns-remapによって解決できます。</p>

<p>ぜひみなさんも、Dockerを使う時はホストをLinuxにしましょう。
そしてDockerコンテナの中ではできるだけ<code class="language-plaintext highlighter-rouge">root</code>で動作させてください。
くれぐれもよろしくお願いします。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.docker.com/">https://www.docker.com/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://matsuand.github.io/docs.docker.jp.onthefly/engine/security/userns-remap/">https://matsuand.github.io/docs.docker.jp.onthefly/engine/security/userns-remap/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://hub.docker.com/r/vvakame/review/">https://hub.docker.com/r/vvakame/review/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">GID</code>（Group ID）の説明は省略します <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">dockerd</code>コマンドの引数でも指定できますが、設定ファイルの方がわかりやすいと思います <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">1000.1000</code>の部分はマッピングされる先頭の<code class="language-plaintext highlighter-rouge">UID.GID</code>です <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7" role="doc-endnote">
      <p><a href="https://github.com/nektos/act">https://github.com/nektos/act</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8" role="doc-endnote">
      <p>とはいえDockerは大変すばらしいツールなので、気前よく支払いたいですね <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2023年5月20日から開催された技術書典14にて頒布した「KLabTechBook Vol.11」に掲載したものです。]]></summary></entry><entry><title type="html">Jupyterカーネル自作入門 (KLabTechBook Vol.10)</title><link href="http://makiuchi-d.github.io/2024/06/04/klabtechbook10-jupyter-kernel.ja.html" rel="alternate" type="text/html" title="Jupyterカーネル自作入門 (KLabTechBook Vol.10)" /><published>2024-06-04T00:00:00+00:00</published><updated>2024-06-04T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2024/06/04/klabtechbook10-jupyter-kernel.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2024/06/04/klabtechbook10-jupyter-kernel.ja.html"><![CDATA[<p>この記事は2022年9月10日から開催された<a href="https://techbookfest.org/event/tbf13">技術書典13</a>にて頒布した「<a href="https://techbookfest.org/product/7NsztMZryqYKS6FEaW9QCK">KLabTechBook Vol.10</a>」に掲載したものです。
<strong>Whitespaceのコードをそのまま紙面に載せました</strong>。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf16">技術書典16</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5">KLabTechBook Vol.13</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2024/tbf16.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
合わせてごらんください。</p>

<p><a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5"><img src="/images/2024-05-29/ktbv13.jpg" width="40%" alt="KLabTechBook Vol.13" /></a></p>

<hr />

<p>Jupyter Lab（あるいはJupyter Notebook）<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>は、ブラウザ上でプログラムを記述し実行できるウェブアプリケーションです。
プログラムと一緒に説明や実行時の入出力を<strong>ノートブック</strong>という形式でまとめて保存でき、実験の記録などに便利なツールです。
機械学習やデータ分析でよく使われているので、ご存知の方も多いと思います。</p>

<p>Project Jupyterは元々はPythonのインタラクティブインタプリタIPythonの派生プロジェクトですが、
プログラムの実行環境が<strong>カーネル</strong>として分離されているため、現在ではPython以外にもコミュニティによるものも含め数十の言語がサポートされています。</p>

<p>この章では、このJupyterに新たな言語のカーネルを自作して追加する方法を解説します。
題材として、<strong>Whitespace</strong><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>を実行するカーネルをGo言語で実装した「whitenote」を用意しました。
紙面の都合上抜粋しての解説となりますので、実際に動かしたりコードの全体を見たい場合はリポジトリをご覧ください。</p>

<ul>
  <li><a href="https://github.com/makiuchi-d/whitenote">https://github.com/makiuchi-d/whitenote</a></li>
</ul>

<p>また、筆者の開発環境は次のとおりです。</p>

<ul>
  <li>Ubuntu 20.04</li>
  <li>Jupyter Lab 3.4.4</li>
  <li>Go 1.19</li>
</ul>

<p><img src="/images/2024-06-04/jupyter.gif" alt="whitenote" />
▲図1 whitenote</p>

<h2 id="jupyterカーネルの基本">Jupyterカーネルの基本</h2>

<p>JupyterのカーネルはJupyterから起動される独立したプログラムで、
基本的には1ノートブックに対し1プロセスが起動されます。
Jupyterとの通信には<strong>ZeroMQ</strong><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>というライブラリを利用します。
このため、ZeroMQが利用できるものであれば、どんな言語でもカーネルを開発できます。</p>

<p>公式のドキュメントにもカーネルの作り方の解説があります<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
Pythonで実装する場合は<code class="language-plaintext highlighter-rouge">ipykernel.kernelbase.Kernel</code>を拡張することで簡単に実装できますが、
ここでは他言語でも真似できるよう、ZeroMQを直接操作する方法を紹介します。</p>

<h3 id="zeromqとは">ZeroMQとは</h3>

<p>Jupyterが利用するZeroMQは、軽量な非同期メッセージングライブラリです。
ZeroMQ自体はC++で開発されていますが、多くの言語で利用できるようにライブラリとバインディングが用意されていて<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>、
相互に通信できるようになっています。</p>

<p>ZeroMQではインターフェイスとして、TCPなどのソケットをラップしたような使い勝手の<strong>ソケット</strong>が提供されます。
このソケットにはさまざまなタイプ、たとえばメッセージを分配したり、ルーティングを自動で行ってくれるものなどが用意されています。
これらを組み合わせることで、Pub/Subや分散タスク処理のようなN対Nの通信を柔軟に組み立てることができます。</p>

<p>Go言語でZeroMQを利用するにはいくつかの選択肢があります。
公式サイトで紹介されている、goczmq<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>、pubbe/zmq4<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>のほか、
Go言語のみで再実装されたgo-zeromq/zmq4<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">8</a></sup>などがあります。</p>

<p>ここでは、他言語でも利用できる<strong>libzmq</strong>をシンプルにラップしているpubbe/zmq4を使うことにしました。
Ubuntu（focal, jammy）やDebian（bullseye）では次のコマンドでlibzmqをインストールできます。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt install libzmq3-dev libzmq5
</code></pre></div></div>

<h3 id="通信に使うソケット">通信に使うソケット</h3>

<p>Jupyterのカーネルは、表1の5つのソケットを使用します。</p>

<p>▼表1 ソケット一覧</p>

<table>
  <thead>
    <tr>
      <th>名前</th>
      <th>タイプ</th>
      <th>役割</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Shell</td>
      <td>ROUTER</td>
      <td>コードの実行や各種情報のリクエストを受け付ける</td>
    </tr>
    <tr>
      <td>IOPub</td>
      <td>PUB</td>
      <td>標準出力や状態をJupyterに通知する</td>
    </tr>
    <tr>
      <td>Stdin</td>
      <td>ROUTER</td>
      <td>標準入力への入力をJupyterにリクエストし受け取る</td>
    </tr>
    <tr>
      <td>Control</td>
      <td>ROUTER</td>
      <td>Shellと並行しての情報の取得や、終了リクエストを受け付ける</td>
    </tr>
    <tr>
      <td>HB</td>
      <td>REP</td>
      <td>疎通確認（HeartBeat）の送受信を行う</td>
    </tr>
  </tbody>
</table>

<p>コードの実行のようなJupyterからのリクエストは、<strong>Shellソケット</strong>に届きます。
つまりカーネルの基本動作はShellソケットに届いたリクエストを順次処理していくことです。
その過程で入出力があれば、IOPubやStdinのソケットを使って通信します。</p>

<p>Jupyterとカーネルの通信は基本的に1対1ですが、複数のリクエストを並行して送受信できるようにROUTERタイプのソケットが使われています。</p>

<h3 id="メッセージの基本構造">メッセージの基本構造</h3>

<p>メッセージの構造は公式ドキュメントでも解説されているのですが<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">9</a></sup>、
libzmqを直接使って実装するには説明が不十分なので注意が必要です<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">10</a></sup>。</p>

<p>さっそくドキュメントには書かれていないのですが、Jupyterとの通信はZeroMQのマルチパートメッセージで行います。
これは、複数のブロックをまとめてひとつのメッセージとして扱うものです。</p>

<p>pubbe/zmq4では、<code class="language-plaintext highlighter-rouge">RecvMessageBytes()</code>と<code class="language-plaintext highlighter-rouge">SendMessage()</code>を利用します。
libzmqのAPIとしては、送受信時に<code class="language-plaintext highlighter-rouge">ZMQ_SNDMORE</code>、<code class="language-plaintext highlighter-rouge">ZMQ_RCVMORE</code>を使うことになります<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">11</a></sup>。</p>

<p>▼リスト1 マルチパートメッセージの送受信関数</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">)</span> <span class="n">RecvMessageBytes</span><span class="p">(</span><span class="n">flags</span> <span class="n">zmq4</span><span class="o">.</span><span class="n">Flag</span><span class="p">)</span> <span class="p">(</span><span class="n">msg</span> <span class="p">[][]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span>
<span class="k">func</span> <span class="p">(</span><span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">)</span> <span class="n">SendMessage</span><span class="p">(</span><span class="n">parts</span> <span class="o">...</span><span class="k">interface</span><span class="p">{})</span> <span class="p">(</span><span class="n">total</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span>
</code></pre></div></div>

<p>メッセージの内容は表2に示すブロックの列になっています。
このうち<code class="language-plaintext highlighter-rouge">{header}</code>、<code class="language-plaintext highlighter-rouge">{parent_header}</code>、<code class="language-plaintext highlighter-rouge">{metadata}</code>、<code class="language-plaintext highlighter-rouge">{content}</code>はそれぞれ
JSONエンコードされた辞書データです。</p>

<p>▼表2 メッセージの内容</p>

<table>
  <thead>
    <tr>
      <th>ブロック</th>
      <th>内容</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"&lt;IDS|MSG&gt;"</code></td>
      <td>メッセージの先頭を表すデリミタ文字列</td>
    </tr>
    <tr>
      <td>HMAC</td>
      <td>検証のためのシグネチャ（16進数文字列）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{header}</code></td>
      <td>メッセージの種別を表すヘッダ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{parent_header}</code></td>
      <td>親メッセージのヘッダ（ない場合は”<code class="language-plaintext highlighter-rouge">{}</code>“）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{metadata}</code></td>
      <td>メタデータ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{content}</code></td>
      <td>メッセージのコンテンツ</td>
    </tr>
    <tr>
      <td>…</td>
      <td>追加データがある場合はブロックが続く</td>
    </tr>
  </tbody>
</table>

<p>ROUTERタイプのソケットで通信する場合、メッセージ本体の前にZeroMQが利用するID（ZmqID）が付加されます。
ZeroMQでよくあるソケットの組み合わせ、たとえばROUTER-DEALERパターンなどでは、
このZmqIDはソケットが自動的に付け外ししてくれるので意識する必要はありません。
しかしROUTERソケットを直接扱う場合、
つまりShell、Stdin、Controlのソケットの処理では、このZmqIDを適切に操作しなくてはなりません。</p>

<p>ROUTERの詳細はZeroMQのガイドブック<sup id="fnref:12" role="doc-noteref"><a href="#fn:12" class="footnote" rel="footnote">12</a></sup>に書かれているので、興味のある方は参照ください。</p>

<h2 id="最小のカーネル">最小のカーネル</h2>

<p>カーネルとして最低限必要なのは次の4つです。</p>

<ul>
  <li>起動してもらえるようカーネルを登録する</li>
  <li>通信に使うソケットを準備する</li>
  <li><code class="language-plaintext highlighter-rouge">kernel_info_request</code>に応答する</li>
  <li>HeartBeatに応答する</li>
</ul>

<h3 id="カーネルの登録">カーネルの登録</h3>

<p>カーネルはJupyterとは独立したプログラムなので、まずはJupyterに起動してもらえるよう登録します。
具体的には、特定のディレクトリに<code class="language-plaintext highlighter-rouge">kernel.json</code>ファイルを配置することで登録します。
このファイルには、カーネルのコマンドやパラメータを記載します（リスト2）。
詳細は公式ドキュメントをご覧ください<sup id="fnref:13" role="doc-noteref"><a href="#fn:13" class="footnote" rel="footnote">13</a></sup>。</p>

<p>▼リスト2 kernel.json</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"argv"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"whitenote"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"{connection_file}"</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"display_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Whitespace"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"whitespace"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">"argv"</code>がカーネルのコマンドとパラメータです。
<code class="language-plaintext highlighter-rouge">"{connection_file}"</code>は、後述する通信のための情報が書かれたファイルのパスに置き換えられます。
他に必要なパラメータがある場合ここに追加します。
<code class="language-plaintext highlighter-rouge">"display_name"</code>がJupyter上に表示される名前です。
Jupyterでカーネルが選択されると、この指定にしたがってコマンドが起動されます。</p>

<p>ロゴ画像を設定するには<code class="language-plaintext highlighter-rouge">logo-64x64.png</code>というPNGファイルを同じディレクトリに配置します<sup id="fnref:14" role="doc-noteref"><a href="#fn:14" class="footnote" rel="footnote">14</a></sup>。
画像がなくても名前の頭文字がロゴ画像として使われるので、必須ではありません。</p>

<p>ファイルを用意したら次のコマンドで配置します。
OSによって異なりますが、Linuxでは<code class="language-plaintext highlighter-rouge">~/.local/share/jupyter/kernels</code>に
<code class="language-plaintext highlighter-rouge">--name</code>で指定した名前のディレクトリが作られ、コピーされます。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jupyter kernelspec install --name=whitenote --user {kernel.jsonのディレクトリ}
</code></pre></div></div>

<p>正しく登録されているかは、Jupyterの画面や次のコマンドで確認できます。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jupyter kernelspec list
</code></pre></div></div>

<h3 id="ソケットの準備">ソケットの準備</h3>

<p>通信に使うソケットの接続情報は、起動パラメータで指定される<code class="language-plaintext highlighter-rouge">"{connection_file}"</code>という名のJSONファイルで渡されます。
これにはソケットの接続プロトコル、ポート番号、IPアドレス、そしてメッセージの署名に使うアルゴリズムとキーが含まれます。</p>

<p>▼リスト3 connection_fileの内容</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"shell_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">49835</span><span class="p">,</span><span class="w">
  </span><span class="nl">"iopub_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">53257</span><span class="p">,</span><span class="w">
  </span><span class="nl">"stdin_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">34911</span><span class="p">,</span><span class="w">
  </span><span class="nl">"control_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">42447</span><span class="p">,</span><span class="w">
  </span><span class="nl">"hb_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">55339</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"127.0.0.1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ef710209-2e9d78e0f61f5ec628d0c840"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"transport"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tcp"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"signature_scheme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hmac-sha256"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"kernel_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"whitenote"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>単純なJSONファイルなので、Go言語では標準ライブラリで読み取ることができます。
whitenoteではリスト4の構造体にマッピングしています。</p>

<p>▼リスト4 ConnectionInfo構造体</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">ConnectionInfo</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">SignatureScheme</span> <span class="kt">string</span> <span class="s">`json:"signature_scheme"`</span>
    <span class="n">Transport</span>       <span class="kt">string</span> <span class="s">`json:"transport"`</span>
    <span class="n">StdinPort</span>       <span class="kt">int</span>    <span class="s">`json:"stdin_port"`</span>
    <span class="n">ControlPort</span>     <span class="kt">int</span>    <span class="s">`json:"control_port"`</span>
    <span class="n">IOPubPort</span>       <span class="kt">int</span>    <span class="s">`json:"iopub_port"`</span>
    <span class="n">HBPort</span>          <span class="kt">int</span>    <span class="s">`json:"hb_port"`</span>
    <span class="n">ShellPort</span>       <span class="kt">int</span>    <span class="s">`json:"shell_port"`</span>
    <span class="n">Key</span>             <span class="kt">string</span> <span class="s">`json:"key"`</span>
    <span class="n">IP</span>              <span class="kt">string</span> <span class="s">`json:"ip"`</span>
<span class="p">}</span>
</code></pre></div></div>

<p>この情報を元にJupyterとの通信に使うソケットを作り、ポートに紐づけるコードをリスト5に示します。
5つのソケットは<code class="language-plaintext highlighter-rouge">Sockets</code>構造体にまとめました。</p>

<p>▼リスト5 ソケットの準備</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Sockets</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">conf</span>    <span class="o">*</span><span class="n">ConnectionInfo</span>
    <span class="n">shell</span>   <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span>
    <span class="n">control</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span>
    <span class="n">stdin</span>   <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span>
    <span class="n">iopub</span>   <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span>
    <span class="n">hb</span>      <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">bindSocket</span><span class="p">(</span><span class="n">typ</span> <span class="n">zmq4</span><span class="o">.</span><span class="n">Type</span><span class="p">,</span> <span class="n">transport</span><span class="p">,</span> <span class="n">ip</span> <span class="kt">string</span><span class="p">,</span> <span class="n">port</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span> <span class="p">{</span>
    <span class="n">sock</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">zmq4</span><span class="o">.</span><span class="n">NewSocket</span><span class="p">(</span><span class="n">typ</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">sock</span><span class="o">.</span><span class="n">Bind</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s://%s:%d"</span><span class="p">,</span> <span class="n">transport</span><span class="p">,</span> <span class="n">ip</span><span class="p">,</span> <span class="n">port</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">sock</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">newSockets</span><span class="p">(</span><span class="n">conf</span> <span class="o">*</span><span class="n">ConnectionInfo</span><span class="p">)</span> <span class="o">*</span><span class="n">Sockets</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">&amp;</span><span class="n">Sockets</span><span class="p">{</span>
        <span class="n">conf</span><span class="o">:</span>    <span class="n">conf</span><span class="p">,</span>
        <span class="n">shell</span><span class="o">:</span>   <span class="n">bindSocket</span><span class="p">(</span><span class="n">zmq4</span><span class="o">.</span><span class="n">ROUTER</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">Transport</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">ShellPort</span><span class="p">),</span>
        <span class="n">control</span><span class="o">:</span> <span class="n">bindSocket</span><span class="p">(</span><span class="n">zmq4</span><span class="o">.</span><span class="n">ROUTER</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">Transport</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">ControlPort</span><span class="p">),</span>
        <span class="n">stdin</span><span class="o">:</span>   <span class="n">bindSocket</span><span class="p">(</span><span class="n">zmq4</span><span class="o">.</span><span class="n">ROUTER</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">Transport</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">StdinPort</span><span class="p">),</span>
        <span class="n">iopub</span><span class="o">:</span>   <span class="n">bindSocket</span><span class="p">(</span><span class="n">zmq4</span><span class="o">.</span><span class="n">PUB</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">Transport</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IOPubPort</span><span class="p">),</span>
        <span class="n">hb</span><span class="o">:</span>      <span class="n">bindSocket</span><span class="p">(</span><span class="n">zmq4</span><span class="o">.</span><span class="n">REP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">Transport</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">conf</span><span class="o">.</span><span class="n">HBPort</span><span class="p">),</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="o">...</span> <span class="p">(</span><span class="n">略</span><span class="p">)</span>

    <span class="n">socks</span> <span class="o">:=</span> <span class="nb">new</span> <span class="n">Sockets</span><span class="p">(</span><span class="n">conf</span><span class="p">)</span>

    <span class="k">go</span> <span class="n">socks</span><span class="o">.</span><span class="n">shellHandler</span><span class="p">()</span>
    <span class="k">go</span> <span class="n">socks</span><span class="o">.</span><span class="n">controlHanlder</span><span class="p">()</span>
    <span class="k">go</span> <span class="n">socks</span><span class="o">.</span><span class="n">hbHandler</span><span class="p">()</span>

    <span class="o">...</span> <span class="p">(</span><span class="n">略</span><span class="p">)</span>
</code></pre></div></div>

<p>これらのソケットはすべて、カーネル側でBindしてJupyterからの接続を待ち受ける形をとります。
実際の接続処理はZeroMQがバックグラウンドで行ってくれます。
あとは待っていればJupyter側からメッセージを送ってくるので、それをハンドラ関数で処理していくことになります。</p>

<h3 id="shellハンドラの実装">Shellハンドラの実装</h3>

<p>カーネルに接続したJupyterは、最初にShellソケットに<code class="language-plaintext highlighter-rouge">kernel_info_request</code>を送ってきます。
最小のカーネルでも、このリクエストにだけは応答しなければなりません。</p>

<p>このリクエストに対してカーネルは<code class="language-plaintext highlighter-rouge">kernel_info_reply</code>を返し、IOPub経由で状態を<code class="language-plaintext highlighter-rouge">"idle"</code>として通知します。
また、Controlソケットにも<code class="language-plaintext highlighter-rouge">kernel_info_request</code>が送られてきますが、Shellソケットで応答するので、そちらは読み捨てます。</p>

<p>ここではまず、リスト6にShellのハンドラメソッド<code class="language-plaintext highlighter-rouge">shellHandler</code>を示し、
その内容について詳しく説明していきます。</p>

<p>▼リスト6 Shellのハンドラメソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">shellHandler</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">{</span>
        <span class="c">// メッセージの受信</span>
        <span class="n">msg</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">recvRouterMessage</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">shell</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"shell: recv: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
            <span class="k">continue</span>
        <span class="p">}</span>

        <span class="c">// headerのデコード</span>
        <span class="k">var</span> <span class="n">hdr</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">(</span><span class="n">msg</span><span class="o">.</span><span class="n">Header</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">hdr</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"shell: header: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
            <span class="k">continue</span>
        <span class="p">}</span>

        <span class="c">// メッセージ種別ごとの処理</span>
        <span class="k">switch</span> <span class="n">hdr</span><span class="p">[</span><span class="s">"msg_type"</span><span class="p">]</span> <span class="p">{</span>

        <span class="k">case</span> <span class="s">"kernel_info_request"</span><span class="o">:</span>
            <span class="c">// kernel_info_replyの送信</span>
            <span class="n">s</span><span class="o">.</span><span class="n">sendRouter</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">shell</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="s">"kernel_info_reply"</span><span class="p">,</span> <span class="n">kernelInfo</span><span class="p">)</span>

            <span class="c">// 状態を"idle"に</span>
            <span class="n">s</span><span class="o">.</span><span class="n">sendState</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">stateIdle</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="メッセージの受信">メッセージの受信</h3>

<p>メッセージを受信する関数をリスト7に示します。
ShellはROUTERなので、先頭にZmqIDが付加されます。
ROUTERが多段になっている場合、ZmqIDが複数ブロックになっていることもあります。
ZmqIDとメッセージの区切りは、デリミタ文字列<code class="language-plaintext highlighter-rouge">"&lt;IDS|MSG&gt;"</code>のブロックによって識別します。</p>

<p>▼リスト7 ROUTERソケットからのMessage読み込み</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">delimiter</span> <span class="o">=</span> <span class="s">"&lt;IDS|MSG&gt;"</span>

<span class="k">type</span> <span class="n">Message</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">ZmqID</span>    <span class="p">[][]</span><span class="kt">byte</span>
    <span class="n">Header</span>   <span class="p">[]</span><span class="kt">byte</span>
    <span class="n">Parent</span>   <span class="p">[]</span><span class="kt">byte</span>
    <span class="n">Metadata</span> <span class="p">[]</span><span class="kt">byte</span>
    <span class="n">Content</span>  <span class="p">[]</span><span class="kt">byte</span>
    <span class="n">Buffers</span>  <span class="p">[][]</span><span class="kt">byte</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">recvRouterMessage</span><span class="p">(</span><span class="n">sock</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">mb</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">sock</span><span class="o">.</span><span class="n">RecvMessageBytes</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">err</span>
    <span class="p">}</span>

    <span class="c">// デリミタを探す</span>
    <span class="k">var</span> <span class="n">d</span> <span class="kt">int</span>
    <span class="k">for</span> <span class="n">d</span> <span class="o">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">d</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">mb</span><span class="p">);</span> <span class="n">d</span><span class="o">++</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">bytes</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span><span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="p">],</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">delimiter</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">break</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">d</span> <span class="o">&gt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">mb</span><span class="p">)</span><span class="o">-</span><span class="m">5</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid message: %v,%v, %v"</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">mb</span><span class="p">),</span> <span class="n">mb</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="n">msg</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">Message</span><span class="p">{</span>
        <span class="n">ZmqID</span><span class="o">:</span>    <span class="n">mb</span><span class="p">[</span><span class="o">:</span><span class="n">d</span><span class="p">],</span>
        <span class="n">Header</span><span class="o">:</span>   <span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">2</span><span class="p">],</span>
        <span class="n">Parent</span><span class="o">:</span>   <span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">3</span><span class="p">],</span>
        <span class="n">Metadata</span><span class="o">:</span> <span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">4</span><span class="p">],</span>
        <span class="n">Content</span><span class="o">:</span>  <span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">5</span><span class="p">],</span>
        <span class="n">Buffers</span><span class="o">:</span>  <span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">6</span><span class="o">:</span><span class="p">],</span>
    <span class="p">}</span>

    <span class="c">// シグネチャの検証</span>
    <span class="n">sig</span> <span class="o">:=</span> <span class="kt">string</span><span class="p">(</span><span class="n">mb</span><span class="p">[</span><span class="n">d</span><span class="o">+</span><span class="m">1</span><span class="p">])</span>
    <span class="n">mac</span> <span class="o">:=</span> <span class="n">calcHMAC</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">conf</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Header</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Parent</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Metadata</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Content</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">sig</span> <span class="o">!=</span> <span class="n">mac</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">msg</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid hmac: %v %v"</span><span class="p">,</span> <span class="n">sig</span><span class="p">,</span> <span class="n">mb</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">msg</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="シグネチャの検証">シグネチャの検証</h4>

<p>デリミタの次のブロックは、メッセージの検証のためのシグネチャです。
受信時の検証をスキップしたり、送信時もシグネチャを空文字列とすることで検証を無効にもできますが、簡単なので実装してしまいます。</p>

<p>アルゴリズムは<code class="language-plaintext highlighter-rouge">ConnectionInfo</code>の<code class="language-plaintext highlighter-rouge">"signature_scheme"</code>で指定されますが、いまのところSHA256のHAMC固定です。
また、HMACのキーも<code class="language-plaintext highlighter-rouge">ConnectionInfo</code>の<code class="language-plaintext highlighter-rouge">"key"</code>として渡されます。
このキーを使い、受信したメッセージの<code class="language-plaintext highlighter-rouge">{header}</code>、<code class="language-plaintext highlighter-rouge">{parent_header}</code>、<code class="language-plaintext highlighter-rouge">{metadata}</code>、<code class="language-plaintext highlighter-rouge">{content}</code>を
この順に連結したもののハッシュを計算し検証します。
追加データ（<code class="language-plaintext highlighter-rouge">Buffers</code>）はここに含みません。</p>

<p>▼リスト8 HMACの計算</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">calcHMAC</span><span class="p">(</span><span class="n">key</span> <span class="kt">string</span><span class="p">,</span> <span class="n">header</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="n">metadata</span><span class="p">,</span> <span class="n">content</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">h</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha256</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">key</span><span class="p">))</span>
    <span class="n">h</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">header</span><span class="p">)</span>
    <span class="n">h</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">parent</span><span class="p">)</span>
    <span class="n">h</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">metadata</span><span class="p">)</span>
    <span class="n">h</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">hex</span><span class="o">.</span><span class="n">EncodeToString</span><span class="p">(</span><span class="n">h</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="メッセージの種別">メッセージの種別</h4>

<p>メッセージの<code class="language-plaintext highlighter-rouge">{header}</code>はリスト9のようなJSONオブジェクトです。
<code class="language-plaintext highlighter-rouge">shellHandler</code>では辞書<code class="language-plaintext highlighter-rouge">map[string]any</code>としてデコードしています。</p>

<p>▼リスト9 メッセージの<code class="language-plaintext highlighter-rouge">{header}</code></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-08-13T06:32:13.893Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"msg_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c1735592-e938-4d8a-b7a2-769d795f65d0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"msg_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kernel_info_request"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"session"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aa3af91f-a747-42c7-b0b8-a02179aee1e1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5.2"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>ここで必要なのは、メッセージ種別を表す<code class="language-plaintext highlighter-rouge">"msg_type"</code>だけです。
カーネルが処理しないメッセージは単に読み捨てるだけでよいので、
ここでは<code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>のメッセージのみ処理します。</p>

<h4 id="kernel_info_replyの送信"><code class="language-plaintext highlighter-rouge">kernel_info_reply</code>の送信</h4>

<p><code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>に対しては、<code class="language-plaintext highlighter-rouge">"kernel_info_reply"</code>という<code class="language-plaintext highlighter-rouge">msg_type</code>のメッセージを返します。
このときメッセージの<code class="language-plaintext highlighter-rouge">{content}</code>はリスト10のようにカーネルの情報をまとめたJSONオブジェクトです。
これに<code class="language-plaintext highlighter-rouge">{header}</code>などを合わせてメッセージを組み立て送信します。
このカーネル情報は基本的に固定値なので、<code class="language-plaintext highlighter-rouge">init()</code>で初期化して保持しています。</p>

<p>また、後で必要となる<code class="language-plaintext highlighter-rouge">sessionId</code>と、基本的に空のままの<code class="language-plaintext highlighter-rouge">metadata</code>も
起動中変更されることはないので、同じようにグローバルに保持することにします。</p>

<p>▼リスト10 固定値の初期化</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="p">(</span>
    <span class="n">sessionId</span>  <span class="kt">string</span> <span class="c">// プロセスごとにユニークなID</span>
    <span class="n">kernelInfo</span> <span class="p">[]</span><span class="kt">byte</span> <span class="c">// カーネル情報</span>

    <span class="n">metadata</span>  <span class="o">=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"{}"</span><span class="p">)</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">init</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">sid</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">uuid</span><span class="o">.</span><span class="n">NewRandom</span><span class="p">()</span>
    <span class="n">sessionId</span> <span class="o">=</span> <span class="n">sid</span><span class="o">.</span><span class="n">String</span><span class="p">()</span>

    <span class="n">kernelInfo</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
        <span class="s">"status"</span><span class="o">:</span>                 <span class="s">"ok"</span><span class="p">,</span>
        <span class="s">"protocol_version"</span><span class="o">:</span>       <span class="s">"5.3"</span><span class="p">,</span>
        <span class="s">"implementation"</span><span class="o">:</span>         <span class="s">"whitenote"</span><span class="p">,</span>
        <span class="s">"implementation_version"</span><span class="o">:</span> <span class="s">"0.1"</span><span class="p">,</span>
        <span class="s">"language_info"</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
            <span class="s">"name"</span><span class="o">:</span>               <span class="s">"whitespace"</span><span class="p">,</span>
            <span class="s">"version"</span><span class="o">:</span>            <span class="s">"0.1"</span><span class="p">,</span>
            <span class="s">"mimetype"</span><span class="o">:</span>           <span class="s">"text/x-whitespace"</span><span class="p">,</span>
            <span class="s">"file_extension"</span><span class="o">:</span>     <span class="s">".ws"</span><span class="p">,</span>
            <span class="s">"pygments_lexer"</span><span class="o">:</span>     <span class="s">""</span><span class="p">,</span>
            <span class="s">"codemirror_mode"</span><span class="o">:</span>    <span class="s">""</span><span class="p">,</span>
            <span class="s">"nbconvert_exporter"</span><span class="o">:</span> <span class="s">""</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="s">"banner"</span><span class="o">:</span> <span class="s">""</span><span class="p">,</span>
    <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>次に、ヘッダを構築する関数はリスト11のようにしました。
<code class="language-plaintext highlighter-rouge">msg_type</code>だけ指定すれば構築できるようにしてあります。</p>

<p>▼リスト11 ヘッダ構築関数</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">newHeader</span><span class="p">(</span><span class="n">msgtype</span> <span class="kt">string</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
    <span class="n">mid</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">uuid</span><span class="o">.</span><span class="n">NewRandom</span><span class="p">()</span>
    <span class="n">h</span> <span class="o">:=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
        <span class="s">"date"</span><span class="o">:</span>     <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Format</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">RFC3339</span><span class="p">),</span> <span class="c">// 現在時刻</span>
        <span class="s">"msg_id"</span><span class="o">:</span>   <span class="n">mid</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="c">// メッセージ毎にユニークなUUID</span>
        <span class="s">"username"</span><span class="o">:</span> <span class="s">"kernel"</span><span class="p">,</span>
        <span class="s">"session"</span><span class="o">:</span>  <span class="n">sessionId</span><span class="p">,</span>    <span class="c">// プロセスごとにユニークなUUID</span>
        <span class="s">"msg_type"</span><span class="o">:</span> <span class="n">msgtype</span><span class="p">,</span>
        <span class="s">"version"</span><span class="o">:</span>  <span class="s">"5.3"</span><span class="p">,</span>
    <span class="p">}</span>
    <span class="n">hdr</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">hdr</span>
<span class="p">}</span>
</code></pre></div></div>

<p>ShellソケットはROUTERなので、送信するときにはZmqIDがメッセージの先頭に必要です。
ここでは親メッセージ、<code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>の<code class="language-plaintext highlighter-rouge">ZmqID</code>をそのまま使います。
また、<code class="language-plaintext highlighter-rouge">{parent_header}</code>も親メッセージの<code class="language-plaintext highlighter-rouge">{header}</code>です。</p>

<p>これで返信に必要な情報が揃いました。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ZmqID</code> : 親メッセージの<code class="language-plaintext highlighter-rouge">ZmqID</code></li>
  <li>HMAC : <code class="language-plaintext highlighter-rouge">calcHMAC()</code>で計算</li>
  <li><code class="language-plaintext highlighter-rouge">{header}</code> : <code class="language-plaintext highlighter-rouge">msg_type</code>を<code class="language-plaintext highlighter-rouge">"kernel_info_reply"</code>として構築</li>
  <li><code class="language-plaintext highlighter-rouge">{parent_header}</code> : 親メッセージの<code class="language-plaintext highlighter-rouge">{header}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{metadata}</code> : <code class="language-plaintext highlighter-rouge">{}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{content}</code> : カーネル情報 <code class="language-plaintext highlighter-rouge">kernelInfo</code></li>
</ul>

<p>これらを順番どおりに結合してソケットの<code class="language-plaintext highlighter-rouge">SendMessage()</code>で送信します。
この処理を<code class="language-plaintext highlighter-rouge">sendRouter()</code>メソッドとしてまとめました（リスト12）。</p>

<p>▼リスト12 sendRouterメソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">sendRouter</span><span class="p">(</span>
    <span class="n">sock</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">,</span> <span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">msgtype</span> <span class="kt">string</span><span class="p">,</span> <span class="n">content</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>

    <span class="n">hdr</span> <span class="o">:=</span> <span class="n">newHeader</span><span class="p">(</span><span class="n">msgtype</span><span class="p">)</span>
    <span class="n">phdr</span> <span class="o">:=</span> <span class="n">parent</span><span class="o">.</span><span class="n">Header</span>
    <span class="n">mac</span> <span class="o">:=</span> <span class="n">calcHMAC</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">conf</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">hdr</span><span class="p">,</span> <span class="n">phdr</span><span class="p">,</span> <span class="n">metadata</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
    <span class="n">data</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="n">any</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">parent</span><span class="o">.</span><span class="n">ZmqID</span><span class="p">)</span><span class="o">+</span><span class="m">6</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">p</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">parent</span><span class="o">.</span><span class="n">ZmqID</span> <span class="p">{</span>
        <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">delimiter</span><span class="p">)</span>   <span class="c">// "&lt;IDS|IMG&gt;"</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">mac</span><span class="p">)</span>         <span class="c">// HMAC</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">hdr</span><span class="p">)</span>         <span class="c">// {header}</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">phdr</span><span class="p">)</span>        <span class="c">// {parent_header}</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">metadata</span><span class="p">)</span>    <span class="c">// {metadata}</span>
    <span class="n">data</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>     <span class="c">// {content}</span>
    <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">sock</span><span class="o">.</span><span class="n">SendMessage</span><span class="p">(</span><span class="n">data</span><span class="o">...</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="状態の通知">状態の通知</h4>

<p><code class="language-plaintext highlighter-rouge">"kernel_info_reply"</code>を返した後、カーネルはコードの実行準備が整ったことをJupyterに伝えます。
これはIOPubソケットに対して<code class="language-plaintext highlighter-rouge">"idle"</code>状態を通知することで行います。
この状態通知メソッドを<code class="language-plaintext highlighter-rouge">sendState()</code>としてリスト13のように定義しました。</p>

<p>▼リスト13 stateの送信</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="p">(</span>
    <span class="n">stateIdle</span> <span class="o">=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">`{"execution_state":"idle"}`</span><span class="p">)</span>
    <span class="n">stateBusy</span> <span class="o">=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">`{"execution_state":"busy"}`</span><span class="p">)</span>
<span class="p">)</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">send</span><span class="p">(</span><span class="n">sock</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">,</span> <span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">msgtype</span> <span class="kt">string</span><span class="p">,</span> <span class="n">content</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">hdr</span> <span class="o">:=</span> <span class="n">newHeader</span><span class="p">(</span><span class="n">msgtype</span><span class="p">)</span>
    <span class="n">phdr</span> <span class="o">:=</span> <span class="n">parent</span><span class="o">.</span><span class="n">Header</span>
    <span class="n">mac</span> <span class="o">:=</span> <span class="n">calcHMAC</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">conf</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">hdr</span><span class="p">,</span> <span class="n">phdr</span><span class="p">,</span> <span class="n">metadata</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
    <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">sock</span><span class="o">.</span><span class="n">SendMessage</span><span class="p">(</span><span class="n">delimiter</span><span class="p">,</span> <span class="n">mac</span><span class="p">,</span> <span class="n">hdr</span><span class="p">,</span> <span class="n">phdr</span><span class="p">,</span> <span class="n">metadata</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">sendState</span><span class="p">(</span><span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">state</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">s</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">iopub</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="s">"status"</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">{content}</code>は<code class="language-plaintext highlighter-rouge">{"execution_state":"idle"}</code>とします。
このバイト列は不変でかつ何度も使うことになるので、<code class="language-plaintext highlighter-rouge">"busy"</code>のものと合わせてグローバルに保持しました。
<code class="language-plaintext highlighter-rouge">{header}</code>は<code class="language-plaintext highlighter-rouge">msg_type</code>を<code class="language-plaintext highlighter-rouge">"status"</code>とし、
<code class="language-plaintext highlighter-rouge">{parent_header}</code>は<code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>のものにします。</p>

<p>IOPubは<code class="language-plaintext highlighter-rouge">PUB</code>ソケットなので、ZmqIDは必要ありません。
デリミタ（<code class="language-plaintext highlighter-rouge">"&lt;IDS|MSG&gt;"</code>）から順にマルチパートメッセージを送ります。</p>

<p>ここまで実装したら、Jupyterはカーネルをきちんと起動できるようになります。
<code class="language-plaintext highlighter-rouge">"kernel_info_reply"</code>を正しく返せなかったり、<code class="language-plaintext highlighter-rouge">"idle"</code>状態にできなかったりすると、
Jupyterはしつこく<code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>を何度も送ってきます。
もしそのような挙動になったら、今一度実装を見直してみてください。</p>

<h3 id="controlとhbheartbeatのハンドラ">ControlとHB（HeartBeat）のハンドラ</h3>

<p>Controlソケットには<code class="language-plaintext highlighter-rouge">"kernel_info_request"</code>のほか、いくつかのリクエストが届きます。
Jupyterのカーネルでは、処理しないリクエストは単に読み捨てることになっています。
また、シャットダウン要求<code class="language-plaintext highlighter-rouge">"shutdown_request"</code>も届きますが、
これを無視してもJupyterからはSIGINTが送られてくるので、シグナルハンドラを変更していないなら自動的に終了してくれます。
ということで、Controlのハンドラはリスト14のように、すべて読み捨てるだけの実装としました。</p>

<p>▼リスト14 Controlハンドラの実装</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">controlHandler</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">{</span>
        <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">recvRouterMessage</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">control</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>HBソケットには疎通確認のメッセージが届きます。
このメッセージはそのままHBソケットで送り返すことで疎通していることを伝えます。</p>

<p>メッセージをひとつひとつ<code class="language-plaintext highlighter-rouge">Recv()</code>、<code class="language-plaintext highlighter-rouge">Send()</code>するループを書いてもよいのですが、
ZeroMQの組み込みProxyを使うこともできます（リスト15）。</p>

<p>▼リスト15 組み込みProxyによるHBHandler</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">hbHandler</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">zmq4</span><span class="o">.</span><span class="n">Proxy</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">hb</span><span class="p">,</span> <span class="n">s</span><span class="o">.</span><span class="n">hb</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>これで最小の何もしないカーネルが実装できました。
コードの実行要求<code class="language-plaintext highlighter-rouge">"execute_request"</code>に対して何もしていないので、
Jupyter上で実行ボタンを押してもなにも起こりませんが、通信はできています。</p>

<h2 id="whitespaceとは">Whitespaceとは</h2>

<p>ここからは、Jupyterに新たな言語としてWhitespaceのカーネルを実際に組み込んでみます。
Whitespaceを選択したのは、実装が簡単なことに加え、調べた限り誰も作っていなさそう<sup id="fnref:15" role="doc-noteref"><a href="#fn:15" class="footnote" rel="footnote">15</a></sup>だったからです。</p>

<p>Whitespaceは難解プログラミング言語のひとつで、2003年4月1日にEdwin BradyとChris Morrisによって開発、発表されました。
公式サイトはすでに消滅していますが、Internet Archiveで見ることができます。</p>

<p>この言語の特徴はなんといっても、スペース、タブ、改行という空白文字3種のみで記述することです。
それ以外の文字は全て無視されます。
リスト16に「<code class="language-plaintext highlighter-rouge">Hello!</code>」と表示するプログラムを示します<sup id="fnref:16" role="doc-noteref"><a href="#fn:16" class="footnote" rel="footnote">16</a></sup>。</p>

<p>▼リスト16 <code class="language-plaintext highlighter-rouge">Hello!</code>と表示するプログラム</p>
<pre><code class="language-whitespace">   	  	   
	
     		  	 	
	
     		 		  
 
 	
  	
     		 				
	
     	    	
	
  


</code></pre>

<p>Whitespaceはヒープメモリを備えたスタックベースの言語で、表3の命令セットで構成されます。
便宜上、スペースをS、タブをT、改行をNとして表記します。
詳細は公式サイトのチュートリアル<sup id="fnref:17" role="doc-noteref"><a href="#fn:17" class="footnote" rel="footnote">17</a></sup>をご覧ください。</p>

<p>▼表3 Whitespaceの命令セット</p>

<table>
  <thead>
    <tr>
      <th>命令</th>
      <th>引数</th>
      <th>意味</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SS</td>
      <td>数値</td>
      <td>数値をスタック先頭にPush</td>
    </tr>
    <tr>
      <td>SNS</td>
      <td> </td>
      <td>スタック先頭のアイテムを複製</td>
    </tr>
    <tr>
      <td>STS</td>
      <td>数値</td>
      <td>スタックのN番目のアイテムを先頭にコピー</td>
    </tr>
    <tr>
      <td>SNT</td>
      <td> </td>
      <td>スタック先頭の2つを入れ替え</td>
    </tr>
    <tr>
      <td>SNN</td>
      <td> </td>
      <td>スタック先頭のアイテムを破棄</td>
    </tr>
    <tr>
      <td>STN</td>
      <td>数値</td>
      <td>先頭のアイテムを保持したままN個のアイテムを破棄</td>
    </tr>
    <tr>
      <td>TSSS</td>
      <td> </td>
      <td>加算</td>
    </tr>
    <tr>
      <td>TSST</td>
      <td> </td>
      <td>減算</td>
    </tr>
    <tr>
      <td>TSSN</td>
      <td> </td>
      <td>乗算</td>
    </tr>
    <tr>
      <td>TSTS</td>
      <td> </td>
      <td>除算</td>
    </tr>
    <tr>
      <td>TSTT</td>
      <td> </td>
      <td>剰余</td>
    </tr>
    <tr>
      <td>TTS</td>
      <td> </td>
      <td>先頭アイテムを2番目の示すアドレスのヒープに保存</td>
    </tr>
    <tr>
      <td>TTT</td>
      <td> </td>
      <td>先頭の示すアドレスのヒープから値をスタックに取り出す</td>
    </tr>
    <tr>
      <td>NSS</td>
      <td>ラベル</td>
      <td>ラベルを設置</td>
    </tr>
    <tr>
      <td>NST</td>
      <td>ラベル</td>
      <td>サブルーチン呼び出し</td>
    </tr>
    <tr>
      <td>NSN</td>
      <td>ラベル</td>
      <td>ラベルへジャンプ</td>
    </tr>
    <tr>
      <td>NTS</td>
      <td>ラベル</td>
      <td>スタック先頭が0ならラベルへジャンプ</td>
    </tr>
    <tr>
      <td>NTT</td>
      <td>ラベル</td>
      <td>スタック先頭が負ならラベルへジャンプ</td>
    </tr>
    <tr>
      <td>NTN</td>
      <td> </td>
      <td>サブルーチン呼び出し元へ戻る</td>
    </tr>
    <tr>
      <td>NNN</td>
      <td> </td>
      <td>プログラム終了</td>
    </tr>
    <tr>
      <td>TNSS</td>
      <td> </td>
      <td>スタック先頭を文字として出力</td>
    </tr>
    <tr>
      <td>TNST</td>
      <td> </td>
      <td>スタック先頭を数値として出力</td>
    </tr>
    <tr>
      <td>TNTS</td>
      <td> </td>
      <td>入力から1文字読み、スタック先頭の示すヒープに保存</td>
    </tr>
    <tr>
      <td>TNTT</td>
      <td> </td>
      <td>入力から数値を読み、スタック先頭の示すヒープに保存</td>
    </tr>
  </tbody>
</table>

<h2 id="インタプリタの実装">インタプリタの実装</h2>

<p>whitenoteのwspaceパッケージ<sup id="fnref:18" role="doc-noteref"><a href="#fn:18" class="footnote" rel="footnote">18</a></sup>にWhitespaceインタプリタを実装しました。
実装の詳細はリポジトリを見ていただくとして、ここではインタプリタの本体である<code class="language-plaintext highlighter-rouge">wspace.VM</code>の使い方を簡単に紹介します。</p>

<p>▼リスト17 <code class="language-plaintext highlighter-rouge">wspace.VM</code>の使い方</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">vm</span> <span class="o">:=</span> <span class="n">wspace</span><span class="o">.</span><span class="n">New</span><span class="p">()</span>

<span class="n">err</span> <span class="o">:=</span> <span class="n">vm</span><span class="o">.</span><span class="n">Load</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"   </span><span class="se">\t\t</span><span class="s"> </span><span class="se">\t\n\t\n</span><span class="s"> </span><span class="se">\t\n\n\n</span><span class="s">"</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
    <span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">err</span> <span class="o">=</span> <span class="n">vm</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
    <span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">wspace.VM</code>では、コードの読み込み<code class="language-plaintext highlighter-rouge">vm.Load()</code>と実行<code class="language-plaintext highlighter-rouge">vm.Run()</code>が分かれています。
Whitespaceの文法上、ラベルの定義より前にそのラベルへのジャンプ命令が出現しうるため、
実行する前にコード全体を読み込んでおかないと適切にジャンプできません。</p>

<p>また、<code class="language-plaintext highlighter-rouge">vm.Load()</code>を複数回実行することで、VM内部の命令列（<code class="language-plaintext highlighter-rouge">vm.Program</code>）にプログラムを追記できるようにしました。
これにより、Jupyter上で最初のコードセルにサブルーチンを記述し、それを呼び出すコードを次のセルに分けて書くような使い方ができます<sup id="fnref:19" role="doc-noteref"><a href="#fn:19" class="footnote" rel="footnote">19</a></sup>。</p>

<p>▼リスト18 <code class="language-plaintext highlighter-rouge">VM.Load()</code>メソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Load loads code segment to VM</span>
<span class="c">// return: segment number, read size, error</span>
<span class="k">func</span> <span class="p">(</span><span class="o">*</span><span class="n">wspace</span><span class="o">.</span><span class="n">VM</span><span class="p">)</span> <span class="n">Load</span><span class="p">(</span><span class="n">code</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</code></pre></div></div>

<p>コードの実行は<code class="language-plaintext highlighter-rouge">vm.Run()</code>で、入出力に<code class="language-plaintext highlighter-rouge">io.Reader</code>と<code class="language-plaintext highlighter-rouge">io.Writer</code>を渡します。
標準入出力以外を渡したいときも、これらのインターフェイスを実装することで対応できる、Go言語ではよくある形です。</p>

<p>▼リスト19 <code class="language-plaintext highlighter-rouge">VM.Run()</code>メソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Run the program.</span>
<span class="k">func</span> <span class="p">(</span><span class="o">*</span><span class="n">wspace</span><span class="o">.</span><span class="n">VM</span><span class="p">)</span> <span class="n">Run</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">in</span> <span class="n">io</span><span class="o">.</span><span class="n">Reader</span><span class="p">,</span> <span class="n">out</span> <span class="n">io</span><span class="o">.</span><span class="n">Writer</span><span class="p">)</span> <span class="kt">error</span>
</code></pre></div></div>

<h2 id="カーネルへの組み込み">カーネルへの組み込み</h2>

<p>Jupyterからのコード実行リクエストは<code class="language-plaintext highlighter-rouge">"execute_request"</code>としてShellソケットに届きます。
<code class="language-plaintext highlighter-rouge">{content}</code>はリスト20のようなJSONで、<code class="language-plaintext highlighter-rouge">"code"</code>に実行すべきコードが入っています。</p>

<p>▼リスト20 execute_requestのcontent</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"silent"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"store_history"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"user_expressions"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
  </span><span class="nl">"allow_stdin"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"stop_on_error"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"   </span><span class="se">\t</span><span class="s2">    </span><span class="se">\t\n\t\n</span><span class="s2">  !"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>カーネルにVMを組み込んでコードを実行するには、起動時にVMを初期化しておき、
この<code class="language-plaintext highlighter-rouge">"execute_request"</code>ごとに<code class="language-plaintext highlighter-rouge">vm.Load()</code>と<code class="language-plaintext highlighter-rouge">vm.Run()</code>を実行することになります。
これを組み込んだShellハンドラはリスト21のようになります。</p>

<p>▼リスト21 execute_requestを処理するShellハンドラ</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">shellHandler</span><span class="p">(</span><span class="n">vm</span> <span class="o">*</span><span class="n">wspace</span><span class="o">.</span><span class="n">VM</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">execCount</span> <span class="o">:=</span> <span class="m">0</span>
    <span class="k">for</span> <span class="p">{</span>
        <span class="o">...</span> <span class="p">(</span><span class="n">略</span><span class="p">)</span>

        <span class="c">// メッセージ種別による分岐</span>
        <span class="k">switch</span> <span class="n">hdr</span><span class="p">[</span><span class="s">"msg_type"</span><span class="p">]</span> <span class="p">{</span>

        <span class="k">case</span> <span class="s">"kernel_info_request"</span><span class="o">:</span>
            <span class="o">...</span> <span class="p">(</span><span class="n">略</span><span class="p">)</span>

        <span class="k">case</span> <span class="s">"execute_request"</span><span class="o">:</span>
            <span class="c">// "busy"状態に変更（処理を終えたら"idle"に戻す）</span>
            <span class="n">s</span><span class="o">.</span><span class="n">sendState</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">stateBusy</span><span class="p">)</span>

            <span class="n">execCount</span><span class="o">++</span>

            <span class="c">// 入力した場所から実行できるようにする</span>
            <span class="n">vm</span><span class="o">.</span><span class="n">PC</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">vm</span><span class="o">.</span><span class="n">Program</span><span class="p">)</span>
            <span class="n">vm</span><span class="o">.</span><span class="n">Terminated</span> <span class="o">=</span> <span class="no">false</span>

            <span class="c">// コードの読み込み</span>
            <span class="k">var</span> <span class="n">content</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span>
            <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">(</span><span class="n">msg</span><span class="o">.</span><span class="n">Content</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">content</span><span class="p">)</span>
            <span class="n">code</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">content</span><span class="p">[</span><span class="s">"code"</span><span class="p">]</span><span class="o">.</span><span class="p">(</span><span class="kt">string</span><span class="p">))</span>
            <span class="n">_</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">vm</span><span class="o">.</span><span class="n">Load</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendStderr</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%v: %v"</span><span class="p">,</span> <span class="n">lineNum</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">pos</span><span class="p">),</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()))</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendExecuteErrorReply</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">shell</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">execCount</span><span class="p">,</span> <span class="s">"LoadingError"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendState</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">stateIdle</span><span class="p">)</span>
                <span class="k">continue</span>
            <span class="p">}</span>

            <span class="c">// 実行</span>
            <span class="n">out</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span><span class="p">)</span>
            <span class="n">in</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">stdinReader</span><span class="p">{</span><span class="n">socks</span><span class="o">:</span> <span class="n">s</span><span class="p">,</span> <span class="n">parent</span><span class="o">:</span> <span class="n">msg</span><span class="p">,</span> <span class="n">stdout</span><span class="o">:</span> <span class="n">out</span><span class="p">}</span>
            <span class="n">err</span> <span class="o">=</span> <span class="n">vm</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="n">in</span><span class="p">,</span> <span class="n">out</span><span class="p">)</span>
            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">out</span><span class="o">.</span><span class="n">Bytes</span><span class="p">())</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendStdout</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">out</span><span class="o">.</span><span class="n">Bytes</span><span class="p">()))</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                <span class="n">op</span> <span class="o">:=</span> <span class="n">vm</span><span class="o">.</span><span class="n">CurrentOpCode</span><span class="p">()</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendStderr</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span>
                    <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%v: %v: %v"</span><span class="p">,</span> <span class="n">lineNum</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">op</span><span class="o">.</span><span class="n">Pos</span><span class="p">),</span> <span class="n">op</span><span class="o">.</span><span class="n">Cmd</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()))</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendExecuteErrorReply</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">shell</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">execCount</span><span class="p">,</span> <span class="s">"RuntimeError"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
                <span class="n">s</span><span class="o">.</span><span class="n">sendState</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">stateIdle</span><span class="p">)</span>
                <span class="k">continue</span>
            <span class="p">}</span>

            <span class="n">s</span><span class="o">.</span><span class="n">sendExecuteOKReply</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">shell</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">execCount</span><span class="p">)</span>
            <span class="n">s</span><span class="o">.</span><span class="n">sendState</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">stateIdle</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">"execute_request"</code>に限らず、Shellに届いたリクエストを処理するときは最初に状態を<code class="language-plaintext highlighter-rouge">"busy"</code>にし、処理を終えたら<code class="language-plaintext highlighter-rouge">"idle"</code>に戻します。
これを忘れるとリプライが正しく反映されないことがあります<sup id="fnref:20" role="doc-noteref"><a href="#fn:20" class="footnote" rel="footnote">20</a></sup>。</p>

<h3 id="コードの読み込み">コードの読み込み</h3>

<p><code class="language-plaintext highlighter-rouge">wspace.VM</code>は読み込んだコードを命令列<code class="language-plaintext highlighter-rouge">vm.Program</code>とともに、
その実行位置を指し示すプログラムカウンタ<code class="language-plaintext highlighter-rouge">mv.PC</code>を持っています。
また、プログラム終了命令を実行したりエラーになって停止したことを示す<code class="language-plaintext highlighter-rouge">vm.Terminated</code>フラグもあります。</p>

<p>直前の実行で停止した場合、<code class="language-plaintext highlighter-rouge">vm.PC</code>は最後に実行した命令を指したままですし、
<code class="language-plaintext highlighter-rouge">vm.Terminated</code>が<code class="language-plaintext highlighter-rouge">true</code>になっていると続けて実行できません。
ここでは新たに読み込んだ場所から実行してほしいので、<code class="language-plaintext highlighter-rouge">vm.PC</code>を読み込み済みの<code class="language-plaintext highlighter-rouge">vm.Program</code>の末尾を指すようにし、
<code class="language-plaintext highlighter-rouge">vm.Terminated</code>も<code class="language-plaintext highlighter-rouge">false</code>にしておきます。</p>

<p>その後、送られてきたリクエストの<code class="language-plaintext highlighter-rouge">"code"</code>をそのまま<code class="language-plaintext highlighter-rouge">vm.Load()</code>で読み込みます。
読み込みエラー時は<code class="language-plaintext highlighter-rouge">stderr</code>にメッセージを表示してから<code class="language-plaintext highlighter-rouge">"execute_reply"</code>をエラーとして返すのですが、この詳細は後述します。</p>

<h3 id="出力">出力</h3>

<p><code class="language-plaintext highlighter-rouge">VM</code>からの出力は標準ライブラリの<code class="language-plaintext highlighter-rouge">bytes.Buffer</code>で受け取るようにしました。
実行中の出力をバッファリングしておき、終了後にまとめてJupyterの標準出力に送信します。</p>

<p>送信に使うメソッドはリスト22の<code class="language-plaintext highlighter-rouge">sendStdout()</code>です。
IOPubソケットに、メッセージタイプを<code class="language-plaintext highlighter-rouge">"stream"</code>、<code class="language-plaintext highlighter-rouge">{content}</code>に出力内容を入れて送信します。</p>

<p>▼リスト22 標準出力の送信メソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">sendStdout</span><span class="p">(</span><span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">output</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">content</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
        <span class="s">"name"</span><span class="o">:</span> <span class="s">"stdout"</span><span class="p">,</span>
        <span class="s">"text"</span><span class="o">:</span> <span class="n">output</span><span class="p">,</span>
    <span class="p">})</span>
    <span class="n">s</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">iopub</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="s">"stream"</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>標準エラー出力にしたいときは、<code class="language-plaintext highlighter-rouge">{content}</code>の<code class="language-plaintext highlighter-rouge">"name"</code>を<code class="language-plaintext highlighter-rouge">"stderr"</code>にします。
これも<code class="language-plaintext highlighter-rouge">sendStderr()</code>として定義しました。</p>

<h3 id="入力">入力</h3>

<p>Stdinソケットの使い方はShellソケットとは逆で、カーネルからJupyterに対してリクエストを投げます。
プログラムの実行中に標準入力を受け取る必要ができた時にリクエストを投げ、
それを受取ったJupyterは画面上に入力ボックスを表示します。
そしてユーザの入力をリプライとして返してくるので、カーネルはそれを受け取りプログラムに伝えます。</p>

<p><img src="/images/2024-06-04/input.png" alt="input" />
▲図2 入力ボックス</p>

<p><code class="language-plaintext highlighter-rouge">VM</code>への入力は<code class="language-plaintext highlighter-rouge">io.Reader</code>、つまり<code class="language-plaintext highlighter-rouge">Read()</code>メソッドをもつインターフェイスです。
VMが入力を要求する命令を処理する時、この<code class="language-plaintext highlighter-rouge">Read()</code>メソッドを呼び出します<sup id="fnref:21" role="doc-noteref"><a href="#fn:21" class="footnote" rel="footnote">21</a></sup>。
したがって、<code class="language-plaintext highlighter-rouge">Read()</code>の中でStdinソケットにリクエスト投げてリプライを受け取り、それを返すような型を実装することになります。</p>

<p>そのような型として、<code class="language-plaintext highlighter-rouge">stdinReader</code>を実装しました（リスト23）。</p>

<p>▼リスト23 stdinReader</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">stdinReader</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">socks</span>  <span class="o">*</span><span class="n">Sockets</span>
    <span class="n">parent</span> <span class="o">*</span><span class="n">Message</span>
    <span class="n">stdout</span> <span class="o">*</span><span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span>
    <span class="n">buf</span>    <span class="p">[]</span><span class="kt">byte</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">i</span> <span class="o">*</span><span class="n">stdinReader</span><span class="p">)</span> <span class="n">Read</span><span class="p">(</span><span class="n">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="c">// stdoutのフラッシュ </span>
    <span class="k">if</span> <span class="n">out</span> <span class="o">:=</span> <span class="n">i</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">Bytes</span><span class="p">();</span> <span class="nb">len</span><span class="p">(</span><span class="n">out</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
        <span class="n">i</span><span class="o">.</span><span class="n">socks</span><span class="o">.</span><span class="n">sendStdout</span><span class="p">(</span><span class="n">i</span><span class="o">.</span><span class="n">parent</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">out</span><span class="p">))</span>
        <span class="n">i</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">Reset</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="n">buf</span> <span class="o">:=</span> <span class="n">i</span><span class="o">.</span><span class="n">buf</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
        <span class="c">// stdinをJupyterに要求し受け取る</span>
        <span class="n">b</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">i</span><span class="o">.</span><span class="n">socks</span><span class="o">.</span><span class="n">getStdin</span><span class="p">(</span><span class="n">i</span><span class="o">.</span><span class="n">parent</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="n">err</span>
        <span class="p">}</span>
        <span class="n">buf</span> <span class="o">=</span> <span class="n">b</span>
    <span class="p">}</span>
    <span class="n">n</span> <span class="o">:=</span> <span class="nb">copy</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">buf</span><span class="p">)</span>
    <span class="n">i</span><span class="o">.</span><span class="n">buf</span> <span class="o">=</span> <span class="n">buf</span><span class="p">[</span><span class="n">n</span><span class="o">:</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Read()</code>メソッドで最初に行っているのは、出力のフラッシュ処理です。
入力を求めるプログラムでは大抵、何を入力するのか示す文字列を出力してから入力を受け付けます。
たとえばWhitespaceのサンプルのCalculator<sup id="fnref:22" role="doc-noteref"><a href="#fn:22" class="footnote" rel="footnote">22</a></sup>では、次のように表示しています。
このような表示を先に出力するために、バッファリングされている出力をフラッシュするようにしました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wspace calc.ws
Enter some numbers, then -1 to finish
Number:
</code></pre></div></div>

<p>入力の要求と取得をしているのは、ちょうど中央あたりの<code class="language-plaintext highlighter-rouge">getStdin()</code>メソッドです。
この中でStdinソケットと通信しています。</p>

<p>Whitespaceで文字の入力を受け取るには、1文字ずつ読む命令を使います。
しかし、Jupyterでの入力テキストボックスは1行単位で入力するようになっているので、
毎回入力を要求して1文字しか使わないのは直感に反しますし、非効率です。
なので、ここでは受取った入力をバッファリングし、
バッファに入力が残っているときはJupyterへの要求はせずにバッファの内容を切り出して返すようにしています。</p>

<p>一方、このバッファリングされた入力は、次の<code class="language-plaintext highlighter-rouge">"execute_request"</code>には引き継ぎません。
<code class="language-plaintext highlighter-rouge">"execute_request"</code>はノートブックのセル単位で行われ、
入力のテキストボックスもそのセルのすぐ下の入出力エリアに表示されます。
このため、前のセルの実行時の入力が混ざってしまうのは望ましくないと考え、
<code class="language-plaintext highlighter-rouge">stdinReader</code>は<code class="language-plaintext highlighter-rouge">"execute_request"</code>ごとに初期化するようにしました。</p>

<p>続いて、Stdinソケットで入力を要求し受け取る<code class="language-plaintext highlighter-rouge">getStdin()</code>をリスト24に示します。</p>

<p>▼リスト24 Stdinソケットで通信するメソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">getStdin</span><span class="p">(</span><span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">s</span><span class="o">.</span><span class="n">sendRouter</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">stdin</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="s">"input_request"</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">`{"prompt":"","password":false}`</span><span class="p">))</span>
    <span class="n">msg</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">recvRouterMessage</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">stdin</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">err</span>
    <span class="p">}</span>
    <span class="k">var</span> <span class="n">d</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
    <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">(</span><span class="n">msg</span><span class="o">.</span><span class="n">Content</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">)</span>
    <span class="k">return</span> <span class="nb">append</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">d</span><span class="p">[</span><span class="s">"value"</span><span class="p">]),</span> <span class="sc">'\n'</span><span class="p">),</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>StdinソケットはROUTERなので、メッセージを書き込むにはZmqIDが必要です。
ここではShellソケットで受信した<code class="language-plaintext highlighter-rouge">"execute_request"</code>のZmqIDと同じものを設定すれば大丈夫です。
というのも、Jupyter側でStdinにはShellと同じZmqIDを設定しているためです。</p>

<p>リクエストメッセージの<code class="language-plaintext highlighter-rouge">msg_type</code>は<code class="language-plaintext highlighter-rouge">"input_request"</code>で、<code class="language-plaintext highlighter-rouge">{content}</code>は<code class="language-plaintext highlighter-rouge">"prompt"</code>文字列と<code class="language-plaintext highlighter-rouge">"password"</code>フラグを指定します。
このメッセージを、Shellと同じように<code class="language-plaintext highlighter-rouge">sendRouterMessage()</code>で送信します。
するとJupyter上で入力のテキストボックスが表示されます。</p>

<p>テキストボックスに入力してエンターキーを押すと、Stdinソケットに<code class="language-plaintext highlighter-rouge">"input_reply"</code>メッセージが届きます。
<code class="language-plaintext highlighter-rouge">{content}</code>はリスト25のようになっています。</p>

<p>▼リスト25 <code class="language-plaintext highlighter-rouge">"input_reply"</code>の<code class="language-plaintext highlighter-rouge">{content}</code></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hello"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>入力値の<code class="language-plaintext highlighter-rouge">"value"</code>には末尾に改行は付いていません。
Whitespaceでは、数値の入力では末尾に改行（またはEOF）を要求します。
また、一般的な標準入力では、大抵のターミナルで行単位で末尾の改行を含めて入力されます。
この挙動に合わせたほうが都合がよいので、改行文字<code class="language-plaintext highlighter-rouge">'\n'</code>を末尾に追加して入力値としました。</p>

<h3 id="execute_reply"><code class="language-plaintext highlighter-rouge">"execute_reply"</code></h3>

<p>コードの実行が終わったら<code class="language-plaintext highlighter-rouge">"execute_reply"</code>を送信します。
<code class="language-plaintext highlighter-rouge">{content}</code>はリスト26のようなJSONです。
<code class="language-plaintext highlighter-rouge">"execution_count"</code>はJupyter上で実行したコードの左に表示される番号です。
<code class="language-plaintext highlighter-rouge">"execute_request"</code>を処理する毎に<code class="language-plaintext highlighter-rouge">execCount</code>をインクリメントしてこの値としています。</p>

<p>▼リスト26 execute_replyのcontent</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"execution_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>エラー時は<code class="language-plaintext highlighter-rouge">"status"</code>を<code class="language-plaintext highlighter-rouge">"error"</code>にするほか、
エラーの名前と内容を示す<code class="language-plaintext highlighter-rouge">"ename"</code> <code class="language-plaintext highlighter-rouge">"evalue"</code>などのフィールドを加えますが、Jupyter上には表示されないようです。
ユーザーに見せるメッセージは<code class="language-plaintext highlighter-rouge">stderr</code>へ出力するようにしましょう。</p>

<p>▼リスト27 <code class="language-plaintext highlighter-rouge">"execute_reply"</code>を送信するメソッド</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">sendExecuteOKReply</span><span class="p">(</span><span class="n">sock</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">,</span> <span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">count</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">content</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">`{"status":"ok","execution_count":%d}`</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
    <span class="n">s</span><span class="o">.</span><span class="n">sendRouter</span><span class="p">(</span><span class="n">sock</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="s">"execute_reply"</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">content</span><span class="p">))</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Sockets</span><span class="p">)</span> <span class="n">sendExecuteErrorReply</span><span class="p">(</span>
    <span class="n">sock</span> <span class="o">*</span><span class="n">zmq4</span><span class="o">.</span><span class="n">Socket</span><span class="p">,</span> <span class="n">parent</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="n">count</span> <span class="kt">int</span><span class="p">,</span> <span class="n">ename</span><span class="p">,</span> <span class="n">evalue</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>

    <span class="n">content</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span>
        <span class="s">"status"</span><span class="o">:</span>          <span class="s">"error"</span><span class="p">,</span>
        <span class="s">"execution_count"</span><span class="o">:</span> <span class="n">count</span><span class="p">,</span>
        <span class="s">"ename"</span><span class="o">:</span>           <span class="n">ename</span><span class="p">,</span>
        <span class="s">"evalue"</span><span class="o">:</span>          <span class="n">evalue</span><span class="p">,</span>
        <span class="s">"traceback"</span><span class="o">:</span>       <span class="p">[]</span><span class="n">any</span><span class="p">{},</span>
    <span class="p">})</span>
    <span class="n">s</span><span class="o">.</span><span class="n">sendRouter</span><span class="p">(</span><span class="n">sock</span><span class="p">,</span> <span class="n">parent</span><span class="p">,</span> <span class="s">"execute_reply"</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>これでWhitespaceをJupyter上で実行できるようになりました。
余談ですが、Jupyterは空文字列しかないセルは実行してくれません（<code class="language-plaintext highlighter-rouge">"execute_request"</code>を送信してくれません）。
Whitespaceのコードを実行するときは最低1文字は見える文字を混ぜておく必要があります。</p>

<h2 id="おわりに">おわりに</h2>

<p>この章では、Jupyterのカーネルの実装方法を、Whitespaceのカーネル「whitenote」のコードを使って解説しました。
細かいお約束が多いため長くなってしまいましたが、必要な実装はそれほど多くなく、
シンプルな仕組みになっていることが分っていただけたと思います。</p>

<p>ぜひ皆さんも、お気に入りの言語のJupyterカーネルを自作してみてください。</p>

<hr />

<h3 id="コラム-タブ文字を入力するには">コラム: タブ文字を入力するには</h3>
<p>JupyterのコードセルでWhitespaceのコードを入力しようとすると、タブキーを押してもタブ文字が入力されないことに気づくと思います。
これは、タブキーがコード補完に割り当てられているためです。</p>

<p>Jupyterがコードを補完するとき、カーネルには<code class="language-plaintext highlighter-rouge">"complete_request"</code>が送られます。
ここで補完候補を複数返すとJupyter上で選択するUIが表示されますが、候補が1つしかないときは直接それが入力されます。</p>

<p>つまり、<code class="language-plaintext highlighter-rouge">"complete_request"</code>に対してタブ文字だけを補完候補として返すことで、タブ文字を入力できるようになります。
実装の詳細はwhitenoteのリポジトリをご覧ください。</p>

<p>ただし、行頭から空白文字しかない場合は補完ではなく、自動インデントになってしまいます。（しかもタブ文字ではなくスペースで！）
この挙動はJavascriptのCodeMirror<sup id="fnref:23" role="doc-noteref"><a href="#fn:23" class="footnote" rel="footnote">23</a></sup>によるもので、カーネルからは挙動を変えられそうにはありません。</p>

<p>Whitespaceを記述するときは行頭になにか見える文字を入力しておくと快適に入力できます。</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://jupyter.org/">https://jupyter.org/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://web.archive.org/web/20150618184706/http://compsoc.dur.ac.uk/whitespace/">https://web.archive.org/web/20150618184706/http://compsoc.dur.ac.uk/whitespace/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://zeromq.org/">https://zeromq.org/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://jupyter-client.readthedocs.io/en/latest/kernels.html">https://jupyter-client.readthedocs.io/en/latest/kernels.html</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://zeromq.org/get-started/#pick-your-language">https://zeromq.org/get-started/#pick-your-language</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><a href="https://github.com/zeromq/goczmq">https://github.com/zeromq/goczmq</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7" role="doc-endnote">
      <p><a href="https://github.com/pebbe/zmq4">https://github.com/pebbe/zmq4</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8" role="doc-endnote">
      <p><a href="https://github.com/go-zeromq/zmq4">https://github.com/go-zeromq/zmq4</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:9" role="doc-endnote">
      <p><a href="https://jupyter-client.readthedocs.io/en/latest/messaging.html">https://jupyter-client.readthedocs.io/en/latest/messaging.html</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:10" role="doc-endnote">
      <p>Pythonで実装する場合はライブラリが隠蔽しているのでしっかりとは書いていないのでしょう。 <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:11" role="doc-endnote">
      <p><a href="http://api.zeromq.org/master:zmq-send">http://api.zeromq.org/master:zmq-send</a>、<a href="http://api.zeromq.org/master:zmq-recv">http://api.zeromq.org/master:zmq-recv</a> <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:12" role="doc-endnote">
      <p><a href="https://zguide.zeromq.org/">https://zguide.zeromq.org/</a> 日本語訳:<a href="https://www.cuspy.org/diary/2015-05-07-zmq/">https://www.cuspy.org/diary/2015-05-07-zmq/</a> <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:13" role="doc-endnote">
      <p><a href="https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernelspecs">https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernelspecs</a> <a href="#fnref:13" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:14" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">logo-32x32.png</code>は使われていません。<a href="https://github.com/ipython/ipython/pull/6537">https://github.com/ipython/ipython/pull/6537</a> <a href="#fnref:14" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:15" role="doc-endnote">
      <p>ググラビリティが低いため、見つけられていないだけかもしれません。 <a href="#fnref:15" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:16" role="doc-endnote">
      <p>可視化するとこうなります: SSSTSSTSSSNTNSSSSSTTSSTSTNTNSSSSSTTSTTSSNSNSTNSSTNSSSSSTTSTTTTNTNSSSSSTSSSSTNTNSSNNN <a href="#fnref:16" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:17" role="doc-endnote">
      <p><a href="https://web.archive.org/web/20150618184706/http://compsoc.dur.ac.uk/whitespace/tutorial.php">https://web.archive.org/web/20150618184706/http://compsoc.dur.ac.uk/whitespace/tutorial.php</a> <a href="#fnref:17" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:18" role="doc-endnote">
      <p><a href="https://github.com/makiuchi-d/whitenote/tree/main/wspace">https://github.com/makiuchi-d/whitenote/tree/main/wspace</a> <a href="#fnref:18" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:19" role="doc-endnote">
      <p>セルごとに実行されてしまうので、サブルーチンを記述するセルの先頭に終了命令を置くなど工夫が必要です <a href="#fnref:19" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:20" role="doc-endnote">
      <p>ZeroMQのメッセージ送信は非同期で行われるため、Shellへのreply送信とIOPubへのbusy/idle通知の順序が入れ替わることがありえます。その際の挙動は未定義とされていて、ちょっと危ういシステムです。 <a href="#fnref:20" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:21" role="doc-endnote">
      <p>実際の実装では効率化のため、<code class="language-plaintext highlighter-rouge">ReadByte()</code>も実装しています。 <a href="#fnref:21" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:22" role="doc-endnote">
      <p><a href="https://web.archive.org/web/20150717115008/http://compsoc.dur.ac.uk/whitespace/calc.ws">https://web.archive.org/web/20150717115008/http://compsoc.dur.ac.uk/whitespace/calc.ws</a> <a href="#fnref:22" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:23" role="doc-endnote">
      <p><a href="https://codemirror.net/">https://codemirror.net/</a> <a href="#fnref:23" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2022年9月10日から開催された技術書典13にて頒布した「KLabTechBook Vol.10」に掲載したものです。 Whitespaceのコードをそのまま紙面に載せました。]]></summary></entry><entry><title type="html">オンライン対戦を支える独自シリアライズフォーマット (KLabTechBook Vol.9)</title><link href="http://makiuchi-d.github.io/2024/06/02/klabtechbook9-wsnet2-serializer.ja.html" rel="alternate" type="text/html" title="オンライン対戦を支える独自シリアライズフォーマット (KLabTechBook Vol.9)" /><published>2024-06-02T00:00:00+00:00</published><updated>2024-06-02T00:00:00+00:00</updated><id>http://makiuchi-d.github.io/2024/06/02/klabtechbook9-wsnet2-serializer.ja</id><content type="html" xml:base="http://makiuchi-d.github.io/2024/06/02/klabtechbook9-wsnet2-serializer.ja.html"><![CDATA[<p>この記事は2022年1月22日から開催された<a href="https://techbookfest.org/event/tbf12">技術書典12</a>にて頒布した「KLabTechBook Vol.9」に掲載したものです。</p>

<p>現在開催中の<a href="https://techbookfest.org/event/tbf16">技術書典16</a>オンラインマーケットにて新刊「<a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5">KLabTechBook Vol.13</a>」を頒布（電子版無料、紙+電子 500円）しています。
また、既刊も在庫があるものは物理本を<a href="https://techbookfest.org/organization/5654456649646080">オンラインマーケット</a>で頒布しているほか、
<a href="https://www.klab.com/jp/blog/tech/2024/tbf16.html">KLabのブログ</a>からもすべての既刊のPDFを無料DLできます。
合わせてごらんください。</p>

<p><a href="https://techbookfest.org/product/3CTYX4wj9wwBr13qJRYwA5"><img src="/images/2024-05-29/ktbv13.jpg" width="40%" alt="KLabTechBook Vol.13" /></a></p>

<hr />

<p style="background-color:lightcyan;border-left:0.3em solid cyan;padding:0.5em">
<strong>ℹ️</strong>
この記事で言及している同期通信基盤は、その後「<a href="https://github.com/KLab/wsnet2">WSNet2</a>」というOSSとしてGitHub上にて公開しています。
</p>

<h2 id="はじめに">はじめに</h2>

<p>近年のモバイルオンラインゲームでは、対戦や協力プレイといった同期通信が当たり前になっています。
KLabでももちろんそのようなゲームをリリースしており、Photon<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>のようなサードパーティのサービスを使うこともありますが、
いくつかのタイトルでは独自の同期通信の仕組みを使っています。
筆者はこの数年、この同期通信基盤の開発と運用に携わってきました。</p>

<p>KLabの多くのタイトルはUnityで制作していますが、この同期通信基盤では部屋管理とデータ中継のサーバーをGo言語で実装しており、
各クライアントからHTTPとWebSocketでこれらのサーバーに接続する構成を取っています。
また、さまざまなプロジェクトで同じサーバーをそのまま使えるような汎用的な作りにしています。</p>

<p>この章では、KLabの同期通信基盤のために開発した独自のシリアライズフォーマットについて、
その特徴や工夫した点を紹介したいと思います。</p>

<h2 id="なぜ独自フォーマットが必要だったのか">なぜ独自フォーマットが必要だったのか</h2>

<p>ネットワークを介してデータを送信するには、何らかの方法でビット列に変換する必要があります。
そして受信したビット列は、元のデータに復元しなければプログラムからは利用できません。
クライアントはC#なので、値だけでなく型も送受信の前後で同じにならないと困ってしまいます。</p>

<p>C#同士だけであれば、C#の標準ライブラリの<code class="language-plaintext highlighter-rouge">System.Runtime.Serialization</code>を使うこともできるかもしれません。
しかし今回作っているものは、部屋を管理するためにGo製のサーバーでもデータを読み取る必要があるため、Goでも読み取りやすい形式が必要でした。</p>

<p>世の中にはC#でもGoでも、あるいは他の言語でも使えるような汎用フォーマットもあります。
しかし、たとえばJSONやMessagePack<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>ではC#よりも型が少ないため送信元の型を完全に復元することができませんし、
C#のときの型がGoからは分らなくなってしまいます。
あるいはProtocolBuffers<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>のように共通の定義から事前にコード生成するものもありますが、
利用するデータ型を追加するためにはクライアントだけでなくサーバーも合わせて更新する必要があります。
これでは多くのプロジェクトで使える共通のサーバーを作るには不向きです。
できるならクライアントだけで独自のデータ型を追加できるのが理想です。</p>

<p>このようなニッチな要求を満たすものはまず存在しないので作ることにしました。
ここで紹介するシリアライザはGitHubにて公開しています<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。
あわせてご覧ください<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>。</p>

<h2 id="独自フォーマットの特徴">独自フォーマットの特徴</h2>

<p>C#のプリミティブ型に加え、独自定義型も特定インターフェイスを実装することでシリアライズできます。
加えて、シリアライズ可能な型を要素にもつリストや配列、文字列キーの辞書型もサポートし、ネストもできます。</p>

<p>この辞書型は部屋のプロパティとしても利用しており、Goでも辞書（<code class="language-plaintext highlighter-rouge">map</code>）として扱うためにキーの型を固定する必要がありました。
Photonでも部屋のプロパティ（<code class="language-plaintext highlighter-rouge">RoomInfo.CustomProperties</code>）は文字列キーの辞書を採用していますし、扱いやすさを優先して文字列キーとしました。</p>

<p>またGoでリストや辞書をデシリアライズするとき、各要素をバイト列のスライス（<code class="language-plaintext highlighter-rouge">[]byte</code>）のまま保持し、
必要になるまでデシリアライズしない遅延デシリアライズを実現したほか、
同じ型同士の単純な大小比較であればバイト列のまま比較できるようにしました。
このメリットは後ほど紹介したいと思います。</p>

<p>クライアント側C#の実装も、パフォーマンス対策としてboxingの回避やオブジェクトの再利用ができるよう実装しています。
その詳細はリポジトリの<code class="language-plaintext highlighter-rouge">SerialReader</code>、<code class="language-plaintext highlighter-rouge">SerialWriter</code>クラスをご覧いただくとして、
ここではフォーマットの概要を説明します。</p>

<h2 id="フォーマットの概要">フォーマットの概要</h2>

<p>基本的には、1byteの型情報とそれに続く型ごとのデータのバイト列で構成されます。
扱える型は先述のとおり、C#のほとんどのプリミティブ型と、独自定義型、シリアライズ可能な型のリストや辞書です。</p>

<p><img src="/images/2024-06-02/format.png" alt="基本的な形" />
<br />▲図1 基本的な形</p>

<p>▼表1 型情報と対応するC#の型一覧</p>

<table>
  <tbody>
    <tr>
      <td>0: Null</td>
      <td>11: ULong (<code class="language-plaintext highlighter-rouge">ulong</code>)</td>
      <td>22: Bytes (<code class="language-plaintext highlighter-rouge">byte[]</code>)</td>
    </tr>
    <tr>
      <td>1: False (<code class="language-plaintext highlighter-rouge">bool</code>)</td>
      <td>12: Float (<code class="language-plaintext highlighter-rouge">float</code>)</td>
      <td>23: Chars (<code class="language-plaintext highlighter-rouge">char[]</code>)</td>
    </tr>
    <tr>
      <td>2: True (<code class="language-plaintext highlighter-rouge">bool</code>)</td>
      <td>13: Double (<code class="language-plaintext highlighter-rouge">double</code>)</td>
      <td>24: Shorts (<code class="language-plaintext highlighter-rouge">short[]</code>)</td>
    </tr>
    <tr>
      <td>3: SByte (<code class="language-plaintext highlighter-rouge">sbyte</code>)</td>
      <td>14: Decimal (<code class="language-plaintext highlighter-rouge">decimal</code>)<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup></td>
      <td>25: UShorts (<code class="language-plaintext highlighter-rouge">ushort[]</code>)</td>
    </tr>
    <tr>
      <td>4: Byte (<code class="language-plaintext highlighter-rouge">byte</code>)</td>
      <td>15: Str8 (<code class="language-plaintext highlighter-rouge">string</code>)</td>
      <td>26: Ints (<code class="language-plaintext highlighter-rouge">int[]</code>)</td>
    </tr>
    <tr>
      <td>5: Char (<code class="language-plaintext highlighter-rouge">char</code>)</td>
      <td>16: Str16 (<code class="language-plaintext highlighter-rouge">string</code>)</td>
      <td>27: UInts (<code class="language-plaintext highlighter-rouge">uint[]</code>)</td>
    </tr>
    <tr>
      <td>6: Short (<code class="language-plaintext highlighter-rouge">short</code>)</td>
      <td>17: Obj (独自定義クラス)</td>
      <td>28: Longs (<code class="language-plaintext highlighter-rouge">long[]</code>)</td>
    </tr>
    <tr>
      <td>7: UShort (<code class="language-plaintext highlighter-rouge">ushort</code>)</td>
      <td>18: List (<code class="language-plaintext highlighter-rouge">List&lt;object&gt;</code>)</td>
      <td>29: ULongs (<code class="language-plaintext highlighter-rouge">ulong[]</code>)</td>
    </tr>
    <tr>
      <td>8: Int (<code class="language-plaintext highlighter-rouge">int</code>)</td>
      <td>19: Dict (<code class="language-plaintext highlighter-rouge">Dictionary&lt;string, object&gt;</code>)</td>
      <td>30: Floats (<code class="language-plaintext highlighter-rouge">float[]</code>)</td>
    </tr>
    <tr>
      <td>9: UInt (<code class="language-plaintext highlighter-rouge">uint</code>)</td>
      <td>20: Bools (<code class="language-plaintext highlighter-rouge">bool[]</code>)</td>
      <td>31: Doubles (<code class="language-plaintext highlighter-rouge">double[]</code>)</td>
    </tr>
    <tr>
      <td>10: Long (<code class="language-plaintext highlighter-rouge">long</code>)</td>
      <td>21: SBytes (<code class="language-plaintext highlighter-rouge">sbyte[]</code>)</td>
      <td>32: Decimals (<code class="language-plaintext highlighter-rouge">decimal[]</code>)<sup id="fnref:6:1" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup></td>
    </tr>
  </tbody>
</table>

<p>ここからはそれぞれの型について、その型ごとのシリアライズ方法を解説していきます。</p>

<h3 id="null値とbool型">Null値とbool型</h3>

<p>さきほど、フォーマットの基本は1byteの型とそれに続くデータと説明しましたが、いきなり例外的なものたちです。
<code class="language-plaintext highlighter-rouge">bool</code>型は<code class="language-plaintext highlighter-rouge">true</code>または<code class="language-plaintext highlighter-rouge">false</code>ですが、それを型情報に加えて1byteのデータで表すのはもったいないので、
型情報をTrueとFalseの2種類に分け、データを持たない形としました。
また、Null値は型をもたない<code class="language-plaintext highlighter-rouge">null</code>として、これも1byteの型情報のみで表現します。
このようなやり方はMessagePackを参考にしました。</p>

<p>リスト1にbool型のシリアライズとデシリアライズのC#実装を掲載します。
boxingを避けるために、書き込み（<code class="language-plaintext highlighter-rouge">Write</code>メソッド）は引数の型でメソッドオーバーロードし、
読み出しは型ごとのメソッド（<code class="language-plaintext highlighter-rouge">Read+型名</code>）を定義しています。</p>

<p>▼リスト1 bool型のシリアライズ・デシリアライズ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">SerialWriter</span>
<span class="p">{</span>
    <span class="kt">int</span>    <span class="n">pos</span><span class="p">;</span> <span class="c1">// 書き込み位置</span>
    <span class="kt">byte</span><span class="p">[]</span> <span class="n">buf</span><span class="p">;</span> <span class="c1">// 書き込みバッファ</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;Bool値を書き込む&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Write</span><span class="p">(</span><span class="kt">bool</span> <span class="n">v</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">expand</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>                                     <span class="c1">// バッファが足りなければ1byte拡張</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)(</span><span class="n">v</span> <span class="p">?</span> <span class="n">Type</span><span class="p">.</span><span class="n">True</span> <span class="p">:</span> <span class="n">Type</span><span class="p">.</span><span class="n">False</span><span class="p">);</span> <span class="c1">// 型情報としてTrueかFalseを書き込む</span>
        <span class="n">pos</span><span class="p">++;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SerialReader</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;Bool値として読み出す&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="nf">ReadBool</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="nf">checkType</span><span class="p">(</span><span class="n">Type</span><span class="p">.</span><span class="n">True</span><span class="p">,</span> <span class="n">Type</span><span class="p">.</span><span class="n">False</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">t</span> <span class="p">==</span> <span class="n">Type</span><span class="p">.</span><span class="n">True</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;先頭1byteを読み、引数のTypeだったらそれを返し、それ以外のときは例外送出&lt;/summary&gt;</span>
    <span class="n">Type</span> <span class="nf">checkType</span><span class="p">(</span><span class="n">Type</span> <span class="n">want1</span><span class="p">,</span> <span class="n">Type</span> <span class="n">want2</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">checkLength</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>               <span class="c1">// 1byte以上あるか確認</span>
        <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="p">(</span><span class="n">Type</span><span class="p">)</span><span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">t</span> <span class="p">!=</span> <span class="n">want1</span> <span class="p">&amp;&amp;</span> <span class="n">t</span> <span class="p">!=</span> <span class="n">want2</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">SerializationException</span><span class="p">(</span><span class="s">"invalid type"</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">pos</span><span class="p">++;</span>
        <span class="k">return</span> <span class="n">t</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="整数型浮動小数点数型">整数型、浮動小数点数型</h3>

<p>整数型は1byteの型情報に続けて、数値をBigEndianで書き込みます。
MessagePackのように節約したフォーマットではなく、64bitの<code class="language-plaintext highlighter-rouge">long</code>型はそのまま8byteで記録する単純な形です。
このとき、符号なし整数はそのままですが、符号付き整数は下駄履き表現、つまり<code class="language-plaintext highlighter-rouge">short</code>の場合<code class="language-plaintext highlighter-rouge">128</code>を加えて、
<code class="language-plaintext highlighter-rouge">-128</code>を<code class="language-plaintext highlighter-rouge">127</code>を<code class="language-plaintext highlighter-rouge">255</code>となるようにして書き込みます。
こうすることで、バイト列を先頭から単純に比較していくだけで、元の値の大小関係が分かるようになります。</p>

<p>▼リスト2 符号付きshort型のシリアライズ・デシリアライズ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">SerialWriter</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;Short値を書き込む&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Write</span><span class="p">(</span><span class="kt">short</span> <span class="n">v</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">expand</span><span class="p">(</span><span class="m">3</span><span class="p">);</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">Type</span><span class="p">.</span><span class="n">Short</span><span class="p">;</span>
        <span class="n">pos</span><span class="p">++;</span>
        <span class="kt">var</span> <span class="n">n</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">v</span> <span class="p">-</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="kt">short</span><span class="p">.</span><span class="n">MinValue</span><span class="p">;</span> <span class="c1">// 下駄履き表現に変換</span>
        <span class="nf">Put16</span><span class="p">(</span><span class="n">n</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;16bit値をBigEndianで書き込む&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Put16</span><span class="p">(</span><span class="kt">int</span> <span class="n">v</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)((</span><span class="n">v</span> <span class="p">&amp;</span> <span class="m">0xff00</span><span class="p">)</span> <span class="p">&gt;&gt;</span> <span class="m">8</span><span class="p">);</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">+</span><span class="m">1</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)(</span><span class="n">v</span> <span class="p">&amp;</span> <span class="m">0xff</span><span class="p">);</span>
        <span class="n">pos</span> <span class="p">+=</span> <span class="m">2</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SerialReader</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;Short値として読み出す&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="kt">short</span> <span class="nf">ReadShort</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nf">checkType</span><span class="p">(</span><span class="n">Type</span><span class="p">.</span><span class="n">Short</span><span class="p">);</span>
        <span class="k">return</span> <span class="p">(</span><span class="kt">short</span><span class="p">)(</span><span class="nf">Get16</span><span class="p">()</span> <span class="p">+</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="kt">short</span><span class="p">.</span><span class="n">MinValue</span><span class="p">);</span> <span class="c1">// 下駄履き表現から戻す</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;16bit値を読み出す&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="nf">Get16</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nf">checkLength</span><span class="p">(</span><span class="m">2</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">n</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">&lt;&lt;</span> <span class="m">8</span><span class="p">;</span>
        <span class="n">n</span> <span class="p">+=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">+</span><span class="m">1</span><span class="p">];</span>
        <span class="n">pos</span> <span class="p">+=</span> <span class="m">2</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">n</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>浮動小数点数でもIEEE 754の表現からビット操作して、整数型と同じようにバイト列のまま大小比較できるようにしました。
詳しくは筆者のblog記事<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>をご覧ください。</p>

<h3 id="文字列型">文字列型</h3>

<p>文字列型は可変長なので、1byteの型情報に続けてデータ長を書いておきます。
ゲームで通信しあう文字列は短いものが多いので、データ長は1byteで表現したいところですが、
チャットのような機能を作る場合は255文字では足りないかもしれません。
そこで型情報をStr8とStr16の2つに分け、255byte以下は前者で文字列長を1byte、それ以上長いものは後者で文字列長を2byteとしました。</p>

<p>データのエンコーディングはUTF-8とします。
これはGoの文字列の内部エンコーディングがUTF-8なので、バイト列からそのまま文字列にキャストできるようにするためです。</p>

<p>▼リスト2 文字列型のシリアライズ・デシリアライズ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">SerialWriter</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;文字列を書き込む&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Write</span><span class="p">(</span><span class="kt">string</span> <span class="n">v</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">v</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">Write</span><span class="p">();</span> <span class="c1">// Type.Null書き込み</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">len</span> <span class="p">=</span> <span class="n">utf8</span><span class="p">.</span><span class="nf">GetByteCount</span><span class="p">(</span><span class="n">v</span><span class="p">);</span> <span class="c1">// UTF-8でのデータ長</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="p">&lt;=</span> <span class="kt">byte</span><span class="p">.</span><span class="n">MaxValue</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">expand</span><span class="p">(</span><span class="n">len</span><span class="p">+</span><span class="m">2</span><span class="p">);</span>
            <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">Type</span><span class="p">.</span><span class="n">Str8</span><span class="p">;</span>
            <span class="n">pos</span><span class="p">++;</span>
            <span class="nf">Put8</span><span class="p">(</span><span class="n">len</span><span class="p">);</span> <span class="c1">// 1byteでデータ長を記録</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="p">&lt;=</span> <span class="kt">ushort</span><span class="p">.</span><span class="n">MaxValue</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">expand</span><span class="p">(</span><span class="n">len</span><span class="p">+</span><span class="m">3</span><span class="p">);</span>
            <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">Type</span><span class="p">.</span><span class="n">Str16</span><span class="p">;</span>
            <span class="n">pos</span><span class="p">++;</span>
            <span class="nf">Put16</span><span class="p">(</span><span class="n">len</span><span class="p">);</span> <span class="c1">// 2byteでデータ長を記録</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">SerializationException</span><span class="p">(</span><span class="s">"too long"</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="n">utf8</span><span class="p">.</span><span class="nf">GetBytes</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">pos</span><span class="p">);</span> <span class="c1">// UTF-8として書き込み</span>
        <span class="n">pos</span> <span class="p">+=</span> <span class="n">len</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SerialReader</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;文字列として読み出す&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="nf">ReadString</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="nf">checkType</span><span class="p">(</span><span class="n">Type</span><span class="p">.</span><span class="n">Str8</span><span class="p">,</span> <span class="n">Type</span><span class="p">.</span><span class="n">Str16</span><span class="p">,</span> <span class="n">Type</span><span class="p">.</span><span class="n">Null</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">t</span> <span class="p">==</span> <span class="n">Type</span><span class="p">.</span><span class="n">Null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="c1">// データ長はStr8なら1byte, Str16なら2byte</span>
        <span class="kt">var</span> <span class="n">len</span> <span class="p">=</span> <span class="p">(</span><span class="n">t</span> <span class="p">==</span> <span class="n">Type</span><span class="p">.</span><span class="n">Str8</span><span class="p">)</span> <span class="p">?</span> <span class="nf">Get8</span><span class="p">()</span> <span class="p">:</span> <span class="nf">Get16</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">str</span> <span class="p">=</span> <span class="n">utf8</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
        <span class="n">pos</span> <span class="p">+=</span> <span class="n">len</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">str</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="独自定義クラス">独自定義クラス</h3>

<p>独自定義クラスをシリアライズできるようにするには、<code class="language-plaintext highlighter-rouge">IWSNet2Serializable</code>インターフェイスを実装し、
<code class="language-plaintext highlighter-rouge">WSNet2Serializer.Register</code>メソッドでClassIDを事前に登録する必要があります。
このClassIDとクラスの対応関係は通信するすべてのクライアントで一致している必要があります。
クライアントは基本的に同じソースからビルドするはずなので、一致させるのは容易でしょう。</p>

<p>▼リスト3 IWSNet2SerializableインターフェイスとRegisterメソッド</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IWSNet2Serializable</span>
<span class="p">{</span>
    <span class="k">void</span> <span class="nf">Serialize</span><span class="p">(</span><span class="n">SerialWriter</span> <span class="n">writer</span><span class="p">);</span>
    <span class="k">void</span> <span class="nf">Deserialize</span><span class="p">(</span><span class="n">SerialReader</span> <span class="n">reader</span><span class="p">,</span> <span class="kt">int</span> <span class="n">size</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">WSNet2Serializer</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">delegate</span> <span class="kt">object</span> <span class="nf">ReadFunc</span><span class="p">(</span><span class="n">SerialReader</span> <span class="n">reader</span><span class="p">,</span> <span class="kt">object</span> <span class="n">recycle</span><span class="p">);</span>

    <span class="k">static</span> <span class="n">Hashtable</span> <span class="n">registeredTypes</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Hashtable</span><span class="p">();</span> <span class="c1">// 型-&gt;ClassIDのマッピング</span>
    <span class="k">static</span> <span class="n">ReadFunc</span><span class="p">[]</span> <span class="n">readFuncs</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ReadFunc</span><span class="p">[</span><span class="m">256</span><span class="p">];</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;独自定義クラスを登録&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">Register</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="kt">byte</span> <span class="n">classID</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="k">class</span><span class="err">,</span> <span class="nc">IWSNet2Serializable</span><span class="p">,</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="k">typeof</span><span class="p">(</span><span class="n">T</span><span class="p">);</span>
        <span class="n">registeredTypes</span><span class="p">[</span><span class="n">t</span><span class="p">]</span> <span class="p">=</span> <span class="n">classID</span><span class="p">;</span>

        <span class="c1">// SerialReader.ReadObject&lt;T&gt;() は型Tがわからないと呼べない</span>
        <span class="c1">// ClassIDだけから呼び出せるように無名関数を保持しておく</span>
        <span class="n">readFuncs</span><span class="p">[</span><span class="n">classID</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="n">reader</span><span class="p">,</span> <span class="n">obj</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">reader</span><span class="p">.</span><span class="n">ReadObject</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">obj</span> <span class="k">as</span> <span class="n">T</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>シリアライズ後のデータは図2のような形になります。
ClassIDが1byteなため登録できるクラスは256種類に限られますが、普通のゲームであれば十分な数です。
また、ClassIDの後にデータサイズがあることで、中身をデシリアライズすることなくデータを切り出すことができます。
これにより、データ部分をバイト列として切り出しておき、必要になってからデシリアライズする遅延デシリアライズができます。</p>

<p><img src="/images/2024-06-02/serialobj.png" alt="独自定義クラスのシリアライズイメージ" />
▲図2 独自定義クラスのシリアライズイメージ</p>

<p>ここでリスト4に独自定義クラスの例として、チェスの駒を表すクラスを定義してみます。</p>

<p>▼リスト4 独自定義クラスの例</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ChessPiece</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">PieceType</span> <span class="n">Type</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">PositionX</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">PositionY</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Serialize</span><span class="p">(</span><span class="n">SerialWriter</span> <span class="n">writer</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">writer</span><span class="p">.</span><span class="nf">Write</span><span class="p">((</span><span class="kt">byte</span><span class="p">)</span><span class="n">Type</span><span class="p">);</span> <span class="c1">// PieceTypeは1byteで</span>
        <span class="c1">// 盤面は8x8なので座標も1byteにまとめて</span>
        <span class="n">writer</span><span class="p">.</span><span class="nf">Write</span><span class="p">((</span><span class="kt">byte</span><span class="p">)(</span><span class="n">PositionX</span> <span class="p">*</span> <span class="m">8</span> <span class="p">+</span> <span class="n">PositionY</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Deserialize</span><span class="p">(</span><span class="n">SerialReader</span> <span class="n">reader</span><span class="p">,</span> <span class="kt">int</span> <span class="n">size</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Type</span> <span class="p">=</span> <span class="p">(</span><span class="n">PieceType</span><span class="p">)</span><span class="n">reader</span><span class="p">.</span><span class="nf">ReadByte</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">pos</span> <span class="p">=</span> <span class="n">writer</span><span class="p">.</span><span class="nf">ReadByte</span><span class="p">();</span>
        <span class="n">PositionX</span> <span class="p">=</span> <span class="n">pos</span> <span class="p">/</span> <span class="m">8</span><span class="p">;</span>
        <span class="n">PositionY</span> <span class="p">=</span> <span class="n">pos</span> <span class="p">%</span> <span class="m">8</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Serialize</code>メソッドでは<code class="language-plaintext highlighter-rouge">SerialWriter</code>経由でデータを書き込んでいくため、
書き込み毎に型情報が付加される形でシリアライズされます。
若干冗長な気もしますが、このおかげでGoでも独自定義クラスの中にどのような型の値が含まれているか読み取ることができます。</p>

<p>またチェスの盤面は8×8なので、XとYの座標を1byteにまとめることで通信量を減らしています。
ゲームで使うオブジェクトでは、このようなゲーム仕様に基づく最適化ができたり、マスタデータのIDなど一部のメンバだけ送れば済むようなケースがよくあります。
このため、リフレクションを使った自動的なシリアライズなどは行わず、若干面倒かもしれませんが自分で書く形としました。
<code class="language-plaintext highlighter-rouge">Serialize</code>と<code class="language-plaintext highlighter-rouge">Deserialize</code>で対応がとれていないと正しく動かなくなってしまいますが、ユニットテストで担保するとよいでしょう。</p>

<p>▼リスト5 独自定義クラスのシリアライズ・デシリアライズ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">SerialWriter</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;独自定義クラスのオブジェクトを書き込む&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">void</span> <span class="n">Write</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">T</span> <span class="n">v</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="k">class</span><span class="err">,</span> <span class="nc">IWSNet2Serializable</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">v</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">Write</span><span class="p">();</span> <span class="c1">// nullのときは型なしNullを書き込むだけ</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="n">v</span><span class="p">.</span><span class="nf">GetType</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">id</span> <span class="p">=</span> <span class="n">types</span><span class="p">[</span><span class="n">t</span><span class="p">];</span>

        <span class="nf">expand</span><span class="p">(</span><span class="m">4</span><span class="p">);</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">Type</span><span class="p">.</span><span class="n">Obj</span><span class="p">;</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">pos</span><span class="p">+</span><span class="m">1</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">id</span><span class="p">;</span>     <span class="c1">// ClassID 書き込み</span>
        <span class="n">pos</span> <span class="p">+=</span> <span class="m">4</span><span class="p">;</span>                  <span class="c1">// サイズを書き込む領域分進める</span>
        <span class="kt">var</span> <span class="n">start</span> <span class="p">=</span> <span class="n">pos</span><span class="p">;</span>

        <span class="n">v</span><span class="p">.</span><span class="nf">Serialize</span><span class="p">(</span><span class="k">this</span><span class="p">);</span> <span class="c1">// 独自定義クラスのSerialize()呼び出し</span>

        <span class="c1">// Serializeで書き込んだサイズを埋める</span>
        <span class="kt">var</span> <span class="n">size</span> <span class="p">=</span> <span class="n">pos</span> <span class="p">-</span> <span class="n">start</span><span class="p">;</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">start</span><span class="p">-</span><span class="m">2</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)((</span><span class="n">size</span> <span class="p">&amp;</span> <span class="m">0xff00</span><span class="p">)</span> <span class="p">&gt;&gt;</span> <span class="m">8</span><span class="p">);</span>
        <span class="n">buf</span><span class="p">[</span><span class="n">start</span><span class="p">-</span><span class="m">1</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)(</span><span class="n">size</span> <span class="p">&amp;</span> <span class="m">0xff</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SerialReader</span>
<span class="p">{</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
    <span class="c1">/// &lt;summary&gt;独自定義クラスとして読み出す&lt;/summary&gt;</span>
    <span class="k">public</span> <span class="n">T</span> <span class="n">ReadObject</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">T</span> <span class="n">recycle</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="k">class</span><span class="err">,</span> <span class="nc">IWSNet2Serializable</span><span class="p">,</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">checkType</span><span class="p">(</span><span class="n">Type</span><span class="p">.</span><span class="n">Obj</span><span class="p">,</span> <span class="n">Type</span><span class="p">.</span><span class="n">Null</span><span class="p">)</span> <span class="p">==</span> <span class="n">Type</span><span class="p">.</span><span class="n">Null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">cid</span> <span class="p">=</span> <span class="n">classIDs</span><span class="p">[</span><span class="k">typeof</span><span class="p">(</span><span class="n">T</span><span class="p">)];</span>
        <span class="kt">var</span> <span class="n">id</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="nf">Get8</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">id</span> <span class="p">!=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">cid</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">SerializationException</span><span class="p">(</span><span class="s">"class id mismatch"</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">size</span> <span class="p">=</span> <span class="nf">Get16</span><span class="p">();</span>
        <span class="nf">checkLength</span><span class="p">(</span><span class="n">size</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">obj</span> <span class="p">=</span> <span class="n">recycle</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">obj</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">obj</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">T</span><span class="p">();</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">start</span> <span class="p">=</span> <span class="n">pos</span><span class="p">;</span>
        <span class="n">obj</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span>
        <span class="n">pos</span> <span class="p">=</span> <span class="n">start</span> <span class="p">+</span> <span class="n">size</span><span class="p">;</span>

        <span class="k">return</span> <span class="n">obj</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">(</span><span class="err">略</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>デシリアライズする<code class="language-plaintext highlighter-rouge">ReadObject&lt;T&gt;</code>メソッドでは、<code class="language-plaintext highlighter-rouge">recycle</code>というオブジェクトを引数として受け取ります。
データ受信時にオブジェクトを新たに作るのではなく再利用することで、メモリアロケーションを減らすことができます。</p>

<h3 id="リストと辞書">リストと辞書</h3>

<p>リスト型は型情報に続いて1byteの要素数、その後にシリアライズした要素のデータ長とデータがが繰り返し配置される形でシリアライズされます。
それぞれの要素データは、型情報とその型ごとのデータのバイト列からなる、シリアライズされたデータです。
このような構造のため、何重にもネストすることができます。</p>

<p><img src="/images/2024-06-02/seriallist.png" alt="リスト型のシリアライズイメージ" />
▲図3 リスト型のシリアライズイメージ</p>

<p>辞書型もリスト型と同じように、1byteの要素数と要素の繰り返しからなる形です。
各要素は1byteのキー長、キー文字列データ、シリアライズした要素のデータ長とデータが並びます。</p>

<p><img src="/images/2024-06-02/serialdict.png" alt="辞書型のシリアライズイメージ" />
▲図4 辞書型のシリアライズイメージ</p>

<p>リストも辞書も、各要素データの長さがデータの前にあることで、要素の中身をデシリアライズすることなくバイト列として切り出すことができます。
特に辞書型は部屋のプロパティとしても使われていて、データを切り出せることがGoでの扱いやすさに繋がっています。
これについては後で解説します。</p>

<h3 id="プリミティブ型の配列">プリミティブ型の配列</h3>

<p><code class="language-plaintext highlighter-rouge">int[]</code>のようなプリミティブ型の配列は、<code class="language-plaintext highlighter-rouge">int</code>がシリアライズ可能なのでリスト型としてもシリアライズできます。
しかし、リスト型では要素ごとにサイズや型情報が入り効率がよくありません。
なので、数値型やbool型の配列は専用の型としてシリアライズできるようにしました。</p>

<p>型によって要素データのサイズは固定なので、リストのようにデータサイズを書き込まず、
単純に値だけをシリアライズしたデータを並べます。
さらにbool型の配列は1ビット単位で効率的に格納します。</p>

<p><img src="/images/2024-06-02/serialints.png" alt="int型配列のシリアライズイメージ" />
▲図5 辞書型のシリアライズイメージ</p>

<h2 id="サーバーでの部屋のプロパティ">サーバーでの部屋のプロパティ</h2>

<p>各部屋には、クライアントが自由に設定できるプロパティとして、文字列キーの辞書を用意しています。
このプロパティは部屋にいる全クライアントに共有される他、部屋の検索やランダム入室の際のフィルタリングにも利用します。</p>

<p>この辞書は、Goのサーバーでは<code class="language-plaintext highlighter-rouge">map[string][]byte</code>型になっていて、
辞書の各要素の値はバイト列（<code class="language-plaintext highlighter-rouge">[]byte</code>）のままデシリアライズせずに保持しています。</p>

<h3 id="プロパティの値の変更">プロパティの値の変更</h3>

<p>プロパティの値を変更するときは、リスト6のように変更するキーと値だけの辞書をクライアントから送ります。</p>

<p>▼リスト6 クライアントから送る辞書型データ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">dict</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">object</span><span class="p">&gt;()</span>
<span class="p">{</span>
    <span class="p">{</span><span class="s">"Turn"</span><span class="p">,</span> <span class="m">1</span><span class="p">},</span>
    <span class="p">{</span><span class="s">"WhitePawn4"</span><span class="p">,</span> <span class="k">new</span> <span class="nf">ChessPiece</span><span class="p">(){</span> <span class="n">Type</span><span class="p">=</span><span class="n">PieceType</span><span class="p">.</span><span class="n">Porn</span><span class="p">,</span> <span class="n">PositionX</span><span class="p">=</span><span class="m">3</span><span class="p">,</span> <span class="n">PositionY</span><span class="p">=</span><span class="m">4</span> <span class="p">}},</span>
<span class="p">};</span>

<span class="n">room</span><span class="p">.</span><span class="nf">ChangeRoomProperty</span><span class="p">(</span><span class="n">publicProps</span><span class="p">=</span><span class="n">dict</span><span class="p">);</span>
</code></pre></div></div>

<p>Goの中継サーバーはこの辞書を@<list>{readdic}のようにデシリアライズし、文字列キーと値データのバイト列を取り出します。</list></p>

<p>▼リスト7 Goでの辞書のデシリアライズ</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">type</span> <span class="n">Dict</span> <span class="n">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">byte</span>

<span class="n">func</span> <span class="nf">unmarshalDict</span><span class="p">(</span><span class="n">src</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="n">Dict</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">count</span> <span class="p">:=</span> <span class="nf">get8</span><span class="p">(</span><span class="n">src</span><span class="p">[</span><span class="m">1</span><span class="p">:])</span> <span class="c1">// 要素数</span>
    <span class="n">l</span> <span class="p">:=</span> <span class="m">2</span>
    <span class="n">dict</span> <span class="p">:=</span> <span class="nf">make</span><span class="p">(</span><span class="n">Dict</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="p">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">count</span><span class="p">;</span> <span class="n">i</span><span class="p">++</span> <span class="p">{</span>
        <span class="n">lk</span> <span class="p">:=</span> <span class="nf">get8</span><span class="p">(</span><span class="n">src</span><span class="p">[</span><span class="n">l</span><span class="p">:])</span>     <span class="c1">// キー長</span>
        <span class="n">l</span> <span class="p">+=</span> <span class="m">1</span>
        <span class="n">key</span> <span class="p">:=</span> <span class="n">src</span><span class="p">[</span><span class="n">l</span> <span class="p">:</span> <span class="n">l</span><span class="p">+</span><span class="n">lk</span><span class="p">]</span>    <span class="c1">// キーデータ</span>
        <span class="n">l</span> <span class="p">+=</span> <span class="n">lk</span>
        <span class="n">lv</span> <span class="p">:=</span> <span class="nf">get16</span><span class="p">(</span><span class="n">src</span><span class="p">[</span><span class="n">l</span><span class="p">:])</span>    <span class="c1">// データ長</span>
        <span class="n">l</span> <span class="p">+=</span> <span class="m">2</span>
        <span class="n">dict</span><span class="p">[</span><span class="kt">string</span><span class="p">(</span><span class="n">key</span><span class="p">)]</span> <span class="p">=</span> <span class="n">src</span><span class="p">[</span><span class="n">l</span> <span class="p">:</span> <span class="n">l</span><span class="p">+</span><span class="n">lv</span><span class="p">]</span> <span class="c1">// データのバイト列</span>
        <span class="n">l</span> <span class="p">+=</span> <span class="n">lv</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">dict</span><span class="p">,</span> <span class="n">l</span><span class="p">,</span> <span class="n">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>このようにサーバー側での辞書のデシリアライズでは、各要素の値のデータをバイト列のスライスとして切り出しています。
Goのスライスは元の配列の参照になっているため、メモリのコピーが発生せず高速です。
そしてこのスライスをそのまま部屋のプロパティとして保持します。</p>

<h2 id="部屋のフィルタリング">部屋のフィルタリング</h2>

<p>部屋検索では部屋のプロパティを参照する柔軟なフィルタリングができるようにしました。
フィルタリングの条件は、たとえば「レベル10~15の範囲かつ、赤チームまたは黄色チーム」はリスト8のような形<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">8</a></sup>で指定します。</p>

<p>▼リスト8 フィルタリング条件のイメージ</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
    </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Team"</span><span class="p">,</span><span class="w"> </span><span class="err">Equal</span><span class="p">,</span><span class="w"> </span><span class="err">(byte)Team.Red</span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Level"</span><span class="p">,</span><span class="w"> </span><span class="err">GreaterOrEqual</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Level"</span><span class="p">,</span><span class="w"> </span><span class="err">LessOrEqual</span><span class="p">,</span><span class="w"> </span><span class="mi">15</span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Team"</span><span class="p">,</span><span class="w"> </span><span class="err">Equal</span><span class="p">,</span><span class="w"> </span><span class="err">(byte)Team.Yellow</span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Level"</span><span class="p">,</span><span class="w"> </span><span class="err">GreaterOrEqual</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="s2">"Level"</span><span class="p">,</span><span class="w"> </span><span class="err">LessOrEqual</span><span class="p">,</span><span class="w"> </span><span class="mi">15</span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>このように条件を<code class="language-plaintext highlighter-rouge">{キー, 演算子, 値}</code>の二重配列として表し、内側の配列はAND結合、外側はOR結合にしています。
どんなに複雑な条件指定も、分配法則やド・モルガンの法則で変換すれば必ずこの形に変形できます。</p>

<p>この形にしておくと、サーバー側はリスト9のようにシンプルなループでフィルタリングできます。</p>

<p>▼リスト9 フィルター</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">type</span> <span class="n">PropQuery</span> <span class="k">struct</span> <span class="err">{</span>
     <span class="nc">Key</span> <span class="kt">string</span>
     <span class="n">Op</span>  <span class="n">OpType</span> <span class="c1">// operation type (==, !=, &lt;, &lt;=, &gt;, &gt;=)</span>
     <span class="n">Val</span> <span class="p">[]</span><span class="kt">byte</span> <span class="c1">// value</span>
<span class="p">}</span>

<span class="n">func</span> <span class="nf">filter</span><span class="p">(</span><span class="n">rooms</span> <span class="p">[]*</span><span class="n">Room</span><span class="p">,</span> <span class="n">queries</span> <span class="p">[][]</span><span class="n">PropQuery</span><span class="p">)</span> <span class="p">[]*</span><span class="n">Room</span> <span class="p">{</span>
    <span class="n">filtered</span> <span class="p">:=</span> <span class="nf">make</span><span class="p">([]*</span><span class="n">Room</span><span class="p">,</span> <span class="m">0</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">room</span> <span class="p">:=</span> <span class="n">rooms</span> <span class="p">{</span>

        <span class="c1">// OR結合：一つでもマッチしている条件群があればRoomを追加</span>
        <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">qs</span> <span class="p">:=</span> <span class="n">queries</span> <span class="p">{</span>

            <span class="c1">// AND結合：全てマッチしていたらこの条件群はマッチ</span>
            <span class="n">match</span> <span class="p">:=</span> <span class="k">true</span>
            <span class="k">for</span> <span class="n">j</span> <span class="p">:=</span> <span class="n">range</span> <span class="n">queries</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">!</span><span class="n">queries</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">].</span><span class="nf">match</span><span class="p">(</span><span class="n">room</span><span class="p">.</span><span class="n">Property</span><span class="p">[</span><span class="n">queries</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">].</span><span class="n">Key</span><span class="p">])</span> <span class="p">{</span>
                    <span class="n">match</span> <span class="p">=</span> <span class="k">false</span>
                    <span class="k">break</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="c1">// マッチしていたので結果に追加</span>
            <span class="k">if</span> <span class="n">match</span> <span class="p">{</span>
                <span class="n">filtered</span> <span class="p">=</span> <span class="nf">append</span><span class="p">(</span><span class="n">filtered</span><span class="p">,</span> <span class="n">room</span><span class="p">)</span>
                <span class="k">break</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">filtered</span>
<span class="p">}</span>
</code></pre></div></div>

<p>値の比較はこれまで説明してきたとおり、バイト列をそのままバイト単位で比較します。
数値型の場合、型が一致していれば大小関係もバイト列のまま比較できます。
このようなサーバー側の実装だけでは、数値以外でも大小関係の比較を指定できてしまう形式なのですが、
そこはクライアント側のフィルタ条件生成クラスで大小比較は数値型だけになるように担保しています。</p>

<p>▼リスト10 マッチするか判定</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">func</span> <span class="p">(</span><span class="n">q</span> <span class="p">*</span><span class="n">PropQuery</span><span class="p">)</span> <span class="nf">match</span><span class="p">(</span><span class="n">val</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
    <span class="n">ret</span> <span class="p">:=</span> <span class="n">bytes</span><span class="p">.</span><span class="nf">Compare</span><span class="p">(</span><span class="n">val</span><span class="p">,</span> <span class="n">q</span><span class="p">.</span><span class="n">Val</span><span class="p">)</span> <span class="c1">// バイト列のまま比較</span>
    <span class="k">switch</span> <span class="n">q</span><span class="p">.</span><span class="n">Op</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">OpEqual</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">==</span> <span class="m">0</span>
    <span class="k">case</span> <span class="n">OpNot</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">!=</span> <span class="m">0</span>
    <span class="k">case</span> <span class="n">OpLessThan</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">&lt;</span> <span class="m">0</span>
    <span class="k">case</span> <span class="n">OpLessOrEqual</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">&lt;=</span> <span class="m">0</span>
    <span class="k">case</span> <span class="n">OpGreaterThan</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">&gt;</span> <span class="m">0</span>
    <span class="k">case</span> <span class="n">OpGreaterOrEqual</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">ret</span> <span class="p">&gt;=</span> <span class="m">0</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="さいごに">さいごに</h2>

<p>この章では、KLabの同期通信基盤の独自のシリアライズフォーマットを紹介しました。
汎用性を犠牲にして自分たちの用途に合わせているため、そのまま使える場面は少ないと思いますが、
細かい工夫点やテクニックが何かの参考になれば幸いです。</p>

<p>また、シリアライザだけでなくこの同期通信基盤そのものについても、
今後何らかの発表をしていきたいと思っておりますのでご期待下さい。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.photonengine.com/">https://www.photonengine.com/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://msgpack.org/">https://msgpack.org/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://developers.google.com/protocol-buffers">https://developers.google.com/protocol-buffers</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://github.com/KLab/wsnet2-serializer">https://github.com/KLab/wsnet2-serializer</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>紙面に掲載したコード片は一部簡略化などの変更をしています <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">decimal</code>型は定義していますが未実装です <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:6:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:7" role="doc-endnote">
      <p><a href="/2020/12/09/float-comparable.ja.html">http://makiuchi-d.github.io/2020/12/09/float-comparable.ja.html</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8" role="doc-endnote">
      <p>実際には、HTTPのBodyにMessagePack形式で他のパラメータとともにこのようなフィルタ条件を入れて送っています <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[この記事は2022年1月22日から開催された技術書典12にて頒布した「KLabTechBook Vol.9」に掲載したものです。]]></summary></entry></feed>