Vimにmrubyインターフェースを組み込んでみた

週末の遊びとして、Vimにmrubyインターフェースを組込む実験をしてみた。
ほとんどCで書いた経験が無いので、出来るかわからんなーと思っていたが、構文を実行するだけなら何とか実現できたので、とりあえずまとめておく。
ほとんどmrubyというよりVimの話なんだけど。

mrubyについて調べる

流石にもう試してる人は結構居るみたいなので、mrubyをCのプログラムから実行して結果を取得するのはすぐに分かった。
しかし、なんかいくつかパターンがあるっぽいので繰り返し実行するのに良さそうなのをチョイス。
実行方法の違いが良く分かってない。

#include <mruby.h>
#include <mruby/compile.h>
#include <stdio.h>

int main() {
  mrb_state *mrb;
  mrbc_context *cxt;

  mrb = mrb_open();

  if (mrb == NULL) {
    printf("Error\n");
  }

  cxt = mrbc_context_new(mrb);
  mrb_load_string_cxt(mrb, "puts \"Hello, world\"", cxt);

  mrbc_context_free(mrb, cxt);
  mrb_close(mrb);
  return 0;
}

Vimのif_ruby.cを読む

RubyのコードをCに組込む方法はある程度知っているので、Vimがどういう作法でそれを読んでいるのかを調べる。
マルチプラットフォームな分岐が混じっていて非常に読みづらかったが、要はensure_ruby_initializedという関数で、ruby_init_stack()とruby_init()を読んで、load_pathを設定し、vimとやりとりするオブジェクトを仕込んでいるらしい。
そして、呼び出しのキック元はex_ruby()とかex_rubydo()とかの関数で、それぞれがVimの:rubyコマンド等に対応している。
その対応関係は、ex_docmd.cとかex_cmds.hあたりで定義されている。


ensure_ruby_initializedはこんな感じの関数。

static int ensure_ruby_initialized(void)
{
    if (!ruby_initialized)
    {
#ifdef DYNAMIC_RUBY
	if (ruby_enabled(TRUE))
	{
#endif
#ifdef _WIN32
	    /* suggested by Ariya Mizutani */
	    int argc = 1;
	    char *argv[] = {"gvim.exe"};
	    NtInitialize(&argc, &argv);
#endif
	    {
#if defined(RUBY19_OR_LATER) || defined(RUBY_INIT_STACK)
		ruby_init_stack(ruby_stack_start);
#endif
		ruby_init();
	    }
#ifdef RUBY19_OR_LATER
	    {
		int dummy_argc = 2;
		char *dummy_argv[] = {"vim-ruby", "-e0"};
		ruby_process_options(dummy_argc, dummy_argv);
	    }
	    ruby_script("vim-ruby");
#else
	    ruby_init_loadpath();
#endif
	    ruby_io_init();
	    ruby_vim_init();
	    ruby_initialized = 1;
#ifdef DYNAMIC_RUBY
	}
	else
	{
	    EMSG(_("E266: Sorry, this command is disabled, the Ruby library could not be loaded."));
	    return 0;
	}
#endif
    }
    return ruby_initialized;
}

:mrubyコマンドを動かせるようにする

マルチプラットフォームとかはとりあえずスルー。
Vimとのデータのやり取りも置いといて、:mrubyコマンドで構文を実行する所までを目指す。

#ifdef HAVE_CONFIG_H
# include "auto/config.h"
#endif

#include <stdio.h>
#include <string.h>

#include <mruby.h>
#include <mruby/proc.h>
#include <mruby/compile.h>

#include "vim.h"
#include "version.h"

static int mruby_initialized = 0;
static int ensure_mruby_initialized(void);
static mrb_state* vimMrb;

void ex_mruby(exarg_T *eap)
{
    char *script = NULL;
    mrbc_context *cxt;

    script = (char *)script_get(eap, eap->arg);
    if (!eap->skip && ensure_mruby_initialized())
    {
      cxt = mrbc_context_new(vimMrb);
      if (script == NULL) {
        puts((char *)eap->arg);
        mrb_load_string_cxt(vimMrb, (char *)eap->arg, cxt);
        mrbc_context_free(vimMrb, cxt);
      } else {
        EMSG(_("no mruby script"));
      }
    }
    vim_free(script);
}

static int ensure_mruby_initialized(void)
{
  if (!mruby_initialized)
  {
    vimMrb = mrb_open();
    mruby_initialized = 1;
    return mruby_initialized;
  } else {
    return mruby_initialized;
  }
}

void mruby_end()
{
  mrb_close(vimMrb);
}


非常に適当だが、とりあえずif_ruby.cからコードの構造をパクってきてでっち上げた。


続いて、ex_cmds.hとex_docmd.cを弄る。

diff -r f6cacdc34495 src/ex_cmds.h
--- a/src/ex_cmds.h	Wed Aug 07 21:13:23 2013 +0200
+++ b/src/ex_cmds.h	Sun Aug 11 16:00:41 2013 +0900
@@ -785,6 +785,8 @@
 			NEEDARG|EXTRA|NOTRLCOM),
 EX(CMD_runtime,		"runtime",	ex_runtime,
 			BANG|NEEDARG|FILES|TRLBAR|SBOXOK|CMDWIN),
+EX(CMD_mruby,		"mruby",		ex_mruby,
+			RANGE|EXTRA|NEEDARG|CMDWIN),
 EX(CMD_ruby,		"ruby",		ex_ruby,
 			RANGE|EXTRA|NEEDARG|CMDWIN),
 EX(CMD_rubydo,		"rubydo",	ex_rubydo,
diff -r f6cacdc34495 src/ex_docmd.c
--- a/src/ex_docmd.c	Wed Aug 07 21:13:23 2013 +0200
+++ b/src/ex_docmd.c	Sun Aug 11 16:00:41 2013 +0900
@@ -289,6 +289,9 @@
 # define ex_rubydo		ex_ni
 # define ex_rubyfile		ex_ni
 #endif
+#ifndef FEAT_MRUBY
+# define ex_mruby		ex_script_ni
+#endif
 #ifndef FEAT_SNIFF
 # define ex_sniff		ex_ni
 #endif


mrubyをちゃんと片付けられるように、mruby_endの呼び出しをmain.cに加える。

diff -r f6cacdc34495 src/main.c
--- a/src/main.c	Wed Aug 07 21:13:23 2013 +0200
+++ b/src/main.c	Sun Aug 11 16:00:41 2013 +0900
@@ -1478,6 +1478,9 @@
 #ifdef FEAT_RUBY
     ruby_end();
 #endif
+#ifdef FEAT_MRUBY
+    mruby_end();
+#endif
 #ifdef FEAT_PYTHON
     python_end();
 #endif


関数のプロトタイプ宣言を追加するために、proto/if_mruby.proを作成。

diff -r f6cacdc34495 src/proto/if_mruby.pro
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/proto/if_mruby.pro	Sun Aug 11 16:00:41 2013 +0900
@@ -0,0 +1,4 @@
+/* if_mruby.c */
+void mruby_end __ARGS((void));
+void ex_mruby __ARGS((exarg_T *eap));
+/* vim: set ft=c : */


ヘッダの読み込みを追加する。

diff -r f6cacdc34495 src/proto.h
--- a/src/proto.h	Wed Aug 07 21:13:23 2013 +0200
+++ b/src/proto.h	Sun Aug 11 16:00:41 2013 +0900
@@ -196,6 +196,9 @@
 # ifdef FEAT_RUBY
 #  include "if_ruby.pro"
 # endif
+# ifdef FEAT_MRUBY
+#  include "if_mruby.pro"
+# endif
 
 /* Ugly solution for "BalloonEval" not being defined while it's used in some
  * .pro files. */


後はせっせとautoconfのスクリプトを弄って、--enable-mrubyinterpみたいなオプション足したり、FEAT_MRUBYを定義したり、って感じでコンパイルをでっち上げる。


実際には、autoconfを弄る前に手動でMakefileを直で弄ってコンパイル通らねー、と頭を悩ませたりしてた。
一通り通って動かせるようになってからautoconfを弄っている。


結果、こんな感じでconfigureしてコンパイルすれば動くようになった。

% ./configure --with-features=huge --enable-multibyte --enable-luainterp=yes --with-luajit --with-lua-prefix=/usr/local --enable-rubyinterp=yes --enable-mrubyinterp=yes --with-libmruby=/Users/joker/mruby/build/host/lib/libmruby.a --with-mruby-include=/Users/joker/mruby/include
% make




なんか表示が狂ってておかしいけど、とりあえず実行出来てるっぽい。
パっと見:rubyコマンドと何も変わらないので、少し寂しいが結構楽しかった。


出力はさておき、Vim側とのインターフェースをちゃんと書けば、mrubyでvimプラグインとか書けるかもしれないし、mrbgemsの資産使えたりするんじゃね?とちょっと思っている。
が、そこまでやる気が続くかは分からないw
正直、C書いたりautoconf弄ったりするの辛い…。LLerには厳しい世界であった。
ぬるま湯最高!


編集差分のgistは → https://gist.github.com/joker1007/6203801