C#
でプログラムを書くときは当たり前のように名前空間を使用していると思います。
例えば以下のような場合、System
名前空間を使用しています。
System.Console.WriteLine("test");
プログラムの先頭にusing System;
を書いている場合は毎回System
を書きませんがこれは先頭部分で名前空間を使用しています。
今回は名前空間を全く使用せずにプログラミングができるかどうかを検証していきます。
レギュレーション
レギュレーションは以下の通りです。
- クラスにアクセスするときに名前空間をつけてはならない
-
using
ディレクティブを使用してはならない -
dynamic
を使用しない
上記以外のことなら何をしてもです。
dynamic
を使用しないというレギュレーションは特に意味ありませんがdynamic
無しのほうが面白そうなのでつけました。
ターゲットフレームワーク
.NET Core 2.0
ゴール
文字列を画面に表示する。
名前空間禁止の難しさ
全く名前空間を使わないのはなかなか難しいです。
基本的に標準ライブラリのクラスに直接アクセスすることはできません。
名前空間を書かなくてもアクセスできるものは以下の通りです。
-
int
やstring
のようなビルトインタイプ -
typeof
式によるSystem.Type
- 配列やタプルのようにリテラルが用意されているもの
- 上記のものからメンバを通してアクセスできるもの
これ以外のものは使いたくても使えません。
例えばList
もDictionary
も何一つとして使用することができません。
既存のインターフェースを実装した型を作成することもできません。
実装する
基本方針
画面に文字を表示するためにはSystem.Console.WriteLine(System.String)
を呼び出す必要があります。
何とかしてこれをリフレクション経由で呼び出します。
リフレクションと聞くとtypeof(System.Console)
と行きたいところですがこれはできません。
どうにかしてSystem.Console
のSystem.Type
を取得しなければなりません。
今回はSystem.Reflection.Assembly
を通してSystem.Console
を取得する方法を考えてみます。
通常System.Reflection.Assembly
はSystem.Reflection.Assembly.GetEntryAssembly
やSystem.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.GetEntryAssembly
のMethodInfo
を取得するのですが、
そのためには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
を取得することができません。
System.Reflection.RuntimeAssembly
はSystem.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;
}
}
これで好きにアセンブリのロードができます
foreach (var assemblyName in assembly.GetReferencedAssemblies())
{
var loadedAssembly = assemblyLoadMethodInfo.Invoke(null, new[] { assemblyName });
}
実はまだ問題があります。
.NET Core 2.0
でこのコードを実行すると例外が発生します。
TargetException
でググるとインスタンスのメソッドを第一引数null
で呼んでいるなどの情報が見つかるのですが、今回の場合は確実にスタティックなメソッドです。
スタックトレースを見るとRuntimeMethodInfo.InvokeArgumentsCheck
が例外を投げていることが分かりました。
ソースコードを参考にするとどうやらm_invocationFlags
にINVOCATION_FLAGS_NO_INVOKE
が入っていると例外が投げられるようです。
打つ手がなくなった気がするのですがとりあえずm_invocationFlags
のINVOCATION_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
になってたりしますが些細な問題です。
すべての関数はリフレクション経由で呼び出せばいいだけです。
これで終了です。
????????
よくわからないですが、実際にプログラム中でSystem.Console
が使用されていないと参照アセンブリに含まれないようです。
仕方ないので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" });
今度こそ終わりです
まとめ
とても大変だった割にはあんまりおもしろくなかったかもしれません。
私の知識不足で微妙な感じになってしまいました。
今回は文字列表示だけでしたがリフレクションを駆使すればもっと高度なこともできるのではないでしょうか。
通常の方法では例外のキャッチを実現することはできないと思いますが、ILの動的生成を使えば解決可能なのかなという気がします。
(やる気はないので他の人に任せます。)
ちなみにターゲットを.NET Framework
に変更するとSystem.Console
がmscorlib
に含まれるためGetReferencedAssemblies
の手法で動きます。
またSystem.Console
アセンブリロードができないため.NET Core
用のコードのままでは失敗してしまいます。
今回書いたソースコードは以下にあります。
yaegaki/CSharp-HelloWorld-WithoutNamespace