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

既存の.NET アセンブリにMono.Cecilでコードを差し込む

Created at: | Tag: .NET C#

すでにビルドされた.NETのアプリケーションやもしかするとUnityのゲームかもしれませんが、そういった一度作ってしまったアプリケーションに後からコードを差し込みたくなることがまれにあります。

例えばビルドしたアプリケーションをテストするために、後からコードを差し込んで診断用のコードを動かしたいといったパターンです。テスト用のコードをアプリケーションには含めておきたくない場合にはプラグインするかコードを差し込むかになります。

ここではMono.Cecilを利用してコードを後から差し込む方法を説明します。

Mono.Cecilとは

Mono.Cecilは.NETアセンブリを解析したり、書き換えたりするための便利なライブラリです。このライブラリを使うことで既存のコードを書き換えて新しいアセンブリを出力できます。

簡単な差し込み方

Mono.Cecilでコードを書き換えて既存のコードに自分のコードを差し込んでみます。

例えば以下のようなプログラム ConsoleApp1.exe があるとします。

using System;

namespace ConsoleApp1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello Konnichiwa!");
        }
    }
}

そこでこの Console.WriteLine の呼び出しより前に処理を差し込みます。差し込みたい処理は以下のようなコードで別なアセンブリ(.dll)にしておきます。

using System;
namespace ClassLibrary1
{
    public class Class1
    {
        /// <summary>
        /// 差し込んだ側から呼び出してほしいメソッド
        /// </summary>
        public static void Initialize()
        {
            Console.WriteLine("差し込み案件です!");
        }
    }
}

そしてstaticなメソッドにしておくことで呼び出すためのILを最低限とでき、コードを差し込んだ側のスタックやローカル変数のつじつま合わせを考えなくてよくなるので単純になります。もちろん元のコードやオブジェクトに依存した処理を行いたいときは何らかの処理が必要となります。

ILの書き換えをする

というわけでILの書き換えをして新たに作ったアセンブリのメソッドを呼び出すように書き換えます。具体的には以下のようなコードとなります。

// ConsoleApp1.exe と ClassLibrary1.dll があるディレクトリ
var baseDir = @"Path\To\ConsoleAppDirectory";

// 対象となるアセンブリと型、メソッド名
var targetAssemblyName = "ConsoleApp1.exe";
var targetTypeName = "ConsoleApp1.Program";
var targetMethodName = "Main";

// 差し込みたいアセンブリと型、メソッド名
var injectAssemblyName = "ClassLibrary1.dll";
var injectTypeName = "ClassLibrary1.Class1";
var injectMethodName = "Initialize";

// 出力アセンブリ名
var outputAssembly = "ConsoleApp1_Injected.exe";

var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);

var asm = AssemblyDefinition.ReadAssembly(
    Path.Combine(baseDir, targetAssemblyName),
    new ReaderParameters
    {
        AssemblyResolver = assemblyResolver,
    }
);
var targetType = asm.MainModule.GetType(targetType);
var targetMethod = targetType.Methods.First(x => x.Name == targetMethodName);

var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
var injectType = asmInject.MainModule.GetType(injectTypeName);
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);
var injectMethodRef = targetMethod.Module.Import(injectMethod);

var ilProcessor = targetMethod.Body.GetILProcessor();
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));

asm.Write(Path.Combine(baseDir, outputAssembly));

まず、AssemblyResolverを用意します。これはCecilでアセンブリを追加したりするときに正しく見つけるために必要となります。

var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);

次に差し込み先となるアセンブリを読み込んで、型とメソッド(差し込み先)を取得します。

// 差し込み先アセンブリを読む
var asm = AssemblyDefinition.ReadAssembly(
    Path.Combine(baseDir, targetAssembly),
    new ReaderParameters
    {
        AssemblyResolver = assemblyResolver,
    }
);
// 型を探してくる
var targetType = asm.MainModule.GetType(targetType);
// 差し込み先メソッドを探してくる
var targetMethod = targetType.Methods.First(x => x.Name == targetMethod);

差し込む先が見つかったら次は差し込みたい処理を読み込んで探します。

// 差し込みたいアセンブリを読み込む
var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
// 差し込みたい処理の型を探す
var injectType = asmInject.MainModule.GetType(injectTypeName);
// 差し込みたいメソッドを探す
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);

差し込み先に参照を追加し、その差し込み先内でメソッドの参照を得ます。これは差し込み先で差し込まれる側のアセンブリに対する参照情報を使うので必要です(MethodDefinitionは直接アセンブリをまたぐことはできない)。

var injectMethodRef = targetMethod.Module.Import(injectMethod);

そして呼び出すためのコードを差し込み先のメソッドに書き込みます。

// 差し込み先のメソッドのILを操作するILProcessorを取得
var ilProcessor = targetMethod.Body.GetILProcessor();
// 一番最初のIL Instructionの前に差し込み先メソッドを呼ぶ call opcode を差し込む
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));

これが…

IL_0000:  nop
IL_0001:  ldstr      "Hello Konnichiwa!"
IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
IL_000b:  nop
IL_000c:  ret

こうじゃ。

IL_0000:  call       void [ClassLibrary1]ClassLibrary1.Class1::Initialize()
IL_0005:  nop
IL_0006:  ldstr      "Hello Konnichiwa!"
IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
IL_0010:  nop
IL_0011:  ret

最後に書き換えたアセンブリを書き出して終了です。

asm.Write(Path.Combine(baseDir, outputAssembly));

実行結果

これで書き換えたアセンブリ (ConsoleApp1_Injected.exe) を実行すると以下のような出力となります。

差し込み案件です!
Hello Konnichiwa!

まとめ

Mono.Cecilで簡単にアセンブリを書き換えられるのでビルドしてしまったアプリの魔改造がはかどります。普段、ビルド後のアセンブリを書き換えたい機会は多くないと思いますがいざというときに処理を差し込む一つの方法としては覚えておいて損はないのではないでしょうか。

サンプルコードは https://github.com/mayuki/Sample.MonoCecil に置いておきますのでどうぞご利用ください。

Google HomeからBRAVIA(Android TV)の音量を変更できない問題

Created at: | Tag: GoogleHome

Google Homeでは「OK Google, (デバイス)の音量を何%にして」と呼びかけるとAndroid TVというかChromecastであれば音量設定できる、ということ機能があります。

ところがSONYのBRAVIA (Android TV)と外部サウンドシステム(サウンドバーなど)を組み合わせていると正常に動作しないようです。Nougatな機種ではわかりませんが、現状初代のMarshmallowな機種(X8500C)では挙動がおかしく正常に設定できません。

解決方法

いろいろ試した結果、この問題を解決する方法は2つありますが根本解決はできないようです(場合によってはどうにもならない…)。

一つはサウンドシステムの利用をやめてテレビからのオーディオ出力にする、ですがこれは現実的ではありません。

もう一つはサウンドシステムがHT-NT3のような単体でChromecastに対応しているのであれば、サウンドシステムの名前を「スピーカー」とすることで「スピーカーの音量を10%にして」ということで直接コントロールできます。

ちなみにHT-NT3を接続して操作した場合には音量10%の指定はテレビで音量10、サウンドバー側でVol 5相当になります。

まあしかし他にもちゃんとキャストされないことがあったり全体的に挙動が怪しいのできびしいですね…。SONYさん頑張ってください…。

Microsoft Edge on iOS Previewがきた

Created at: | Tag: Browser Edge

どのぐらいの人が待っていたのかわかりませんが(少なくとも自分は待っていた)、Microsoft Edgeのモバイル版が発表されました。今までEdgeを利用していてもGoogle Chromeのようにモバイルで同期できないのが不便でしたがこれで解消されるとうれしいですね。

プレビューテストの登録をしていたら利用できるようになったという通知がTestFlightに来たので試してみました。

レンダリングエンジン

なおレンダリングエンジンはEdgeHTMLを移植したわけではないと解説されています。安心なのか面白くないのか何とも言えないところですね。

プレビュー版(2017/10/06、v2.5)でできること & できないこと

使ってみた感じ

プレビュー版ということで粗削りだったりモッサリなところも多いのですがPCと同期できるようになったのはいいですね。今までできなかったことができるようになるということだけでもよい話なので今後に期待です。

まあ逆に言えばGoogle Chrome for iOSと同様に、同期する必要性が薄いならSafariでいいとも言えます。

スクリーンショット

ペタペタ貼っておきます。

新しいタブを開いたところ。Windows 10と同様によくアクセスするサイト、アドレス&検索ボックス、ニュースフィードが表示される。

アドレスバーの横のマイナスっぽいアイコンを押すとQRコードの読み取り、写真を撮って画像検索、写真を選択して画像検索が可能。

現在開いているタブの一覧ページ(フッターボタンの右から2番目のタブっぽいアイコンで開く)。

履歴が同期されるのでWindows 10側で開いたことのあるページが候補として出てくるっぽい。

画面端からスワイプで戻る。

フッターボタンの真ん中のアイコンをタップでPC(Windows 10 Fall Creators Update以降)に送る、Hand Off的なやつ。ここで端末を選択するとそっちのEdgeで開かれます。

右下のその他機能のメニュー。共有はここに突っ込まれています。

お気に入り同期。よくよく考えたらお気に入りほとんど使ってないのでほとんど入ってなかった…。

設定ページです。

プライバシー設定ページ。

検索エンジンの設定ではGoogleも選べるので安心です。

Browser Platform Status Trackerを.NET Core 2.0に更新した

Created at: | Tag: C# .NET ASP.NET Browser

3年ぐらい前からWebブラウザのプラットフォーム機能の開発状況が公開されるようになり、その更新を追いかけて変更内容を一覧するサイトBrowser Platform Status Trackerを公開しています。

最近までASP.NET MVC 5と.NET 4.6で動作していたのですが.NET Coreの機運の高まりを感じたので、.NET Core 2.0 + ASP.NET MVC Core 2.0に移行しました。といっても2か月ぐらい前に一度.NET Core 1.1にして、その後.NET Core 2.0にという感じです。まあそもそもサイトの性質的には動的じゃなくてもいい説もありますがそこは気にせず…。

.NET Core 1.1 + ASP.NET MVC Core化のタイミングで折角なのでHTTPSを有効にして、ドメインもAzureのApp Service(azurewebsites.net)だったので platformstatus.io というドメインへも変更しています。

サイト自体はコンパクトなのでASP.NET Coreへの移行は大した苦労はなかったのですが、やはりRazorテンプレートにヘルパーがなくなった所に関しては既存のASP.NET MVCのアプリを移行する際のハードルになると感じました。数が少ない場合には頑張ってPartialにすればよいのですが、ヘビーに使っている場合には使い勝手も違うので移行不能に近い状態になりそうです。

気づいたこととか

ASP.NET Core 2.0でリバースプロキシの後ろにある場合にスキームを維持したい

Created at: | Tag: C# .NET ASP.NET

ASP.NET Core 2.0でアプリケーションがリバースプロキシの後ろにあるケースで、リバースプロキシがHTTPSで受け、アプリにはHTTPで受け渡すような構成というのはよくあるかと思います。

しかしASP.NET Coreは標準のままでは Request.Scheme には"http"が入ることになり、"https"が入っていなくて不都合が…という状況が発生します。

そんな時は大抵のリバースプロキシが送り出すであろう X-Forwarded-* ヘッダーフィールドを反映するミドルウェアがあるのでそれを有効にすることで、リバースプロキシにアクセスされた際の情報を維持できます。

その機能を有効にするには UseForwardedHeaders メソッドでミドルウェアを使用するようにし、その際にどのヘッダーフィールドを使うかを指定します。次の例は上位サーバーからのリクエストの X-Forwareded-Proto (スキーム)を見るようにするという設定です。

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
});