Rubyアドベントカレンダー(その1)10日目の記事です。
技術アドベントカレンダーの原点?に立ち返り、自分の書いたものではなくTipsの話をします。
今回はここ数年よく使うようになったgem、fiddleの紹介をしようと思います*1。
fiddleとは/FFIとは
fiddleとはRubyにおいて、FFI(Foreign Function Interface)をするためのgemです。
そのものずばり ffi というgemがあるためそちらを使う方も多いようですが、fiddleは現在標準ライブラリであるので、Rubyをインストールしたらそのまま含まれており、またRubyコアチームによりメンテナンスされているようです。
さて、ではそのFFIとは何かという話になります。「あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構」という説明がされたりしますが、(ことC言語に詳しくない方には)ピンとこないかもしれません。ほんの少しC言語を書いてピンとくるようにしましょう。
共有ライブラリを作る
ということでC言語を書きます。心配なさらず、今回は雰囲気だけわかれば大丈夫です...。
実行する環境はLinux、Ubuntu 20.04以降を推奨します*2。Vagrant上の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の場合の話をしますが*4、Linuxの共有ファイルは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 さんの記事です。