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

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をペコペコ書きます。

そしてフィルターチェーンを作るところをヘルパー経由に書き換え。

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 で事足ります。

案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 がついているだけなのでプログラムの挙動には影響を与えないのもよいですね。

このハックは実際にMagicOnionのフィルター周りに入れていてスタックトレースの見やすさを向上させています。

ハックなし

Before

ハックあり

After

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

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