Quantcast
Channel: Vimタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 5608

C#からneovimを動かす

$
0
0

はじめに

みなさん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で動く予定ですが、一切確認していないので全く動かないかもしれません)
というわけで

varnvimHost=newNeovimClient<NeovimHost>(newNeovimHost("path to nvim.exe"));varnvimTcp=newNeovimClient<NeovimTcp>(newNeovimTcp("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");varret=nvimHost.Func("vim_get_current_line");// ret: testnvimHost.Action<string>("vim_command","q");

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

NeovimFuncInfoinfo=nvimHost.GetFuncInfo("vim_command");List<Type>paramType=info.ParamTypeCs;// paramType : System.StringTypereturnType=info.ReturnTypeCs;// returnType: System.Voidstringdscr=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
privateobjectCreateAction<T1>(stringname,stringreturnType,List<string>param,boolasync,boolcanFail){Expression<Action<T1>>func=(p=>neovimIO.Request(name,newobject[]{p},true));returnfunc.Compile();}

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

NeovimClient.cpp
privateobjectCreateFunc<T1>(stringname,stringreturnType,List<string>param,boolasync,boolcanFail){if(typeof(T1)==typeof(long)){Expression<Func<long>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].AsInt64orExt());returnfunc.Compile();}if(typeof(T1)==typeof(long[])){Expression<Func<long[]>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].AsList().Select(i=>i.AsInt64orExt()).ToArray());returnfunc.Compile();}if(typeof(T1)==typeof(string)){Expression<Func<string>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].AsString());returnfunc.Compile();}if(typeof(T1)==typeof(string[])){Expression<Func<string[]>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].AsList().Select(i=>i.AsString()).ToArray());returnfunc.Compile();}if(typeof(T1)==typeof(bool)){Expression<Func<bool>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].AsBoolean());returnfunc.Compile();}if(typeof(T1)==typeof(object)){Expression<Func<object>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1].ToObject());returnfunc.Compile();}if(typeof(T1)==typeof(MessagePackObject)){Expression<Func<MessagePackObject>>func=(()=>neovimIO.Request(name,newobject[]{},true)[1]);returnfunc.Compile();}thrownewNotImplementedException();}

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

NeovimClient.cpp
privateobjectCreateExpression(stringname,stringreturnType,List<string>param,boolasync,boolcanFail){if(param.Count>6){thrownewNotImplementedException();}varisAction=NeovimUtil.ConvTypeNeovimToCs(returnType)==typeof(void);if(isAction&&param.Count==0){returnCreateAction(name,returnType,param,async,canFail);}else{if(!isAction){param.Add(returnType);}varcreatorName=(isAction)?"CreateAction":"CreateFunc";varmethods=typeof(NeovimClient<T>).GetMethods(BindingFlags.NonPublic|BindingFlags.Instance);varbaseMethod=methods.First(m=>m.Name==creatorName&&m.GetGenericArguments().Count()==param.Count);varmethod=baseMethod.MakeGenericMethod(param.Select(i=>NeovimUtil.ConvTypeNeovimToCs(i)).ToArray());returnmethod.Invoke(this,newobject[]{name,returnType,param,async,canFail});}}

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

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

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

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

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

NeovimClient.cpp
returnmethod.Invoke(this,newobject[]{name,returnType,param,async,canFail});

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


Viewing all articles
Browse latest Browse all 5608

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>