esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Ruby組み込みライブラリの型を書くのはそんなに大変じゃなかった

aikyoです。

はじめに

Ruby3.0で、型定義を書けるRBSが導入されました。 私は以前にRubyのFileクラスの型定義を書いたので、Cで書かれたRubyの組み込みライブラリの型定義を書くのもそこまで大変ではないよということについて書いていきます。

現在のRBSの状況

ただ、現在のRBSでは、まだRuby本体に組み込まれているクラスについても型定義が書かれていないものがあります。 なので、型検査をしたいと思ってもRBSに型定義がなければ自分で書くことになります。

Fileクラスの型定義を書いたとき

型定義を書こうと思ったはいいものの。何を参考に型を書くといいのかよくわかりませんでした。現在は TypeProfで型定義を生成してくれたりしますが、正しく推定できない場合があります。 そうなると、やはりソースコードを読んで型定義を書くしかないと思いました。

私はそれまでにRubyのソースコードをほとんど読んだことがなく、Cも詳しくありませんでした。 そのため、Rubyのソースコードを読むのは心理的な抵抗がありましたが、やってみると意外と難しいものではありませんでした。 というのも型定義を書くためには処理を正確に知る必要はなく、関数の引数と戻り値の型がわかれば良いからです。 それであれば型が分かるところまで処理を追うだけで良いのでだいぶ楽でした。

具体例

例として、File.chmod について見ていきます。

File.chmodのドキュメントによると、chmod(mode, *filename) -> Integerのように、第1引数にmode, 第2引数以降にfilenameをとってIntegerを返すようです。

それではRubyのソースコードを見ていきます。なお、Rubyのソースコードは3.0.0を使っています。

/* file.c */
static VALUE
rb_file_s_chmod(int argc, VALUE *argv, VALUE _)
{
    mode_t mode;
    apply2args(1);
    mode = NUM2MODET(*argv++);
    return apply2files(chmod_internal, argc, argv, &mode);
}

第1引数

File.chmod の第1引数が NUM2MODET に渡されています。 NUM2MODETRB_NUM2INT のエイリアスで NUM2INT と同じです。

/* include/ruby/internal/arithmetic/mode_t.h */
#define NUM2MODET RB_NUM2INT

/* include/ruby/internal/arithmetic/int.h */
#define NUM2INT    RB_NUM2INT

NUM2INT はドキュメントに記載があります。https://docs.ruby-lang.org/ja/latest/function/NUM2INT.html NUM2INTFixnumFloatBignum または to_int で型変換できる値をとります。 念の為、さらに処理を追って引数の型を特定してもよいのですが長くなるのでここではこれで止めておきます。

File.chmod の使い方として Float はさすがに除いてよいとして第1引数は Integer または to_int で型変換できるものとなります。 RBSでは、to_int で型変換できるというものが既に定義されています。

# core/builtin.rbs
interface _ToI
  def to_i: -> Integer
end

type int = Integer | _ToInt

これらを使うと、File.chmodの第1引数の型は int とできます。

第2引数

次にFile.chmodの第2引数を見ていきます。

/* file.c */
static VALUE
rb_file_s_chmod(int argc, VALUE *argv, VALUE _)
{
    mode_t mode;
    apply2args(1);
    mode = NUM2MODET(*argv++);
    return apply2files(chmod_internal, argc, argv, &mode);
}

argvをインクリメントしているため apply2files に渡されているのはFile.chmodの第2引数以降となります。 File.chmod の第2引数が apply2files に渡されているのでその定義を見ていきます。 apply2files はここにのせるには長いので、File.chmod の第2引数にあたる argv に関する部分のみのせておきます。

/* file.c */
static VALUE
apply2files(int (*func)(const char *, void *), int argc, VALUE *argv, void *arg)
{
    ...
    for (aa->i = 0; aa->i < argc; aa->i++) {
    VALUE path = rb_get_path(argv[aa->i]);
        ...
    }
    ...
}

File.chmod の第2引数以降を順に rb_get_path の引数として渡していますので、 rb_get_path を見ます。

/* file.c */

VALUE
rb_get_path(VALUE obj)
{
    return rb_get_path_check_convert(rb_get_path_check_to_string(obj));
}

VALUE
rb_get_path_check_to_string(VALUE obj)
{
    VALUE tmp;
    ID to_path;

    if (RB_TYPE_P(obj, T_STRING)) {
    return obj;
    }
    CONST_ID(to_path, "to_path");
    tmp = rb_check_funcall_default(obj, to_path, 0, 0, obj);
    StringValue(tmp);
    return tmp;
}

rb_get_path に渡された引数は rb_get_path_check_to_string に渡されます。 rb_get_path_check_to_string では引数が String ならそのまま、そうでないなら引数に to_pathto_str があればそれを実行して String に変換しています。 これらのことから、File.chmod の第2引数以降は可変長でそれぞれは String または to_pathto_str を持つものとわかります。

戻り値

最後に戻り値の型を考えます。 apply2files については戻り値の部分だけのせておきます。

/* file.c */
static VALUE
rb_file_s_chmod(int argc, VALUE *argv, VALUE _)
{
    mode_t mode;
    apply2args(1);
    mode = NUM2MODET(*argv++);
    return apply2files(chmod_internal, argc, argv, &mode);
}

static VALUE
apply2files(int (*func)(const char *, void *), int argc, VALUE *argv, void *arg)
{
    ...
    return LONG2FIX(argc);
}

chmod の戻り値は apply2files と同じで、 LONG2FIX(argc) の戻り値と同じなので Integer とわかります。

最終的な型定義

結果として以下のような型定義を書くことができました。

def self.chmod: (int mode, *(string | _ToPath) file_name) -> Integer

まとめ

  • Cで書かれたRubyの組み込みライブラリでも型を書くだけなら気にするべきことは多くない。
  • Cに詳しくなくてもなんとかなる。

おわりに

Cで書かれたRubyの組み込みライブラリの型定義を書いたときのことについて紹介してきました。 これを読んだ方が型定義を書くときの参考になれば幸いです。