LoginSignup
4
4

More than 5 years have passed since last update.

C#からneovimを動かす

Last updated at Posted at 2016-01-02

はじめに

みなさんneovim使ってますか?個人的には年末にようやく乗り換えたところです。折角乗り換えたので何かサポートツールでも作ってみようかと思い(こことか見てると夢は広がるなぁ、と)、neovimのmsgpack-rpcを叩けるC#ライブラリを作ってみました。本当はライブラリはもっとサクッと作ってツールメインになるはずでしたが、リフレクション使ったAPI動的生成とかやってると意外と時間がかかってしまったのでとりあえずライブラリのみ公開します。

動作確認環境

  • Windows10
  • Visual Studio 2015
  • neovim 0.1.1

リンク

Github
Nuget

使い方

使うべきクラスはNeovimClient<T> where T : INeovimIOです。INeovimIOがneovimとの通信I/Fになっていて、現時点では内部でプロセス生成して標準入出力経由で通信するNeovimHostと、外部のneovimプロセス(環境変数NVIM_LISTEN_ADDRESS指定で起動したもの)にTCP/IP経由で通信するNeovimTcpの2つの実装があります。(2015/1/3追記:需要はない気もしますがUNIXドメインソケットを実装したNeovimSocketも追加しました。UNIX環境上のMonoで動く予定ですが、一切確認していないので全く動かないかもしれません)
というわけで

    var nvimHost = new NeovimClient<NeovimHost>( new NeovimHost( "path to nvim.exe" ) );
    var nvimTcp = new NeovimClient<NeovimTcp>( new NeovimTcp( "127.0.0.1", 10000 ) );
    nvimHost.Init();
    nvimTcp.Init();

のような感じでインスタンス生成します。Initは例外を吐く可能性がある(nvim.exeが見つからないとかTCP接続失敗とか)ので適宜キャッチするなりしてください。このときneovimからvim_get_api_info経由で使用可能なAPI一覧を取得するので、neovimのバージョンには依存しないはずです。(実際にはui_attachなどUI系のAPIはvim_get_api_info経由では取れないようなので、それだけソース上に直書きしています。このあたりの事情ってどうなってるんでしょうか?)
実際のAPI呼び出しは

    nvimHost.Action<string>( "vim_set_current_line", "test" );
    var ret = nvimHost.Func( "vim_get_current_line" ); // ret: test
    nvimHost.Action<string>( "vim_command", "q" );

という感じで戻り値がある場合はFunc<T...>、ない場合はAction<T...>で呼び出します。この時の型引数はAPIごとに異なり

    NeovimFuncInfo info = nvimHost.GetFuncInfo( "vim_command" );
    List<Type> paramType  = info.ParamTypeCs;  // paramType : System.String
    Type       returnType = info.ReturnTypeCs; // returnType: System.Void
    string     dscr       = info.Description;  // dscr      : void Action<String>( String name, String str )

のようにTypeでも取得可能ですが、NeovimFuncInfo.Descriptionに関数プロトタイプ風の記述を入れてあるのでそれを見るのが簡単です。(呼び出し側コードをParamTypeCsとか参照しながら動的に生成するのでもなければそれで十分かと。ParamTypeCsの使い道としてはneovimのバージョンアップ等で引数が変わる場合に備えて型チェックをするくらいでしょうか)
また、neovimからの通知はNotificationReceivedイベントで受け取れます。ui_attachした時のredrawイベント発生は確認しました。

とりあえず、いくつかの簡単なAPI呼び出しはテストしましたが、上手く動かないものもあるかもしれません。特にBuffer/Windowなどを引数に取る系は怪しいです…

解説

API動的生成周りは意外と時間がかかったので備忘録を兼ねて簡単な解説を。動的生成している本体はCreateAction<T...>/CreateFunc<T...>です。

NeovimClient.cpp
        private object CreateAction<T1>( string name, string returnType, List<string> param, bool async, bool canFail ) {
            Expression<Action<T1>> func = ( p => neovimIO.Request( name, new object[] { p }, true ) );
            return func.Compile();
        }

ここでExpressionを使って最終的に呼び出されるべき関数neovimIO.Requestを与えています。System.Action<T>等と同様に引数の個数毎に(とりあえず引数6個まで)用意しています。また、Funcは戻り値の型変換があるため

NeovimClient.cpp
        private object CreateFunc<T1>( string name, string returnType, List<string> param, bool async, bool canFail ) {
            if( typeof( T1 ) == typeof( long              ) ) { Expression<Func<long             >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsInt64orExt() );                                     return func.Compile(); }
            if( typeof( T1 ) == typeof( long[]            ) ) { Expression<Func<long[]           >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsList().Select( i => i.AsInt64orExt() ).ToArray() ); return func.Compile(); }
            if( typeof( T1 ) == typeof( string            ) ) { Expression<Func<string           >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsString() );                                         return func.Compile(); }
            if( typeof( T1 ) == typeof( string[]          ) ) { Expression<Func<string[]         >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsList().Select( i => i.AsString() ).ToArray() );     return func.Compile(); }
            if( typeof( T1 ) == typeof( bool              ) ) { Expression<Func<bool             >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsBoolean() );                                        return func.Compile(); }
            if( typeof( T1 ) == typeof( object            ) ) { Expression<Func<object           >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].ToObject() );                                         return func.Compile(); }
            if( typeof( T1 ) == typeof( MessagePackObject ) ) { Expression<Func<MessagePackObject>> func = ( () => neovimIO.Request( name, new object[] {}, true )[1] );                                                    return func.Compile(); }
            throw new NotImplementedException();
        }

のように戻り値毎に書き分けています。このあたり暗黙の型変換定義とかですっきりするのでしょうか?(といっても変換元の型は型変数なので簡単にはいきそうにないですが…)
これらの呼び出し元が以下になります。

NeovimClient.cpp
        private object CreateExpression( string name, string returnType, List<string> param, bool async, bool canFail ) {
            if( param.Count > 6 ) {
                throw new NotImplementedException();
            }

            var isAction = NeovimUtil.ConvTypeNeovimToCs( returnType ) == typeof( void );
            if (  isAction && param.Count == 0 ) {
                return CreateAction( name, returnType, param, async, canFail );
            } else {
                if( !isAction ) {
                    param.Add( returnType );
                }
                var creatorName = ( isAction ) ? "CreateAction" : "CreateFunc";
                var methods = typeof( NeovimClient<T> ).GetMethods( BindingFlags.NonPublic | BindingFlags.Instance );
                var baseMethod = methods.First( m => m.Name == creatorName && m.GetGenericArguments().Count() == param.Count );
                var method = baseMethod.MakeGenericMethod( param.Select( i => NeovimUtil.ConvTypeNeovimToCs( i ) ).ToArray() );
                return method.Invoke( this, new object[] { name, returnType, param, async, canFail } );
            }
        }

CreateExpressionの引数がneovimのvim_get_api_infoからくる情報そのものです。paramに引数の型(neovimの型定義なのでstringです)が入っているので

NeovimClient.cpp
                var creatorName = ( isAction ) ? "CreateAction" : "CreateFunc";
                var methods = typeof( NeovimClient<T> ).GetMethods( BindingFlags.NonPublic | BindingFlags.Instance );
                var baseMethod = methods.First( m => m.Name == creatorName && m.GetGenericArguments().Count() == param.Count );

で、型変数の数がparamと一致するCreateAction/CreateFuncを探してきて

NeovimClient.cpp
                var method = baseMethod.MakeGenericMethod( param.Select( i => NeovimUtil.ConvTypeNeovimToCs( i ) ).ToArray() );

で、型変数を渡して実際のCreateAction/CreateFuncを生成します。(NeovimUtil.ConvTypeNeovimToCsでneovimの型定義からC#のTypeクラスへ変換しています )

NeovimClient.cpp
                return method.Invoke( this, new object[] { name, returnType, param, async, canFail } );

最後にInvokeAction/Funcを生成して返します。ここで戻り値型が何になるかはAPIの引数の個数次第なのでobjectで返し、使用時にキャストすることになります。

4
4
0

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
4