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
に渡されています。 NUM2MODET
は RB_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
NUM2INT
は Fixnum
、 Float
、 Bignum
または 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_path
や to_str
があればそれを実行して String
に変換しています。
これらのことから、File.chmod
の第2引数以降は可変長でそれぞれは String
または to_path
や to_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の組み込みライブラリの型定義を書いたときのことについて紹介してきました。 これを読んだ方が型定義を書くときの参考になれば幸いです。