この記事はC# Advent Calendar 19日目の参加エントリです。昨日はMuto Masayukiさんでした。
sorry in advance (?)
実はもうちょっと具体的に動くコードを書いて、その説明ということにしたかったのですが、マシントラブルで数日間stuckしてしまいまして、肝心の部分はテキスト中心になります。あらかじめごめんなさい。
今回のお題: MonoDevelop C#バインディング
C# Advent Calendar経由で来られた方はご存じなかろうと思いますが、2ヶ月ほど前にMonoDevelop/GTK#勉強会というものがありました。そこでは、MonoDevelopとは一体何なのか、IDEであるところのMonoDevelopはどのような機能を実装する(期待されている)のか、といった話をしました。興味がある方は、勉強会の時に使ったスライドより、MonoDevelop勉強会事前資料と称した資料を見ていただければと思います。
さて、実は、この事前資料でほとんど取り上げなかった話題があります。C#ソースコードエディタについてです。C#の潜在能力を活用できるソースコードエディタの提供は、.NET系開発環境の最重要課題です。コード編集機能が貧弱なエディタのIDEを使うなら、手に馴染んだエディタを片手に戦に臨んだ方が良いでしょう*1。
そんなわけで、今回は、その落ち穂拾い…というわけではありませんが、IDEの最重要課題であるところのソースコードエディタ、ひいてはその背景にある言語バインディングについて、調べてみたことを、いろいろまとめてみようと思います。一応C# Advent Calendar参加エントリなので、CSharpバインディングを主に取り上げます。この記事を読み終わることには、あなたも独自の言語のMonoDevelopバインディングを実装できるようになっている、というのが本稿の目標です…といきたかったのですが、前述の通り、時間が足りなかったので無理でした。ごめんなさい。(まあそんな短時間ではいずれにしろ無理ですが。)
地味な、あるいはマニアックな興味の話題だと思われるかもしれませんが、ここでひとつ考えてみたいことがあります。C#はVisual Studioによる補完などのサポートが非常に強力である点が長所として挙げられる言語です(あ、Java+Eclipseもそうですね)。また、Visual StudioにはReSharperという強力なコード編集サポートのアドインも存在します(まあわたしはVSで仕事しないので使ったことがないのですが)。MonoDevelopでもこれらが提供する機能を提供している、あるいは無いものも実現できる、というのは、ちょっとばかり刺激的なことだと思えませんか? そして、IDE側の仕組みさえわかれば、他の言語のバインディングも同様に高機能な編集機能が用意できるかもしれません。
ソースコードエディタの機能要件
さて、(ここは実はMonoDevelop勉強会で話した内容と多少かぶりますが)IDEのソースコードエディタには、単なるテキストエディタとは異なり、何が求められるのでしょうか? たとえば
- ソースコードのシンタックス ハイライティング(色付けや強調など)
- 識別子・メンバー・キーワードの自動補完
- 文法エラーやコンパイルエラー箇所へのジャンプ
- 変数・メンバー・型の定義へのジャンプ
…といった機能が挙げられるかと思います。実はシンタックス ハイライティングは正規表現マッチングでもそれなりに実現できるので、それほど難しくありません。コンパイルエラー箇所へのジャンプも、コンパイラのエラー出力を解析して適切なファイルと行番号さえ得てしまえば簡単です(ちなみにMonoDevelopはVisual Studio互換のエラー位置取得機能を実装しているので、cscなどのエラー出力をエラーメッセージ パッドに反映できます)。
それ以外、たとえば識別子の定義内容を取得したり、動的に文法エラーを表示するためには、ソースコードを解析して、コンパイルの有無にかかわらず、不完全なソースコードから型やローカル変数宣言などを取得できなければなりません。これは割と高度な機能です。完全なソースコードのコンパイラを作成するだけでも大変なのに、不完全なソースコードを読み取って、その中から拾える限りの情報を拾ってやらないといけないわけです。
Compiler as a Service
ソースコードの解析が、特にリードオンリーではないかたちで実現すると、実は他にもいろいろな加工機能が実装できます。いわゆるリファクタリングというやつです。ソースツリー群の全てをなめて、特定のローカル変数やメンバーへの参照を全て取得し、宣言とそれらの参照の全てをテキストエディタ上で書き換えれば、変数の安全なリネームが可能ですね。
加工可能なソースコード ツリーがあると、リファクタリングの他にも、以下のような機能が実現できます:
- スマート インデント
- ソースコード整形
- さまざまなコード自動生成(型/メンバーの実装、未定義シンボルからの変数宣言追加など)
- 品質に問題のあるコードの検出と修正案の提示・適用
このような、コンパイラが拡張されてIDE等のアプリケーションとインタラクティブな機能を提供するものを、compiler as a service (CaaS)あるいはlanguage serviceと呼びます。CaaS、どう読んだらいいのかわからんので、以降はlanguage serviceと書くことにします。
language serviceは、プログラミング言語の話題の中では、割とホットな部類ではないかと思います。language serviceは、主に静的型付けされた言語で、実装するアドバンテージがあって、たとえばなまのJavascriptでこれを実装するのは、不可能とは言いませんが、いかんせん型情報が少ないので(関数が何を返すのかも分からないとか)、旨みが小さいです。そう、Microsoftが最近リリースしたもので、JavaScriptに型情報をフレーバーしたTypeScriptという言語がありましたが、これはまさにlanguage serviceを実装するために作られたと言っても良いでしょう。TypeScriptには、最初のベータリリースの時点で、language serviceが含まれています。DeNAのJSXも話題になりましたが、これにもlanguage serviceを実装したインタラクティブなエディタがありますね(ソースはこの辺かな?)。あとわたしがちょっとだけ興味があるのは、pythonに(ちょっとだけ?)静的型付けを持ち込んだと言っているCythonですが、この辺はまだlanguage serviceは無さそう。って、余談はこのくらいにしておきましょうか。
NRefactory
さて、MonoDevelopには、これらの機能が既に備わっています。正確には、NRefactoryという、まさにlanguage serviceを実装するためのライブラリを、組み込んで使っています。実はNRefactoryを作っているのはSharpDevelopの開発メンバーで、MonoDevelopとコードを共有しながら開発しているというわけです(ちなみにSharpDevelopの最初のリーダーMike Kruegerは、今はXamarinのMonoDevelopチームで仕事しています)。
NRefactoryは、C#を中心として、コード分析やリファクタリングの機能を実現できるフレームワークとなっています。VBサポートも実装されつつあるようですが、まだ正式ではなさそうです。NRefactoryは、MonoDevelopやSharpDevelopからは独立して利用できます。NRefactoryについては、 neue.cc - "とあるRoslynではないC# Parser、NRefactoryの紹介" で簡単に使用例を紹介されています(あ、ここまでRoslynに関する言及ゼロだった)。
アドイン機構のちょっとしたおさらい
さて、NRefactoryさえわかればOKかというと、残念ながら全くそういうことはなく、NRefactoryの機能をMonoDevelopはどうやって組み込んでいるのかがわからないと、同様の追加リファクタリング機能や別言語での実装をすることもできません。(あれ、そういうのはいらないですかそうですか…)
というわけで、そろそろ本題に入ろうと思います(!?)。今回はMonoDevelopの実際のソースを適宜追いかけます。ですので、まあこれを読んでいる皆さんはたいがいgithubからソースを落としていると思いますが、ソースを手元に用意しながら読み進めると話がより簡単にわかるかもしれません。オンラインで直接見ながら読んだほうが早いという方はこちらからどうぞ。ええまあ誰も手元には持っていませんよね…
さて、先の事前資料でも言及しているのですが、MonoDevelopはその大部分をアドイン機構の上に設計しています。C#バインディングも例外ではありません。MonoDevelopのアドインは、mono-addinsという独自のアドインフレームワークに基づいており*2、IDEの拡張ポイント(設定メニューのどこに項目を追加するか、とか、プロジェクトテンプレートはどこに追加できるか、とか)は(慣例的に)addin.xmlというファイルで記述します。
このaddin.xmlはアドインに含まれるアセンブリのリソースとして含まれ、monodevelopはアドインの全アセンブリをスキャンして、addin.xmlを発見し次第、内容を解析して拡張ポイントを追加する、ということを行います。(ちなみにアドインは単体のアセンブリではなく、アセンブリの集合体をzipにした.mpackという拡張子のパッケージになります。複数のアドインが同一のアセンブリをアドイン アセンブリとしてmpackに含むことがあります。たとえば、MonoDevelop.VersionControl.dllは、Subversion addinにもGit addinにも含まれます。)
MonoDevelop言語バインディングの種類と機能の範囲
C#アドインは、MonoDevelop.CSharpBinding.dllというアセンブリに含まれていて、ソースは main/src/addins/CSharpBindingにあります。アドイン記述xmlはこちらです。長いですね。ひとつの言語をサポートするのにこんなにたくさんの拡張ポイントを実装しないといけないのか!!と思われたかもしれませんが*3、このaddin.xmlが長い理由は、ひとえにC#アドインが無駄に(枕詞)高機能だという点につきます。
今日の主眼はC#アドインに置いているのですが、C#アドインには、言語バインディング一般に実装されている拡張ポイントと、一部の高度な言語バインディングでのみ提供されている拡張ポイントと、ほとんどC#でのみ用いられている拡張ポイントがあります。特に一般的なものを押さえておけば、これをフィルタリングして興味深い部分だけを追いかけることもできるでしょう。
少し具体的にわかりやすくするために、ここでいくつかの他言語のバインディングを持ちだして、比較してみようと思います。
(1) Cバインディング
開発チームに「あれはメンテしてない」と言われてしまうCバインディングですが…monodevelopツリーの main/src/addins/CBinding にあり、gccベースのC/C++/Objective-C/Objective-C++のソースからなるデスクトップの実行ファイルあるいはライブラリのプロジェクトをビルドして実行できます。拡張子c/cpp/m/mmなどは言語バインディングCBindingに関連付けられます。テキストエディタには、基本的なコード補完などを実装したCTextEditorExtensionsが関連付けられます。また、ソースコードパーサーにもCDocumentParserが定義されています。これで、基本的なソースツリー解析が実現できています。他にも、GUI上の各種コマンドやオプション ダイアログの内容などが定義されています。プロジェクトのストックアイコンなども定義されます。.NETのMSBuildを使う言語ではないので、ビルド処理はCProjectで独自に行うよう定義されます。
このバインディングは、それなりに一般的な(割と高度なほうの)構成です。簡単な構文解析は行いますが、高度なシンボル解決は行わず、高度なリファクタリング機能は提供しません。
(2) md-haxebinding
これはHaXeの開発をMonoDevelopで行うためのバインディングで、機能的にはそれなりにシンプルです。HaXeとNMEのプロジェクトが定義されています(その中でビルド処理も定義されています)。割と興味深いのは、TextEditorExtensionとしてコード補完を実装できている点です。これは、内部的にhaxeのlanguage serviceをProcessとして実行して、その「サーバー」にコード補完候補を問い合わせているわけです。他の拡張ポイントの多くは、CBindingとかぶるので、説明は省略します。わたしも書きたくないし皆さんも読みたくないでしょう。
外部にlanguage serviceのサーバー プロセスを立ちあげてコード補完を行うアプローチは、実はF#でも採られています。ただ、fsharpbindingのmonodevelop版では、language serviceではProcessを立ちあげませんが(インプロセスで処理できるため)、emacs版では、fsiのサーバー プロセスを立ち上げるという、真逆の構成です(そうするしかないので)。
12/24訂正: emacs版プラグインは、シンタックス ハイライティングや自動インデントはやってくれてもコード補完まではやってくれなそうな感じです(emacs使いではないので未確認)。興味のある方は各自試してみてください。
(F#バインディングについても書くつもりだったのですが、くどくなりそうなので省略します。)
(3) C#バインディング
さて、ようやくC#バインディングです。C#はMSBuildタスクが使えるので、そこにプロジェクト種別を登録します。プロジェクト テンプレートやGUIの環境設定などはこれまでと同じです。
C#の言語サポートまわりで新しく登場するのは、まずReferenceFinderの定義です。"Find All References"を実装しています。
テキストエディタ拡張も、コード補完以外に、スマートインデント、使用シンボルのハイライティング(カーソル上のシンボルと同一のものはエディタ上でハイライトされます)、型・メンバーリストのツールバーが実装されています。
C#コード整形の定義もあります。コード整形にはさまざまなフォーマッティング ポリシーがあるので、それらも別途定義されています。たまにMonoDevelopのコードフォーマットが気に入らん!みたいな話を見かけますが、Visual Studio形式のフォーマッティング ポリシーも実はあるんですねー。
型定義情報の取得についても定義が いくつか ありますが、これは次に少し詳しく説明します。
C#については、解析済みツリー情報を活用したコード生成機能がいくつか定義されています。メンバーのオーバーライドを自動生成したり、自動的にプロパティを全部列挙するToString()のオーバーライドを生成したり、GetHashCode()なども自動的に面倒を見るかたちでEquals()を生成したり、といった機能が実装されています。(知っていました?)
エディタのコンテキスト アクションという機能も拡張されています。コンテキスト アクションは、カーソル位置のコンテキストによってリファクタリング操作などを追加するものです。例えばローカル変数の宣言で識別子のリファクタリング操作を開くと、「フィールドに変更」とか「宣言と代入を分離」などといったものが登場します。定義はCodeActionSourceがひとつだけですが、そこから列挙される機能が多すぎるので、設定画面のsshotを代わりに出しておきます。
NRefactoryを使うと、コード上で問題がありそうな部分を検出することもできますが、これも定義されています。ReSharperみたいですね。これも定義はCodeIssueSourceひとつだけですが、そのソースにはさまざまな問題検出が定義されていて、これも設定画面で調整できるので、そちらを見てください。
最後に、C#のエディタでは、カーソル位置のテキストに対してtooltipが表示されます。これはMonoDevelop coreの機能を使いまわすよう定義されていますが、そのtooltip providerはテキストエディタのresolver providerを使うようになっており、C#のresolver providerは別途定義されています。これで、コンテキストに応じてクラス定義やメソッド定義を簡潔に表示しているわけですね。
2013年1月5日追記: CSharpBindingのtooltip providerが、MonoDevelop coreの使い回しというのは正確ではありませんでした。このaddin.xmlで指定されているMonoDevelop.SourceEditor.LanguageItemTooltipProviderというクラスは、実はMonoDevelop.SourceEditor2.dll内部とCSharpBinding.dll内部の両方に存在していたのでした(!!!) 両方存在しているので、CSharpBinding.dllの中にある型が使用されていたというわけです。こんなの当然混乱するわ…!
…どうでしょう? どれも大まかにしか説明しなかったのですが、それでも量が多くて追い切れません。しかし、C#アドインにどんな機能があって、どの部分を拡張すれば期待通りの操作を追加できるのか、なんとなくフィーリングでわかってもらえたでしょうか。あるいは、MonoDevelopでNRefactoryをどのようにadaptしているかを、実際のコードを見てもらえると、NRefactoryを他のコーディング環境で使いまわす時にどう実装すればいいか、ひとつの参考になるのではないかと思います。そんな人はいないですかそうですか。
型情報の構築とTypeSystemParserの実装
さて、MonoDevelopアドインの説明の最後の話として、説明を先送りしていた型情報の構築について、ハイレベルの範囲で説明します(実装にはあまり踏み込まない)。
MonoDevelopのC#バインディングでは、ソースコード パーサーは2つのレベルで行われます。これらは相互に依存しません。
folding parserは、ソースコードの折り畳める部分を解析します。コメントとregionを解析してregionのブロックを拾いあげられれば終わりです。type system parserは、ソーステキスト全体を解析して、#ifブロックと、型やメンバーを解析したコンパイルユニット (ソースツリーのトップレベルノード)を構築します(type system parserのブロック解析とfolding parserのブロック解析は、かぶっていません。前者は#regionで、後者はプロジェクト設定などでスキップし、ツリー構築に影響する可能性のある#ifです)。ここで重要なのは明らかにtype system parserの方ですね。
type system parserを構築するには、本格的なソースコード パーサーが必要になります。というわけで、C#実装においては、当然のようにmcsのパーサー実装Mono.CSharpを活用しています。ただし、mcsのツリーをそのまま使うのではなく、NRefactoryで操作できるようなSyntaxTreeを構築して、さらにそれをもとにMonoDevelopが期待するParsedDocumentの実装を生成しています。返されるツリーは、まだこの時点では「未解決」です。つまり、識別子は型参照かもしれず、フィールド参照かもしれず、ローカル変数参照かもしれません。
型解決はNRefactoryに含まれるC#実装であるCSharpAstResolverが行なっていると思いますが、わたしはまだそこまで追いかけていないので、ソースツリー解析の話もこの辺までとしたいと思います。
おまけ: TypeScriptバインディング
さて、ここまで長々とC#アドインについて説明してきました。非常に膨大なアドインですが、全体像を何となく把握することは出来たのではないかと思います。せっかくなので、他の言語バインディングを実装してみたいと思いませんでしたか? 思いませんかそうですか。
というわけで、最後にちょっとだけ、わたしが試験的にいじっているTypeScriptバインディングの話を書こうと思います。ソースコードはこちらにあります。まだ使えませんが。
https://github.com/atsushieno/md-typescript
各エディタにおけるLanguage serviceのサポート状況
先にも言及しましたが、TypeScriptは最初からlanguage serviceをサポートしている言語です。そしてIDEのサポートがまだVisual Studioしかありません。さすがMicrosoft言語。TypeScriptの公開と同時に、MS Open Technologyからemacs, sublime text 2, viのプラグインが公開されましたが、これらはあくまでシンタックス ハイライティングしかしてくれませんでした。これではあまりおいしくありません。
LanguageServiceの実装は公開されているので、少しhackを加えればコード補完も実現できそうなものです。ここで日本の開発者が活躍します。まずvimにコード補完が実装されました。ほどなくしてemacsにコード補完が実装されました。そしてsublime text 2でも実装されました。日本人ばかりですね(国外でもこれらの後に実装を公開した人はいるようです)。
たぶんこの辺のプラグインの次の課題は、いかに「プロジェクト」を構築して複数ファイルにわたるソースをストレスなく編集できるようにするか、ということになるだろうなあと想像しています(使っていないので、実際どんな感じになっているかはちょっとわかりません)。そもそもtsc自体をビルドできるようになっているべきじゃないかなあと思うわけですが、Visual Studioアドインですら無いですよねコレ。
プロジェクトモデルのサポートとコード補完の方針模索
それはさておき、わたしもコード補完ができるエディタがほしいなあと思っていたので、10月にTypeScriptハッカソンに参加した時に、とりあえずMonoDevelopで使えるようにしてみようと思って、簡単なnodeベースのプロジェクトモデルを作ったのでした(結局その日はあんまりうまくいかなかったわけですが!)。ちなみにプロジェクトモデルの基本構造もよく分かっていなかったので、HaxeBindingをベースに構築しました。
MonoDevelopなら、プロジェクトモデルの構築は簡単で、エディタとシンタックスハイライティングやコード補完との繋ぎ込みも実装されていて難しいことはないだろう、と考えたわけですね。その結果が今回のエントリの内容ということになるわけですが!
プロジェクトモデルは何となく出来たので、language serviceを活用した機能の実装について考えようと思ったわけですが、nodeサーバーを立ててlanguage serviceをホストする方法は、Windowsだとちょっと嬉しくないわけです。面倒くさいので。むしろlanguage serviceのjavascriptをJurassic Javascriptエンジンなどを使って直接実行してしまえばいいのではないかと思いました。Jurassicならjsをevalした結果もObjectInstanceというかたちでC#から参照しやすいです。ちなみにこのJurassic、monoだとDynamicILInfoの実装が不完全で動かない(!)んですね。仕方なくSilverlightモードでビルドして動かしているのですが、tscのjsを動かすのに開発機でも30秒くらいかかって、このままじゃちょっと事前ロードしておかないと実用にならん感じではあります。
ts2cs: JavaScriptブリッジの自動生成
まあそれは措いておいて、TypeScriptのLanguageServiceをJurassic経由で生成して必要なオブジェクトを取得して…といったことを試していたのですが、language serviceがどんな機能をもっているかもよくわかっていない状態で片っ端からC#のコードでバインドメソッドを書いていて(たとえばこんな感じ)、これはきりがないということに早晩気づきました。自動生成したほうが早いよね?と思ったわけです。
んが、まだtypescript自体もlanguage serviceのAPIもtscの使い方もよく分かっていない状態でそんなものを書けるとは思えず(多分ですが現状のlangauge serviceのAPIでそこまでやるのはちょっと面倒)、とりあえず既存のtscのコードを書き換えて.jsの代わりに.csを生成するという荒業で、language serviceのAPIを俯瞰してコーディングできるようなmanaged dllを作ることにしました。一応出来たものはcodeplexのforkとして置いてありますが、tscとは全く異なる別物です。
http://typescript.codeplex.com/SourceControl/network/forks/atsushieno/ts2cs/
実際には、いったん最小限のC#スタブを作成しておいて、そのコンパイル済みdllを今度はC#でReflectionを使って本物のproxy実装のC#ソースを生成する、というややこしい段取りになっていますが、tsで型システムのAPIをいじるより、C#で実装したほうがはるかに楽なのでこうなっています。実際C#の方は基本部分が1日で出来ました(tsでやっていたら何日かかったことか…)。ともあれ、生成されたソースはコレになります。
このtypescriptのソースに直接手を入れるhackは非常に出来が悪く、typescriptの新しいバージョンが出たら(特にgenericsがサポートされたら)直ちに使い物にならなくなりますが、まあtscに手を加える作業も確か実質4日くらいでしたし、それまでにlanguage service APIをC#ベースで勉強してまともな実装をフルスクラッチで作っても良いだろうと思っています。
[12/20追記: オブジェクトの生成にかかるJavascriptブリッジの実装がおかしくてインスタンスが生成できていなかった問題を修正しました。]
TypeScriptブリッジをTypeSystemParserに組み込む
さてここからが本番だ…! と思ったわけですが、冒頭にも書きました通り、ちょっとマシントラブルでstuckしていたこともあって、ここまで辿りつけなかったんですね。そんなわけで、ここから先はまだ出来ていません。ゴメンナサイ。
ちなみに、TypeSystemParserに繋ぎ込むのは無理なんじゃないかという気もしています。コード補完なんかは、実のところtype system parserはなくてもいいんですね(HaxeBindingなどがやっているので、気になる方はそちらを見てください)。そこから先に手をつけるかもしれません。
最後に
ふぅ…やっと終わった。ここまで読まれた方はまずいないと思いますが、おつかれさまでした & ありがとうございました。長くなりましたがわたしの参加記事は以上です。明日はyone64さんです。
*1:あ、emacs vs. viだなんて誰も言っていませんよ
*2:MEFみたいなものですが、mono-addinsははるかに前から存在しています。System.Addinsとも違います
*3:まあこの文章も長いし、この辺まで来ると読んでいる人も片手で足りるくらいですよね