Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyのFiddleで任意のCライブラリを操作する方法 ― 知られざる標準ライブラリの隠れた実力

Fiddleは、Ruby 1.9.xで標準ライブラリに追加された、あまり知られていないモジュールです。Rubyから直接Cライブラリとやり取りできるだけでなく、実行中のRubyインタプリタそのものを調査・改変することさえ可能です。

Fiddleの仕組みは、libffiという広く使われているCライブラリをラップすることに基づいています。libffiは、ある言語で書かれたコードから別の言語で書かれた関数を呼び出せるようにするライブラリです。「ffi」は「Foreign Function Interface(外部関数インターフェース)」の略称です。さらに、Fiddleの活用範囲はC言語にとどまりません。使い方をマスターすれば、Rustをはじめ、FFIをサポートする他の言語で書かれたライブラリも利用できるようになります。

それでは、Fiddleを見ていきましょう。まずはシンプルな例から始めて、最後にシリアルポート経由でArduinoにアクセスするところまで進みます。心配いりません。高度なC言語の知識はほとんど必要ないのですから。

シンプルな例

通常のRubyでは、メソッドを呼び出す前に必ず定義が必要です。Fiddleでも同じことが言えます。Cの関数を直接呼び出すことはできず、まずC関数のラッパーを作成し、そのラッパーを呼び出すことになります。

以下の例では、C言語の対数関数「log」をラップしています。要するに、RubyのMath.logと同じ機能を実現しているわけです。

require 'fiddle'

# ライブラリを「オープン」するため、ディスク上の場所をFiddleに伝えます
# この例はOS X Yosemiteで動作します
libm = Fiddle.dlopen('/usr/lib/libSystem.dylib')

# C関数"log"のラッパーを作成
log = Fiddle::Function.new(
  libm['log'],            # オープンした数学ライブラリから関数を取得
  [Fiddle::TYPE_DOUBLE],  # 引数は1つ。double型(RubyのFloatに相当)
  Fiddle::TYPE_DOUBLE     # 戻り値もdouble型
)

# ラッパー経由でC関数を呼び出す
puts log.call(3.14159)

コードを美しく整える

先ほどの例は動作しますが、やや冗長です。100個もの関数をラップすることになったら、コードがどれだけ煩雑になるか想像できるでしょう。そこでFiddleには便利なDSLが用意されています。Fiddle::Importerミックスイン経由で利用できます。

このミックスインを使えば、外部関数を集めたモジュールを簡単に作成できます。次の例では、複数の対数関数を含むモジュールを作っています。

require 'fiddle'
require 'fiddle/import'

module Logs
  extend Fiddle::Importer
  dlload '/usr/lib/libSystem.dylib'
  extern 'double log(double)'
  extern 'double log10(double)'
  extern 'double log2(double)'
end

# 外部関数をまるでRubyのメソッドのように呼び出せます!
puts Logs.log(10)   # 2.302585092994046
puts Logs.log10(10) # 1.0
puts Logs.log2(10)  # 3.321928094887362

シリアルポートの制御

さて、何年も買おうか迷っていたArduinoをついに購入したとしましょう。シリアルポート経由で1秒に1回「hello world」というテキストをパソコンへ送信している状態です。

このデータをRubyで読めたら素敵だと思いませんか? 実は、Rubyの標準IOメソッドでもシリアルポートから読み取ることは可能です。しかし、本格的なハードウェアハッカーを目指すなら、これらのIOメソッドではシリアル入力を必要な細かさで設定できません。

幸いなことに、C標準ライブラリにはシリアルポートを扱うために必要な関数がすべて揃っています。そしてFiddleを使えば、これらのC関数にRubyからアクセスできるのです。

もちろん、同じことを実現してくれるgemはすでに存在します。私がこのコードを書いたときは、rubyserial gemから少しインスピレーションを得ました。しかし、ここでの目的はあくまで自分の手でやり方を学ぶことにあります。

termios.hの中身を覗いてみる

C言語では、拡張子が.hで終わるファイルはヘッダファイルと呼ばれます。ライブラリがサードパーティのコード(つまり私たちのコード)に対して公開している関数や定数の一覧が含まれています。Cライブラリをラップする最初のステップは、このファイルを見つけて開き、中身を確認することです。

今回使用するライブラリはtermiosという名前です。OS X Yosemiteでは、ヘッダファイルは/usr/include/sys/termios.hにあります。Linuxでは別の場所に配置されているでしょう。

以下がtermios.hの内容です。分かりやすくするためにかなり簡略化しています。

typedef unsigned long   tcflag_t;
typedef unsigned char   cc_t;
typedef unsigned long   speed_t;

struct termios {
    tcflag_t    c_iflag;    /* 入力フラグ */
    tcflag_t    c_oflag;    /* 出力フラグ */
    tcflag_t    c_cflag;    /* 制御フラグ */
    tcflag_t    c_lflag;    /* ローカルフラグ */
    cc_t        c_cc[NCCS]; /* 制御文字 */
    speed_t     c_ispeed;   /* 入力速度 */
    speed_t     c_ospeed;   /* 出力速度 */
};

int tcgetattr(int, struct termios *);
int tcsetattr(int, int, const struct termios *);
int tcflush(int, int);

このコードで注目すべき点は3つあります。まずtypedefによる型定義があり、次に接続の設定情報を保持するデータ構造体、そして最後に使用するメソッド群です。

Fiddleのimporterを使えば、これらのセクションをほぼそのままRubyコードに持ち込めます。ひとつずつ見ていきましょう。

型エイリアスとtypedef

C言語ではデータ型にエイリアス(別名)を作るのが一般的です。たとえばtermios.hでは、単なるlong整数であるspeed_tという新しい型を定義しています。Fiddle importerはtypealias機能によってこれらを扱えます。

# `typedef unsigned long tcflag_t;` と同等
typealias "tcflag_t", "unsigned long"

構造体(Struct)

C言語にはクラスやモジュールが存在しません。複数の変数をひとまとめにしたい場合は構造体(struct)を使います。Fiddleには、Rubyで構造体を扱うための優れた仕組みが用意されています。

まず、Fiddle importer内で構造体を定義します。ご覧のとおり、ヘッダファイルからほぼコピー&ペーストで済みます。

Termios = struct [
  'tcflag_t c_iflag',
  'tcflag_t c_oflag',
  'tcflag_t c_cflag',
  'tcflag_t c_lflag',
  'cc_t         c_cc[20]',
  'speed_t  c_ispeed',
  'speed_t  c_ospeed',
]

mallocメソッドを使えば、構造体の「インスタンス」を作成できます。値の設定や取得は、普通のRubyクラスのインスタンスとほぼ同じ感覚で行えます。

s = Termios.malloc
s.c_iflag = 12345

すべてを組み合わせる

typedefと構造体について学んだことを、先ほどの関数の扱い方と組み合わせれば、動作するtermiosライブラリのラッパーが完成します。

ラッパーには、シリアルポートの設定に必要なメソッドだけを含めます。残りの部分はプレーンなRubyで書けば十分です。

require 'fiddle'
require 'fiddle/import'

# このモジュールの内容はほぼtermios.hからの直接コピーです
# Yosemiteでは /usr/include/sys/termios.h にあります
module Serial
  extend Fiddle::Importer
  dlload '/usr/lib/libSystem.dylib'

  # 型定義
  typealias "tcflag_t", "unsigned long"
  typealias "speed_t", "unsigned long"
  typealias "cc_t", "char"

  # 設定データを保持する構造体。
  # `Serial::Termios.malloc()` のようにインスタンス化します
  Termios = struct [
    'tcflag_t   c_iflag',
    'tcflag_t   c_oflag',
    'tcflag_t   c_cflag',
    'tcflag_t   c_lflag',
    'cc_t         c_cc[20]',
    'speed_t    c_ispeed',
    'speed_t    c_ospeed',
  ]

  # シリアルデバイスを扱う関数
  extern 'int   tcgetattr(int, struct termios*)'       # シリアルデバイスの設定を取得
  extern 'int   tcsetattr(int, int, struct termios*)'  # シリアルデバイスの設定を変更
  extern 'int   tcflush(int, int)'                     # デバイスの全バッファをフラッシュ
end

ライブラリを使ってみる

次の例では、Rubyでシリアルデバイスを開き、ファイルディスクリプタを取得します。続いてtermiosの関数を使ってデバイスを読み取り用に設定し、実際にデータを読み込みます。

ここではいくつかのマジックナンバーを使用しています。これらは、Arduinoとの通信に必要な多数のビット単位の設定オプションを組み合わせた結果です。マジックナンバーの出所に興味がある方は、関連するブログ記事を参照してください。

file = open("/dev/cu.wchusbserial1450", 'r')
fd = file.to_i

# 設定構造体の新しいインスタンスを作成
config = Serial::Termios.malloc

# デフォルトの設定オプションを構造体に読み込む
Serial.tcgetattr(fd, config);

# Arduinoにとって重要な設定オプションを指定
# マジックナンバーになってしまい申し訳ありません
config.c_ispeed = 9600;
config.c_ospeed = 9600;
config.c_cflag = 51968;
config.c_iflag = 0;
config.c_oflag = 0;

# readが戻る前に12文字受信するまで待機
config.c_cc[17] = 12;
# readが戻るまでの最小待機時間はなし
config.c_cc[16] = 0;

# 設定を保存
Serial.tcsetattr(fd, 0, config);

# Arduinoの再起動を待つため1秒スリープ
sleep(1)

# 未読のシリアル入力があれば破棄
Serial.tcflush(fd, 1)

buffer = file.read(12)
puts "#{ buffer.size } bytes: #{ buffer }"

file.close

シリアルリーダーの動作確認

Arduinoにhello worldを連続出力するプログラムを書き込めば、Rubyスクリプトでそのデータを読み取れるようになります。

RubyのFiddleで任意のCライブラリを操作する方法 ― 知られざる標準ライブラリの隠れた実力 このArduinoプログラムは「hello world」をシリアルに繰り返し書き込むだけのシンプルなものです

シリアルモニターを実行すると、次のように表示されます。

RubyのFiddleで任意のCライブラリを操作する方法 ― 知られざる標準ライブラリの隠れた実力

  1. Rubyのgrepメソッド徹底解説!使い方とselectとの違いを実例で学ぶ

    今回はRubyのgrepメソッドについて詳しく見ていきます。 このメソッドにはどんな便利さがあるのでしょうか? grepを使えば、配列(Array)や範囲(Range)など、Enumerableモジュールを含むオブジェクトから、条件に一致する要素だけを効率よく抽出できます。 「それってselectでも同じことができるのでは?」 確かにそうですが、grepはselectとは異なる仕組みで動作し、得られる結果も異なります。この違いを理解すれば、コードをより簡潔に書けるようになりますよ。 それでは、実際の例を見てみましょう。 Ruby grepメソッドの実例集 まず、次のような配列を用意します。

  2. Rubyのmapメソッド完全ガイド!配列・ハッシュのデータ変換を実例で解説

    Rubyのmapメソッドは、配列(Array)・ハッシュ(Hash)・範囲(Range)といったコレクションに対して使用できる強力なメソッドです。 mapの主な用途は、データの変換です。 例えば、文字列の配列があった場合、すべての文字列を順番に処理して、各文字を大文字に変換することができます。 また、Userオブジェクトのリストがある場合も同様です。 それらを変換して、対応するメールアドレスや電話番号など、Userクラスに定義された任意の属性のリストを作成できます。 それでは、具体的な使い方を見ていきましょう! Ruby mapメソッドの基本構文 mapの構文は次のようになっています。 a