2025-06-30
Created at:
お財布忘れ太郎。
componentize-dotnet で wasm 吐くの、.NET 10 Preview 入れていれば、テンプレートインストール、dotnet new, dotnet build だけで出来るようになってたので良さそう。
ビックカメラSuicaカード申し込み太郎。Nintendo Switch 2…というのもあるけど1番使う家電量販店なので。そして折角なので JCB です。
Created at:
お財布忘れ太郎。
componentize-dotnet で wasm 吐くの、.NET 10 Preview 入れていれば、テンプレートインストール、dotnet new, dotnet build だけで出来るようになってたので良さそう。
ビックカメラSuicaカード申し込み太郎。Nintendo Switch 2…というのもあるけど1番使う家電量販店なので。そして折角なので JCB です。
Created at:
文具女子博 2025という文房具をたくさん展示、販売しているイベントに。何が女子なのかはよくわかってません。
最近は紙に書くことをしないので文房具は良さそうだけど使わない…と冷静になりがちなので買うものはないかと思っていたけど、意外と買ったかも。
LIHIT LAB. のクリアファイルのカバーは何というかすごい。クリアファイルのカバーって。いやわかりますが。つい買ってしまった…。
有楽町のヒューリックホールで LAWSON presents 豊崎愛⽣ カバーコンサート2025 AT living Ⅱ 〜sing of youth〜 夜の部。
カバーコンサートは定期的にやって欲しいですね。あとおからじ終わったことで生のソロトークを聞ける機会が減ったのが惜しい…。
日記エントリーのメタデータに date を指定していなかったのでオーダーが破滅してたのに今さら気づきました。あとで適当になおします。
Created at:
今日もネコチャンはクソネコムーブをキメる。
今日も流行りのエージェンティックコーディングで雑アプリを作ってみたり。今回は Android で動かしたいのでとりあえず .NET MAUI にしてみたけど、まあ意外と目的は達成できた。多少のデバッグガイドは必要でしたが。
最初 Android エミュレーターで動かすのが面倒だから iOS で多少動くところまでやってから Android のほうで作業し始めたけど、ちゃんとクロスプラットフォームらしくなんとなくスッと動いてちょっと感動。
特に公開するわけではないものなら自分でコードを書かない分、雑なコードでも良いというか、自分でやるときのちゃんと設計や実装をしたくなる葛藤と戦わないですむのがいいかも。
あとはデバッグまで自動運転して欲しいけど、CLI, Web, ネイティブ GUI の順番でお膳立てがめんどそうかも。
mono 見てたら天城さんの声が。来期は主役もあるしイイデスネ。
Created at:
せこんさんとか kyoさんとかのとこをみてて、100周ぐらい回ってブログというか日記かもと思い、久々に更新できるようにしてみたり。
今まで更新がめんどくさくなっていた理由としてそもそも VSCode で更新して GitHub にプッシュする、というフローがだるくて、特に画像を貼りたいと思うと面倒が加速する…という問題がありました。
技術的な記事なら Zenn とかでいいか、みたいなところもあり。
というわけでなにか別なツールはないものか探していたのですが、キーボードショートカットでペーストしたり、Git 上に置いたりができるようなやつはそんなになくて、自分で作るのもなーと悩んでいたのです。
しかし今 AI が盛り上がっている…ということで丸投げして GitHub のリポジトリーを直接編集するエディターを作ってもらえばいいのでは?と思って試したら解決。すごい。自分だけのツールがお手軽に作れるのはいい時代ですね。
サイトそのものの見た目も調整したいけどまあそれはのちのち。
会社でつかっている Surface Precision Mouse のホイールスクロールが壊れて無段階からもどらなくなってしまって死。
昨日のことではあるけどやっと年金生活に。ほぼデイリーぐらいだけど2年はかからなかった程度。
Created at:
Azure の Azure Container Apps (Preview) ではカスタムドメインを設定して、外部からのアクセスを受け付けるようにできます。
詳しくはしばやんさんのブログを参照していただきたいのですが、現時点ではカスタムドメインの設定時に HTTPS 向けの証明書のアップロードが必要です。
つまりドメインに対する証明書を何らかの方法で作成/用意する必要があり、しばやんさんの記事では Acmebot を用意して Let’s Encrypt で証明書を作成、更新していく方法が紹介されています。
今回触っていた環境は個人で適当に立てているサイトなので動かすものを増やしたくないなと思っていたのですが、元々 Cloudflare を前に置いていたので Cloudflare が自動で発行、更新する証明書を使うようにすれば丸投げできそうな気がしてきました。が、しかし Container Apps は HTTPS 必須なのかカスタムドメインの登録時にどうにしても証明書が必要です。
Container Apps のカスタムドメイン設定に必要な証明書をどうするか、というところで Cloudflare にはオリジン証明書を作るという機能があるのでこれを使います。
オリジン証明書は Cloudflare とオリジン、この場合だと Container Apps の通信でのみ使用する目的の証明書で今回の用途にはぴったりです。デフォルトでは15年という長い期限で作成され、個人のサイトならそっちが先に朽ちるか Managed Certificates がくると思うのでよさそうです(今回はある程度放っておきたいのが目的)。
オリジン証明書は Cloudflare の管理画面の SSL/TLS → オリジンサーバー → オリジン証明書 で発行できます。発行すると証明書と秘密鍵がでてきますので PEM 形式で手元に保存します。特に秘密鍵はページを閉じると確認できなくなるのでしっかり保存しておきます。
次に発行した証明書と秘密鍵を Container Apps に登録します。ところが登録時にパスワードを求められたりしてそのままでは取り込めないので OpenSSL で pfx 形式 (PKCS#12) に変換します。
cat certificate.pem private.pem > origin.pem
openssl pkcs12 -export -in origin.pem -out origin.pfx
pfx 形式の証明書を作成したら Container Apps に登録します。Azure Portal の コンテナー アプリ環境 → (コンテナーアプリ) → 証明書 で “証明書の追加” で追加を行えます。
注意点として現時点では “証明書名” に大文字アルファベットを含む文字列を指定すると “この証明書名は既に使用されています。別の証明書名をお試しください。” という謎のエラーが出るので小文字で入力してください。
証明書が作成出来たら後はカスタムドメインの設定を行い、証明書として先ほど登録したものを選択すれば完了です。
Container Apps が Managed Certificates にはやく対応してほしいですね。
Created at:
Source Generator を触っていて気づいたのですが Visual Studio 2019 version 16.9 の段階でいくつか API が追加されていました。
追加されたものは次のようなあるといいよねといった API が増えている感じです。
GeneratorInitializationContext.RegisterForPostInitialization
メソッドISyntaxContextReceiver
インターフェースVisual Studio 2019 であれば 16.9 以降、NuGet パッケージであれば Microsoft.CodeAnalysis.CSharp 3.9.0 を参照すれば使用できます。
GeneratorInitializationContext.RegisterForPostInitialization
メソッドGeneratorInitializationContext.RegisterForPostInitialization
メソッドは Source Generator が読み込まれて初期化された後に呼び出されるコールバックを登録できます。
これは Source Generator で必要となる属性用のコードを追加したいパターンに役立ちます。例えば次のようなコードがサンプルにあります。
[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
private const string attributeText = @"
using System;
namespace AutoNotify
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
[System.Diagnostics.Conditional(""AutoNotifyGenerator_DEBUG"")]
sealed class AutoNotifyAttribute : Attribute
{
public AutoNotifyAttribute()
{
}
public string PropertyName { get; set; }
}
}
";
public void Initialize(GeneratorInitializationContext context)
{
// Register the attribute source
context.RegisterForPostInitialization((i) => i.AddSource("AutoNotifyAttribute", attributeText));
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
...
}
今までソースで指定したい属性をどうするかという問題があった(一度生成させてその属性を使うのか、手で追加するのかとか)のがこれで解決できそうです。
ISyntaxContextReceiver
インターフェースコンパイラーがソースコードを処理する際に Source Generator を呼び出す ISyntaxReceiver
インターフェースがあります。
例えばクラス定義に対するコードジェネレートを行いたいときには自分でシンタックスツリーを走査して集めるのではなく、ツリーの走査はコンパイラーに任せて、 ClassDeclarationSyntax を集めておいて最後に処理するといった使い方です。
ISyntaxReceiver
インターフェースは OnVisitSyntaxNode
メソッドのみをもち引数として SyntaxNode
を受ける形でしたが、 ISyntaxContextReceiver
では GeneratorSyntaxContext
を受け取る形になります。
このコンテキストオブジェクトにはセマンティックモデルが含まれているので、Receiver で必要なものをかき集めるときに、コードの解析された情報にアクセスできるようになります。
例えば属性はセマンティックモデルを通してシンボルを取得して、GetAttributes で取得できるので、Source Generator 用の属性がついていることを Receiver が集める段階で確認できます。こちらもサンプルコードが例としてわかりやすいです。
16.9 で追加された API なので古い Visual Studio や .NET SDK では動かない可能性があります(試していない)。
Rider 2021.1.2 ではビルドは問題ないのですが Rider 側が対応していないようでエディター上では正しく動作しなかったりしたので、2021年5月頭時点では使うにはちょっと早いかもしれません。
Created at:
これは Blazor Advent Calendar 2020 の25日目のエントリーです。
Blazor はお手軽に Single Page Application を作れるのですがコンポーネントのライフサイクル、ステート関連について知らないと若干不思議な仕組みで動いているように見えます。
例えばプロジェクトテンプレートの Counter は @onclick="IncrementCount"
でプライベート変数 currentCount
をインクリメントするとページのカウントがアップしますが、これは初見ではなかなか不思議な挙動です。Blazor は currentCount
の変更をどうやって知ったのか?としばらく不思議に思っていました。そういったこともライフサイクルを知ることで理解できます。
基本的には ASP.NET Core Blazor ライフサイクル というドキュメントに書いてあるのですがこれはそれを補完する目的のエントリーです。
Blazor コンポーネントはコンポーネントのステート(状態)が変更されるたびに再レンダリングを行うという流れが基本となります。これ自体は他のフレームワークと大きく変わるところではないと思います。
これらの多くのライフサイクルに関連するものは Microsoft.AspNetCore.Components.ComponentBase クラス に実装されています。
コンポーネントの初回のレンダリング、つまり初めてページが表示されたときの処理の流れは次のようになっています。
初めに SetParametersAsync が呼び出され、初期化とパラメータのセットを行います。この OnInitializedAsync (初期化), OnParameterSetAsync (パラメータセット) では非同期処理を行うことができますが、その場合は Task の完了を待たずにレンダリングが行われて完了次第再レンダリングが行われます。
つまり非同期初期化の場合、コンポーネントの HTML をレンダリングが行われるタイミングではまだ値がそろっていない場合がある点には注意が必要です。
private NanikaObject _nanika;
protected override async Task OnInitializedAsync()
{
await Task.Delay(1000);
_nanika = new NanikaObject();
await base.OnInitializedAsync();
}
...
@* OnInitializedAsync が終わるまで _nanika は null *@
<h1>@_nanika.Description</h1>
BuildRenderTree メソッドは Blazor 上のドキュメントツリー構造(仮想DOM的なもの)を構築するものです。通常は Razor Component (.razor) ファイルから自動生成され、手でツリーを作るようなカスタムコンポーネントを作らない限りは触ることはありません。中身が気になる場合には obj ディレクトリの中を覗いてみると生成物を確認できます。
なお Server-side Blazor に関してはレンダリングモードが ServerPrerendered の場合、 SetParametersAsync + BuildRenderTree が2回呼び出されます。1回目はプリレンダリングのための実行です。またプリレンダリングはサーバーサイドで完成系を返す都合、SetParametersAsync の完了を待ってからのレンダリングとなります。
OnInitialized{Async} と OnParametersSet{Async} の違いは、OnInitialized がコンポーネントの初期化であるのに対して、OnParametersSet はプロパティの変更です。
つまり [Parameter]
として受けている値が変わったときに呼び出されるもので React で言うなら props の変更のような感じでしょうか。
例えば次のようなコンポーネントを用意して…
<p>@Count</p>
@code {
[Parameter]
public int Count { get; set; }
protected override Task OnParametersSetAsync()
{
Console.WriteLine($"Count={Count}");
return base.OnParametersSetAsync();
}
}
Counter.razor にぶら下げてみます。
<Nantoka Count="currentCount" />
これでカウンターを更新すると OnParametersSetAsync が呼び出されることを確認できます。
そもそも Blazor におけるステートとは何かステートという特別な入れ物があるわけでもなく、あるのは何らかの何らかステートが変更されたということを通知する仕組みです。
データそのものはプライベート変数であったり、Parameter であったりするかもしれませんが Blazor としては「何か状態が変わったことを知る」ということが重要になります。
Blazor はステートの変更通知を受けると、コンポーネントの再レンダリングを行い、表示を更新します。
ステートの変更通知はどのようなタイミングで発生するのかですが、おおまかに下記の3パターンで発生します。
@bind
や @onclick
とか)3パターンと書きましたが2つ目と3つ目は暗黙的に StateHasChanged を呼び出しているというのが実際のところです。
StateHasChanged はステートが変更されたことを通知するメソッドです。このメソッドを呼び出すと再レンダリング候補としてマークされます。何が起こるのかは後ほど少し説明します。
多くのケースでは StateHasChanged を明示的に呼び出す必要はありませんが、ロジック側から表示を更新したいケースでは呼び出しを必要とします。
例えばサーバーからのデータ受信やタイマー処理といったユーザー操作によるイベントではないが表示を更新するようなパターンです。この場合は自らステートが変更されたので再レンダリングしてほしいという意思を伝えるために StateHasChanged を呼び出す必要があります。
冒頭でプロジェクトテンプレートの Counter でクリックしてプライベート変数を更新するだけでページの表示が変わって不思議、と書きましたがそのような挙動になる理由はここにあります。
Blazor の ComponentBase ではイベントハンドラーの実行時に StateHasChanged を呼び出しています。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L313
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
// After each event, we synchronously re-render (unless !ShouldRender())
// This just saves the developer the trouble of putting "StateHasChanged();"
// at the end of every event callback.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
これにより「クリックイベントが発生する」→「プライベート変数を更新する」→「StateHasChanged を呼び出す」→「再レンダリング」→「値が表示に反映される」という流れになっているというわけです。
変数を監視していたり、プロキシになっていたりするわけではなくイベントが発生したら StateHasChanged (=ステート変更通知)が呼び出されているだけという単純な仕組みです。
コンポーネントの Parameter に渡される値が変化した場合もステートの変更がありと認識されます。
これも ComponentBase.SetParametersAsync の中で StateHasChanged を呼びだしているので、値の変更があると SetParametersAsync が呼び出されることで結果的にステートの変更があった扱いになります。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L277
private Task CallOnParametersSetAsync()
{
OnParametersSet();
var task = OnParametersSetAsync();
// If no async work is to be performed, i.e. the task has already ran to completion
// or was canceled by the time we got to inspect it, avoid going async and re-invoking
// StateHasChanged at the culmination of the async work.
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
// the synchronous part of OnParametersSetAsync has run.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
StateHasChanged メソッドを呼ぶと次のような流れで処理が実行されます。
初回と似ていますが異なるのは初期化周りのメソッド(OnInitialized)が呼び出されないことと ShouldRender が呼ばれることです。
ShouldRender メソッドはレンダリングする必要があるかどうかを返すことができるものです。このメソッドのデフォルト実装は常に true
を返しますが、カスタムコンポーネントは再レンダリングを抑える目的でオーバーライドできます。
StateHasChanged の実装は ComponentBase クラスにあり、protected として公開されています。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L100
このメソッド自体は比較的シンプルでやっていることは次のようになっています。
先ほどの流れで ShouldRender の後に BuildRenderTree が呼び出されると書いていましたが、実際は一度レンダラーのキューに詰めてから BuildRenderTree が呼び出されます。
Blazor のライフサイクルやレンダリングでは StateHasChanged が重要な役割となります。
しかしイベントハンドラーのような暗黙的に呼び出されているケースもあり、Blazor の仕組みをあまり知らずに使っていても画面が更新されるので思わぬところでハマる可能性もあります。そういった罠を踏む前にある程度流れを抑えておくことをお勧めします。
Created at:
Visual Studio からの発行や dotnet publish -c Release
などを実行したとき、IL Linker 実行中に以下のような AssemblyResolutionException
がスローされます。
Fatal error in Mono IL Linker
C:\Users\Tomoyo\.nuget\packages\microsoft.aspnetcore.components.webassembly.build\3.2.1\targets\Blazor.MonoRuntime.targets(326,5): error : Unhandled exception. Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535' [C:\Users\Tomoyo\Source\Repos\BlazorApp6\BlazorApp6\BlazorApp6.csproj]
---> Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535'
at Mono.Cecil.BaseAssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
at Mono.Linker.AssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
at Mono.Linker.LinkContext.Resolve(IMetadataScope scope)
at Mono.Linker.LinkContext.Resolve(IMetadataScope scope)
at Mono.Linker.LinkContext.ResolveReferences(AssemblyDefinition assembly)
at Mono.Linker.Steps.LoadReferencesStep.ProcessReferences(AssemblyDefinition assembly)
at Mono.Linker.Steps.LoadReferencesStep.ProcessAssembly(AssemblyDefinition assembly)
at Mono.Linker.Steps.BaseStep.Process(LinkContext context)
at Mono.Linker.Pipeline.ProcessStep(LinkContext context, IStep step)
at Mono.Linker.Pipeline.Process(LinkContext context)
at Mono.Linker.Driver.Run(ILogger customLogger)
at Mono.Linker.Driver.Execute(String[] args, ILogger customLogger)
at Mono.Linker.Driver.Main(String[] args)
IL Linker は依存しているアセンブリ参照をすべて検索して、不要な IL を削っていくというビルドステップです。
この例外(エラー)は依存先のアセンブリを探しているときにアセンブリが見つからなかった場合に発生するもので、通常 NuGet でパッケージ参照されているものに対しては発生しません。
しかし依存パッケージの作り次第では例外が発生する場合があります。例えば Microsoft.CodeAnalysis.Workspaces.Common
のようなパッケージを参照すると発生します。
これは Microsoft.CodeAnalysis.Workspaces.Common
が SQLitePCLRaw.bundle_green
パッケージを PrivateAssets="all"
として参照しているため、実際のパッケージにはパッケージ参照として SQLitePCLRaw.bundle_green
が含まれないことでアプリケーションから参照したときにパッケージが解決されないのでアセンブリが見つからないということが発生します。
つまりパッケージの依存としては扱わないもののアセンブリを参照はあるという状況で発生します。必須ではないパッケージなどの場合にはこういった構成になります。
解決方法としては次の2つの方法があります。
見つからないといわれているアセンブリが含まれているパッケージをプロジェクトから参照します。
SQLitePCLRaw.bundle_green
のようなものは参照してもエラーとなるのでそういった場合には IL Linker 自体を無効にします。
<PropertyGroup>
<BlazorWebAssemblyEnableLinking>false</BlazorWebAssemblyEnableLinking>
</PropertyGroup>
Created at:
Surface Pro X も発売され早数か月、今や多くの方が ARM64 版 Windows をご利用中かと思いますが(要出典)、Visual Studio Code の ARM64 版はまだリリースされていない状況です。もちろん x86 版をエミュレーションで利用できますが、Electron アプリはパフォーマンス的にかなり不利ですので ARM64 ネイティブなものが欲しいところです。
Visual Studio Code のリポジトリで ARM64 対応がないかなと眺めていたところ ARM64 ビルドをやっていく PR が作られているのを発見し、マージされるのを watch していたのですが先日ついにマージされました。
というわけで自分で VSCode リポジトリからビルドしてみたところ、手順が意外とわかりにくかったのでまとめておきます。
大体は vscode リポジトリの今トリビュートガイドのビルド手順に従って環境を用意します。
まず、クロスコンパイルする形になるので x64 の Windows 環境が必要になります。もしかするとコンパイラーなどは動くかもしれませんが死ぬほど遅いでしょうし、Node.js もメモリーの都合なのか x64 版を用意するように書かれているので素直に x64 環境でビルドするのがよいでしょう(AzureやAWSで適当にVM立ててビルドするとか)。
Python とコンパイラーは npm install -g windows-build-tools
でインストールできます。VSCode のガイドには --vs2015
を指定して Visual Studio 2015 で…のようなことが書いてありますがそのまま Visual Studio 2017 でもビルドできます。
コンパイラーは windows-build-tools
ではなく Visual Studio 2017 Build Tools を入れることでも大丈夫です。Visual Studio 2019 の場合でも 2017 のコンパイラーが入っていればビルドできそうな気がします。
さらに ARM64 版ビルドを作る場合には Visual Studio Installer から個別のコンポーネントとして Visual C++ compilers and libraries for ARM64
というものを入れておく必要があります。
Visual C++ 2010 Redistributable (x86) も必要です。これはビルド途中で使われるリソース書き換えツールの rcedit を動かすのに必要です。
ツールがそろったらコードを clone して、yarn でモジュールをインストールします。
git clone https://github.com/microsoft/vscode.git
と、yarn でモジュールをインストールする前にビルド対象のアーキテクチャを環境変数で設定しておきます。
cd vscode
set npm_config_arch=arm64
set npm_config_target_arch=arm64
設定したら yarn を実行します。
yarn install
すると多分途中で %USERPROFILE%\AppData\Local\node-gyp\Cache\12.4.0\arm64\node.lib
がないというようなエラーになるかと思います。
node-gyp で使われる lib がないということなので次の場所からダウンロードしてきて放り込みます。
https://unofficial-builds.nodejs.org/download/release/v12.15.0/win-arm64/
本当はバージョンを合わせるべきな気がしますが、必ずしも同じバージョンのバイナリがあるわけではないのでそれっぽいバージョンを放り込みます。
ファイルを置いたらもう一度 yarn install
を実行すると最後まで通るはずです。
ここまできたらあとはビルドするだけですが、その前に少し VSCode のビルド設定を変更しておきます。
まず前提として VSCode リポジトリは Microsoft からリリースされている Visual Studio Code そのものではないのです。
VSCode リポジトリは Code - OSS
というオープンソースな Visual Studio Code 部分です。一方 Microsoft がバイナリでリリースしている Visual Studio Code は Code - OSS
にブランディングや Marketplace のエンドポイント設定、テレメトリーの有効化などのカスタマイズを行って、プロプラエタリなライセンスでリリースしているものとなっています。Chromium と Google Chrome とかも似たような感じかもしれません。
つまり Code - OSS
そのままだと Marketplace を使えないので、API エンドポイントを設定してあげます。そのあたりは VSCode リポジトリを元にコミュニティーバイナリビルドを作っている VSCodium を参考にして product.json を編集します。
diff --git a/product.json b/product.json
index 075a1d0ada..093f517644 100644
--- a/product.json
+++ b/product.json
@@ -20,6 +20,9 @@
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new",
"urlProtocol": "code-oss",
+ "quality": "stable",
+ "extensionAllowedBadgeProviders": ["api.bintray.com", "api.travis-ci.com", "api.travis-ci.org", "app.fossa.io", "badge.fury.io", "badge.waffle.io", "badgen.net", "badges.frapsoft.com", "badges.gitter.im", "badges.greenkeeper.io", "cdn.travis-ci.com", "cdn.travis-ci.org", "ci.appveyor.com", "circleci.com", "cla.opensource.microsoft.com", "codacy.com", "codeclimate.com", "codecov.io", "coveralls.io", "david-dm.org", "deepscan.io", "dev.azure.com", "flat.badgen.net", "gemnasium.com", "githost.io", "gitlab.com", "godoc.org", "goreportcard.com", "img.shields.io", "isitmaintained.com", "marketplace.visualstudio.com", "nodesecurity.io", "opencollective.com", "snyk.io", "travis-ci.com", "travis-ci.org", "visualstudio.com", "vsmarketplacebadge.apphb.com", "www.bithound.io", "www.versioneye.com"],
+ "extensionsGallery": {"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery", "cacheUrl": "https://vscode.blob.core.windows.net/gallery/index", "itemUrl": "https://marketplace.visualstudio.com/items"},
"extensionAllowedProposedApi": [
"ms-vscode.references-view"
],
準備ができたらビルドを実行してコンパイルして配布物一式を作成します。
set NODE_ENV=production
yarn gulp vscode-win32-arm64
yarn gulp vscode-win32-arm64-archive
vscode-win32-arm64-archive
を実行すると .build\win32-arm64\archive\VSCode-win32-arm64.zip
という ZIP ファイルが出来上がります。
というわけでこの一式を ARM64 環境へもっていって展開すれば Visual Studio Code 的なものを使えます(インストーラーは2020年4月現在ビルドできません)。
ビルドの準備の途中でも書きましたが、手元でビルドしたものは Microsoft からリリースされるものとは異なります。
名前が違うこともあり設定は Visual Studio Code とは別の場所に保存されるものになります(つまり公式リリースが出た場合でも設定は別になります)。
ライセンスが違うのが実はちょっと罠なので注意が必要です。というのも Microsoft がリリースしている VSCode 拡張のライセンスは Microsoft 公式から配布されている Microsoft Visual Studio Code とともに使うことが許可されているものがあります(例えば Remote とか)ので注意してください。
Created at:
このエントリーはC# その2 Advent Calendar 2019のエントリーです。
突然ですがスタックトレースから何らか自分の書いたメソッドを隠したいなと思ったことはないでしょうか?普段はそんなことを考える必要はないのですがアプリの下回りやミドルウェア的なものを作っているとたまにそういった場面に遭遇します。
例えば ASP.NET Core MVC の ActionFilter のようなフィルターチェーン的なものを作るといったパターンがあるとします。まず次のような感じのフィルター定義があって…
interface IFilter
{
void Invoke(object context, Action<object> next);
}
class AFilter : IFilter
{
public void Invoke(object context, Action<object> next)
{
Console.WriteLine("A Begin");
next(context);
Console.WriteLine("A End");
}
}
class BFilter : IFilter
{
public void Invoke(object context, Action<object> next)
{
Console.WriteLine("B Begin");
next(context);
Console.WriteLine("B End");
}
}
そのフィルターをつなげて呼び出すというような仕組みです。
// フィルターの一覧を作る (後ろに行くほうが内側 = 外側が AFilter, 内側が BFilter)
var filters = new IFilter[] { new AFilter(), new BFilter() };
// フィルターに渡す次のアクションの変数(その際最後になるアクションを入れておく)
Action<object> next = (context) =>
{
// 一番深いところでスタックトレースを取得する
Console.WriteLine(Environment.StackTrace);
};
// フィルターチェーンを作る
// 内側から外に向かってつないでいく
foreach (var filter in filters.Reverse())
{
// next をラムダにキャプチャーする必要がある
var next_ = next;
next = (context) => filter.Invoke(context, next_);
}
// 呼び出す
next(null);
このコードの実行結果はおおよそこんな感じになります。
A Begin
B Begin
at System.Environment.get_StackTrace()
at Program.<>c.<Main>b__4_0(Object context) in C:\Program.cs:line 9
at Program.BFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 44
at Program.<>c__DisplayClass4_1.<Main>b__1(Object context) in C:\Program.cs:line 17
at Program.AFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 35
at Program.<>c__DisplayClass4_1.<Main>b__1(Object context) in C:\Program.cs:line 17
at Program.Main() in C:\Program.cs:line 21
(略)
B End
A End
結果を見ていただくとわかるのですがスタックトレースに Program.<>c.<Main>b__4_0(Object context)
といった呼び出しが現れていますが、単にキャプチャーして渡すだけのメソッドなのでユーザーにはほぼ意味のないものです。とはいえコード上でラムダを挟んでいるので出てくるのは当然です。
そこで何とかしてこのような些末な呼び出しをスタックトレースから隠すためのハックを今回ご紹介します。
StackTraceHiddenAttribute
を使う.NET Core 2.0 には StackTraceHiddenAttribute
という属性が追加されていて、その属性のついたメソッドはスタックトレースから除外されるようになっています。
ということはこれを使えば解決しそうですが残念ながらこの属性は internal
です。効果を考えたらそんなカジュアルに使われても困るので内部向けに用意したというところでしょうか。
とはいえそれでも動的アセンブリ生成なら無理やり属性を引っ張り出してくっつけることができるはず…!イメージとしては次のようなヘルパークラスの InvokeNext
の部分を動的に作ってうまいことするような感じです。
/// <summary>
/// 次のフィルター呼び出しと、フィルターのメソッドを保持するクラス。
/// 以前のラムダのキャプチャーと同等。
/// </summary>
public class InvokeHelper
{
/// <summary>フィルターのメソッド</summary>
public Action<object, Action<object>> Invoke;
/// <summary>次のフィルターの呼び出しとなるデリゲート</summary>
public Action<object> Next;
public InvokeHelper(Action<object, Action<object>> invoke, Action<object> next)
{
Invoke = invoke;
Next = next;
}
[StackTraceHidden]
public void InvokeNext(object context)
{
Invoke(context, Next);
}
}
ということでILをペコペコ書きます。
/// <summary> | |
/// 次のフィルター呼び出しと、フィルターのメソッドを保持するクラス。 | |
/// 以前のラムダのキャプチャーと同等。 | |
/// </summary> | |
public class InvokeHelper<TArg1, TDelegateNext> | |
where TDelegateNext : Delegate | |
{ | |
/// <summary>フィルターのメソッド</summary> | |
public Action<TArg1, TDelegateNext> Invoke; | |
/// <summary>次のフィルターの呼び出しとなるデリゲート</summary> | |
public TDelegateNext Next; | |
private static readonly Func<InvokeHelper<TArg1, TDelegateNext>, TDelegateNext> InvokeNextFactory; | |
public InvokeHelper(Action<TArg1, TDelegateNext> invoke, TDelegateNext next) | |
{ | |
Invoke = invoke; | |
Next = next; | |
} | |
static InvokeHelper() | |
{ | |
var fieldInvoke = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Invoke"); | |
var fieldNext = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Next"); | |
var methodInvoke = typeof(Action<TArg1, TDelegateNext>).GetMethod("Invoke"); | |
// 動的にアセンブリを作る | |
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"InvokeHelper-{typeof(TArg1).FullName}"), AssemblyBuilderAccess.RunAndCollect); | |
var module = asmBuilder.DefineDynamicModule("DynamicModule"); | |
var type = module.DefineType("InvokeHelper"); | |
// メソッドを定義する | |
// 内容は (arg1) => helper.Invoke(arg1, helper.Next); と同じになる | |
var method = type.DefineMethod("InvokeNext", MethodAttributes.Static | MethodAttributes.Public, typeof(void), new[] { typeof(InvokeHelper<TArg1, TDelegateNext>), typeof(TArg1) }); | |
// StackTraceHiddenAttribute を引っ張り出してきてメソッドにくっつけておく | |
var attrStacktraceHiddenAttribute = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute"); | |
method.SetCustomAttribute(new CustomAttributeBuilder(attrStacktraceHiddenAttribute.GetConstructor(Array.Empty<Type>()), Array.Empty<object>())); | |
{ | |
var il = method.GetILGenerator(); | |
// invoke = arg0.Invoke; | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldInvoke); | |
// next = arg0.Next; | |
// invoke(arg1, next); | |
il.Emit(OpCodes.Ldarg_1); | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldNext); | |
il.Emit(OpCodes.Callvirt, methodInvoke); | |
il.Emit(OpCodes.Ret); | |
} | |
// 保持している Invoke/Next のペアから次の next のデリゲートを生成するファクトリーを作る | |
InvokeNextFactory = (helper) => (TDelegateNext)type.CreateType().GetMethod("InvokeNext").CreateDelegate(typeof(TDelegateNext), helper); | |
} | |
/// <summary>保持した値を使用するフィルターのnextに渡すためのデリゲートを生成する</summary> | |
public TDelegateNext GetDelegate() => InvokeNextFactory(this); | |
} |
そしてフィルターチェーンを作るところをヘルパー経由に書き換え。
foreach (var filter in filters.Reverse())
{
next = new InvokeHelper<object, Action<object>>(filter.Invoke, next).GetDelegate();
}
そして実行すると…。
A Begin
B Begin
at System.Environment.get_StackTrace()
at Program.<>c.<Main>b__4_0(Object context) in C:\Program.cs:line 9
at Program.BFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 44
at Program.AFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 35
at Program.Main() in C:\Program.cs:line 21
(略)
B End
A End
ばっちり BFitler
と AFitler
の間にあった呼び出し行が消えました!めでたしめでたし。
案1でめでたしめでたしとなるかと思いきや、実は案1を試している間に気づいたのですが StackTraceHiddenAttribute
をつけなくても消えます。案1の下記の二行を削除してみても結果は変わりません。
var attrStacktraceHiddenAttribute = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute");
method.SetCustomAttribute(new CustomAttributeBuilder(attrStacktraceHiddenAttribute.GetConstructor(Array.Empty<Type>()), Array.Empty<object>()));
どうも CoreCLR の中も少し調べてみたのですがよくわからず、動的生成されたメソッドがスタックトレース的に何らかの特別扱いされることがあるようです。
というわけで動的にメソッドを定義できれば理屈は謎ですが消えますし、アセンブリを生成しなくとも DynamicMethod
で事足ります。
/// <summary> | |
/// 次のフィルター呼び出しと、フィルターのメソッドを保持するクラス。 | |
/// 以前のラムダのキャプチャーと同等。 | |
/// </summary> | |
class InvokeHelper<TArg1, TDelegateNext> | |
where TDelegateNext : Delegate | |
{ | |
/// <summary>フィルターのメソッド</summary> | |
public Action<TArg1, TDelegateNext> Invoke; | |
/// <summary>次のフィルターの呼び出しとなるデリゲート</summary> | |
public TDelegateNext Next; | |
private static readonly Func<InvokeHelper<TArg1, TDelegateNext>, TDelegateNext> InvokeNextFactory; | |
public InvokeHelper(Action<TArg1, TDelegateNext> invoke, TDelegateNext next) | |
{ | |
Invoke = invoke; | |
Next = next; | |
} | |
static InvokeHelper() | |
{ | |
var fieldInvoke = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Invoke"); | |
var fieldNext = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Next"); | |
var methodInvoke = typeof(Action<TArg1, TDelegateNext>).GetMethod("Invoke"); | |
var method = new DynamicMethod("InvokeNext", typeof(void), new[] { typeof(InvokeHelper<TArg1, TDelegateNext>), typeof(TArg1) }, restrictedSkipVisibility: true); | |
{ | |
var il = method.GetILGenerator(); | |
// invoke = arg0.Invoke; | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldInvoke); | |
// next = arg0.Next; | |
// invoke(arg1, next); | |
il.Emit(OpCodes.Ldarg_1); | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldNext); | |
il.Emit(OpCodes.Callvirt, methodInvoke); | |
il.Emit(OpCodes.Ret); | |
} | |
InvokeNextFactory = (helper) => (TDelegateNext)method.CreateDelegate(typeof(TDelegateNext), helper); | |
} | |
/// <summary>保持した値を使用するフィルターのnextに渡すためのデリゲートを生成する</summary> | |
public TDelegateNext GetDelegate() => InvokeNextFactory(this); | |
} |
[MethodImpl(MethodImplOptions.AggressiveInlining)]
を使うStackTraceHiddenAttribute
のあたりのコードを見ていて気が付いたのですが .NET Core 3.0 以降では [MethodImpl(MethodImplOptions.AggressiveInlining)] がついているかどうかもチェックしています。
これは通常JITでインライン化されるとスタックトレースから消えてしまうけれども、Tiered Compilation の Tier 0 ではインライン化されないので表示されてしまったりして一貫性がないので属性がついているものは丸ごと非表示にしてしまおうということのようです。
ということで裏技っぽいですが [MethodImpl(MethodImplOptions.AggressiveInlining)]
をつければ非表示になります。内容的にもインライン化されたところで困るものでもないですしよさそうです。
ヘルパークラスのイメージとしてはこんな感じです。
public class InvokeHelper
{
/// <summary>フィルターのメソッド</summary>
public Action<object, Action<object>> Invoke;
/// <summary>次のフィルターの呼び出しとなるデリゲート</summary>
public Action<object> Next;
public InvokeHelper(Action<object, Action<object>> invoke, Action<object> next)
{
Invoke = invoke;
Next = next;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InvokeNext(object context)
{
Invoke(context, Next);
}
}
ということでこれをまとめると .NET Core 3.0 以前では動的メソッド定義、それ以外では AggressiveInlining
をつけておくというのがよさそうです。シンプルですし、もし挙動が変わってスタックトレースが出るようになっても AggressiveInlining
がついているだけなのでプログラムの挙動には影響を与えないのもよいですね。
/// <summary> | |
/// 次のフィルター呼び出しと、フィルターのメソッドを保持するクラス。 | |
/// 以前のラムダのキャプチャーと同等。 | |
/// </summary> | |
class InvokeHelper<TArg1, TDelegateNext> | |
where TDelegateNext : Delegate | |
{ | |
/// <summary>フィルターのメソッド</summary> | |
public Action<TArg1, TDelegateNext> Invoke; | |
/// <summary>次のフィルターの呼び出しとなるデリゲート</summary> | |
public TDelegateNext Next; | |
private static readonly Func<InvokeHelper<TArg1, TDelegateNext>, TDelegateNext> InvokeNextFactory; | |
public InvokeHelper(Action<TArg1, TDelegateNext> invoke, TDelegateNext next) | |
{ | |
Invoke = invoke; | |
Next = next; | |
} | |
static InvokeHelper() | |
{ | |
// .NET Core 2.x は .NET Core 4.0.xxxx を返してくる | |
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Core 4")) | |
{ | |
// 動的メソッド定義 | |
var fieldInvoke = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Invoke"); | |
var fieldNext = typeof(InvokeHelper<TArg1, TDelegateNext>).GetField("Next"); | |
var methodInvoke = typeof(Action<TArg1, TDelegateNext>).GetMethod("Invoke"); | |
var method = new DynamicMethod("InvokeNext", typeof(void), new[] { typeof(InvokeHelper<TArg1, TDelegateNext>), typeof(TArg1) }, restrictedSkipVisibility: true); | |
{ | |
var il = method.GetILGenerator(); | |
// invoke = arg0.Invoke; | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldInvoke); | |
// next = arg0.Next; | |
// invoke(arg1, next); | |
il.Emit(OpCodes.Ldarg_1); | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Ldfld, fieldNext); | |
il.Emit(OpCodes.Callvirt, methodInvoke); | |
il.Emit(OpCodes.Ret); | |
} | |
InvokeNextFactory = (helper) => (TDelegateNext)method.CreateDelegate(typeof(TDelegateNext), helper); | |
} | |
else | |
{ | |
// MethodInlining | |
InvokeNextFactory = (helper) => | |
{ | |
var invokeNext = new Action<TArg1>(helper.InvokeNext); | |
return (TDelegateNext)Delegate.CreateDelegate(typeof(TDelegateNext), invokeNext.Target, invokeNext.Method); | |
}; | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
private void InvokeNext(TArg1 context) | |
{ | |
Invoke(context, Next); | |
} | |
/// <summary>保持した値を使用するフィルターのnextに渡すためのデリゲートを生成する</summary> | |
public TDelegateNext GetDelegate() => InvokeNextFactory(this); | |
} |
このハックは実際にMagicOnionのフィルター周りに入れていてスタックトレースの見やすさを向上させています。
スタックトレースは開発において重要な情報なのでノイズは少なければ少ないほうが望ましいですし、アプリケーションの基盤に近い部分ではそういった開発体験を考慮しておくのは重要だと思います。今回のようなハックも機会があればお役立てください。
なお、あくまでハックなのでいつまで効果があるかといった点は保証できませんのであらかじめご了承ください。