Nimrod で ØMQ


最近 C 言語で書かれたライブラリーを使う機会が多く, .NET Framework から使うと Marshal やら GCHandle やら面倒くさいなと思うようになり,何かネイティブ系の言語を身につけたいという欲求から, Nimrod という言語に興味を持って暇なときにいじっています。

Nimrod は C 言語にコンパイルしてからネイティブコードを吐き出すというスタイルをとる命令型の言語です[A]。命令型ですが割と強力な型推論を持っているので,書いていて非常に楽だと感じます。

ØMQ は高パフォーマンスなメッセージングライブラリーで,多くの言語でバインディングが存在します。したがって ØMQ 経由で言語間でデータをやり取りなんていうことも容易にできます。もちろん Nimrod のバインディングもあります。

お題として,日付書式形式のリクエストを受け取ったら時間を返すサーバーを書いてみます。

from strutils import `%`
import times
import zmq

var major, minor, patch : cint  # cint は C 互換の int
zmq.version(major, minor, patch)
echo("ZeroMQ version: $1.$2.$3" % [$major, $minor, $patch])

var connection = open("tcp://*:12345", server=true)
try:
   while true:
      var format = receive(connection)
      var current = getLocalTime(getTime())
      try:
         var result = times.format(current, format)
         send(connection, result)
      except EInvalidValue:  # 受け取った書式指定文字列が正しくない
         send(connection, "Invalid format: " & format)
finally:
   close(connection)

基本的な制御構文や標準入出力,外部パッケージの仕様といった基本的な構文は含んでいるので,なんとなく Nimrod の雰囲気はつかめると思います。

同様にクライアントも書いてみます。大体サーバーと同じような感じです。

import zmq

const prompt = "> "

var connection = open("tcp://localhost:12345", server=false)
try:
   while true:
      write(stdout, prompt)
      var input = readLine(stdin)
      case input
      of "q", "quit":
         # switch と違ってケースを抜けるのではなく外側のループを抜ける
         break
      else:
         send(connection, input)
         var serverTime = receive(connection)
         echo(serverTime)
finally:
   close(connection)

サーバーを起動すると待機します。クライアントを起動して書式指定文字列 (例えば yyyy-MM-dd HH:mm:ss) を入力すると,サーバーで書式指定にしたがって現在時刻をクライアント側に返します。

さて, ZeroMQ のバインディングは Nimrod でどのように実現されているのでしょうか。 ソースコードを眺めてみます[B]

まず次のように DLL 名を指定しています。

when defined(windows): 
  const 
    zmqdll* = "zmq.dll"
elif defined(macosx): 
  const 
    zmqdll* = "libzmq.dylib"
else:
  const 
    zmqdll* = "libzmq.so"

defined はシンボルが定義されているかどうかを調べる関数で, C 言語の #ifdef (C# や F# の #if) に相当するものです。ちなみに DLL の名前には "libfoo(|2.0|1.2|1.1|1.0).so" のように指定して,バージョンの違いなどによる複数の名前の可能性を考慮するすることもできます。ちなみに名前の後ろに付いている * は定数や関数が外部に公開されることを示すもので,ポインタとは関係ありません。

そして DLL に定義されている関数は次のように宣言することで,通常の関数と同様に呼ぶことができます。

proc recv*(s: PSocket, msg: var TMsg, flags: cint): cint{.cdecl, 
    importc: "zmq_recv", dynlib: zmqdll.}

関数宣言の末尾についている {. ... .} の部分を見れば予想が付くように,先ほど定義した zmqdll 内に zmq_recv という名前で定義された関数を cdecl 呼び出します。このように DLL 内の関数を呼び出すのは C# や F# と同様に簡単です。

より複雑なケースはまだわかりませんが, C や C++ で書かれたコードを取り込むことができるみたいなので,ネイティブな外部 DLL と連携する際は C# や F# より楽に書けることが期待できます。

脚注

  1. JavaScript にコンパイルすることもできるらしい。 []
  2. 標準ライブラリーのソースコードはインストールディレクトリの lib ディレクトリにも入っています。 []