Edited at

[C#]絶対名前空間を使ってはいけないC#

More than 1 year has passed since last update.

C#でプログラムを書くときは当たり前のように名前空間を使用していると思います。

例えば以下のような場合、System名前空間を使用しています。

System.Console.WriteLine("test");

プログラムの先頭にusing System;を書いている場合は毎回Systemを書きませんがこれは先頭部分で名前空間を使用しています。

今回は名前空間を全く使用せずにプログラミングができるかどうかを検証していきます。


レギュレーション

レギュレーションは以下の通りです。


  • クラスにアクセスするときに名前空間をつけてはならない


  • usingディレクティブを使用してはならない


  • dynamicを使用しない

上記以外のことなら何をしても:ok_hand:です。

dynamicを使用しないというレギュレーションは特に意味ありませんがdynamic無しのほうが面白そうなのでつけました。


ターゲットフレームワーク

.NET Core 2.0


ゴール

文字列を画面に表示する。


名前空間禁止の難しさ

全く名前空間を使わないのはなかなか難しいです。

基本的に標準ライブラリのクラスに直接アクセスすることはできません。

名前空間を書かなくてもアクセスできるものは以下の通りです。



  • intstringのようなビルトインタイプ


  • typeof式によるSystem.Type

  • 配列やタプルのようにリテラルが用意されているもの

  • 上記のものからメンバを通してアクセスできるもの

これ以外のものは使いたくても使えません。

例えばListDictionaryも何一つとして使用することができません。

既存のインターフェースを実装した型を作成することもできません。


実装する


基本方針

画面に文字を表示するためにはSystem.Console.WriteLine(System.String)を呼び出す必要があります。

何とかしてこれをリフレクション経由で呼び出します。

リフレクションと聞くとtypeof(System.Console)と行きたいところですがこれはできません。

どうにかしてSystem.ConsoleSystem.Typeを取得しなければなりません。

今回はSystem.Reflection.Assemblyを通してSystem.Consoleを取得する方法を考えてみます。

通常System.Reflection.AssemblySystem.Reflection.Assembly.GetEntryAssemblySystem.AppDomainを通して取得します。

今回はこの手法は使用できないため、typeof(XXX).Assemblyを使用することにしました。

typeof(XXX).Assemblyではそのタイプが定義されているSystem.Reflection.Assemblyのインスタンスを取得することができます。


タイプを探す手法の考察

アセンブリからタイプ情報を探すためにはどうしたらいいでしょうか。

アセンブリのメンバ関数にはGetTypesという関数があります。

この関数はアセンブリに含まれるタイプ情報を列挙する関数なのでこれを使用すればタイプ情報を探すことができそうです。

しかし、GetTypesで取得できるのはアセンブリ内のクラスのみです。

今回の場合ではSystem.Consoleが含まれるアセンブリへの参照が取得できなければなりません。

なのでタイプを探す前にまずアセンブリを探す必要があります。

もう一度アセンブリのメンバ関数を見てみるとGetReferencedAssembliesという関数があります。

この関数はアセンブリが参照しているアセンブリの名前を返すというものです。

名前が分かってもアセンブリへの参照が取得できなければ意味がありません。

そこでアセンブリの名前からSystem.Reflection.Assembly.Loadを使用してアセンブリをロードしアセンブリの参照を取得するという方法をとります。

(本当はSystem.Consoleだけでよいなら存在するアセンブリはバージョンごとに固定なのでGetReferencedAssembliesは必要ないですが汎用的にするためにこの手法をとりました。)


アセンブリのロード

アセンブリのロードも簡単ではなく、ここでもリフレクションをフル活用する必要があります。

まずSystem.Reflection.Assembly.GetEntryAssemblyMethodInfoを取得するのですが、

そのためにはSystem.Reflection.Assemblyのタイプ情報が必要です。

System.Reflection.Assemblyのタイプ情報を取得はSystem.Reflection.AssemblyのインスタンスからGetType関数で取得することができます。

var assemblyType = typeof(Program).Assembly.GetType();

と言いたいところですが実際にはtypeof(Program).Assembly.GetType()で帰ってくるクラスはSystem.Reflection.RuntimeAssemblyのインスタンスになっています。

このままではGetMethodsを呼び出しても目的のMethodInfoを取得することができません。

image.png

System.Reflection.RuntimeAssemblySystem.Reflection.AssemblyのサブクラスなのでBaseTypeを取得して使うことにします。

var assemblyType = typeof(Program).Assembly.GetType().BaseType;

MethodInfoの検索は文字列比較で行います。

// 型を書けないので変数をnullで初期化できない

// 絶対にnullが返ってくる引数でGetMethodを呼び出して型推論させる
var assemblyLoadMethodInfo = assemblyType.GetMethod("XXXX");
// stringを引数に使うほうならGetMethodでも大丈夫だけどAssemblyNameのほうを使用したかったので使わない
// var assemblyLoadMethodInfo = assemblyType.GetMethod("Load", new[] { typeof(string) });
foreach (var method in assemblyType.GetMethods())
{
if (method.ToString() == "System.Reflection.Assembly Load(System.Reflection.AssemblyName)")
{
assemblyLoadMethodInfo = method;
break;
}
}

これで好きにアセンブリのロードができます:point_up:

foreach (var assemblyName in assembly.GetReferencedAssemblies())

{
var loadedAssembly = assemblyLoadMethodInfo.Invoke(null, new[] { assemblyName });
}

実はまだ問題があります。

.NET Core 2.0でこのコードを実行すると例外が発生します。

image.png

TargetExceptionでググるとインスタンスのメソッドを第一引数nullで呼んでいるなどの情報が見つかるのですが、今回の場合は確実にスタティックなメソッドです。

スタックトレースを見るとRuntimeMethodInfo.InvokeArgumentsCheckが例外を投げていることが分かりました。

ソースコードを参考にするとどうやらm_invocationFlagsINVOCATION_FLAGS_NO_INVOKEが入っていると例外が投げられるようです。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs#L495

打つ手がなくなった気がするのですがとりあえずm_invocationFlagsINVOCATION_FLAGS_NO_INVOKEをリフレクションを使って削除できるかどうか試してみます。

ただしこれも簡単ではありません。

m_invocationFlagsはフィールドなのでGetField関数を使うのですが、パブリックではないためSystem.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublicフラグを指定しなければなりません。

フラグの論理和はとても難しいです。

単一のフラグでいいならGetEnumValuesで何とかなりますが論理和の場合はそうもいきません。

これを作るためにはEnum.ToObjectを使用する必要があります。

またリフレクションです。

Enumのタイプ情報の取得は既存のEnumのインスタンスからGetTypeで取得します。

あまりに面倒なので詳細は省きます。

下記の実装を参考にしてください。

// Instance | NonPublicのフラグ作成

var bindingFlagsType = assemblyType.Assembly.GetType("System.Reflection.BindingFlags");
// 既存のEnum(MemberType)からenumの型情報取得
var enumType = assemblyLoadMethodInfo.MemberType.GetType().BaseType;
var toObjectMethodInfo = enumType.GetMethod("ToObject", new[] { enumType.GetType().BaseType, typeof(int) });
const int BINDING_FLAGS_INSTANCE_AND_NONPUBLIC = 36;
var instanceAndNonPublic = toObjectMethodInfo.Invoke(null, new object[] { bindingFlagsType, BINDING_FLAGS_INSTANCE_AND_NONPUBLIC });

var typeType = assemblyLoadMethodInfo.GetType().GetType();
var getFieldMethodInfo = typeType.GetMethod("GetField", new[] { typeof(string), bindingFlagsType });
var invocationFlagsField = getFieldMethodInfo.Invoke(assemblyLoadMethodInfo.GetType(), new object[] { "m_invocationFlags", instanceAndNonPublic });

// invocationFlagsFieldがobjectなのでGet/Setもリフレクション経由
var filedGetValueMethodInfo = invocationFlagsField.GetType().GetMethod("GetValue", new[] { typeof(object) });
var filedSetValueMethodInfo = invocationFlagsField.GetType().GetMethod("SetValue", new[] { typeof(object), typeof(object) });

// INITIALIZEDのフラグ設定
var originalInvocationFlags = filedGetValueMethodInfo.Invoke(invocationFlagsField, new object[] { assemblyLoadMethodInfo });
const int INVOCATION_FLAGS_INITIALIZED = 0x00000001;
var invocationFlags = toObjectMethodInfo.Invoke(null, new object[] { originalInvocationFlags.GetType(), INVOCATION_FLAGS_INITIALIZED });
filedSetValueMethodInfo.Invoke(invocationFlagsField, new object[] { assemblyLoadMethodInfo, invocationFlags });

もうすでに何をやっていたか忘れかけてきましたがアセンブリのロード時にエラーが出ていたのでした。

正直このm_invocationFlagsを書き換えられないと思っていたのですが実行しても特にエラーが起きずに書き換えられてしまいました。

これによってアセンブリのロード時のエラーもなくなりました。


実際にタイプを探す

ここまで来たら後はもう消化試合です。

再帰しながらアセンブリをロードして目的のタイプを探すだけです。


タイプを探す再帰関数

public static object FindType(object assembly, object assemblyLoadMethodInfo, string typeName)

{
var getTypeMethodInfo = assembly.GetType().GetMethod("GetType", new[] { typeof(string) });
var consoleType = getTypeMethodInfo.Invoke(assembly, new[] { typeName });
if (consoleType != null)
{
return consoleType;
}
var getReferencedAssembliesMethodInfo = assembly.GetType().GetMethod("GetReferencedAssemblies");
var assemblies = getReferencedAssembliesMethodInfo.Invoke(assembly, new object[0]) as object[];
foreach (var refAssembyName in assemblies)
{
var invokeMethodInfo = assemblyLoadMethodInfo.GetType().GetMethod("Invoke", new [] { typeof(object), typeof(object[]) });
var refAssembly = invokeMethodInfo.Invoke(assemblyLoadMethodInfo, new object[] { null, new object[] { refAssembyName } });
consoleType = FindType(refAssembly, assemblyLoadMethodInfo, typeName);
if (consoleType != null)
{
return consoleType;
}
}

return null;
}



使用する部分

var consoleType = FindType(assembly, assemblyLoadMethodInfo, "System.Console");

var getMethodMethodInfo = consoleType.GetType().GetMethod("GetMethod", new[] { typeof(string), (new []{ typeof(string) }).GetType() });
var writeLineMethodInfo = getMethodMethodInfo.Invoke(consoleType, new object[] { "WriteLine", new[] { typeof(string) } });
var invokeMethodInfo = writeLineMethodInfo.GetType().GetMethod("Invoke", new [] { typeof(object), typeof(object[]) });
invokeMethodInfo.Invoke(writeLineMethodInfo, new object[] { null, new object[] { "Hello, World!!" } });

ネームスペースが書けないので引数が全部objectになってたりしますが些細な問題です。

すべての関数はリフレクション経由で呼び出せばいいだけです。

これで終了です。

image.png

:cold_sweat:????????

よくわからないですが、実際にプログラム中でSystem.Consoleが使用されていないと参照アセンブリに含まれないようです。:ghost:

仕方ないのでGetReferencedAssembliesを捨てて直接System.Consoleを読み込みます。

// アセンブリのロードは文字列直接のほうを使う

var assemblyLoadMethodInfo = assemblyType.GetMethod("Load", new[] { typeof(string) });

// アセンブリのロードとタイプの取得
var consoleAssembly = assemblyLoadMethodInfo.Invoke(null, new[] { "System.Console" });
var getTypeMethodInfo = consoleAssembly.GetType().GetMethod("GetType", new[] { typeof(string) });
var consoleType = getTypeMethodInfo.Invoke(consoleAssembly, new[] { "System.Console" });

今度こそ終わりです:clap:

image.png


まとめ

とても大変だった割にはあんまりおもしろくなかったかもしれません。

私の知識不足で微妙な感じになってしまいました。

今回は文字列表示だけでしたがリフレクションを駆使すればもっと高度なこともできるのではないでしょうか。

通常の方法では例外のキャッチを実現することはできないと思いますが、ILの動的生成を使えば解決可能なのかなという気がします。

(やる気はないので他の人に任せます。)

ちなみにターゲットを.NET Frameworkに変更するとSystem.Consolemscorlibに含まれるためGetReferencedAssembliesの手法で動きます。

またSystem.Consoleアセンブリロードができないため.NET Core用のコードのままでは失敗してしまいます。

今回書いたソースコードは以下にあります。

yaegaki/CSharp-HelloWorld-WithoutNamespace