ぷろじぇくと、みすじら。

2025-06-30

Created at:

お財布忘れ太郎。


componentize-dotnet で wasm 吐くの、.NET 10 Preview 入れていれば、テンプレートインストール、dotnet new, dotnet build だけで出来るようになってたので良さそう。


ビックカメラSuicaカード申し込み太郎。Nintendo Switch 2…というのもあるけど1番使う家電量販店なので。そして折角なので JCB です。

2025-06-29: 文具女子博, AT living II

Created at:

文具女子博 2025という文房具をたくさん展示、販売しているイベントに。何が女子なのかはよくわかってません。

最近は紙に書くことをしないので文房具は良さそうだけど使わない…と冷静になりがちなので買うものはないかと思っていたけど、意外と買ったかも。

LIHIT LAB. のクリアファイルのカバーは何というかすごい。クリアファイルのカバーって。いやわかりますが。つい買ってしまった…。

標識アクスタキーホルダー、クリアファイルカバー、シリコンケース、スティックホチキスと芯、ミニバインダー、黒板消し型画面クリーナー


有楽町のヒューリックホールで LAWSON presents 豊崎愛⽣ カバーコンサート2025 AT living Ⅱ 〜sing of youth〜 夜の部。

カバーコンサートは定期的にやって欲しいですね。あとおからじ終わったことで生のソロトークを聞ける機会が減ったのが惜しい…。

会場限定・ピクチャーチケット


日記エントリーのメタデータに date を指定していなかったのでオーダーが破滅してたのに今さら気づきました。あとで適当になおします。

2025-06-28

Created at:

今日もネコチャンはクソネコムーブをキメる。


今日も流行りのエージェンティックコーディングで雑アプリを作ってみたり。今回は Android で動かしたいのでとりあえず .NET MAUI にしてみたけど、まあ意外と目的は達成できた。多少のデバッグガイドは必要でしたが。

最初 Android エミュレーターで動かすのが面倒だから iOS で多少動くところまでやってから Android のほうで作業し始めたけど、ちゃんとクロスプラットフォームらしくなんとなくスッと動いてちょっと感動。

特に公開するわけではないものなら自分でコードを書かない分、雑なコードでも良いというか、自分でやるときのちゃんと設計や実装をしたくなる葛藤と戦わないですむのがいいかも。

あとはデバッグまで自動運転して欲しいけど、CLI, Web, ネイティブ GUI の順番でお膳立てがめんどそうかも。


mono 見てたら天城さんの声が。来期は主役もあるしイイデスネ。

2025-06-27

Created at:

せこんさんとか kyoさんとかのとこをみてて、100周ぐらい回ってブログというか日記かもと思い、久々に更新できるようにしてみたり。

今まで更新がめんどくさくなっていた理由としてそもそも VSCode で更新して GitHub にプッシュする、というフローがだるくて、特に画像を貼りたいと思うと面倒が加速する…という問題がありました。

技術的な記事なら Zenn とかでいいか、みたいなところもあり。

というわけでなにか別なツールはないものか探していたのですが、キーボードショートカットでペーストしたり、Git 上に置いたりができるようなやつはそんなになくて、自分で作るのもなーと悩んでいたのです。

しかし今 AI が盛り上がっている…ということで丸投げして GitHub のリポジトリーを直接編集するエディターを作ってもらえばいいのでは?と思って試したら解決。すごい。自分だけのツールがお手軽に作れるのはいい時代ですね。

サイトそのものの見た目も調整したいけどまあそれはのちのち。


会社でつかっている Surface Precision Mouse のホイールスクロールが壊れて無段階からもどらなくなってしまって死。


昨日のことではあるけどやっと年金生活に。ほぼデイリーぐらいだけど2年はかからなかった程度。

ランク60

ランク60報酬受け取り

Azure Container Apps (Preview) でカスタムドメインに必要な証明書を Cloudflare で作る

Created at:

tl;dr

Azure Container Apps にカスタムドメインを登録時、証明書が必要

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 形式で手元に保存します。特に秘密鍵はページを閉じると確認できなくなるのでしっかり保存しておきます。

オリジン証明書を pfx 形式 (PKCS#12) に変換する

次に発行した証明書と秘密鍵を Container Apps に登録します。ところが登録時にパスワードを求められたりしてそのままでは取り込めないので OpenSSL で pfx 形式 (PKCS#12) に変換します。

cat certificate.pem private.pem > origin.pem
openssl pkcs12 -export -in origin.pem -out origin.pfx

オリジン証明書を Container Apps に登録する

pfx 形式の証明書を作成したら Container Apps に登録します。Azure Portal の コンテナー アプリ環境 → (コンテナーアプリ) → 証明書 で “証明書の追加” で追加を行えます。

注意点として現時点では “証明書名” に大文字アルファベットを含む文字列を指定すると “この証明書名は既に使用されています。別の証明書名をお試しください。” という謎のエラーが出るので小文字で入力してください。

カスタムドメインの設定

証明書が作成出来たら後はカスタムドメインの設定を行い、証明書として先ほど登録したものを選択すれば完了です。

まとめ

Container Apps が Managed Certificates にはやく対応してほしいですね。

Source Generator の Visual Studio 2019 v16.9 での新 API

Created at:

Source Generator を触っていて気づいたのですが Visual Studio 2019 version 16.9 の段階でいくつか API が追加されていました。

追加されたものは次のようなあるといいよねといった API が増えている感じです。

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月頭時点では使うにはちょっと早いかもしれません。

Blazor のコンポーネントのステートについて

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 と OnParametersSet の違い

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パターンで発生します。

3パターンと書きましたが2つ目と3つ目は暗黙的に StateHasChanged を呼び出しているというのが実際のところです。

StateHasChanged を呼び出したとき

StateHasChanged はステートが変更されたことを通知するメソッドです。このメソッドを呼び出すと再レンダリング候補としてマークされます。何が起こるのかは後ほど少し説明します。

多くのケースでは StateHasChanged を明示的に呼び出す必要はありませんが、ロジック側から表示を更新したいケースでは呼び出しを必要とします。

例えばサーバーからのデータ受信やタイマー処理といったユーザー操作によるイベントではないが表示を更新するようなパターンです。この場合は自らステートが変更されたので再レンダリングしてほしいという意思を伝えるために StateHasChanged を呼び出す必要があります。

イベント発火時 (@bind や @onclick とか)

冒頭でプロジェクトテンプレートの 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 が変わったとき

コンポーネントの 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 が呼ばれると) 何が起こるのか

StateHasChanged メソッドを呼ぶと次のような流れで処理が実行されます。

初回と似ていますが異なるのは初期化周りのメソッド(OnInitialized)が呼び出されないことと ShouldRender が呼ばれることです。

ShouldRender メソッドはレンダリングする必要があるかどうかを返すことができるものです。このメソッドのデフォルト実装は常に true を返しますが、カスタムコンポーネントは再レンダリングを抑える目的でオーバーライドできます。

StateHasChanged の詳細

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 の仕組みをあまり知らずに使っていても画面が更新されるので思わぬところでハマる可能性もあります。そういった罠を踏む前にある程度流れを抑えておくことをお勧めします。

Blazor Web Assembly を publish すると AssemblyResolutionException でエラーとなる

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.CommonSQLitePCLRaw.bundle_green パッケージを PrivateAssets="all" として参照しているため、実際のパッケージにはパッケージ参照として SQLitePCLRaw.bundle_green が含まれないことでアプリケーションから参照したときにパッケージが解決されないのでアセンブリが見つからないということが発生します。

つまりパッケージの依存としては扱わないもののアセンブリを参照はあるという状況で発生します。必須ではないパッケージなどの場合にはこういった構成になります。

解決方法

解決方法としては次の2つの方法があります。

1. 必要なパッケージをプロジェクトで参照する

見つからないといわれているアセンブリが含まれているパッケージをプロジェクトから参照します。

2. IL Linker を無効にする

SQLitePCLRaw.bundle_green のようなものは参照してもエラーとなるのでそういった場合には IL Linker 自体を無効にします。

https://docs.microsoft.com/ja-jp/aspnet/core/blazor/host-and-deploy/configure-linker?view=aspnetcore-3.1

<PropertyGroup>
  <BlazorWebAssemblyEnableLinking>false</BlazorWebAssemblyEnableLinking>
</PropertyGroup>

ARM64 Windows 版 Visual Studio Code をビルドする

Created at:

Surface Pro X も発売され早数か月、今や多くの方が ARM64 版 Windows をご利用中かと思いますが(要出典)、Visual Studio Code の ARM64 版はまだリリースされていない状況です。もちろん x86 版をエミュレーションで利用できますが、Electron アプリはパフォーマンス的にかなり不利ですので ARM64 ネイティブなものが欲しいところです。

Visual Studio Code のリポジトリで ARM64 対応がないかなと眺めていたところ ARM64 ビルドをやっていく PR が作られているのを発見し、マージされるのを watch していたのですが先日ついにマージされました。

Add gulp targets, fix build for Windows on Arm. by richard-townsend-arm · Pull Request #85326 · microsoft/vscode

というわけで自分で 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 を動かすのに必要です。

ビルド準備 (Node)

ツールがそろったらコードを 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 のビルド設定を変更しておきます。

まず前提として 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月現在ビルドできません)。

ARM64 版の制約

Microsoft Visual Studio Code との違い

ビルドの準備の途中でも書きましたが、手元でビルドしたものは Microsoft からリリースされるものとは異なります。

名前が違うこともあり設定は Visual Studio Code とは別の場所に保存されるものになります(つまり公式リリースが出た場合でも設定は別になります)。

ライセンスが違うのが実はちょっと罠なので注意が必要です。というのも Microsoft がリリースしている VSCode 拡張のライセンスは Microsoft 公式から配布されている Microsoft Visual Studio Code とともに使うことが許可されているものがあります(例えば Remote とか)ので注意してください。

.NET Core でスタックトレースからメソッド呼び出しを隠す

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) といった呼び出しが現れていますが、単にキャプチャーして渡すだけのメソッドなのでユーザーにはほぼ意味のないものです。とはいえコード上でラムダを挟んでいるので出てくるのは当然です。

そこで何とかしてこのような些末な呼び出しをスタックトレースから隠すためのハックを今回ご紹介します。

案1. 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

ばっちり BFitlerAFitler の間にあった呼び出し行が消えました!めでたしめでたし。

案2. 動的メソッド生成

案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);
}

案3. [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のフィルター周りに入れていてスタックトレースの見やすさを向上させています。

ハックなし

Before

ハックあり

After

スタックトレースは開発において重要な情報なのでノイズは少なければ少ないほうが望ましいですし、アプリケーションの基盤に近い部分ではそういった開発体験を考慮しておくのは重要だと思います。今回のようなハックも機会があればお役立てください。

なお、あくまでハックなのでいつまで効果があるかといった点は保証できませんのであらかじめご了承ください。