ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

やわらかFFI 〜fiddle.gem 入門〜

Rubyアドベントカレンダー(その1)10日目の記事です。

qiita.com

技術アドベントカレンダーの原点?に立ち返り、自分の書いたものではなくTipsの話をします。

今回はここ数年よく使うようになったgem、fiddleの紹介をしようと思います*1

fiddleとは/FFIとは

fiddleとはRubyにおいて、FFI(Foreign Function Interface)をするためのgemです。

github.com

そのものずばり ffi というgemがあるためそちらを使う方も多いようですが、fiddleは現在標準ライブラリであるので、Rubyをインストールしたらそのまま含まれており、またRubyコアチームによりメンテナンスされているようです。

さて、ではそのFFIとは何かという話になります。「あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構」という説明がされたりしますが、(ことC言語に詳しくない方には)ピンとこないかもしれません。ほんの少しC言語を書いてピンとくるようにしましょう。

共有ライブラリを作る

ということでC言語を書きます。心配なさらず、今回は雰囲気だけわかれば大丈夫です...。

実行する環境はLinuxUbuntu 20.04以降を推奨します*2Vagrant上のVMで試していますが、Dockerの中でも実行は大丈夫だと思います。

まず、英語とスペイン語で「こんにちは、世界」を表示する関数を書きました。引数 is_spanish が0以外ならスペイン語になります。また、引数の値をそのまま戻り値にします。

#include <stdio.h>

int sayhello(int is_spanish) {
  if (is_spanish == 0) {
    printf("Hello, world\n");
  } else {
    printf("Hola, mundo\n");
  }
  return is_spanish;
}

これを「共有ライブラリ」にします。gccというコンパイラを入れて以下のコマンドを実行します。

$ sudo apt install build-essential
$ gcc -shared -fPIC -o libadcal.so adcal.c

libadcal.so というファイルができています。 file コマンドで調べると、なんか難しいことが書いてありますが、「ELF」の「shared object」とのことです。

$ file libadcal.so 
libadcal.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, 
BuildID[sha1]=fb5672164962190d34e40742522b51796ee8d855, not stripped

これは共有ライブラリであるので、この中にある関数は別のプログラムから呼ばないと使えません。

C言語の場合こういう風に呼び出します。まず呼び出したい関数の定義だけを宣言し、次にmain関数でその関数を呼び出します。このC言語のプログラムは、コマンドライン引数の最初の一つ目の、一文字目だけを見て、 s で始まっていれば sayhello(1) を、そうでない場合 sayhello(0) を呼び出します。

int sayhello(int is_spanish);

int main(int argc, char** argv) {
  if (argc > 1 && *argv[1] == 's') {
    sayhello(1);
  } else {
    sayhello(0);
  }
  return 0;
}

これをビルドします。この際いろいろとおまじないがあり... -L . で、その共有ライブラリが置いてあるディレクトリを、 -l adcal で、今回の関数が入ってる共有ライブラリの名前をlib抜き、拡張子抜きで指定します。そういうルールなのです。

今回は最終的に adcal.bin という実行ファイルを作ります。

$ gcc main.c -L . -l adcal -o adcal.bin
$ file ./adcal.bin 
./adcal.bin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, 
BuildID[sha1]=66176d0e95e756416e84690375d9fa98a07bf071, for GNU/Linux 3.2.0, not stripped

さらに実行するためには、 adcal.bin にさっきの共有ライブラリが置いてあるディレクトリを教えてあげる必要があり、そのため LD_LIBRARY_PATH という環境変数を与えてあげます。

$ export LD_LIBRARY_PATH=`pwd`

ようやく実行してみます:

$ ./adcal.bin 
Hello, world
$ ./adcal.bin spanish
Hola, mundo

コマンドライン引数がないときは英語で、 s で始まるコマンドライン引数があるときはスペイン語で「こんにちは世界」をするバイリンガルバイナリができました。

実は、多くのプログラムはこのような共有ライブラリに格納された関数を呼び出すことで成り立っています。C言語などでプログラムを作る場合、どの共有ライブラリを利用するかなどを指定し、プログラムにそのライブラリの情報を埋め込む必要があります。それを「リンク/動的リンク」と呼んだりします*3

プログラムが何を動的リンクしているかは ldd というコマンドでわかります。

$ ldd ./adcal.bin 
        linux-vdso.so.1 (0x00007ffe0d54f000)
        libadcal.so => /home/vagrant/adcal/libadcal.so (0x00007f2b01d4d000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2b01b57000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2b01d59000)

一方で、共有ライブラリの中の関数を事前にリンクせず、実行時に必要なものを判断し、それを呼び出したい場合があります。スクリプト言語を利用する際、コンパイルを経ずに別ライブラリの機能だけ使いたいような時などです。RubyのfiddleやPythonのctypesはそのような要求を満たすライブラリで、スクリプト言語で「FFI」と呼ぶときはそのような機能を指すことが多いです。

Rubyから共有ライブラリの関数を呼び出す - FFIを試す

ということで先程の int sayhello(int);Rubyから、動的に呼び出しましょう。

使うRubyのバージョンです。

$ ruby -v
ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-linux]

ということでようやくRubyのコードですが、 fiddle.gemは以下のような使い方をします。

require 'fiddle/import'

module AdCal
  extend Fiddle::Importer

  # dlload で読み込む共有ライブラリのファイル名を指定
  dlload './libadcal.so'
  # extern で、呼び出す関数とその引数を指定
  # fiddleが、RubyのクラスとCの型を相互に変換してくれます。
  extern 'int sayhello(int is_spanish);'
end

arg = ARGV[0]

if arg && arg.start_with?('s')
  AdCal.sayhello(1)
else
  AdCal.sayhello(0)
end

このRubyスクリプトを実行し、先程のCのプログラムと同じような結果になることを確認します。この際追加でC言語ソースコードコンパイルしていないことに注目してください。

$ ruby adcal.rb 
Hello, world
$ ruby adcal.rb spanish
Hola, mundo

ちなみに、C関数の戻り値もRubyのオブジェクト(今回はInteger)として扱えます。試しに、戻り値を p してみるといいでしょう。

if arg && arg.start_with?('s')
  p AdCal.sayhello(1)
else
  p AdCal.sayhello(0)
end
$ ruby adcal.rb spanish
Hola, mundo
1

Rubyからその他のライブラリの関数を動的に呼ぶ

他のライブラリを試す前に、先ほど作った共有ファイルに戻ります。Linuxの場合の話をしますが*4Linuxの共有ファイルはELFという形式のフォーマットで、 readelf などのコマンドでファイルに含まれる情報を確認できます。

この中には、シンボルテーブルという、コンパイル元のファイル名などプログラムの持つ名前の情報が含まれます。そしてその中に、公開している関数の名前も存在します。試しに見てみましょう。

めっちゃ情報が多いんですが、慌てず。

$ readelf -s ./libadcal.so

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     6: 0000000000001119    52 FUNC    GLOBAL DEFAULT   14 sayhello

Symbol table '.symtab' contains 52 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000000002a8     0 SECTION LOCAL  DEFAULT    1
     2: 00000000000002c8     0 SECTION LOCAL  DEFAULT    2
     3: 00000000000002f0     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000318     0 SECTION LOCAL  DEFAULT    4
     5: 00000000000003c0     0 SECTION LOCAL  DEFAULT    5
     6: 000000000000043a     0 SECTION LOCAL  DEFAULT    6
...
    48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    49: 0000000000001119    52 FUNC    GLOBAL DEFAULT   14 sayhello
    50: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
    51: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@[...]

見るべき情報をgrepで絞っておきます。ということで sayhello 関数もしっかり公開されています。こういった情報をもとにfiddle(の中のlibffi)は動的な関数呼び出しを行うわけですね。

$ readelf -s ./libadcal.so | grep sayhello
     6: 0000000000001119    52 FUNC    GLOBAL DEFAULT   14 sayhello
    49: 0000000000001119    52 FUNC    GLOBAL DEFAULT   14 sayhello

で、今回は理解のために最初に自分で共有ライブラリを作成したのですが、実はOSにはたくさんの共有ライブラリがあらかじめインストールされています。FFIの良いところは、共有ライブラリが事前に存在していれば、スクリプト言語のように別にコンパイルを走らせるのが難しい環境でも、それらの機能を呼び出すことができる点です。

ということで既存のライブラリから、別の関数も直接呼び出してみましょう。

プログラムのプロセスID(PID)を取得する getpid(2) *5はどうでしょうか。 man 2 getpid コマンドで調べると以下のような定義です。

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t getpid(void);

実はこの手の標準的な関数は libc.so に含まれていることがほとんどです。実際にそうか、 readelf で確認します。

$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep getpid
   597: 00000000000dfb00    12 FUNC    WEAK   DEFAULT   16 getpid@@GLIBC_2.2.5
   986: 00000000000dfb00    12 FUNC    GLOBAL DEFAULT   16 __getpid@@GLIBC_2.2.5

ちょっとさっきと違いますが大丈夫です。では同じくfiddleから呼び出してみます。

require 'fiddle/import'

module Getpid
  extend Fiddle::Importer

  # /lib/x86_64-linux-gnu のような標準的なディレクトリの
  # 共有ライブラリは、名前だけ指定でOK
  dlload 'libc.so.6'
  typealias 'pid_t', 'int'
  extern 'pid_t getpid(void);'
end

puts "From FFI: %d" % Getpid.getpid
puts "From Ruby API: %d" % $$

実行して、FFI経由のgetpid()呼び出しと、Ruby自身のAPIが同じPIDを返すことを確認します。

$ ruby getpid.rb 
From FFI: 53733
From Ruby API: 53733

最後に: もっと複雑な場合

いったんここまでで、FFIとは何者なのか、Rubyのfiddleがどういうものかご理解いただければ、本稿の目的は達成できたかなと思います...。

が、実際のFFIでは、有益な関数は大抵構造体とポインタを使うので、最後にその扱いを少しだけ*6

ターゲットとして getpwuid(3) という関数を呼び出してみましょう。これはLinuxなどのユーザIDから名前などの情報を引き出す関数です。例えば id コマンドの中で使われています。

$ id 999
uid=999(systemd-coredump) gid=999(systemd-coredump) groups=999(systemd-coredump)

関数の定義を見てみます。 jman の転載です。

#include <sys/types.h>
#include <pwd.h>

struct passwd *getpwuid(uid_t uid);

struct passwd 構造体はこんな感じらしいです。

struct passwd {
    char   *pw_name;       /* ユーザー名 */
    char   *pw_passwd;     /* ユーザーのパスワード */
    uid_t   pw_uid;        /* ユーザー ID */
    gid_t   pw_gid;        /* グループ ID */
    char   *pw_gecos;      /* ユーザー情報 */
    char   *pw_dir;        /* ホームディレクトリ */
    char   *pw_shell;      /* シェルプログラム */ };

ただ、今回は先頭の char *pw_name; だけ使えばいいので、 Passwd = Libc.struct("char* pw_name") という風にメンバ一つだけの構造体として扱うこととします。たまたま先頭にあるメンバなのでこういうことができます。

さて、コードの全体です。

require 'fiddle/import'

module Libc
  extend Fiddle::Importer

  dlload 'libc.so.6'

  Passwd = Libc.struct("char* pw_name")
  typealias 'uid_t', 'int'
  extern 'struct passwd *getpwuid(uid_t uid);'
end

uid = ARGV[0]&.to_i || raise("require uid")

if uid
  ret = Libc.getpwuid(uid)
  binding.irb
end

込み入った操作をするので途中でIRBで止めています。実行してみましょう。

$ ruby getpwuid.rb 999

From: getpwuid.rb @ line 16 :

    11: 
    12: uid = ARGV[0]&.to_i
    13: 
    14: if uid
    15:   ret = Libc.getpwuid(uid)
 => 16:   binding.irb
    17: end

retには Fiddle::Pointer のオブジェクトが入っています。 Libc::Passwd というクラスが定義されているので、このクラスを作る時に渡してあげると、構造体のように扱える形にしてくれます。

irb(main):001:0> passwd = Libc::Passwd.new(ret)
=> #<Libc::Passwd:0x000056044c7a2758 @entity=#<Fiddle::CStructEntity:0x000056044c6f3320 ptr=0x00007fcd27e70460 size=8 free=0x0000000000000000>>

さっき定義した通り passwd.pw_name を経由して名前にアクセスできますが、これは char * 型、つまりポインタです。 char * 型の場合は to_s で素直にStringに変換でき、名前が分かります。

irb(main):002:0> passwd.pw_name
=> #<Fiddle::Pointer:0x000056044c722ad0 ptr=0x000056044c4417f0 size=0 free=0x0000000000000000>
irb(main):003:0> passwd.pw_name.to_s
=> "systemd-coredump"

FFIを経由した構造体やポインタの操作はコツがありますが、 るりま などにもかなり詳しく使い方が説明されています。私も RbBCC の開発で大変お世話になりました。

まとめ

FFIという機能の話と、その理解に必要な、C言語、共有ライブラリ、リンクやシンボルなどの説明をざっくり行いました。

Rubyからプログラミングを始めた方が、普通にRubyだけを書いていると、この手の「システムプログラミングの初歩的なTips」に触れることは難しいように思っています。今回は、そういう気持ちもあり、不遜にも解説してみました*7

識者の方、この説明はちょっと... というものがあれば優しく教えてください...。

ということで、少しだけ低レイヤへの入り口が開けたのなら何よりです。


[PR] 低レイヤの世界がガンガンに開いたら、mrubyをこの本で覚えましょう。

明日は @mokio さんの記事です。

*1:RBSの持ちネタを使い切ってしまった。

*2:Ubuntuならある程度新しければ動くと思います

*3:やや大雑把な説明なのはご承知ください...

*4:Macなどがどうなっているか確認する時間がなく

*5:2 とか 3 はmanコマンドに渡す項目の種類=セクション番号です。2がいわゆるシステムコール、3がCの標準関数という違いはありますが、よくわからない方はいったんどちらも結局Cの関数のことだ!!と考えて進みましょう

*6:ここまでの節より難しくなります。構造体とポインタは説明をしませんので、理解できる方がターゲットです

*7:それでも難しいかな...