C#からneovimを動かす

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

#動作確認環境
- Windows10
- Visual Studio 2015
- neovim 0.1.1

#リンク
[Github](https://github.com/dalance/NeovimClient)
[Nuget](https://www.nuget.org/packages/NeovimClient)

#使い方
使うべきクラスは`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で動く予定ですが、一切確認していないので全く動かないかもしれません）
というわけで

```csharp
    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呼び出しは

```csharp
    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ごとに異なり

```csharp
    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...>`です。

```csharp: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`は戻り値の型変換があるため

```csharp: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();
        }
```

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

```csharp: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`です）が入っているので

```csharp: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`を探してきて

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

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

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

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