第4回 Rubyインタプリタの組み込み(2)

前田修吾

VIM

今回はVIM(参考文献[1])というテキストエディタへのRubyの組み込みに ついて紹介します。

VIMはviクローンの一つですが、viを忠実的に再現するというよりは、多段 undoやマルチウィンドウ、GUIをサポートするなど、より高機能なエディタを 目指しています。ちなみにVIMは"Vi IMproved"の略で、「改良されたvi」とい う意味を持ちます。

このVIMの開発版には実はすでにRuby用のインタフェイスが組み込まれており、 Fig1のような手順でインストールすることで利用可能になります。 筆者が以前PerlやPythonなどしかサポートされていなかったのを見て、Rubyイ ンタフェイスを作成してパッチを送ったのですが、最初の反応は「Rubyなんて 知らない。もうちょっと有名になったら考えるから、しばらくはパッチで出し てくれ。」といった調子でした。(もちろん実際にはもっと丁寧な文章でした が。)

当時、Rubyは国内ではそこそこ使われていたものの、海外ではまだほとんど使 われていませんでしたから、Bram Moolenaar(VIMの作者)がRubyを知らなかっ たのも無理はありません。それに、実は、筆者は普段はEmacsを使っていて、 VIMはあまり使っていないという負い目があった*1ので、あまり強くはリクエストしませんで した。その後も一度パッチを送ったのですが、あまり反応はなく、VIMのこと はすっかり忘れていました。

ところが、ある日、VIMのソースを見ると、いつの間にかRubyインタフェイス が取り込まれていました。きっと誰かがリクエストしてくれたのでしょう。 最近では海外でもRubyの知名度が上がって来て、rubyのMLの流量は日本語の ML(ruby-list)よりも英語のML(ruby-talk)の方が多いくらいです。素晴らしい ですね。たまに筆者のメールアドレス宛にも英語の質問が来るようになったと いうことだけは悩ましい問題ですが。

-- Fig1. VIMのインストール

  $ tar zxvf vim-6.0ai-src.tar.gz
  $ tar zxvf vim-6.0ai-rt.tar.gz
  $ cd vim60ai/
  $ ./configure --enable-rubyinterp
  $ make
  # make install

VIM上でのRubyの実行

VIM上でRubyプログラムを実行するにはexコマンドを利用します。利用可能な コマンドは以下の3つです。

ruby

rubyコマンドは引数で与えられた文字列をRubyプログラムとして実行しま す。たとえば、Fig2のようにコマンドを入力すると"hello world"と表示 されます。

-- Fig2. hello world

  :ruby print "hello world"
rubydo

rubydoコマンドも引数で与えられた文字列をRubyプログラムとして実行し ますが、指定された範囲(デフォルトはすべての行)の各行に対し、繰り返 し実行します。各行は$_でプログラムに渡され、$_に値をセットするとそ の行の内容を置換することができます。rubydoはフィルタとして動作する わけです。たとえば、Fig3のようにHTMLタグの小文字化を行うことができ ます。(実は普通のvi(?)でも、Fig4のように外部コマンドを呼び出せば、 若干面倒ですが、同じようなことはできます。)

-- Fig3. HTMLタグの小文字化

  :rubydo gsub(/<\/?[A-Za-z]+>/) { |s| s.downcase }

-- Fig4. 外部コマンドの呼び出し

  :%!ruby -pe 'gsub(/<\/?[A-Za-z]+>/) { |s| s.downcase }'
rubyfile

rubyfileコマンドは指定されたRubyスクリプトをロードします。rubyfile コマンドではTABキーによってファイル名の補完を行うことができます。 Fig4はhello.rbというスクリプトをロードする例です。

-- Fig4. ファイルのロード

  :rubyfile hello.rb

Rubyインタフェイスの構成

VIMのRubyインタフェイスは大きく分けて次の2つの部分によって構成されてい ます。

  1. exコマンド
  2. VIM用ライブラリ

exコマンドは先程説明した3つのコマンドで、VIMからRubyを利用するためのイ ンタフェイスを提供する部分です。VIM用ライブラリは、逆に、RubyからVIMの 機能(バッファやウィンドウの操作など)を利用するためのインタフェイスを提 供します。

このように、Rubyを組み込んだアプリケーションでは、多くの場合、

  1. アプリケーションからRubyを利用するためのインタフェイス
  2. Rubyからアプリケーションの機能を利用するためのインタフェイス

という、2つのインタフェイスを提供する必要があります。

前者については、アプリケーションの性質によって、適切なインタフェイスも 異なってきます。VIMのように、すでに何らかのコマンドを実行するための枠 組が用意されているような場合には、それを利用するのがよいでしょう。実装 が簡単だということもあるのですが、ユーザの利便性という観点からも、統一 されたインタフェイスを利用できる方が望ましいからです。

後者については、基本的な考え方としては、拡張ライブラリを作成する場合と 同じです。多くのアプリケーションは、構造体と、その構造体を操作する関数 群を提供しますので、それらをラップするようなクラスライブラリを提供する ことになります。拡張ライブラリの作成については、この連載の第1・2回 [2]で簡単に紹介しています。

exコマンド

それでは実装について見てみることにしましょう。Rubyインタフェイスの主な 処理はif_ruby.cというソースファイルに記述されており、exコマンドを実装 する関数にはex_というprefixが付けられています。これらの関数はex_cmds.h で宣言された、cmdnamesというexコマンドの情報を格納する構造体の配列によっ て、exコマンドとして登録されています。

ex_ruby()

ex_ruby()はrubyコマンドを実装します。(List1) まず ensure_ruby_initialized()を呼んでいますが、これはRubyインタプリタ が初期化されていなければ初期化する関数です。List1の ensure_ruby_initialize()の定義を見ると、常に1を返すようになってい るので、返り値の意味がないように見えますが、実は読みやすさを考えて Windows対応の部分*2を削除したために、このようになっています。 ensure_ruby_initialize()では、ruby_init()によるインタプリタの初期 化の他に、ロードパス($:)の初期化(ruby_init_loadpath())やVIM用のラ イブラリの初期化(ruby_io_init()/ruby_vim_init())を行っています。

script_get()はVIMが提供する関数で、Fig5のようにexコマンドでヒアド キュメントが使われていた場合、その内容を返します。ヒアドキュメント が使われていなかった場合にはコマンドの引数(eap->arg)を利用していま す。文字列を評価するために、前回説明したrb_eval_string()の代りに rb_eval_string_protect()という関数を使っていますが、これは例外やエ ラー(unexpected breakなど)を捕捉するためです。rb_eval_string()を使 うと、例外やエラーの際にSegmentation Faultが発生してしまうので注意 してください。例外やエラーの情報はstateに格納され、正常終了の場合 は0になります。stateが0以外の場合は、エラーに関する情報を表示させ ています。error_print()の実装についてはVIMのソース(CD-ROMに収録)を 参照してください。

-- List1 ex_ruby()

  void ex_ruby(exarg_T *eap)
  {
      int state;
      char *script = NULL;

      if (ensure_ruby_initialized())
      {
          script = script_get(eap, eap->arg);
          if (script == NULL)
              rb_eval_string_protect((char *)eap->arg, &state);
          else
          {
              rb_eval_string_protect(script, &state);
              vim_free(script);
          }
          if (state)
              error_print(state);
      }
  }

  static int ensure_ruby_initialized(void)
  {
      if (!ruby_initialized)
      {
          ruby_init();
          ruby_init_loadpath();
          ruby_io_init();
          ruby_vim_init();
          ruby_initialized = 1;
      }
      return ruby_initialized;
  }

-- Fig5 ヒアドキュメントの利用

  :ruby <<EOF
  x = 100 + 200
  y = x / 2
  print x
  EOF
ex_rubydo()

ex_rubydo()はrubydoコマンドを実装します。(List2) 基本的な部分は ex_ruby()と同じですが、指定された各行について繰り返し実行している 点や、$_の処理を行っていることなどが異なっています。$_の値の取得は rb_lastline_get()、値の設定はrb_lastline_set()で行っています。また、 エラーが発生した場合には、その行で処理を中断しています。

-- List2 ex_rubydo()

  void ex_rubydo(exarg_T *eap)
  {
      int state;
      linenr_T i;

      if (ensure_ruby_initialized())
      {
          if (u_save(eap->line1 - 1, eap->line2 + 1) != OK)
              return;
          for (i = eap->line1; i <= eap->line2; i++) {
              VALUE line, oldline;

              line = oldline = rb_str_new2(ml_get(i));
              rb_lastline_set(line);
              rb_eval_string_protect((char *) eap->arg, &state);
              if (state) {
                  error_print(state);
                  break;
              }
              line = rb_lastline_get();
              if (!NIL_P(line)) {
                  if (TYPE(line) != T_STRING) {
                      EMSG("E265: $_ must be an instance of String");
                      return;
                  }
                  ml_replace(i, (char_u *) STR2CSTR(line), 1);
                  changed();
  #ifdef SYNTAX_HL
                  syn_changed(i); /* recompute syntax hl. for this line */
  #endif
              }
          }
          check_cursor();
          update_curbuf(NOT_VALID);
      }
  }
ex_rubyfile()

ex_rubyfile()はrubyfileコマンドを実装します。(List3) rb_load()の代 りにrb_load_protectを使用していますが、これはex_ruby()や ex_rubydo()でrb_eval_string_protect()を使用しているのと同じ理由に よります。rb_eval_string_protect()やrb_load_protect()の他に、より 一般的な関数としてrb_protect()という関数も用意されています。これは 任意の処理をエラーや例外から保護した状態で行うことができる関数なの ですが、行いたい処理を関数にして、その関数へのポインタを渡す必要が あるので、ちょっと扱いが面倒です。

-- List3 ex_rubyfile()

  void ex_rubyfile(exarg_T *eap)
  {
      int state;

      if (ensure_ruby_initialized())
      {
  	rb_load_protect(rb_str_new2((char *) eap->arg), 0, &state);
  	if (state) error_print(state);
      }
  }

出力ライブラリ

通常printメソッド(Kernel#print)は標準出力にそのまま文字列を出力します。 しかし、アプリケーションにRubyを組み込む場合は、標準出力に出力されては 困るような場合が多いです。典型的な例はGUIアプリケーションです。そこで、 printメソッドの挙動を変える必要が出てきます。

printメソッドの挙動を変えるために、メソッドそのものを再定義することも できますが、printを再定義するだけでは十分ではまだありません。putsや printfなどの他の出力系メソッドの挙動も変える必要があります。しかし、こ れらのメソッドをすべて再定義するのはかなりの手間がかかります。

そこでVIMではprintメソッドの出力先のオブジェクトをすげ替えるという手法 を使っています。printなどの出力系メソッドの出力先は標準出力そのもので はなく、$>という変数が指すオブジェクトになっており、出力時には最終的に $>.writeが呼ばれるようになっています。そのため、writeメソッドを実装し たオブジェクトを$>に設定すれば、printの挙動を変えることができます。$> はCレベルではrb_defoutという変数によって参照することができます。 $>の値を変えるには、rb_defoutに値を代入するだけです。

VIMではList4のように、Objectのインスタンスをrb_defoutに代入し、特異メ ソッドとしてwriteを定義しています。もちろん、writeメソッドを定義したク ラスを作成して、そのクラスのインスタンスをrb_defoutに代入してもかまい ません。writeの実装であるvim_message()(VIM::messageの実装でもあります) では、VIMが提供するMSG()というマクロによってメッセージを表示しています。

-- List4 出力先の変更

  static VALUE vim_message(VALUE self, VALUE str)
  {
      char *buff, *p;

      str = rb_obj_as_string(str);
      buff = ALLOCA_N(char, RSTRING(str)->len);
      strcpy(buff, RSTRING(str)->ptr);
      p = strchr(buff, '\n');
      if (p) *p = '\0';
      MSG(buff);
      return Qnil;
  }

  static void ruby_io_init(void)
  {
      extern VALUE rb_defout;

      rb_defout = rb_obj_alloc(rb_cObject);
      rb_define_singleton_method(rb_defout, "write", vim_message, 1);
      rb_define_global_function("p", f_p, -1);
  }

VIM::Bufferクラス

VIMにはバッファやウィンドウを表現する構造体があり、それらの構造体を定 義する関数群が提供されています。RubyインタフェイスではVIM::Bufferや VIM::Windowといったクラスによって、バッファやウィンドウを操作できるよ うになっています。ここではVIM::Bufferについて説明しますが、VIM::Window も基本的な構造は同じです。

オブジェクトの生成

VIMではバッファはbuf_Tという構造体によって表現され、VIM::Bufferはbuf_T をラップするクラスになっています。VIM::Bufferオブジェクトは buffer_new()という関数によって生成されます。(List5)

buf_TにはRubyインタフェイスのためにruby_refというメンバがあり、生成さ れたVIM::Bufferオブジェクトの参照はruby_refに格納されるようになって います。これは同じbuf_T構造体に対して、何度もVIM::Bufferオブジェクトを 生成する必要をなくすためです。buffer_new()ではruby_refがNULLでない場合 は、新しくVIM::Bufferオブジェクトを生成する代りに、ruby_refの値をVALUE にキャストして返しています。

ただ、この方法には1点問題があります。それはGC(ガーベージコレクション) の問題です。RubyのGCはマークアンドスイープ方式と呼ばれる手法を採用して います。マークアンドスイープ方式では、GCはマークとスイープという2つの フェーズに分かれています。マークフェーズではグローバル変数やスタックな どからオブジェクトの参照をたどって、まだ使用されているすべてのオブジェ クトをマークします。そして、スイープフェーズでマークされていない(すな わち、もう使用されていない)オブジェクトをごみとして回収します。

問題は、ruby_refからオブジェクトが参照されていることをRubyインタプリタ は知らないため、ruby_ref以外からの参照がなくなってしまうと、マークフェー ズでオブジェクトがマークされなくなってしまうことです。マークされないわ けですから、オブジェクトはスイープフェーズでごみとして回収されてしまい ます。そして、回収されたオブジェクトにruby_refによってアクセスしてしま うと深刻な問題を引き起こすことになります。

buffer_new()では、この問題を避けるために、ruby_refにVIM::Bufferオブジェ クトの参照を格納した後で、objtblというRubyレベルではアクセスできないグ ローバル変数から参照されるHashオブジェクトに、VIM::Bufferオブジェクト をオブジェクトのIDをキーにして格納しています。objtblによってRubyインタ プリタからVIM::Bufferオブジェクトへの参照を把握するため、GCでオブジェ クトが回収されてしまう問題を防ぐことができます。

実際にユーザがVIM::Bufferオブジェクトを得るためには、 VIM::Buffer.currentやVIM::Buffer[]などのクラスメソッドを利用します。 buffer_s_current()ではVIMの変数であるcurbuf(カレントバッファ)を buffer_new()でラップして返しています。

-- List5 VIM::Bufferオブジェクトの生成

  static VALUE buffer_new(buf_T *buf)
  {
      if (buf->ruby_ref) {
          return (VALUE) buf->ruby_ref;
      }
      else {
          VALUE obj = Data_Wrap_Struct(cBuffer, 0, 0, buf);
          buf->ruby_ref = (void *) obj;
          rb_hash_aset(objtbl, rb_obj_id(obj), obj);
          return obj;
      }
  }

  static VALUE buffer_s_current()
  {
      return buffer_new(curbuf);
  }

  static void ruby_vim_init(void)
  {
      objtbl = rb_hash_new();
      rb_global_variable(&objtbl);
      ...
      cBuffer = rb_define_class_under(mVIM, "Buffer", rb_cObject);
      rb_define_singleton_method(cBuffer, "current", buffer_s_current, 0);
      ...
  }

オブジェクトの解放

buffer_new()で生成されたVIM::Bufferオブジェクトはすべてobjtblに登録さ れることになります。そのため、このままではバッファがなくなって VIM::Bufferオブジェクトが必要なくなった場合も、オブジェクトが回収され ずごみが残ってしまいます。そこで、必要がなくなったオブジェクトを回収す るために、バッファが削除される時にruby_buffer_free()という関数(List6) を呼ぶようにしています。ruby_buffer_free()ではruby_refがNULLでない場合、 ruby_refが指すオブジェクトのIDをobjtblから削除します。この処理によって すべての参照がなくなるため、GC時にオブジェクトが回収されるようになりま す。

また、オブジェクトをRData構造体にキャストしてdataメンバにNULLを代入し ていますが、このメンバはbuf_Tポインタを保持しているメンバです。NULLを 代入しているのは、バッファが削除された後に、VIM::Bufferオブジェクトか らバッファにアクセスされることを防ぐためです。バッファが削除されたから といって、VIM::Bufferオブジェクトへの参照がなくなるとはかぎらない(たと えば、グローバル変数から参照されているかもしれません)ため、このような 処理が必要になります。

-- List6 VIM::Bufferオブジェクトの解放

  void ruby_buffer_free(buf_T *buf)
  {
      if (buf->ruby_ref) {
          rb_hash_aset(objtbl, rb_obj_id((VALUE) buf->ruby_ref), Qnil);
          RDATA(buf->ruby_ref)->data = NULL;
      }
  }

メソッド

VIM::Bufferオブジェクトによってバッファを操作するためには、バッファを 操作するためのメソッドが必要です。

List7のbuffer_append()はVIM::Buffer#appendという、指定した行の後ろに行 を追加するメソッドの定義です。オブジェクトからbuf_Tポインタを取り出す ためにget_buf()という関数を利用しています。get_buf()では取り出したポイ ンタがNULLでないかどうかチェックしています。NULLであった場合はすでにバッ ファが削除されていますので、例外を発生させています。buf_Tポインタを取 り出した後はVIMの関数を利用して、バッファに行を追加する処理を行ってい ます。

-- List7 VIM::Buffer#appendの定義

  static buf_T *get_buf(VALUE obj)
  {
      buf_T *buf;

      Data_Get_Struct(obj, buf_T, buf);
      if (buf == NULL)
          rb_raise(eDeletedBufferError, "attempt to refer to deleted buffer");
      return buf;
  }

  static VALUE buffer_append(VALUE self, VALUE num, VALUE str)
  {
      buf_T *buf = get_buf(self);
      buf_T *savebuf = curbuf;
      char *line = STR2CSTR(str);
      long n = NUM2LONG(num);

      if (n >= 0 && n <= buf->b_ml.ml_line_count && line != NULL) {
          curbuf = buf;
          if (u_inssub(n + 1) == OK) {
              mark_adjust(n + 1, MAXLNUM, 1L, 0L);
              ml_append(n, (char_u *) line, (colnr_T) 0, FALSE);
              changed();
          }
          curbuf = savebuf;
          update_curbuf(NOT_VALID);
      }
      else {
          rb_raise(rb_eIndexError, "index %d out of buffer", n);
      }
      return str;
  }

  static void ruby_vim_init(void)
  {
      ...
      rb_define_method(cBuffer, "append", buffer_append, 2);
      ...
  }

おわりに

さて、前回に引き続き、Rubyインタプリタの組み込みについて解説してきまし たがいかがでしたでしょうか。話題の性質上、コードはC言語が中心になって しまった*3ので、もっとRuby自体につい て知りたいという方には申し訳ありませんでした。Cを扱うのはこれくらいに して、次回からはRubyプログラミングに関する話題を取り上げたいと考えてい ます。

参考文献

[1]

The VIM (Vi IMproved) Home Page, <URL:http://www.vim.org/>

[2]

C MAGAZINE 2001年5月号「第1回 拡張ライブラリの作成(1)」, C MAGAZINE 2001年6月号「第1回 拡張ライブラリの作成(2)」, <URL:http://www.shugo.net/article/>


*1 パッチを送った時のメー ルもEmacsで書いていました:-p
*2 Windowsの場合は、Rubyのダイナミックリンクに失 敗すると、エラーメッセージを表示して0を返すようになっています。
*3 C MAGAZINEなのだからよいような気もするのですが…C MAGAZINEのCはコンピュータのCでしたっけ。