NaClの前田です。
今年はdRuby 20周年です。というわけで、dRubyを使ってTextbringerのためのemacsclient的なものを作ってみたので紹介します。
emacsclientはすでに動作しているEmacsのプロセスでファイルを開いたり、Emacs Lispを実行するためのコマンドです。
Emacs側でM-x server-startした上で、
$ emacsclient <ファイル名>
とすると、Emacs側で指定したファイルが開かれ、emacsclientは終了を待ちます。
EmacsでC-x #とタイプすると編集が終了し、emacsclientも終了します。
$ emacsclient -n <ファイル名>
とすると、編集終了を待たずにすぐに戻ってきます。
# emacsclient -e '(message "foo")'
とすると、Emacs上でEmacs Lispが実行されます。
dRubyは、(場合によっては他のマシンの)他のプロセスのオブジェクトに対してメソッド呼び出しを行うためのライブラリです。プロトコルを考えたり、APIの定義ファイルを用意したりといった手間がないので、双方のプログラムがRubyで書かれていれば簡単に協調することができます。
ただし、インターネット上でサーバを公開するような用途は想定していませんので、注意が必要です。
先日松江で行ったワークショップのスライドと配布資料が公開されていますので、そちらも合わせて参照ください。
サーバ側の実装は以下のとおりです。
require "drb"
module Textbringer
module Commands
define_command(:server_start,
doc: "Start Textbringer server.") do
uri = CONFIG[:server_uri] ||
"drbunix:" + File.expand_path("server.sock", "~/.textbringer")
options = CONFIG[:server_options] || { UNIXFileMode: 0600 }
DRb.start_service(uri, Server.new, options)
end
define_command(:server_kill,
doc: "Kill Textbringer server.") do
DRb.stop_service
end
define_command(:server_edit_done,
doc: "Finish server edit.") do
queue = Buffer.current[:client_wait_queue]
if queue.nil?
raise EditorError, "No waiting clients"
end
if Buffer.current.modified? &&
y_or_n?("Save file #{Buffer.current.file_name}?")
save_buffer
end
kill_buffer(Buffer.current, force: true)
queue.push(:done)
end
end
class Server
def eval(s)
with_redisplay do
Controller.current.instance_eval(s).inspect
end
end
def visit_file(filename, wait: true)
queue = Queue.new if wait
with_redisplay do
find_file(filename)
Buffer.current[:client_wait_queue] = queue if wait
end
queue.deq if wait
end
private
def with_redisplay
foreground! do
begin
yield
ensure
Window.redisplay
end
end
end
end
end
ポイントは
:done
)を渡しています。といったあたりです。
クライアント側の実装は以下のとおりです。
require "drb"
require "optparse"
eval = false
wait = true
uri = "drbunix:" + File.expand_path("server.sock", "~/.textbringer")
opt = OptionParser.new
opt.banner = "Usage: tbclient [OPTIONS] FILE"
opt.on("--uri URI", "Specify the URI of the server") do |val|
uri = val
end
opt.on("-e", "--eval", "Evaluate FILE as a Ruby expression") do
eval = true
end
opt.on("-n", "--no-wait", "Don't wait for the server") do
wait = false
end
args = ARGV.dup
opt.parse!(args)
if args.empty?
STDERR.puts(opt.help)
exit 1
end
arg = args.first
tb = DRbObject.new_with_uri(uri)
if eval
puts tb.eval(arg)
else
tb.visit_file(File.expand_path(arg), wait: wait)
end
ポイントは DRbObject.new_with_uri(uri)
でサーバ上のオブジェクトの参照を得ることくらいで、あとは普通のオブジェクトのようにメソッドを呼び出すだけです。簡単ですね!
今回の例ではクライアント側ではDRb.start_serviceを呼んでいませんが、サーバ側にマーシャルできないオブジェクトを渡す(リモートオブジェクトのメソッド呼び出しの引数で与える)時は、クライアント側でもDRb.start_serviceが必要になります。
dRubyを使うことで、emacsclientのようなものを簡単に作ることができました。
やはりテキストエディタをRubyで拡張できると便利ですね。