スワッガヌを曞き、埌悔しない方法

画像

か぀お、私の同僚はバックログタスクに陥りたした。「内郚REST-apiずの察話を敎理しお、契玄の倉曎がすぐにコンパむル゚ラヌに぀ながるようにしたす」。 䜕がもっず簡単だろうか -しかし、結果のサボテンを䜿っお䜜業するこずで、.Net Coreに移行しお䞭間アセンブラの手動コヌド生成ず新しいコンパむラの孊習を行う前に、ドキュメントを吞うこずを䜙儀なくされたした。 C 個人的には、ランタむムずコンパむラヌ自䜓の構造の䞡方で、倚くの興味深いこずを発芋したした。 ハブロフスクの䜏民がすでに知っおいるこずもあれば、思考に圹立぀食べ物もあるず思いたす。

第1幕コピヌペヌスト


これは普通の兞型的なタスクであり、私の友人はすでに明らかなこずに぀いお長い間考えたがらなかったため、結果は非垞に早く珟れたした。 WCFではRESTサヌビスが私たちのものであったため、サヌビスむンタヌフェヌスが移行される䞀般的なアセンブリMyProj.Abstracitonsが導入されたした。 その䞭で、サヌビスむンタヌフェヌスを実装し、リク゚ストをプロキシし、結果をデシリアラむズするクラスに曞き蟌む必芁がありたした。 アむデアは単玔でした。同じむンタヌフェむスを実装するクラむアントでそれぞれのサヌビスを蚘述するず、サヌビスのメ゜ッドを倉曎するずすぐに、コンパむル゚ラヌがポップアップしたす。 そしお、関数の匕数を倉曎する人は、それが正しくシリアル化されおいるこずを確認するず仮定したす。 次のようになりたした。


 public class FooClient : BaseClient<IFooService> { private static readonly Uri _baseSubUri public FooClient() : base(BaseUri, _baseSubUri, LogManager.GetCurrentClassLogger()) {} [MethodImpl(MethodImplOptions.NoInlining)] public Task<Foo> GetFoo(int a, DateTime b, double c) { return GetFoo<Foo>(new Dictionary<string, object>{ {“a”, a}, {“b”, b.ToString(SerializationConstant.DateTimeFormat)}}, new Dictionary<string, object>{ {“c”, c.ToString(SerializationConstant.FloatFormat)}}); } } 

BaseClient<TService>は、 HttpClientような薄いラッパヌで、呌び出すメ゜ッドこの堎合はGetFoo を決定し、そのURLを蚈算し、リク゚ストを送信し、回答をGetFooし、結果をデシリアラむズ必芁に応じおしお枡したす。


それは



原則ずしお、難しくはなく、 NoInliningたが、30幎生の20番目のメ゜ッドを曞いた埌、たったく同じタむプでしたが、人々は垞にNoInliningを曞くのを忘れおいNoInlining 。そのため、すべおが壊れたした小さなクむズ1なぜだず思いたすか私は自分自身に質問をしたした「どうにかしお人間にこれに近づくこずは可胜ですか」。 しかし、タスクは既にマスタヌにマヌゞされおおり、䞊から「ゎミを出さずに機胜を飲んだ」ず蚀われたした。 しかし、私はあらゆる皮類のラッパヌを曞くのに1日3時間を費やすずいう考えが奜きではありたせんでした。 䞀連の属性は蚀うたでもありたせんが、人々は定期的にシリアル化を自分の倉曎やそのような痛みず同期させるこずを忘れおいたした。 したがっお、次の週末に到達し、状況を䜕らかの圢で改善するために着手し、数日間、圌は代替゜リュヌションをスケッチしたした。


第2幕反射


ここでのアむデアはさらにシンプルでした。同じこずをするこずを私たちが劚げおいるのは、手ではなく動的に生成しおいるのでしょうか 完党に同䞀のタスクがありたす。入力匕数を取埗し、それらをqueryString匕数甚の2぀の蟞曞に倉換し、残りを芁求本䜓ぞの匕数ずしお倉換し、これらのパラメヌタヌで暙準のHttpClientを呌び出すだけです。 その結果、同じSerializationConstantすべおの問題は、このハンドラヌで1回しか蚘述されなかったずいう事実によっお解決され、1回正しく実装でき、垞に正しい結果が埗られたした。 それほど長くない喫煙文曞ずスタックオヌバヌフロヌの埌、MVPは準備ができおいたした。


ここで、サヌビスを䜿甚するには、次のようにしたす。


  1. むンタヌフェヌスを䜜成する

     public interface ISampleClient : ISampleService, IDisposable { } 
  2. 私たちは小さなラッパヌを曞いおいたすさらなる䜿甚の䟿宜䞊。

     public static ISampleClient New(Uri baseUri, TimeSpan? timeout = null) { return BaseUriClient<ISampleClient>.New(baseUri, Constant.ServiceSampleUri, timeout); } 
  3. 私たちは䜿甚したす


     [Fact] public async Task TestHelloAsync() { var manager = new ServiceManager(); manager.RunAll(BaseAddress); using (var client = SampleClient.New(BaseAddress)) { var hello = await client.GetHello(); Assert.Equal(hello, "Hello"); } manager.CloseAll(); } 


免責事項

もちろん、このテストでは、実際の芁求を行う実際のWCFサヌビスが発生するため、厳密に蚀えばこれは単䜓テストではありたせん。 しかし、私たちは皆、私たちの過ちから孊びたす。今、私は䞭毒を閉じ蟌めお、すべおを異なるやり方でしたすが、その時、私はただ方法を知りたせんでした。


すべおが非垞に単玔で、理解できるように、特別なクラスの継承や属性の付加などの特別な魔法は必芁ありたせん。 倉数ずメ゜ッド名は自動的に衚瀺されたす。 䞀般的に、矎しさ。 さらに、毎回サヌビスの名前で定数文字列を指定するのが面倒ではない堎合、項目2​​は省略できたす。


どのように機胜したすか 実際、十分な黒魔術で。 プロキシメ゜ッドを生成する䞻な郚分は次のずおりです。


 private static void ImplementMethod(TypeBuilder tb, MethodInfo interfaceMethod) { var wcfOperationDescriptor = ReflectionHelper.GetUriTemplate(interfaceMethod); var parameters = GetLamdaParameters(interfaceMethod); var newDict = Expression.New(typeof(Dictionary<string, object>)); var uriDict = Expression.Variable(newDict.Type); //    queryString var bodyDict = Expression.Variable(newDict.Type); //       var wcfRequest = Expression.Variable(typeof(IWcfRequest)); var dictionaryAdd = newDict.Type.GetMethod("Add"); var body = new List<Expression>(parameters.Length) //      var dict = new Dictionary<...> { Expression.Assign(uriDict, newDict), Expression.Assign(bodyDict, newDict) }; for (int i = 1; i < parameters.Length; i++) { var dictToAdd = wcfOperationDescriptor.UriTemplate.Contains("{" + parameters[i].Name + "}") ? uriDict : bodyDict; //    ,    uri ,        body.Add(Expression.Call(dictToAdd, dictionaryAdd, Expression.Constant(parameters[i].Name, typeof(string)), Expression.Convert(parameters[i], typeof(object)))); //      } var wcfRequestType = ReflectionHelper.GetPropertyInterfaceImplementation<IWcfRequest>(); //    ,     T,      var wcfProps = wcfRequestType.GetProperties(); var memberInit = Expression.MemberInit(Expression.New(wcfRequestType), Expression.Bind(Array.Find(wcfProps, info => info.Name == "Descriptor"), GetCreateDesriptorExpression(wcfOperationDescriptor)), Expression.Bind(Array.Find(wcfProps, info => info.Name == "QueryStringParameters"), Expression.Convert(uriDict, typeof(IReadOnlyDictionary<string, object>))), Expression.Bind(Array.Find(wcfProps, info => info.Name == "BodyPrameters"), Expression.Convert(bodyDict, typeof(IReadOnlyDictionary<string, object>)))); body.Add(Expression.Assign(wcfRequest, Expression.Convert(memberInit, wcfRequest.Type))); var requestMethod = GetRequestMethod(interfaceMethod); //   (GetResult  Execute),      body.Add(Expression.Call(Expression.Field(parameters[0], "Processor"), requestMethod, wcfRequest)); var bodyExpression = Expression.Lambda ( Expression.Block(new[] { uriDict, bodyDict, wcfRequest }, body.ToArray()), parameters ); var implementation = bodyExpression.CompileToInstanceMethod(tb, interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual); //      tb.DefineMethodOverride(implementation, interfaceMethod); } 

小さなクむズ2

行c ReflectionHelper.GetPropertyInterfaceImplementation<IWcfRequest>()泚意しおください。 なぜそれが必芁だず思いたすか リフレクションのリフレクションは、単に曞くのではなく、自分が望むものを生成するコヌドを曞く方が面癜いですか


ここでの䞻なポむントは、匏を䜿甚しおメ゜ッドの本䜓を生成し、すべおの匕数を本䜓たたはqueryStringのいずれかに配眮し、CompileToInstanceMethod拡匵機胜を䜿甚しおデリゲヌトにではなく、すぐにクラスメ゜ッドにコンパむルするこずです。 これはそれほど難しいこずではありたせんが、䜜業バヌゞョンが埗られるたで数十回の反埩が行われ、正しいバヌゞョンが結晶化されたした。


 internal static class XLambdaExpression { public static MethodInfo CompileToInstanceMethod(this LambdaExpression expression, TypeBuilder tb, string methodName) { var paramTypes = expression.Parameters.Select(x => x.Type).ToArray(); var proxyParamTypes = new Type[paramTypes.Length - 1]; Array.Copy(paramTypes, 1, proxyParamTypes, 0, proxyParamTypes.Length); var proxy = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, expression.ReturnType, proxyParamTypes); var method = tb.DefineMethod($"<{proxy.Name}>__Implementation", MethodAttributes.Private | MethodAttributes.Static, proxy.ReturnType, paramTypes); expression.CompileToMethod(method); proxy.GetILGenerator().EmitCallWithParams(method, paramTypes.Length); return proxy; } } 

最も悲しいこずは、これが比范的読みやすいオプションであり、CompileToMethodアプレットがそこから削陀されたため、Coreに移動した埌は攟棄しなければならなかったこずです。 その結果、匿名デリゲヌトを生成できたすが、クラスメ゜ッドは生成できたせん。 そしお、これが必芁なものです。 したがっお、牛のバヌゞョンでは、これはすべお叀いものに眮き換えられたす いいね ILGenerator。 この堎合の兞型的なトリックは、Cコヌドを蚘述し、ildasmで解析し、それがどのように機胜するかを確認するこずです。ここで、䞀般的なケヌスをカバヌするために埮調敎する必芁がありたす。 ILを自分で蚘述しようずするず、99のケヌスで、 共通蚀語ランタむムが無効なプログラムを怜出するこずができたす:)゚ラヌ。 しかし、この堎合、最終的なコヌドは、比范的読みやすい匏よりも理解するのがはるかに困難です。


このCookieを暹皮から取り出す問題に぀いおは、 ここで説明したす リストの最初の項目に興味がありたすが、芁求はかなり死んでいるように芋えたす。 しかし、すべおがそれほど悪いわけではありたせん。さらに良い解決策が芋぀かったからです


第3幕コンパむラヌのカバヌ䞋


画像

この党䜓を100回曞き盎し、デバッグした埌、コンパむルの段階でこれをすべおできないのはなぜかず思いたした。 はい、生成されたタむプのオヌバヌヘッドをキャッシュするこずで、クラむアントの䜿甚はごくわずかです。Activator.CreateInstanceを呌び出すだけで枈みたす。これは、特にシングルトンずしお䜿甚できるため、HTTPリク゚スト党䜓を䜜成するコンテキストでは些现なこずです。 サヌビスURL以倖の状態はありたせん。 それでも、ここには適切な制限がありたす。


  1. 生成されたコヌドを芋お把握するこずはできたせん。 原則ずしお、これは必芁ありたせん。 それは原始的ですが、最終的な䜜業コヌドを䜜成するたで、なぜ意図したずおりに機胜しないのかに぀いお倚くを掚枬する必芁がありたした。 結論ただ楜しみながら動的ビルドをデバッグする
  2. クラむアントは垞にクラむアントず同じむンタヌフェヌスを持っおいる必芁がありたす。 い぀䞍快ですか たずえば、サヌバヌに同期アプリケヌションがあるが、クラむアントではHTTP芁求であるため、非同期である必芁がありたす。 したがっお、ストリヌムをブロックしお応答を埅぀か、すべおのサヌバヌメ゜ッドを非同期にする必芁がありたす。必芁のない堎合でも、サヌビスに匷制的にTask.FromResultを配眮させたす。
  3. 実行時にリフレクションを取り陀くこずは垞に玠晎らしいこずです

ちょうどその時、Roslynに぀いお興味深いこずをたくさん聞きたした。Roslyn-Microsoftの新しいモゞュラヌコンパむラで、プロセスをより深く掘り䞋げるこずができたす。 圓初、LLVMのように、目的の倉換甚のミドルりェアを簡単に䜜成できるこずを本圓に期埅しおいたしたが、ドキュメントを読んだ埌、Roslynで䞍芁なゞェスチャヌなしで本栌的なコヌド生成を行うこずはできないずいう印象を受けたしたこれはLINQをloopに眮き換えるプロゞェクトで行われたすが、明らかな理由であたり䟿利ではありたせん、たたは「ここでコンマを忘れおしたったので、挿入しおください」ずいうスタむルのアナラむザヌです。 そしお、私はこのトピックの蚀語リポゞトリをgithubにする興味深い機胜リク゚ストに遭遇したした tyts が、その埌、2぀の問題がすぐに珟れたした最初に、この機胜の非垞に長いリリヌス前に、そしお次に、圌らはそれが䜜業フォヌムは、私には䜕の助けにもなりたせん。 すべおがそれほど悪くはありたせんでしたが、コメントで私が必芁なこずをしおいるように芋える興味深いプロゞェクトぞのリンクを私に䞎えおくれたからです。


数日掘り進めお基本的なプロゞェクトをマスタヌした埌、私はそれがうたくいくこずに気付きたした そしお、それは本来どおりに機胜したす。 魔法のようなものです。 通垞のコンパむラヌの䞊に独自のコンパむラヌを䜜成するのずは異なり、ここでは、゜リュヌションに簡単にプラグむンできる通垞のnugetパッケヌゞを䜜成したす。ビルド䞭に、汚い䜜業を行いたす。この堎合、サヌビスのクラむアントコヌドを生成したす。 スタゞオず完党に統合されおいるため、䜕もする必芁はありたせん-ナンセンス。 確かに、゜リュヌションの最初のむンストヌル埌のバックラむトは機胜したせんが、゜リュヌションの再構築ず再発芋埌は、バックラむトずIntelliSenseの䞡方が機胜したす 確かに、すべおが機胜するわけではありたせん。たずえば、<inheritdoc />を䜿甚しおむンタヌフェむスから拡匵ドキュメントを衚瀺する方法はただわかりたせん。䜕らかの理由で、スタゞオはこれを実行したくないだけです。 さお、倧䞈倫、䞻なこずは完了です-クラスが生成され、それらが動䜜し、生成の結果は垞にナゲットを介しおワンクリックで蚭定され、スパむされ修正されたす。 望みどおりのすべお。


ナヌザヌの堎合、䜿甚方法は次のようになりたす。


画像


むンタヌフェヌスを蚘述し、いく぀かの属性をハングアップし、コンパむルするだけで、生成されたクラスを䜿甚できたす。 PostSharpは必芁ありたせん 冗談。


それでは、どのように機胜したすか


第4幕決勝


圓初、私は深く行く぀もりはありたせんでした、なぜなら すでに私の芁件を完党に満たす完成したラむブラリがありたしたが、アナラむザを䜜成しおパッケヌゞを䜜成するだけでした。 しかし、提䟛されたAPIの䞍適切な䜿甚、たたはラむブラリ自䜓の゚ラヌたたは欠点のために、珟実はより深刻で、゚ラヌをキャッチするこずが刀明したしたが、避けられない報埩は䟝然ずしお私を远い越したした。 最終的にすべおが䞊の写真のように起動するように、私はそれを把握し、密茞しなければなりたせんでした。


実際、ほずんどすべおの゜ルトは新しい.Net Coreツヌルチェヌンにありたす。


 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <PackageType>DotnetCliTool</PackageType> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp1.0</TargetFramework> <AssemblyName>dotnet-codegen</AssemblyName> </PropertyGroup> </Project> 

本質的に、これはプロゞェクトを構築するずきにミドルりェアを決定する方法です。 その埌、コンパむラはdotnet-codegenが䜕であるかを理解し、呌び出すこずができたす。 プロゞェクトをビルドするず、次のように衚瀺されたす。


画像

ビルドをクリックしたずきたたは単にファむルを保存したずきでも


  1. CodeGeneration.Roslyn.TasksアセンブリからGenerateCodeFromAttributesがありたす。これは、 Microsoft.Build.Utilities.ToolTaskを継承し、プロゞェクトのアセンブリ䞭にこれらすべおの開始を決定したす。 実際、出力りィンドりでこのタスクの䜜業が少し高くなっおいたす。
  2. テキストファむルCodeGeneration.Roslyn.InputAssemblies.txtたす。 CodeGeneration.Roslyn.InputAssemblies.txt 、収集するアセンブリぞのフルパスが曞き蟌たれたす。
  3. CodeGeneration.Roslyn.ToolがCodeGeneration.Roslyn.Tool 、分析甚のファむルのリスト、入力アセンブリなどを取埗したす。 䞀般的に、䜜業に必芁なすべおのもの。
  4. それでは、すべおが簡単です。プロゞェクト内のICodeGeneratorむンタヌフェむスのすべおの子孫を芋぀け、コヌドが生成する唯䞀のGenerateAsyncメ゜ッドを呌び出したす。
  5. コンパむラは、objディレクトリから生成された新しいファむルを自動的に取埗し、結果のアセンブリに远加したす

その結果、このラむブラリの珟圚のバヌゞョンでは、䞀郚のクラスに属性を掛けお、文字通り100行のコヌドを蚘述できたす。これに基づいお、必芁なすべおが生成されたす。 別のアセンブリのクラスを生成できないずいう制限がありたす。぀たり、生成されたクラスは垞にコンパむルする同じアセンブリに远加されたすが、原則ずしおそれず共存できたす。


远加の行為芁玄


このラむブラリを曞いたずき、誰かに圹立぀ず期埅しおいたしたが、それでややがっかりしたした。 Swaggerは同じタスクを実行したすが、同時にクロスプラットフォヌムであり、䟿利なむンタヌフェヌスを備えおいたす。 しかし、それにもかかわらず、私の堎合は、単にタむプを倉曎し、ファむルを保存しおすぐにコンパむル゚ラヌを取埗できたす。 すべおが開始されたもの


画像


最埌に倧事なこずを蚀い忘れたしたが、私はこのすべおを実装するこずで倚くの喜びを埗たした。たた、私には思えるように、蚀語ずコンパむラに぀いおの十分な知識を埗たした。 したがっお、私は蚘事を曞くこずにしたした倚分䞖界は新しいスワガヌを必芁ずしないかもしれたせんが、コヌド生成が必芁な堎合、T4は軜spするかあなたに合わず、反射は私たちのオプションではありたせん、そしおここに玠晎らしい仕事をする玠晎らしいツヌルがありたす、玠晎らしい珟圚のパむプラむンに統合され、その結果、nougatパッケヌゞのように広がりたす。 はい、スタゞオからのバックラむトが含たれおいたす ただし、゜リュヌションの第1䞖代ず再発芋埌のみ。


私は、このプロセスをコアフレヌムワヌクではない倧人のフレヌムワヌクで詊したこずはないので、すぐに蚀う必芁がありたす。 ただし、このパッケヌゞのタヌゲットには、 portable-net45+win8+wpa81 、 portable-net4+win8+wpa81 、さらにはnet20が含たれるこずを考慮するず、特別な問題はありたせん。 そこに䜕か、䜙分な䟝存関係、 NIHが気に入らない堎合でも、独自の、よりコヌシャのある実装をい぀でも䜜成できたす。コヌドには倚くのメリットがありたす。 別の萜ずし穎はデバッグです。私はこのすべおをデバッグする方法を理解しおいたせんでした。コヌドは盲目的に曞かれたした。 しかし、ネむティブCodeGeneration.RoslynラむブラリのCodeGeneration.Roslyn者は、プロゞェクトの構造を芋るだけで間違いなく必芁な知識を持っおいたすが、最終的にはそれらを䜿甚したせんでした。


そしお今、私は明確な良心をもっお蚀うこずができたす私は私が別のsw歩を曞いたこずを党く埌悔しおいたせん。


参照



私のプロゞェクトはすべおMITラむセンス、fork-study-breakの䞋にありたすが、私は苊情はありたせん:)


圓初、これらはすべお完党に機胜するプロゞェクトずしお蚈画されおいたしたが、実際の芁件の結果ずしお珟れたため、最悪の堎合、マむナヌなドピルカの埌、すべおを実皌働で䜿甚できたす。


もちろん、質問に察する答えは次のずおりです。


  1. MethodImplOptions.NoInliningは、呌び出す必芁があるメ゜ッドの名前を決定するために䜿甚されたす。 なぜなら ほずんどのメ゜ッドは非垞に単玔であるため、倚くは文字通り単䞀行であり、コンパむラヌはそれらをむンラむン化するこずを奜みたす。 ご存知のように、コンパむラヌは32バむト未満の本䜓でメ゜ッドをむンラむン化したすただ倚くの条件がありたすが、これに焊点を圓おるこずはありたせん。それらはすべおここで満たされたした。したがっお、倚数の匕数を持぀メ゜ッドが正垞に呌び出されるずいう面癜いバグを芋るこずができたす-実行時に゚ラヌをスロヌする 適切なメ゜ッドが芋぀からず、コヌルスタックの最䞊郚に到達したす。

      MethodBase method = null; for (var i = 0; i < MAX_STACKFRAME_NESTING; i++) { var tempMethod = new StackFrame(i).GetMethod(); if (typeof(TService).IsAssignableFrom(tempMethod.DeclaringType)) { method = tempMethod; break; } } 
  2. 実際のずころ、reflexメ゜ッドを䜜成したずき、珟圚のRemoteClient.Coreアセンブリではなく、動的に䜜成されたクラスにクラスを远加しおいるずは本圓に思っおいたせんでした。 これは非垞に重芁です。 その結果、すべおの機胜をテストし、これらすべおが機胜するずいう自信を獲埗した埌、 WcfRequestクラスがパブリックであるこずがWcfRequest 。 「障害」は、「実装はプラむベヌトであり、むンタヌフェむスのみが衚瀺されるべきだ」ず考えたした。 そしお、内郚属性を蚭定したす。 そしお、それはすべお壊れたした。 理由は簡単です。芪アセンブリA.dll内郚クラスをむンスタンス化しようずするアセンブリA.Dynamicalygenerated.dllを生成し、アクセス゚ラヌで自然にクラッシュしたす。 さお、これは、アセンブリ間に䞍愉快な埪環䟝存を埗るずいう事実を数えおいたせん。 その結果、すべおのプロパティにセッタヌを単玔に远加する「ダミヌクラス」の動的生成は、かなり単玔な゜リュヌションであるこずが刀明し、同時に、生成されたアセンブリに぀いおA.dll盎接䟝存し、テヌルを持たないずいう意味で䟿利になりたした。裏偎。

Source: https://habr.com/ru/post/J342566/


All Articles