4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

おにぎり万太郎Advent Calendar 2019

Day 24

RubyからCプログラムを利用する

Last updated at Posted at 2019-12-23

FFI

Foreign function Interface の略で、別のプログラミング言語で記述された関数を利用するための機能です。
Rubyでは、ffiというgemや、Rubyに梱包される標準ライブラリのFiddleなどが同等の機能を提供しています。

今回は、gemである提供するffiを使って、RubyからCプログラムを利用してみたいと思います。

値渡し

まずはffiに慣れていきましょう。
呼び出し元のコードは以下です。

lib.c
int add(int a, int b) {
    return a + b;
}

int型の引数を2つ取って、それらの加算結果を返すaddを定義しました。

このaddをRubyから呼び出すために、共有ライブラリ(*.so)の形式に変換する必要があります。
以下のコマンドを使って変換します。

$ gcc -shared lib.c -o libadd.so

gccコンパイラに-sharedオプションを渡すと、共有ライブラリとしてオブジェクトファイルを吐き出してくれます。
Rubyから呼び出すファイルはlibadd.soです。

add.rb
require 'ffi'

module AddFFI
  extend FFI::Library
  ffi_lib 'libadd.so'

  attach_function :add, [:int, :int], :int
end

puts AddFFI.add(1, 2)

ffi_libメソッドで、先程生成した共有ライブラリを読み込みます。

そしてattach_functionで、Cプログラムで定義した関数を、Rubyから扱えるようにマッピングします。
第1引数にメソッド名、第2引数にCで定義した関数の引数の型を、第3引数に返り値の型を与えてあげます。
メソッド名は、Cプログラム内で定義した関数と同じである必要があります。

このRubyファイルを実行します。

$ ruby add.rb
3

無事FFIを介してCプログラムを呼び出すことができました。

ポインタを使って渡す

参照渡しとは変数のメモリ番地を渡すことで、変数の値を共有する仕組みです。値はコピーされないので、参照渡し先でその変数についての情報を書き換えると、元の保持していた変数の値も変化します。

[追記]
参照渡しとは変数のメモリ番地を渡すことで、変数の値を共有する仕組みです。値はコピーされないので、参照渡し先でその変数についての情報を書き換えると、元の保持していた変数の値も変化します。
筆者の参照渡しの理解が誤っており、@shiracamus さん および @c-yan さんからご指摘を頂きました。
参照渡しという言葉の定義については本記事のコメント欄に、お二人からのご教示があるので、そちらをご覧ください。(ご指摘ありがとうございました!)

sansyo.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int *p, *q;
    p = (int *)malloc(sizeof(int));
    *p = 1;
    printf("%d\n", *p);

    // ここで参照渡し
    // [追記] ここは参照渡しではなく参照の値渡し(詳細はコメントで)
    q = p;

    *q = 2;
    printf("%d\n", *p);
}

これを実行すると以下のようになり、*pの値が、*qへの代入後に変化していることがわかります。

$ gcc -o sansyo sansyo.c
$ ./sansyo
1
2

やりたいことはCプログラム内で、アドレス値(ポインタ)を返す関数を定義して、Rubyからはそのアドレスを参照することで値を取得する、ということです。

作成したCプログラムは以下です。

add_sansyo.c
#include <stdio.h>
#include <stdlib.h>

int *calc(int a, int b) {
    int *p;
    p = (int *)malloc(sizeof(int));
    *p = a + b;
    printf("sansyo function address = %p\n", p);
    return p;
}

int main(int argc, char *argv[]) {
    int *q = calc(1, 2);
    printf("calc(1, 2)              = %d\n", *q);
    printf("calc(1, 2)'s address    = %p\n", q);
}

デバッグも兼ねて、mainの中でcalcを呼び出しています。
Rubyへアドレス値を返したいので、calcはint型のポインタを返すようにしています。

実行結果は以下です。意図したとおりに動いていそうです。

$ gcc -o add_sansyo add_sansyo.c
$ ./add_sansyo
sansyo function address = 0x7f94d54026b0
calc(1, 2)              = 3
calc(1, 2)'s address    = 0x7f94d54026b0

このCプログラムをadd_sansyo.soとして共有ライブラリへ変換して、Rubyがロードします。

sansyo.rb
require 'ffi'

module AddFFI
  extend FFI::Library
  ffi_lib "add_sansyo.so"

  attach_function :sansyo, [:int, :int], :pointer
end

result = AddFFI.sansyo(1, 2)
puts result
puts result.read(:int)

ffiリポジトリrubydocを参考にしています。

attach_functionで行うマッピングについては、その名もズバリ、:pointerという形で、返り値を指定してあげます。
AddFFI.sansyoメソッドの返り値は、FFI::Pointerクラスが返ってくるので、readというインスタンスメソッドで、そのアドレスの値を取得しています。

実行すると以下のようになり、Cプログラムで参照しているアドレスをRubyから触ることで、関数の出力結果を意図したように取得できていることが理解できます。

$ ruby sansyo.rb
sansyo function address = 0x7fa1aad31860
#<FFI::Pointer address=0x00007fa1aad31860>
3

まとめ

Rubygemsのffiを使って、Cプログラムで書かれた関数を値やポインタを使ってRubyから呼び出すことができました。
言語の特徴を組み合わせすることでプログラムを最適化する、という考え方は個人的にとても気に入りました。

4
3
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?