Posted at

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

ネタ元:マークシート選択式問題における数式処理の活用

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

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

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

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


ncで実験してみる

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

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

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


ターミナル1

$ nc -l 12345


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


ターミナル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の説明はマニュアルを参照してください。

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

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:sendclient: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とは別のファイルに書きます。


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セッションを起動します。


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 を以下の内容で用意しておきます。


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.texdofile("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とやりとりできます:

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とのやりとりにはソケットを使うのが良いでしょう。