kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

カテゴリーアーカイブ: .NET Framework

.NET Core 1.1 で msbuild はどうなったのか


一度は nuget統合がらみで project.json とかに移行するという形でdiscon言われた msbuild ですが、結局息を吹き返し、Visual Studio 2017 RC で .NET Core App (ASP.NET vNext 改め ASP.NET Core でのWebアプリを含む)のビルドランナーとして復活を果たしました。

この msbuild について、ビルドのカスタム手法について諸々調べてみましたので blog にまとめてみます。

デフォルトの csproj からの targets / props の読み込み

実際にプロジェクトを作って csproj を開いてもらうと解りますが、msbuild の Import による targets の取り込みがありません。過去の知識だけを頼りに調べようとすると一体何が起こってるのやらで即詰みしますね。

いつ増えたんだ!って感じですが、 msbuild に /pp:出力ファイル名 で import によるファイルインクルードの解決結果を出力するオプションがあるのでこれを使って標準ターゲットを取り込んだ XML を生成すると、その冒頭で回答が出てました。

<?xml version=”1.0″ encoding=”utf-8″?>
<!–
==========<snip>===============================================
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\WebApplication2.csproj
==========<snip>===============================================
–>
<Project ToolsVersion=”15.0″ DefaultTargets=”Build”>
  <!–
==========<snip>===============================================
  <Import Project=”Sdk.props” Sdk=”Microsoft.NET.Sdk.Web”>
  This import was added implicitly because of the Project element’s Sdk attribute specified “Microsoft.NET.Sdk.Web”.

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

==========<snip>===============================================
–>

csproj ファイルの冒頭、Sdk アトリビュートによって、Sdk.props の読み込みが行われていて、Sdk.props は C:\Program Files\dotnet\sdk\… にあるという事でした。

実際の csproj での Project 要素のSdkアトリビュートは以下のようになっています。

<Project ToolsVersion=”15.0″ Sdk=”Microsoft.NET.Sdk.Web”>

この Sdk 属性により関連 props が読み取られます。

この Sdk.props は以下のように終わります。

  <!–

==========<snip>===============================================

  </Import>

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

==========<snip>===============================================

==========<snip>===============================================
  </Import>

C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\WebApplication2.csproj
==========<snip>===============================================
–>

続いて csproj での定義内容が現れ、

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <PropertyGroup>
    <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
  </PropertyGroup>
  <PropertyGroup>
    <UserSecretsId>aspnet-WebApplication2-898cd849-2e88-466a-81b4-e1d20363bffb</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include=”Microsoft.ApplicationInsights.AspNetCore” Version=”2.0.0-beta1″ />
    <PackageReference Include=”Microsoft.AspNetCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Authentication.Cookies” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Identity.EntityFrameworkCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Mvc” Version=”1.1.1″ />
    <PackageReference Include=”Microsoft.AspNetCore.StaticFiles” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.Design” Version=”1.1.0″ PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.SqlServer” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.SqlServer.Design” Version=”1.1.0″ PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.Tools” Version=”1.1.0-msbuild3-final” PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.Extensions.Configuration.UserSecrets” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.Extensions.Logging.Debug” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.VisualStudio.Web.CodeGeneration.Design” Version=”1.1.0-msbuild3-final” PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.VisualStudio.Web.BrowserLink” Version=”1.1.0″ />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include=”Microsoft.EntityFrameworkCore.Tools.DotNet” Version=”1.0.0-msbuild3-final” />
    <DotNetCliToolReference Include=”Microsoft.Extensions.SecretManager.Tools” Version=”1.0.0-msbuild3-final” />
    <DotNetCliToolReference Include=”Microsoft.VisualStudio.Web.CodeGeneration.Tools” Version=”1.0.0-msbuild3-final” />
  </ItemGroup>

csproj の Project 要素内が展開された続きが以下です。

  <!–
==========<snip>===============================================
  <Import Project=”Sdk.targets” Sdk=”Microsoft.NET.Sdk.Web”>
  This import was added implicitly because of the Project element’s Sdk attribute specified “Microsoft.NET.Sdk.Web”.

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets
==========<snip>===============================================
–>
  <!–
***********************************************************************************************
Sdk.targets

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
–>

csproj の Project 要素の末尾に Sdk.targets が暗黙に展開されるようになっています、これが Sdk アトリビュートの仕組みという事ですね。 Project 要素に Sdk アトリビュートを付けると先頭に props 、末尾にtargets が挿入される、そういう仕組みです。

SDK として配布展開される props / targets の参照方法としては非常にシンプルで良いと思います。

csproj.user はどうなった

プリプロセス結果から Import Project を拾ってみた結果は以下です。

長いですけど、結果的に csproj.user の読み込みは無くなってます。

途中赤字にしてるのがプロジェクト配下の props / targets の読み込みでビルドのカスタム化という点で言えば有望株です。

<Import Project=”Sdk.props” Sdk=”Microsoft.NET.Sdk.Web”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.props

<Import Project=”Microsoft.NET.Sdk.DefaultItems.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.DefaultItems.props

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.CSharp.props” Condition=”‘$(MSBuildProjectExtension)’ == ‘.csproj'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.CSharp.props

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.props

<Import Project=”$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props” Condition=”‘$(ImportProjectExtensionProps)’ == ‘true’ and exists(‘$(MSBuildProjectExtensionsPath)’)”>
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\WebApplication2.csproj.nuget.g.props

<Import Project=”$(NuGetPackageRoot)microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props” Condition=”Exists(‘$(NuGetPackageRoot)microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props’)”>
C:\Users\kazuk\.nuget\packages\microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props

<Import Project=”Sdk.targets” Sdk=”Microsoft.NET.Sdk.Web”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.BeforeCommon.targets” Condition=”‘$(IsCrossTargetingBuild)’ != ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.BeforeCommon.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.DefaultOutputPaths.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.DefaultOutputPaths.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.TargetFrameworkInference.targets” Condition=”‘$(TargetFramework)’ != ””>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.TargetFrameworkInference.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.RuntimeIdentifierInference.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.RuntimeIdentifierInference.targets

<Import Project=”$(LanguageTargets)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.CSharp.targets

<Import Project=”$(CSharpTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.CSharp.CurrentVersion.targets

<Import Project=”$(CSharpCoreTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Roslyn\Microsoft.CSharp.Core.targets

<Import Project=”Microsoft.Common.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.Common.targets

<Import Project=”$(CommonTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.Common.CurrentVersion.targets

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter\*” Condition=”‘$(ImportByWildcardAfterMicrosoftCommonTargets)’ == ‘true’ and exists(‘$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.targets\ImportAfter\Microsoft.NuGet.ImportAfter.targets

<Import Project=”$(NuGetRestoreTargets)” Condition=”Exists(‘$(NuGetRestoreTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\NuGet.targets

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter\*” Condition=”‘$(ImportByWildcardAfterMicrosoftCommonTargets)’ == ‘true’ and exists(‘$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.targets\ImportAfter\Microsoft.TestPlatform.ImportAfter.targets

<Import Project=”$(VSTestTargets)” Condition=”Exists(‘$(VSTestTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.TestPlatform.targets

<Import Project=”$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets” Condition=”‘$(ImportProjectExtensionTargets)’ == ‘true’ and exists(‘$(MSBuildProjectExtensionsPath)’)”>
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\WebApplication2.csproj.nuget.g.targets

<Import Project=”$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets” Condition=”Exists(‘$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets’)”>
C:\Users\kazuk\.nuget\packages\microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.targets” Condition=”‘$(IsCrossTargetingBuild)’ != ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.targets

<Import Project=”Microsoft.NET.Sdk.Common.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.Common.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.PackageDependencyResolution.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)Microsoft.PackageDependencyResolution.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.PackageDependencyResolution.targets

<Import Project=”Microsoft.NET.Sdk.DefaultItems.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.DefaultItems.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.DisableStandardFrameworkResolution.targets” Condition=”‘$(DisableStandardFrameworkResolution)’ == ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.DisableStandardFrameworkResolution.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.GenerateAssemblyInfo.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.GenerateAssemblyInfo.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Publish.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Publish.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.PreserveCompilationContext.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.PreserveCompilationContext.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.CSharp.targets” Condition=”‘$(Language)’ == ‘C#'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.CSharp.targets

<Import Project=”$(NuGetBuildTasksPackTargets)” Condition=”Exists(‘$(NuGetBuildTasksPackTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\NuGet.Build.Tasks.Pack\build\NuGet.Build.Tasks.Pack.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets

<Import Project=”$(WebPublishProfileFile)” Condition=”Exists(‘$(WebPublishProfileFile)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\PublishProfiles\DefaultProfile.pubxml

<Import Project=”$(_ComputeTargetsDir)Microsoft.NET.Sdk.Publish.ComputeFiles.targets” Condition=”Exists(‘$(_ComputeTargetsDir)Microsoft.NET.Sdk.Publish.ComputeFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\ComputeTargets\Microsoft.NET.Sdk.Publish.ComputeFiles.targets

<Import Project=”$(_CopyTargetsDir)Microsoft.NET.Sdk.Publish.CopyFiles.targets” Condition=”Exists(‘$(_CopyTargetsDir)Microsoft.NET.Sdk.Publish.CopyFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\CopyTargets\Microsoft.NET.Sdk.Publish.CopyFiles.targets

<Import Project=”$(_TransformTargetsDir)Microsoft.NET.Sdk.Publish.TransformFiles.targets” Condition=”Exists(‘$(_TransformTargetsDir)Microsoft.NET.Sdk.Publish.TransformFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\TransformTargets\Microsoft.NET.Sdk.Publish.TransformFiles.targets

<Import Project=”$(_PublishTargetsDir)Microsoft.NET.Sdk.Publish.$(PublishProtocol).targets” Condition=”Exists(‘$(_PublishTargetsDir)Microsoft.NET.Sdk.Publish.$(PublishProtocol).targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\PublishTargets\Microsoft.NET.Sdk.Publish.FileSystem.targets

<Import Project=”$(_DotNetCLIToolTargetsDir)Microsoft.NET.Sdk.DotNetCLITool.targets” Condition=”Exists(‘$(_DotNetCLIToolTargetsDir)Microsoft.NET.Sdk.DotNetCLITool.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\DotNetCLIToolTargets\Microsoft.NET.Sdk.DotNetCLITool.targets

さて、有望株と言える二つに注目しましょう。

$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props と $(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets を読み込んでいます。

dotnet build /v:diag

での出力から、MSBuildProjectExtensionsPath は Project の obj 配下となります。

MSBuildProjectExtensionsPath = C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\

よりによって obj かよ!って事でこれは残念。NuGet から展開された props / targets を実行するのに使ってるみたいです。

ですが、それの近くに Condition がマッチせずに Import されなかった

<Import Project=”$(DirectoryBuildPropsPath)” Condition=”‘$(ImportDirectoryBuildProps)’ == ‘true’ and exists(‘$(DirectoryBuildPropsPath)’)”>

<Import Project=”$(DirectoryBuildTargetsPath)” Condition=”‘$(ImportDirectoryBuildTargets)’ == ‘true’ and exists(‘$(DirectoryBuildTargetsPath)’)” />

な Import があります。

DirectoryBuildPropsPath はプロジェクトディレクトリの Directory.Build.props、DirectoryBuildTargetsPathはプロジェクトディレクトリの Directory.Build.targets があれば読み込むようになっています。

やってみた

Directory.build.targets をプロジェクト配下に作成します。

<?xml version=”1.0″ encoding=”utf-8″ ?>
<Project>
  <Target Name=”mySampleTarget” AfterTargets=”Build” >
    <Message Text=”mySampleTarget running” Importance=”High”/>
  </Target>
</Project>

dotnet build でビルド実行した結果、定義したターゲットが実行されました。

C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2> dotnet build
Microsoft (R) Build Engine version 15.1.523.56541
Copyright (C) Microsoft Corporation. All rights reserved.   WebApplication2 -> C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\bin\Debug\netcoreapp1.1\WebApplication2.dll   mySampleTarget running C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2>

Visual Studio でビルドしてもいけるよね?

いけるみたいです。上記でのメッセージはビルドの出力ウインドウに出ました。

まとめ

遠回りはした気もしないでもないが、 Directory.build.props / Directory.build.targets をプロジェクトディレクトリに置くだけで msbuild でのビルドプロセスはカスタマイズできます。

その昔のビルドカスタマイズポイントだった csproj.user とかはどうやら無くなってます。

なんかカスタマイズとかしたのに読まれてなくない?とか一体どこのファイルがコレやってるのとかその辺を調べるのには /pp でプリプロセス結果吐かせると便利ですね。(このファイル、Import の解決した結果を完全に吐いてくれるので別環境でビルドするには便利かもしれません。)

あとは /v:diag してログ眺めれば不思議な事なんてない(震え声

広告

async/await はTaskには依存してないです


(追記)mataliloさんに await は依存してないけど async はっ?てツイッターで指摘されて気づきました。async は依存してます。メソッドの内容を Task に包みます。async はそういう意味で現状存在するTaskという選択肢にデフォルトで依存しています。単純に試した範囲内で同一名前空間、同名のクラスに+いくらかのメソッドというレベルの実装では置き換えできませんでした。すいません。(/追記)

 

F#の立ち位置 – 猫とC#について書く代わりにHaskellとF#について書くmatarilloの日記 – haskell

を読んで、色々とちょっと待ってよ感があったので書いてみます。

C#は色々詰め込みすぎってのはまぁそう。色々詰め込みすぎた結果として直行性を失ってる部分もある事はある。

ラムダの中で yield できないとかそういうのは確かに直行性を失ってる。catch ブロック内で async 使えないとかもその通り。

んでもね、async/await が Task にべったりくっついてて剥がしようがないってのは間違った話なんでそれを元に話が膨らんでいくのはちょっと待てなわけです。

 

さて、async/await を適用する場合の要件とかは以下の記事で書いてある通りです。自分が読む限りで間違いはありません。

連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回):第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装 (2/2) – @IT

bleis さんは 並列/並行基礎勉強会でasync/awaitをDisってきた – ぐるぐる~ で「現状の async/await が Task と結びついているのは、Awaitable パターンが要求するシグネチャに戻り値の型に対する規定がないことから来ています。 つまり、Awaitable パターンを実装する Awaitable/Awaiter に「Task」が出てこないのに、裏で勝手に「Task」にラップされてしまうのです。」と書いてますが、そんな事は起こりません、やられていません。

順を追って確認しましょう。

メソッドの戻り値型であるTaskは Awaitable として返された物で Awaitable の利用方法である Awaiter の取得に答えます。要するにメソッドの戻り値として素直に戻り何もラップ等はされません。TaskはAwaitableとして利用するのに必要十分なメソッド(GetAwaiter)を実装していますのでそのようなラップ操作も必要ありません。 (Task<TResult>.GetAwaiter Method)

Task を返しているのはメソッドその物で、C#は GetAwaiter およびその後のAwaiterに言語仕様でのawaiterを満たすメンバが存在する為 await が適用可能であると判定しているだけです。

さて、await が適用されるとC#は Task の GetAwaiter を呼びだすコードを生成します。TaskのGetAwaiter はランタイムライブラリの TaskAwaiter<T> (System.Runtime.CompilerServices namespace)を返します、TaskAwaiter は Task を Awaiter としてラップする為のメソッドを実装しています。 .NET Framework ランタイムライブラリが Task を Awaiter としてTaskAwaiter でラップしているのであり Task でラップされているのではありません。そしてC#は TaskAwaiter を言語仕様で決めた通りに Awaiter として利用しているだけです。

C#言語はもう限界かもといってる文脈で出てくる事柄が何を何がラップするのかのされるの係り受けが違うし、C#言語でなくランタイムが主体だし、勝手にと言ってる事がメソッドシグネチャ通りにTaskAwaiter<T>を返したりって事で、問題の一文はぶっちゃけマルっと違います。

 

そして、TPL が C# での async/await に便宜を計ったのは Task のGetAwaiter ただ一点これだけで async/await の使い方でよく出てくる ContinueWith その他は async/await 登場前の C# 4.0 / .NET 4.0 から存在します。(Task クラス (System.Threading.Tasks) – .NET Framework 4.0) 、async/await の為にランタイムライブラリに追加されたのは TaskAwaiter であり Task を Awaiter として扱う為にラップします。

このように TPL とランタイムライブラリの間も過度に依存しないように実装されており、TPLの寿命という問題でC#言語の言語仕様そのものは揺るがないでしょう(ユーザーコードはいっぺんにTPLと共に寿命を迎えてしまう事になりますが)。C#の言語仕様そのものは TPL には全く依存性は無く、TPLとランタイムの間は C# 言語仕様での async/await のサポートの為に必要最小限に近い良く設計された依存性を持っていると思います。

 

主語がC#言語なのかランタイムなのか、TaskでラップするのかTaskがAwaiterにラップされるのか裏で勝手になのかシグネチャ通りやんなのか、数点の違いですが違うものは違いますという事で、盛り上がってる人達には落ち着け落ち着けと。んで、話題上で関連するランタイムとTPLがべったりくっついてて剥がしようがないとかもなく、良く設計された依存性でつながっている様に自分には見えますって事で、妄想をマジ受けし過ぎなんじゃないでしょうか。

んでC#は直行性とか一貫性を失ってる部分も無いわけじゃないけど、C#がダメだの限界だとかの話は根っこの理解違ってての話が広まるにも程があるだろ程度には看過しがたい状態かなと

.NET 基礎勉強会で IL の話をしてきました


資料現物とかは以下

PowerPoint のファイル http://sdrv.ms/12Gh11k

ソースとかはこちら https://github.com/kazuk/SimpleILer/

 

比較的簡単(当社比)なコードで、 MSIL を逆アセンブル、制御フロー解析、データフロー解析をやっています。

コアロジックは Msil クラス に実装されていて、上から順に ILInstructions で命令の切り出し、GetRuns で基本ブロックに分解、PopulateControlFlowPath で制御フローパスの列挙、 SimulateEvalStack で評価スタックのシミュレーションをやり、結果として各 IL 命令のデータ依存グラフが得られます。

基本ブロック

簡単に言えば、制御構造単位での命令列です。基本ブロックでは末尾を除いて制御フロー命令が無いようになっています。また、基本ブロックの途中には飛び込まない(制御フローが飛び込む場所は基本ブロックの先頭になる)ように切り出しされます。

[制御フロー命令ではない一連の命令] optional [制御フロー命令]

制御フロー命令が無い基本ブロックの末尾には、次のブロックへの無条件分岐が暗黙に存在すると考えても一緒です。

基本ブロックを導入する事で、制御フローの列挙が非常に簡単になります。各基本ブロック内には制御フロー命令は存在しませんから基本ブロック内の命令を完全取り去って nop にしてしまっても、トポロジー的な意味でのメソッド内での制御構造は変化しません。逆にMSIL を吐き出したく、その中である程度複雑な制御トポロジーを扱いたい(=コンパイラを書きたい)なら複数の基本ブロックを作り上げてその基本ブロックを出力ILバイト列にどういう順序で出力するか、基本ブロックを接続する制御フロー命令は何を使えば良いかという順序で考える事になります。

動作フローのパス列挙

基本ブロックに分解された結果の基本ブロックの末尾命令が示すブロック間の接続を Queue を使った幅優先探索をやっています。

例外とかで飛び出す、飛び込む物を認識したい場合にはもうちょっと細工が要りますが要点は一緒です。

単純に無条件分岐なら分岐先基本ブロックが次の探索対象、条件分岐であれば条件成立時と非成立時の両方を探索するようにキューすることになります。当然に多分岐なら多分岐のすべての分岐先を探索します。

評価スタックのシミュレーションとデータフロー解析

今回の実装では評価スタックの要素型は意識していません。何個のデータを Push するかによって命令オフセットをスタックに積み、何個のデータを Pop するかにしたがって、スタックに積まれた命令オフセットを取り込んで、どの命令が積んだデータをどの命令が受け取るかという事だけを求めています。命令オフセットだけあれば再度 IL バイト列を見てあげれば実際の操作もわかるし、そのスタック要素の型も求まりますのでそこのコストが低ければ解析結果に入れる必要とか無いんじゃないのって事です。

デコンパイルやクロスプラットフォーム変換

この関係性を元に “データを受け取る IL 命令 ( データを出したIL命令 , … )” の形式で出力された構造がデモの最後に表示された変な式構造です。 IL 命令が add だったら + を二項演算子の中置記法でだすとかの判定と変換をしつこくやっていけば普通の言語での式になるでしょう。表示せずに式の木構造を何かに出せばもっとなんかできるかもしれません。(.NETの式木のノードを出力すれば式木になるでしょう) LLVM Instruction で出せばLLVMバックエンドでコンパイルできるはずでしょう。

この辺はやりたきゃどうぞの世界です。

まとめ

こんな基礎の上に .NET が乗っかってるんだって話で MSIL 読もうぜ、コードで!って話でした。

自分的には制約ソルバーとか証明って方に進んでいきたいんだけど、それをするには僕の人生ではまだ時間が足りていない感じで妄想段階を出ていませんです。

Doc of code 販売開始のお知らせ


 

滅多にやらない宣伝記事が増えまして申し訳ありませぬ。

というわけで Doc<Code> については最後の宣伝記事になるでしょう。

販売サイトがオープンしましたので、「買ってくれ!」以上のこの記事には意味はございません。

http://kazuk.azurewebsites.net/

 

んで、今月中はキャンペーンを設定します。

日本時間での今月中にライセンスを購入いただくと、ライセンスの付与期間が倍でライセンスキーが発行されます。

1年ライセンスを買っていただくと 2年後に無効になるライセンスキーが送られるという事になります。

販売初期限定という事で、どうかよろしくお願いいたします。

 

会社等での購入をお考えの方は kazuk.dll@kazuk.jp にご連絡ください。見積書、納品書による対応のほか、銀行振り込みでの入金その他は現状メールでの対応とさせて頂きます。

普通の人は全く知らないでもいい MSIL の基礎知識


 

.NET基礎勉強会 http://connpass.com/event/2441/ で ILについてお話させていただく事になったんですが、まぁ 30分枠ぐらいだと、だいぶ話せる事が限られるんで、あらかじめ Blog に記事乗せといた方が良いかなー的に書いておきます。

 

「手元にございますMSIL命令表をご覧ください」 「えっ、どこよ?」

.NET Framework がインストールされている環境であれば、MSIL命令表は入っています。mscorlibアセンブリ、 System.Refrection.Emit 名前空間配下の OpCodes クラスのOpCode型フィールドをリフレクションで舐めてください。

命令表としての活用の仕方にもよりますが、プレフィックスバイト等も命令表には入っています (Prefix1 etc)、自分の用途でプレフィックスとか要らない場合には、そういう物をフィルタしてあげましょう。 OpCode の OpCodeType を見ればそれがプレフィックスなのか判別する事ができます。

MSIL バイト列の見かた

さて、MSILバイト列の見かたです。バイト列そのものは実行時にリフレクションで取るならば、 MethodInfo から MethodBody を引き、MethodBody から GetILAsByteArray メソッドで取得する事ができます。

このバイト列に各 IL 命令がどのように入っているかです。

オペコード

まず、IL オペコードには 1バイトと 2バイトがあります。

命令のサイズは OpCodes を舐めているならば、OpCode の Size プロパティから取得できます。 これが 1の物は 1バイト命令で、2の物は2バイト命令です。

オペコードの値そのものは OpCode 構造体の Value プロパティに入っています。

2バイトのオペコードはMSILバイト列にビッグエンディアンのバイト順で入ります。要するに 0xDEAD の Value を持つ命令のオペコードが 0xDE 0xAD の順でILバイト列に入ります。(1バイトのプレフィックス命令と、1バイト命令がつながって入っているともいえます。) ※ MSILバイト列で唯一のビッグエンディアンです、他はすべてリトルエンディアンになっています。

1バイトオペコード命令

OpCode オペランド(無い場合もある)

2バイトオペコード命令

Prefix
OpCode
OpCode
(LowByte)
オペランド(無い場合もある)

オペランド

オペランドはオペコードによりますが、 OpCode 構造体の OperandType で取得できます。

OperandType の switch だけが特殊ですが、それ以外は単純にオペランドのサイズは固定されています。そのバイト数分をリトルエンディアンとして読みだせばオペランド値として使える値を普通に取得する事ができます。

OperandType が switch の場合、switch のオペランドにはラベル数が入ります、ラベル数分 InlineBrTarget がバイト列に入ります。

ブランチオフセットの起点

ブランチオフセットの起点はブランチ命令の次の命令です。

たとえば IL Offset n に ShortInlineBrTarget で 3 を指定する1バイトのブランチ命令があった場合、 n +1 (opcode size ) +1 (operand size) がブランチオフセットの起点となり、n +1 (opcode size ) +1 (operand size) + 3(operand value) がブランチ先のIL Offset になります。

ILの動作フローを変える物はブランチ命令と、ret、throwとそれに関連する例外処理ぐらいしかありません。ブランチ命令のオフセットを解釈できるようになった今あなたはILのコントロールフロー解析ができるようになったという事です。

まとめ

7/20 日に話す事はここから後の話ですって事で、MSILの基礎の基礎でした。

7/20 日にはMSILの評価スタックと例外フレームについて話したいと思っているのですが、時間枠的に評価スタックの話で一杯かもしれません。

Doc<Code> というツールをリリースします


技術特化 blog なんで、滅多にやらない事なのですが、宣伝させてください。

宣伝かよ

はい、宣伝です。軽く失注続きで本気にこのままだと路頭に迷う事態なので一人ならどうという事は無いのですが嫁子供居る身なので必死なわけで、個人で作って使ってるツールの類に値段を付けて並べてみようかとの第1弾です。

タダで手に入る物で全部回して主義の方はこの先読んでも意味ないのでお帰り下さい。

Doc<Code> “doc of code” とは

.NET Framework アセンブリとその XML ドキュメントファイルを投げ込むとドキュメントを閲覧できる ASP.NET アプリケーションです。

同様なXMLドキュメントコメントの整形を行うアプリケーションとしては Sandcastle があります。Sandcastle プロジェクト自体は現状 Sandcastle Help File Builder でメンテナンスされているようですが、若干プロジェクトに不安定要素があり、ちょっとややこしい事になっているようです。

Sandcastle との違いは、あちらはビルドプロセスでのバッチ処理でHTML、およびヘルプファイルを作成する枠組みですが、Doc<Code> では、ASP.NET アプリケーションとして要求に応じてアセンブリを随時解析してXMLドキュメントと結合表示する仕組みですので、簡単に言えばビルド時に XMLドキュメントファイルの生成以上の追加処理を必要としません。結果的にビルド時間に対するインパクトを殆ど0にする事ができます。

要するにスロービルドの原因である、誰も見ないようなドキュメント生成を、オンライン解析変換処理にする事で開発スループットをより伸ばせるツールという事です。

インストールに必要な物は?インストールの手順は?

IIS 7.5 以降(Express でも可) 、 .NET Framework 4.5が必要です。 VS2012で普通に開発してる環境なら入っている物ですね。

DocOfCode の紹介とインストールデモ等

普通に NuGet からパッケージ入れるだけで入ります。

どういう使い方が想定されていますか?

普通に開発現場でドキュメントを見る用途、および、オープンソースな物を作ってる人がAPIリファレンスをインターネット上に公開するとかに使って良い様に作っています。

インターネット公開サイトに使う場合に、アセンブリを勝手にアップロードされないようにしたい等は、AssemblyController とその関連Viewそのものを外してしまって下さい。(この場合、アップロードはファイルを直接 App_Data に配置する事でもできますし、アセンブリの一覧管理はプロバイダになっていますので、プロバイダを実装する事で管理方法そのものを変える事ができます)

表示のデザイン等に関わる View および、それに当たる css 等はすべてパッケージ内にありますので、NuGet パッケージを取りこんだうえで修正してもらえれば幾らでもカスタマイズできると思います。

使い方

単純に動かすと能書きの書かれた Index ページの上部にアセンブリと設定のリンクがありますので、アセンブリを選んでもらうと、アセンブリをアップロードするためのページへ遷移します。

アップロード時に指定するタイトルはそのままアセンブリの情報ページのパスとなります、アセンブリ内の名前空間や型、メンバ情報はすべてその配下でアクセスされます。

アップロード済みのファイルと同じタイトルでアップロードを行うと、アセンブリファイルおよび、ドキュメントコメントのXMLファイルの入れ替えになります。

後は飛び回って好きにドキュメントをみて下さいという事です。 Code Contracts で XML ドキュメントコメントを出すようにしておけばそれなりに契約内容も表示されます。

名前空間に関するドキュメントコメント

ドキュメンテーションコメントのXML仕様的には N: で名前空間についてのドキュメントを置けることになっているため、このドキュメントタグが付いた要素があれば表示されるようになっています。また、 NDoc / sandcastle と同様に NamspaceDoc という型に対するドキュメントがあれば、名前空間のドキュメントとして表示されるようにしています。

ドキュメンテーションコメント内のタグ

推奨されるドキュメンテーションコメントタグの殆どはレンダリングできるようになっています。

また、各種HTMLタグはそのままHTMLタグとして表示するようになっていますので、HTMLの aタグをドキュメントコメントに記述しておけばリンクになりますし、imgタグを記述しておけばイメージも表示されます。(当然に img はどっか Web サーバにホストしないと見れませんと思いますけど)

高度な使い方とスケーラビリティ等

Doc<Code> は解析対象アセンブリとドキュメントをすべてメモリ上に読み込んで処理を行う為、スケーラビリティのネックは基本的にはメモリ容量という事になります。また、閲覧されるドキュメントをできるだけ高速にレンダリングしたいため、メモリに読み込んだモジュールやドキュメントをできる限りメモリ保持しようとしますのでアセンブリを登録すれば登録したほどメモリを使います。開発中のドキュメント参照支援等で .NET Framework のすべてのアセンブリとドキュメントを閲覧できるサーバを立てるとかはメモリがきついとかが根本的なネックになるでしょう。

結果的に別々のサーバに、このアセンブリはこっちのサーバ、このアセンブリはこっちのサーバといった形で配置する事になります。これらのサーバ間でドキュメントのリンクを通す事がサーバリンク機能で実現できます。

App_Data/LinkedServers.txt ファイルにリンクされるサーバ名:ポート番号を記述しておくと、該当サーバに存在しないアセンブリへのドキュメントのリンクが自動で解決されます。たとえば A サーバに mscorlib を配置して、Bサーバに自前のライブラリのドキュメントを配置し、BサーバのLinkedServers.txt にAサーバを記述しておくと、Bサーバでドキュメントをレンダリングする場合に String 等 mscorlib 由来の型は A サーバへのリンクとしてレンダリングされます。

Doc<Code>サーバ間は Web API で保持しているアセンブリの問い合わせを行っていますので、Aサーバへのアセンブリの追加などを行った時にBサーバ側は特に何もしないでも自動で反映されます。

注:サーバリンクを使ってサーバ間のリンクをする場合、それぞれのサーバにライセンスが必要です。

解析系の独自拡張の実装

Microsoft CCI に関する知識は必須ですが、アセンブリ、名前空間、型、メンバのそれぞれについて、追加で解析処理を行うロジックを入れるフックポイントが用意されています。

これにより、メンバメソッドのサイクロマチック複雑度を計算するロジックを追加すると、メンバの情報取得時にそれが呼び出され、ロジックが出力した結果は View まで引き渡しされます。

今後の開発ロードマップ

現状では各種メタデータおよび、ドキュメントコメントの表示側に注力している形になっていますが、将来的にはドキュメントコメントの入力、編集ができるようにする事を計画しています。

というのも、綺麗に見えるようになれば欲が出るので、使用例とかの Example をドキュメントコメントに色々書きたくなったりするのですが、それをやるにはコメントの中にコードを書かなければならないし、コメントの中に書かれたコードの中で XML 的な制約で < や > をそのまま書けないし色々と痛々しい思いをしなければなりません。

ドキュメントを充実させたいという思いと、ソースをそんなに汚したくないという思いや色々な葛藤の結果としてドキュメントコメントを実際として機能的に捨てているという現場も多いでしょう。

その辺りに対して、ドキュメントとして入れたい物をソース外からドキュメントコメントXMLに出力できる様にするビルド時ツールとか色々絡めて、十分なドキュメントを書いてもコードファイルが汚れない、それを素早く見れて修正を入れたりとかを Web 上のツールとコード上の双方でできる様にするのを次のマイルストーンとして考えています。

お値段は?

インストールされるサーバにつき1か月300円をめどに設定しています。

販売サイトの方を準備中だったりしますが、 1ヵ月300円、3ヵ月900円、6ヵ月1600円、1年で3000円の4通りのラインナップの予定です。

販売方法としては PayPal でのクレジットカード決済で購入するとメールでライセンスファイルが送られてくるという形式、ライセンスファイルはサーバへ配置が必要です。

また、直接 kazuk.dll@kazuk.jp の方にメール頂ければ見積書、納品書等、会社関係で必要な物は出せる形ができます。

んでいつリリースなのよ

とりあえず、NuGet パッケージについては今月中には出します。販売サイトは月明け早々にも立ち上がり予定、実際のライセンス販売開始は来月10日からを予定しています。(このあたり、多少のずれはあるかもしれません)

an Quick Start for Roslyn (2)–IText と TextSpan


というわけで Roslyn でのパース結果の詳細な見方、主に位置情報について。

前回は SyntaxNode を列挙して軽く眺めてみただけですので、その先の詳細を見ていきましょう。

まず、SyntaxNode には全く文字列は保持されていません。SyntaxNode には Span と FullSpan というTextSpan型のプロパティがありこれらのプロパティには開始位置と長さが保持されていて SyntaxTree の IText 上の範囲の文字列が SyntaxNode の文字列を表現します。

IText の取得方法

まずダメな方法です。

各 SyntaxNode には GetFullTextAsIText() があります、このメソッドはその要素の内容を示す IText を返します。結果のIText の中での位置 0 はその要素の先頭を示しますので、もとになるITextとは別のものです SyntaxNode の位置を示すSpan/FullSpanプロパティの値は意味を持ちません。

続いてイケる方法です。

各SyntaxNodeはSyntaxTreeへの参照を持っています。SyntaxNode.SyntaxTree プロパティ、このSyntaxTreeは TryGetRoot メソッドでルート要素を取得したSyntaxTreeへの参照を保持しています。SyntaxTreeのGetTextはITextを返しますので、このITextを利用します。

また IText は StringText クラスで実装されていますので、 文字列を元に IText を構築することができます。IText をパラメータとして ParseCompirationUnit によってSyntaxTreeを取得することができます。このSyntaxTree のGetTextはもとになったStringTextを返します。

            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

結果: True

結果的に SyntaxNode の SyntaxTree プロパティは ParseCompilationUnit の返した SyntaxTree であり、SyntaxTreeから取得された IText は ParseCompilationUnit に渡された、または内部で生成されたITextで、これらは常に同一の参照を返します(imuttableであるといえます)ので元にしたIText を利用する、SyntaxTreeからITextを取得するのは同じ事で、SyntaxNodeからSyntaxTreeを参照してGetTextでITextを利用するのも同じことです。

僕は自分でStringTextで生成したIText (コード中のtext変数)を直接に参照するようにしました。

.NET Framework は純粋さを前提にできないので、各SyntaxNodeがSyntaxTreeの同一の参照を返すという仮定はできないはずだし、SyntaxTreeのGetTextが返すITextが常に同一という仮定もできないはずなので、同一の参照を使い続けるという仮定を実現させるにはできるだけ早い時期に取得したIText をそのまま使う必要があります。また new を明示的にしているのでnullではないという仮定も成立するはずなので null チェックを省略させる意味もあります。枝葉の最適化にコダワル所じゃないのですけど脳内で降りた判定を明かしておくと「同じこと」という物に微妙な効率差が有りそうと踏んで選んでることだけはなんか書きたかったので書いときます。

オプティマイザの観点でいうと仮定できないというと語弊があって、楽観的に仮定してみて仮定に危うさを感じる要素が一つでも見つかればその仮定を取りやめるのが普通だったりします。この楽観的仮定を最後まで通してあげるとオプティマイザが効率的に処理するコードに落としてくれるわけですので、危うさを臭わせないほうが早いコードが出るという事になります。

TextSpanとSyntaxNodeのSpan/FullSpanの違い

さて、ITextがあり、ITextと関連性のあるTextSpanがあればstring IText.GetText( TextSpan ) によって実際の文字列を取得する事ができます。SyntaxNode は Span と FullSpan の二つのTextSpan型プロパティを持っていてそれぞれ構文要素そのものと、その構文要素に付随するデリミタを保持します。

TextSpan は operator == / operator !=の適切な演算子オーバーロードを持っていますので、これらの違いが見える部分だけを抽出してみましょう。

        // 概要:
        //     Determines if two instances of TextSpan are different.
        public static bool operator !=(TextSpan left, TextSpan right);
        //
        // 概要:
        //     Determines if two instances of TextSpan are the same.
        public static bool operator ==(TextSpan left, TextSpan right);

前回のコードを元に Span と FullSpan が異なる要素だけテキストを取得して表示しているのが以下になります。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

            Stack<SyntaxNode> nodes = new Stack<SyntaxNode>();
            SyntaxNode node;
            tree.TryGetRoot(out node);
            nodes.Push(node);

            while (nodes.Count != 0)
            {
                node = nodes.Pop();
                if (node.Span != node.FullSpan)
                {
                    Console.WriteLine("Full:\t"+node.FullSpan +"\'"+ text.GetText(node.FullSpan)+"\'");
                    Console.WriteLine("Span:\t"+node.Span +"\'"+ text.GetText(node.Span)+"\'");
                }

                foreach (var child in node.ChildNodes())
                {
                    nodes.Push(child);
                }
            }
            Console.ReadLine();
        }
    }
}

わかりやすい例としては以下の様な表示が含まれます。

Full:   [136..190)’            Console.WriteLine( Console.ReadLine() );

Span:   [148..188)’Console.WriteLine( Console.ReadLine() );’

FullSpan には前方のスペースおよび、後方の改行が含まれますが、Spanには含まれない事が解ります。

それって何行目?その行は?

IText には int GetLineNumberFromPosition(int position )があり、これにより指定の位置の行の番号を得る事ができます。

行番号、position ともに0始まりのようです。

            SyntaxNode node;
            tree.TryGetRoot(out node);
            Console.WriteLine(node.FullSpan.Start);
            Console.WriteLine(text.GetLineNumberFromPosition(node.FullSpan.Start));

の出力は 0, 0 です。

同様に IText には ITextLine GetLineFromPosition(int position ) があり、指定の位置を含む行を取得することができます。

ITextLine の実装である TextLine は ToString をオーバライドしていないので単純に ToString すると残念な結果である型名だけが出ます。GetText で内容を取得すると、行末の改行を含まない文字列が返されます

ITextLine はいくつかのプロパティを持っていて、これらで行の範囲を示します。

Extent は行の改行を含まない範囲を示すTextSpanを返します。ExtentIncludeLineBreakは同様に改行を含む範囲を返します。Start、Endはそれぞれ行頭の位置、行末の位置を示します。EndIncludeLineBreakは改行を含む行末です。

(2)のまとめ

シンタックスツリーの操作をやるって言っておきながら、書いてみたら長くなったので回を分けるというやつで、今回は SyntaxNode の位置情報である TextSpan と、位置情報と文字列の関係を示す IText 、位置と行の関係を示す ITextLineをもとに SyntaxNode の位置を解説してみました。

次回もシンタックスツリーの操作にはきっと入れずにシンタックスに付随する SyntaxTrivia についての話となると思います。

an Quick Start for Roslyn (1)


というわけで、プロジェクトテンプレートに並行して Roslyn を触り始めてみました。

Roslyn が何であるかについては ufcpp さんが既に書かれてた記事 があるのでそちらを見てもらうとして、Compiler 内部のコンポーネントをユーザーが利用することができる物という認識で良いと思います。追加してシンタックスツリーを作るとそれのテキスト化(ソースコード化)等、いわゆる言語サービスに類するもの、これまでは Visual Studio と C# コンパイラーの内部でしか利用できなかった物が比較的簡単に利用できるようになっています。

自分としてはこれは言語内DSL等に使える他結構色々と使いでのあるツールなのですが、何しろコンパイラーという巨大な仕組みの内部が公開されたという事は大量のクラス、メソッドが表に出てきたわけで見るのも大変である事は事実で、とっかかりを得るのは結構苦労しそうだなと。根が優しい人なので、この辺のとっかかりについて書いておく事で後の人にもっと奥まで進んで貰えるかな等というわけで書いてみます。

disclaimer

この記事は Roslyn の製品版が出る以前に書かれていますので、RTM版では一部の手順や実装が異なる事が十分にあり得ます。blog記事なんて生ものだと思ってる人なので追補とかする気は全然ありませんので、遠い未来から突っ込みされても困ります。「あー、困った困った。」で流しちゃうと思いますけどご了承ください。

インストール

2012/06/16 現在で最新の Roslyn は以下からダウンロードできます。(2012/6/5 リリースのものです)

Microsoft “Roslyn” CTP 

普通にインストールすれば入ると思います。

インストールすると Roslyn のプロジェクトテンプレートが利用可能になります。

image

含まれているプロジェクトテンプレート

Console Application は普通のコンソールアプリケーションですが、それ以降は vsixmanifest が含まれている事からお察しの通り(お察しできないかもしれませんが)、Visual Studio 拡張のプロジェクトです。

以下プロジェクト作成直後のソリューションエクスプローラーの表示をペタペタと。

imageimageimageimageimage

まぁとっかかりとしての選択肢は Console Application 一択でしょう。 VS拡張を実装するのは追加の知識が大量に必要になってしまうので別個の知識として身に着けてからでないと必要知識の山の前に簡単に遭難できますので。

コンパイラーパイプライン

最初に Roslyn をダウンロードしたページの「Related resources」にある Microsoft “Roslyn” Project Overview› を見ると最初の方にある図を引用させてもらいます。これを読みながら一緒に見てもらうとわかりやすいかも。

CompilerPipeline

コンパイラーの内部処理は Parser で文字列なソースから構文木への変換がかかり、該当ソースで定義された要素が Symbols 、参照しているアセンブリが Metadata Importで取り込まれ、Binder で構文木から実際のコードに相当するモデルが構築され、IL Emitter がそれをPEアセンブリとして書き出すわけで、この各段階と各段階をまたぐデータ構造(構文木、シンボルストア…etc)が Roslyn で API 化されています。

Parserの呼び出しと簡易的なシンタックスツリーの表示

Parser には SyntaxTree の公開するスタティックメソッド、ParseCompilationUnit を介してアクセスします。構文木をスタックに積みながら要素の種別とテキストでの内容を表示するサンプルを以下に示します。

using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Roslyn.Compilers; using Roslyn.Compilers.CSharp; using Roslyn.Services; using Roslyn.Services.CSharp; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); var tree = SyntaxTree.ParseCompilationUnit( @"using System; namespace RoslynTest { public class Program { public static void Main( string[] args ) { Console.WriteLine( Console.ReadLine() ); } } } "); stopWatch.Stop(); Console.WriteLine(stopWatch.Elapsed); Console.ReadLine(); Stack<SyntaxNode> nodes = new Stack<SyntaxNode>(); SyntaxNode node; tree.TryGetRoot(out node); nodes.Push( node ); while (nodes.Count != 0) { node = nodes.Pop(); Console.WriteLine(node.Kind); Console.WriteLine(node.GetText()); foreach (var child in node.ChildNodes()) { nodes.Push(child); } } Console.ReadLine(); } } }

 

表示としては以下の様になりました。

image

パースに21ms ぐらい要してる様に見えますが初回実行コスト(Roslyn のアセンブリの遅延ロードやJITコンパイル)が含まれていてこの速度は十分に早いと言っていいと思います。

IEnumerable<SyntaxNode> の foreach だけで実装できている事からわかる通りで簡単に LINQ の餌食になりそうに見えます。ただし、LINQ は再帰的なツリー構造に必ずしも強くないので多少の補完ライブラリが必要でしょうかね。

(1)のまとめ

内容としては Roslyn の ParseCompilationUnit を回して構文木を得た、軽く内容を表示してみて解析ができていることを確認した段階です。

少なくともコードは完全にSyntaxTreeに分解されていますので、既存コードに含まれがちな foreach での多段ネストループを検出するとかはこれで十分にできます。「コード的にはバグじゃないけどこのコードの実装者の頭ちとおかしいんじゃない?」を検出する検出器は検出条件が構文的に決まってしまえばこの段階で作れますのでぜひ作ってみてください。

アセンブリを参照設定していれば使えるわけですので msbuild のカスタムタスクで検出器を回すことができればあなたのソースのビルド時にチェックするとかもきっとできるでしょう。

 

この構文木を編集するなどを(2)でやってみたいと思います。単純な構文変換であればこの処理で十分ですが、意味を維持するという場合には相応の処置が必要なので意味を含む変換は(3)以降になると思います。

BUILD ネタという事で – C# Advent Calender 12/9


//BUILD/ と思わせておいて msbuild と Visual Studio のネタ。

という訳で、「Hay U! 思い通りにビルドをコントロールできてるかい?」

うちの Blog はたびたび msbuild のネタを取り上げるという事で、生粋のビルドヲタクである事は読者の皆様にはバレバレだと思いますが、C# Advent Calender に参加して、ビルドヲタたるものこれは知っとくべきとか、こんぐらい当然なのよっていう物を世間に知らしめておきたいかなと。

msbuild の基礎の基礎(1) 処理対象の項目一覧 ItemGroup とアイテム

msbuild は処理対象の項目の一覧を ItemGroup として定義します。シンプルに csproj の中でファイルやアセンブリ参照が ItemGroup として定義されています。

VIsual Studio でクラスライブラリのプロジェクトを作り、デフォルトである Class1 に加えてClass2,Class3を作った状態の csproj には以下の2つのItemGroupが定義されています。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>

ソースファイルは Compile Itemが作成されていて、参照設定はReference になっていますね。

この Compile/Reference は最終的に Import されている Microsoft.CSharp.Targets を介して csc タスク(C# Compiler Task)に引き渡されてビルドされるというわけ。

単純にコンパイルするソースであれば Compile で単純に足せばよいという事ですが、コンパイル出来ればビルドとか素人の考え、色々な処理の仕方に応じた分類はいくらでも増やしたいものです。

Web やってれば画像をなんらか処理したり、cssやjsをminify したり色々やる事があるでしょう、その他やっている事に応じて色々やりたいことなんて幾らでもあるでしょう。

やる事を増やすには Target / Task を新たに定義してって事になるのですが、処理対象としてはこの ItemGroup を種類で抽出して受け取る事になるので結局は種類を増やす以外は有りません。

単純に Visual Studio でテキストファイルを追加すると種別は Content として入ります。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>
<
ItemGroup>
<
Content Include=TextFile1.txt/>
</
ItemGroup>


この状態のソリューションエクスプローラーとプロパティは以下の通り。

image

このプロパティでのビルドアクションを「なし」に変えると ItemGroup 定義は以下の様になります。

<ItemGroup>
<
None Include=TextFile1.txt/>
</
ItemGroup>

これで ItemGroup の種別が ビルドアクションで選択される事はみなさんの知識になりました。

要するに Visual Studio のファイルプロパティでビルドアクションのリストに必要な ItemGroup 種別を足すことができればテキストなりXMLなりを足してそれに必要な ItemGroup 種別を割り当てる事ができ、それに応じた処理をさせる事ができます。

ここで特殊な ItemGroup を csproj に追加します。

<ItemGroup>
<
AvailableItemName Include=TextToBitmap>
<
Visible>false</Visible>
</
AvailableItemName>
</
ItemGroup>
<
ItemGroup>
<
TextToBitmap Include=TextFile1.txt/>
</
ItemGroup>

AvailableItemName という ItemGroup にTextToBitmap をVisibleメタデータをfalse で追加しました、ここで追加したTextToBitmap を TextFile1.txt に割り当てています。このファイルのプロパティと、ビルドアクションのドロップダウン内容が以下になります。

imageimage

AvailableItemName で指定した項目がビルドアクションのドロップダウンに追加されています、他のクラスとかにも TextToBitmap が選択できるようになったのが確認できるでしょう。

これでビルドの細かい事とか理解していない非ビルドヲタの人にはビルドアクションを指定してねだけで済ませる事ができるわけです。

ItemGroup の操作についてはターゲット内での操作とか含めてこの先もチョロチョロ出てきますが基本中の基本としてはビルド作業の細かい処理に送りつけるファイルを分類する事ができるという事です。

C# Advent Calender なのに C# がここまでで一行も出てこないとか、気にしないで下さい、先の方でちょっとだけ出てきますので。

msbuild の基礎の基礎(2) ターゲット実行順序の制御

ターゲットの構造は Target の属性による入出力ファイルの指定や依存関係によるビルド処理の順序指定と、Target内に記述されたタスクでの処理の定義に分かれます。

標準で Import されている Microsoft.CSharp.Target やその中から Import される Microsoft.Common.Targets の内容を熟知すると立派なビルドヲタなわけですが必要知識が極端に上がるだけで説明の邪魔なので一端 Microsoft.CSharp.Targets の Import をコメントアウトして Build ターゲットを自作してみます。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>
<
ItemGroup>
<
AvailableItemName Include=TextToBitmap>
<
Visible>false</Visible>
</
AvailableItemName>
</
ItemGroup>
<
ItemGroup>
<
TextToBitmap Include=TextFile1.txt/>
</
ItemGroup>
<!–
<Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” /> —>
<
Target Name=Build>
<
Error Text=まだ作ってないよ/>
</
Target>

Error タスクを使ってメッセージを出して単純に失敗する Build ターゲットを作りました、結果はこの通り。

image

意図通り動いている事が確認できました。まず依存関係での処理順序の制御をしてみましょう。

<!– <Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” /> —>
<
Target Name=BeforeBuild>
<
Warning Text=BeforeBuild is running/>
</
Target>
<
Target Name=BuildCore>
<
Warning Text=BuildCore is running/>
</
Target>
<
Target Name=AfterBuild>
<
Warning Text=AfterBuild is running/>
</
Target>
<
Target Name=BuildDependsOnTargets=BeforeBuild; BuildCore; AfterBuild>
<
Error Text=まだ作ってないよ/>
</
Target>

DependsOnTargets で各タスクを作成しました、実行結果は以下の通り、今回は出力ウインドウも付けています。出力ウインドウの表示は本当に実行順序設定どおりなのですが、エラー一覧は色々な並べ替えがあるので注意が必要です。エラー一覧の2番目のカラムの数値で並べ替えすると出力順序に一致します。

結果としては DependsOnTargets で指定したターゲットが指定の順番で実行され、各ターゲットの実行が完了した後にDependsOnTargets を指定したターゲットが実行される事が解ります。これでターゲットの実行順序は設定できる事が解ります。

image

msbuild 4より前ではこれが唯一のビルドターゲットの実行順序の制御方法でした、msbuild 4では AfterTargets, BeforeTargets 属性によって既存のターゲットの実行順序の指定の位置でターゲットを実行する事を指定する事ができます。

After – Before という順序で書いたのはアルファベット順などという事でなく、実行順序の関係がそうだという事です。 AfterTargets で指定したターゲットの後、BeforeTargets で指定したターゲットの前で指定を行ったターゲットが実行されます。

以下のターゲット定義を設定する事で BeforeBuild, BuildCore の間で Transform が実行されます。

<Target Name=Transform
AfterTargets=BeforeBuild
BeforeTargets=BuildCore>
<
Warning Text=Transformが割り込んでみた/>
</
Target>

image

この関係を利用すると Microsoft.CSharp.Targets や Microsoft.Common.Targets での定義済みターゲットの実行が行われる間に割り込みが可能である事が解ります。

msbuild の基礎の基礎 (3) ItemGroup の処理、一括処理と順次処理

一度 Build のみの簡単な構成に戻して Task の呼び出しの例を紹介しましょう。

<Target Name=Build>
<
Warning Text=Example1: @(Compile)/>
<
Warning Text=Example2: @(Compile->’%(filename).generated%(extension)’)/>
<
Warning Text=Example3: %(Compile.filename)/>
</
Target>

若干ややこしそうな記述になりましたがこれをビルドすると以下の出力が出ます。

warning : Example1: Class1.cs;Class2.cs;Class3.cs;Properties\AssemblyInfo.cs
warning : Example2: Class1.generated.cs;Class2.generated.cs;Class3.generated.cs;AssemblyInfo.generated.cs
warning : Example3: Class1
warning : Example3: Class2
warning : Example3: Class3
warning : Example3: AssemblyInfo

Example3 のWarning が1行に関わらず4回、各 cs について実行されている事が解ります。

このようにパラメータの与え方によってmsbuild は自動的にタスクの呼び出し方を区別し繰り返し制御を実行します。

@(項目種別) で指定した場合、項目の集合がそのまま渡されます。

@(項目種別->’射影定義’) で指定した場合、項目の集合が射影されて項目集合として渡されます。(集合の変換)

%(項目種別 .参照するメタデータ) と記述すると参照するメタデータを参照する項目定義を要素に分割して繰り返しタスクないしはターゲットに渡します。

これによりにファイルを個別に処理する方法と複数のファイルを一括してタスク処理する方法を理解した事になります。

しかし標準タスクでできる事なんてたかが知れてるぜっていうビルドヲタの人は次のステップへ進みましょう。

msbuild ビルドヲタの第一歩 msbuild inline タスク

やっと C# Advent Calender らしい内容になりますよー、C#コードで msbuild のビルド処理で使われるタスクを記述する事にします。

まずは普通の inline タスクです。

<UsingTask TaskName=SampleTask
TaskFactory=CodeTaskFactory
AssemblyFile=$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll>
<
ParameterGroup />
<
Task>
<Code Type=FragmentLanguage=cs>
<![CDATA[
Log.LogWarning(“this is inline task”);
]]>
</
Code>
</
Task>
</
UsingTask>
<
Target Name=Build>
<
SampleTask />
</
Target>

Type属性で Fragment を指定した場合 Taskクラスの派生クラスの Execute メソッドの内部を記述する事になります。上記例では TaskクラスのLogプロパティを介して Warning メッセージを出力しています。

image

簡単に言えば Execute メソッド内のコードとして有効なコードであれば基本的には何でも書けます。 メソッドの分割が必要なレベルの複雑なコードでも Func<>, Action<> デリゲートとラムダで上手くやれば何とでもなるのが C# の素晴らしさ、記述規模さえ気にしなければどうにでもなります。

タスクにパラメータが必要であれば ParameterGroup で指定すれば良く、外部アセンブリを参照する事も using による名前空間参照も指定できますので必要であれば LINQ を使う事もできますし、XML処理をXLINQでも簡単です。

ゆとる msbuild ビルドヲタ msbuild inline タスクのソースファイルオプション

ドキュメントには Source 指定ができるとしか書いてなく、例も何にもなくという酷い扱いのSource 指定でのカスタムタスク定義を紹介しましょう。

Source オプションを使う場合にはソースを収容するクラスライブラリプロジェクトを用意すると便利です、自分は半分お約束的に Build.Utilities という名前のクラスライブラリをソリューションのトップに用意しています。

最も簡単な例として単にWarningを吐くタスクから。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Utilities;

namespace Build.Utilities
{
    public class SourceBasedCustomTask : Task {
        public override bool Execute()
        {
            Log.LogWarning("this is Source based custom task");
            return true;
        }
    }
}

これを読み込む csproj 側の記述は以下の通り。

<UsingTask TaskName=SourceBasedCustomTask
TaskFactory=CodeTaskFactory
AssemblyFile=$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll>
<
ParameterGroup />
<
Task>
<
Code
Type=Class
Language=cs
Source=..\Build.Utilities\SourceBasedCustomTask.cs/>
</
Task>
</
UsingTask>

なにがゆとれているか、そうです、インテリセンスです。作成したBuild.Utilitiesプロジェクトで、Microsoft.Build.FrameworkとMicrosoft.Build.Utilities.v4.0を参照する事で普通のコードを書いているのと同様の完全な Visual Studio 支援下でカスタムタスクを書く事が出来ています。

image

Source の位置指定は該当プロジェクトファイルからの相対となりますのでソース管理にそのまま突っ込んであげれば、ビルドサーバだったり同僚のマシンでも普通に動きます。

msbuild のインラインタスクが出る前はビルドを細かく制御したいというビルドヲタクをするには一つ厄介な問題がありました、カスタムタスクアセンブリの配布です、カスタムタスクアセンブリは csproj と共に共同して開発を行う同僚やCI ビルドサーバへ配布する必要がありました。ソリューションの中にバイナリを入れてしまえば良い、それは確かにそうなのですが、そのバイナリを更新する方法や手段は手動?ビルドヲタクに手動でなんかやれって言って素直にやると思って?

これらの問題は単純にソースでタスクを渡せる事で一発解消です、ソース管理にソースを入れるという単純な事で何とでもなってしまいます。

さて、ここまでの内容をもってすれば、ビルド時に少なくともC#で書ける事であれば何でもでき、それをVisual Studioの支援下で記述できるというのは納得してもらえるでしょうか。

出来るのは解ったけど性能は?:インクリメンタルビルドのサポート

はい、ビルドでは性能重要です。ちょっと変な事するとあっというまに毎回コンパイルが走り、依存しているプロジェクトすべてが毎回ビルドされるようになってしまいます。

そうなると開発作業の大半がビルド待ちにもっていかれるわけで、ビルド時にやってくれる事の価値によってはぶっちゃけ邪魔に感じる事でしょう。

出来るのは良い、本当に必要な時だけ実行されるならもっと良いという訳で、ファイルの更新状況に応じてそれをやるかやらないか制御されなければなりません。

msbuildはもちろんインクリメンタルビルドをサポートしていて、ターゲットの Inputs / Outputs 属性でこれが制御されます。

制御ルールとしては単純で、Inputs に記述されたファイルより Outputs に記述されたファイルが新しければターゲットの実行は省略されます。

Inputs / Outputs それぞれは@(項目グループ)で ItemGroup を受ける事が出来ますので、複数のファイルの更新関係を元にターゲットのそれぞれを実行するかを制御できるという事になります。

まとめ

csproj を開いたことないとか言う人は悔い改めてちょうだい。

ToList / ToArray の性能 – GC 特性とCLRメモリヒューリスティックス


neuecc さんの ToArray vs ToList を受けてちょっと書いてみます。

というのも、 .NET Framework にはパフォーマンス上ある一点に断崖絶壁があり、この断崖絶壁を何の気なしに超えてしまうと一気にパフォーマンスが落ちます。この断崖絶壁すなわちオブジェクトのサイズによる CLR のヒューリスティックが発動される時です。

例えばこのヒューリスティックはオブジェクトのサイズが 24KB を超える点で発動します。

CLR2 ではこのヒューリスティックは LOH へのオブジェクトの配置、CLR4では Gen1 に確保する様になっているようです。

このヒューリスティックの発動は性能面ではっきりした影響を表す事に注意してください、最も単純な例でも 20% ~ 30% の性能影響がでます。 (参考: .NET CLR2 と CLR4 の StringBuilder のパフォーマンス )

 

確認してみましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ToArrayPerformance
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopWatch = new Stopwatch();
            for (int elmCount = 3500; elmCount < 8000; elmCount += 100)
            {
                stopWatch.Restart();
                for (int i = 0; i < 100000000/elmCount; i++)
                {
                    int[] result = Enumerable.Range(1,elmCount).ToArray();
                }
                stopWatch.Stop();
                Console.WriteLine( DateTime.Now.TimeOfDay +"\t"+
                    elmCount.ToString() + "\t" + 
                    stopWatch.Elapsed );
            }
            Console.ReadLine();
        }
    }
}

単純に Enumerable.Range を (1,n) する事の n を 3500 –> 8000 まで100 づつ伸ばしています。ループ回数を十分に大きく、しかし nに逆比例させる事で計測期間内で処理する要素数は平均的には一緒です。

09:33:24.0662034        3500    00:00:03.6627269

09:33:27.4823988        3600    00:00:03.4151080

09:33:31.2646151        3700    00:00:03.7812066

09:33:33.9517688        3800    00:00:02.6867959

09:33:37.1399512        3900    00:00:03.1877383

09:33:40.7921601        4000    00:00:03.6520175

09:33:44.7573869        4100    00:00:03.9653936

09:33:48.3675933        4200    00:00:03.6096440

09:33:52.6018355        4300    00:00:04.2337512

09:33:56.5520615        4400    00:00:03.9497644

09:33:59.6342378        4500    00:00:03.0817188

09:34:03.1714401        4600    00:00:03.5373395

09:34:07.0466617        4700    00:00:03.8745989

09:34:10.2968476        4800    00:00:03.2493948

09:34:13.9820584        4900    00:00:03.6850011

09:34:17.8952822        5000    00:00:03.9133208

09:34:21.6224954        5100    00:00:03.7266563

09:34:24.9926882        5200    00:00:03.3696358

09:34:28.5378910        5300    00:00:03.5445853

09:34:31.8280791        5400    00:00:03.2904550

09:34:35.7383028        5500    00:00:03.9096186

09:34:38.5334627        5600    00:00:02.7946198

09:34:41.1226108        5700    00:00:02.5892790

09:34:44.6508126        5800    00:00:03.5274057

09:34:47.9480011        5900    00:00:03.2966803

09:34:52.0562361        6000    00:00:04.1073346

09:34:56.2544763        6100    00:00:04.1981550

09:35:00.3177087        6200    00:00:04.0629688

09:35:04.5789524        6300    00:00:04.2609572

09:35:08.7101887        6400    00:00:04.1306676

09:35:12.1133833        6500    00:00:03.4019436

09:35:16.5896394        6600    00:00:04.4765241

09:35:20.6798733        6700    00:00:04.0892036

09:35:25.0381226        6800    00:00:04.3580106

09:35:29.1703589        6900    00:00:04.1319132

09:35:33.1835885        7000    00:00:04.0123602

09:35:37.2978238        7100    00:00:04.1144444

09:35:41.1280429        7200    00:00:03.8297086

09:35:44.0262086        7300    00:00:02.8976320

09:35:46.4393467        7400    00:00:02.4127087

09:35:50.6465873        7500    00:00:04.2066884

09:35:54.7918244        7600    00:00:04.1448968

09:35:58.6760466        7700    00:00:03.8837421

09:36:03.0412962        7800    00:00:04.3644022

09:36:06.3874876        7900    00:00:03.3454986

3要素目の処理時間を見ると2つのトレンドがある事に気づいてもらえますでしょうか。

6000要素を超えたとたんに 4秒以上処理時間を要する様に変わっています。

コレのパフォーマンスグラフを見ると以下の様になります。

image

端的には %Time in GC ががくっと上がっているのが目に見えて解ります。それを起点として立ち上がってるカウンターは # Gen1 Collections です。

image

int で 各4バイトの6000要素ですので、24KB がこの性能の境界線である事が解ります。

要するにアプリケーションが確保する領域のサイズが頻繁にこの境界を超えると3割ぐらい性能が落ちるという事になりますね。

 

さて、性能狂になる準備は整ったも同然ですね、練習問題です。

  1. 24KBを超えない様に内部バッファを管理してコピーを実行する GcNiYasasiiToArray を実装しましょう。
  2. この GcNiYasasiiToArray が管理する 24KB 以下に制約された中間バッファは当然に相当数のインスタンスが生成される事になります。(大きなデータを扱った場合、同じ24KB程度のサイズのバッファがいくつも作られる)これを再利用する事の有用性と馬鹿馬鹿しさを論じた上で実際に試して有用さ無駄さを評価してください。
    1. 再利用される事によりオブジェクトが長寿命になるという事は24KB以下のオブジェクトは Gen0 に作られるという点に配慮した意味を失うかもしれません。これがパフォーマンスグラフにどう表れるか、性能にどう影響を与えたかを評価しなければなりません。
    2. 配列は確保時にゼロクリアされます、このゼロクリアは要素を上書きするのであれば無駄なことで、これが避けられる様になる事は性能に影響する可能性があります、この性能変化を評価してください。
  3. あなたのアプリケーションに GCへの優しさを意識させると性能が変わりますか?それをパフォーマンスグラフより判断し、改善する事が期待できる場合、改善するべきポイントを CLR Profiler によって発見してください。

LOH は 85KB かららしいですね、この境界でのパフォーマンスグラフの違いを見るというのも練習問題としていいかもしれません。

 

なーんて、練習問題でお茶を濁す教科書風な終わり方をしてみます。Gen1 GCだけで3割性能が変わるわけで、Gen2 のFull GCとか気絶しちゃうぐらい怖い話なんでGCには優しく、女の子にも優しくしましょうね。