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 リポジトリの今トリビュートガイドのビルド手順 に従って環境を用意します。
Windows 10 (x64)
Git for Windows
Node.js (x64, 10.x 以上 12.x 以下)
Yarn
Python 2.7
Visual Studio 2017 ビルドツール (C/C++ コンパイラー)
Visual C++ compilers and libraries for ARM64
Visual C++ 2010 Redistributable (x86)
まず、クロスコンパイルする形になるので 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 075 a1d0ada..093 f517644 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 とか)ので注意してください。
このエントリーは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" );
}
}
そのフィルターをつなげて呼び出すというような仕組みです。
var filters = new IFilter[] { new AFilter(), new BFilter() };
Action<object > next = (context) =>
{
Console.WriteLine(Environment.StackTrace);
};
foreach (var filter in filters.Reverse())
{
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
の部分を動的に作ってうまいことするような感じです。
public class InvokeHelper
{
public Action<object , Action<object >> Invoke;
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をペコペコ書きます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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
の間にあった呼び出し行が消えました!めでたしめでたし。
案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
で事足ります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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
{
public Action<object , Action<object >> Invoke;
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
がついているだけなのでプログラムの挙動には影響を与えないのもよいですね。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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 のフィルター周りに入れていてスタックトレースの見やすさを向上させています。
ハックなし
ハックあり
スタックトレースは開発において重要な情報なのでノイズは少なければ少ないほうが望ましいですし、アプリケーションの基盤に近い部分ではそういった開発体験を考慮しておくのは重要だと思います。今回のようなハックも機会があればお役立てください。
なお、あくまでハックなのでいつまで効果があるかといった点は保証できませんのであらかじめご了承ください。
« 新しいエントリ 1 2 3 4 5 … 40 古いエントリ »