ソケットを使ってLua／LuaTeX文書からMaximaを呼び出す

ネタ元：[マークシート選択式問題における数式処理の活用](https://www.slideshare.net/hamadatatsuyoshi/ss-148398845)

上記のスライドによると、LuaTeXのLuaの機能 (`io.popen`) を使って数式処理システムMaximaを呼び出す時に、数式ごとにMaximaを起動していると遅いそうです。

毎度Maximaを起動するのが遅いのであれば、文書処理の間はMaximaを立ち上げっぱなしにして、何らかのプロセス間通信でMaximaとやりとりする、という方法が考えられます。

Maximaはプロセス間通信の方法として、標準入出力のほかに**ソケット**に対応しています。また、LuaTeXには**LuaSocket**という、Luaでソケットを扱えるライブラリーが付属しています。

ということで、LuaSocketでMaximaと通信してみます。

# ncで実験してみる

いきなりLuaSocketを使う前に、 `nc` コマンドを使ってMaximaのソケット通信機能を試してみます。

サーバーはMaximaを使いたい側が用意します。Maximaの起動時にポート番号を教えると、Maximaがクライアントとしてそこに接続する、という形になります。

実験では、12345番ポートを使ってみます。

```shell-session:ターミナル1
$ nc -l 12345
```

`nc` を起動したのとは別のターミナルでMaximaを立ち上げます。その際、`-s` オプションでポート番号を指定します。

```shell-session:ターミナル2
$ maxima --very-quiet -s 12345
Connecting Maxima to server on port 12345
```

Maximaがソケットに接続すると、ソケット経由で `pid=<プロセスID>` という文字列が送られてきます。

あとは普通にやりとりします。

```:ターミナル1
[Maximaからの出力]pid=51844
[Maximaへの入力　]tex1(expand((1+x)^5));
[Maximaからの出力]                       x^5+5\,x^4+10\,x^3+10\,x^2+5\,x+1
[Maximaへの入力　]tex1(diff(sin(x)*cos(x^2), x));
[Maximaからの出力]                    \cos x\,\cos x^2-2\,x\,\sin x\,\sin x^2
```

# LuaSocketでやってみる

あとは、同様のやりとりをLuaSocketで実装します。LuaSocketの説明は[マニュアル](http://w3.impa.br/~diego/software/luasocket/tcp.html)を参照してください。

Lua標準で外部コマンドを起動する方法は `os.execute` と `io.popen` がありますが、前者は実行するコマンドが完了するまでLua側に制御が帰ってきません。それでは困るので、Maximaコマンドの起動には `io.popen` を使うことにします。

```lua
local socket = require "socket"
local server = assert(socket.bind("*", 0))
local ip, port = server:getsockname()
print("IP:", ip)
print("Port:", port)
local maxima = assert(io.popen(string.format("maxima --very-quiet -s %d", port)))
local client = assert(server:accept())
print("Connection accepted")
print("Maxima stdout:", maxima:read("*l")) -- "Connecting Maxima to server on port XXXXX"
client:settimeout(10)
assert(client:receive("*l")) -- "pid=*****"

client:send("tex1(expand((x+3)^2));\n")
local result = assert(client:receive("*l"))
print("Received", result)

client:send("quit();\n")

print("Closing socket", client:close())
print("Closing pipe", maxima:close())
```

`assert(client:receive("*l"))` の行までが準備（Maximaの起動と通信の確立）で、 `client:send` と `client:receive` の部分が個々の数式に関するMaximaとのやりとりです。

最後に `quit();` を送ってMaximaを自発的に終了させていますが、環境によってはこれがなくても動きます。Linuxでは `quit();` なしで切断しようとするとMaximaがセグフォを起こしました。

LuaScoket等のエラーチェックは、雑に `assert` で済ませています。

このコードを標準のLuaインタープリターで試すにはLuaSocketが必要です。TeX環境が手元のPCに整っているという方なら、LuaTeXのLuaインタープリターである `texlua` を使うと別途LuaSocketを用意する必要がないので楽です。

# Lua(La)TeX文書に埋め込んでみる

あとはLuaTeX文書にこのコードを埋め込めばOKです。

ただし、TeXファイル中にLuaコードをがっつり書くのはだるいので、LuaコードはTeXとは別のファイルに書きます。

```lua:luatex-maxima.lua
local socket = require "socket"
local meta = {}
meta.__index = meta

-- 新しくMaximaセッションを起動する
local function new()
  local server = assert(socket.bind("*", 0))
  local ip, port = server:getsockname()
  print(ip, port)
  local maxima = assert(io.popen(string.format("maxima --very-quiet -s %d", port)))
  local client = assert(server:accept())
  print("Connection accepted")
  -- maxima:read("*l") -- "Connecting Maxima to server on port XXXXX"
  client:settimeout(10)
  assert(client:receive("*l")) -- "pid=*****"
  return setmetatable({_server = server, _client = client, _process = maxima}, meta)
end

-- Maximaセッションを使ってコマンドを実行する
function meta:run(command)
  command = command:gsub("\n*$", "")
  self._client:send(command..";\n")
  local result = assert(self._client:receive("*l"))
  print("Received", result)
  return (result:gsub("%s", " "))
end

-- Maximaセッションを閉じる
function meta:close()
  self._client:send("quit();\n")
  self._client:close()
  self._process:close()
  self._server:close()
end
return {
  new = new,
}
```

LuaLaTeX文書からは `dofile` で上述Luaコードを実行し、Maximaセッションを起動します。

```tex:luatex-maxima-test.tex
\documentclass{article}
\directlua{
  maxima = dofile("luatex-maxima.lua")
  session = maxima:new()
}
\newcommand\maxima[1]{\directlua{tex.print(session:run([[tex1(#1)]]))}}
\begin{document}
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{diff((x+3)^2,x)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x^3+3)^50,x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\directlua{session:close()}
\end{document}
```

あとは、 `lualatex --shell-escape luatex-maxima-test.tex` という感じで処理します。

# 処理時間の比較

比較用に、毎回Maximaを立ち上げる版の `luatex-maxima-nosocket.lua` を以下の内容で用意しておきます。

```lua:luatex-maxima-nosocket.lua
local meta = {}
meta.__index = meta
local function new()
  return setmetatable({}, meta)
end
function meta:run(command)
  command = command:gsub("\n*$", "")
  local maxima = assert(io.popen(string.format("echo '%s;' | maxima --very-quiet", command)))
  local result = maxima:read("*a")
  maxima:close()
  result = (result:gsub("%s", " "))
  print("Received", result)
  return result
end
function meta:close()
end
return {
  new = new,
}
```

こちらを使う場合は、 `luatex-maxima-test.tex` の `dofile("luatex-maxima.lua")` を `dofile("luatex-maxima-nosocket.lua")` に書き換えます。

私のMac環境（High Sierra / TeX Live 2018 / Maxima 5.41.0）で

`$ time lualatex --shell-escape luatex-maxima-test.tex`

を実行したところ、**毎回Maximaを起動する版の処理時間は11.4秒、ソケットで通信する版は3.8秒**でした。Maximaの起動回数を減らすことの効果はやはり大きいようです。数式の量が多ければ差はもっと開くかもしれません。

# おまけ：popenによる双方向通信

popen関数は通常、「プロセスの標準出力を読み取る」または「プロセスの標準入力に書き込む」のどちらかの動作しかできません。

これに対し、FreeBSDのlibcではpopenの第二引数に `"r+"` を指定することで双方向パイプを開けるようです。これを使うと、ソケットを使わなくても次のようにMaximaとやりとりできます：

```lua
local maxima = assert(io.popen("maxima --very-quiet", "r+"))
maxima:setvbuf("line")

maxima:write("tex1((1+x)^2);\n")
print("Maxima stdout:", maxima:read("*l"))

maxima:write("tex1(expand((x+3)^2));\n")
print("Maxima stdout:", maxima:read("*l"))

maxima:close()
```

しかし、popenで双方向通信できるのはFreeBSD系（FreeBSDとMac）だけ（Linux, Windowsでは対応していない）のようなので、文書がMac専用でいいというのでもない限り、Maximaとのやりとりにはソケットを使うのが良いでしょう。