🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

リフレクション

リフレクションとは、プログラムの実行過程でプログラム自身の構造を読み取り、編集する事が出来るプロセスのことを指します

Q&A

解決済

2回答

2395閲覧

標準ライブラリ関数の呼び出しで自作メソッドを実行したい

comet7360

総合スコア9

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

リフレクション

リフレクションとは、プログラムの実行過程でプログラム自身の構造を読み取り、編集する事が出来るプロセスのことを指します

0グッド

0クリップ

投稿2021/03/12 00:28

編集2021/03/12 09:02

以下のようなコードにて、Directory.Exists("hoge")の処理を実行したときに、
System.IO.Direcoty.Exists()ではなく、Injection.DirecotryExists()が実行されるようにしたいです。

Main()内のInjection.MethodInjection()にブレークポイントを設定し、デバッグモードでステップ実行を行っていくと、
期待通り、Injection.DirectoryExists()が実行されるのですが、
ブレークポイントを設定しないままデバッグ実行、もしくはデバッグ無しで実行した場合は、
System.IO.Direcoty.Exists()が実行されてしまいます。

ソースコードは同一にも関わらず、実行方法(ステップ実行かそうでないか)によって処理結果が異なっている状況です。
どなたか解決方法をご存じであれば教えていただきたく、よろしくお願いいたしますm(_ _)m

なお恥ずかしながら、私自身ポインタに詳しくなく、Injection.MethodInjection()も
何処かの海外サイト(stack overflow等)からコピペした処理なので、何をやっているか詳しく把握できているわけではありません。。

環境は以下の通りです。
Visual Studio 2019 Professional ver 16.9.1
.NET Core 3.0 のコンソールアプリケーションです。

C#

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("Hello World!"); 6 7 // Directory.Exists()を書き換え 8 { 9 // Directory.Exists()のMethodInfo 10 MethodInfo original = typeof(Directory).GetMethod("Exists"); 11 12 // 自作メソッドのMethodInfo 13 MethodInfo injection = typeof(Injection).GetMethod("DirectoryExists"); 14 15 // Directory.Exists()の書き換え 16 Injection.MethodInjection(original, injection); 17 } 18 19 // ★ここで、ライブラリ関数でなく自作メソッドを呼び出したい★ 20 Directory.Exists("hoge"); 21 } 22 } 23 24 class Injection 25 { 26 private const int BIT_32 = 4; 27 private const int BIT_64 = 8; 28 29 // 自作メソッド 30 public static void DirectoryExists() 31 { 32 Console.WriteLine("DirectoryExists\n"); 33 } 34 35 public static void MethodInjection(MethodInfo original, MethodInfo injection) 36 { 37 RuntimeHelpers.PrepareMethod(original.MethodHandle); 38 RuntimeHelpers.PrepareMethod(injection.MethodHandle); 39 unsafe 40 { 41 switch (IntPtr.Size) 42 { 43 case BIT_64: 44 long* target = (long*)original.MethodHandle.Value.ToPointer()+1; 45 long* source = (long*)injection.MethodHandle.Value.ToPointer()+1; 46 47 if(*target != *source) 48 { 49 // ★ポインタが指すアドレスの中の値を書き換え? 50 *target = *source; 51 } 52 break; 53 default: 54 break; 55 } 56 } 57 } 58 }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

comet7360

2021/03/12 05:31 編集

ありがとうございます。 まさにそこの、Logmanさんの回答をもとに作成したのがMethodInjection()になります。 AndreasさんのHarmonyも試してみましたが上手くいかず。といった状況です。。 また、そのURLでは自作メソッドを自作メソッドで書き換えているので、私の実施したい、 ライブラリ関数を自作メソッドで書き換えるにはまだ何か足りないのかも。。 と思っています。
BluOxy

2021/03/12 04:27

名前空間付きで Directory.Exists を呼び出すのではいけないのでしょうか。目的によっては 現在の質問内容を解決する という手段を取る必要はなく、別の手段を考えた方が良いのではないかと思います。 ※好奇心や学習目的でこの質問をされている場合、このコメントはスルーしてください
comet7360

2021/03/12 05:37

ありがとうございます。 はい、呼び出し側は、あくまでもSystem.IO.Directory.Exists()を実行する体裁でいたいのです。 そして、上記のメソッドが格納されているアドレスを指しているポインタに、 自作メソッドが格納されているアドレスを設定する、ようなことを実現したいです。 目的は、単体テストのためにライブラリ関数を自作のスタブに差し替えることです。
Zuishin

2021/03/12 05:46

単体テストのためなら依存性注入で良いんじゃないかと思いますが、それではいけないんでしょうか? スタックオーバーフローの質問は十年近く前のもので、当時動いていたものが同じコードで動かないなら、環境が変わったからというのが大きな理由のように思います。 さすがにこんなドキュメントもされてない動作にまで互換性を保つ努力が為されているとは思えませんし、仮に何かの拍子で動いても次のバージョンアップで動かなくなる可能性があります。 テストが何の前触れもなく違う結果を出すようになったんじゃ役に立たないでしょう。
BluOxy

2021/03/12 08:10 編集

単体テストが目的であれば、Zuishinさんが仰るようなDI(依存性の注入)ができる作りにすべきだと思います。そうすれば、注入するオブジェクトをテスト用か本番用かで切り替えるだけで目的が達成できるので。 質問内容のような一般的ではない手段は、選べば選ぶほど何か他の一般的ではない新たな問題が発生するリスクも増えていきます。 一般的でない問題ほど解決のヒントとなる情報は減り、調査するコストも上がるので、そういうリスクが起こる可能性を考慮した場合、その手段はあまりお勧めできません。 (問題解決できるスキルがあれば話は別です)
comet7360

2021/03/12 06:47

ありがとうございます。承知いたしました。 単体テストでこの手段は使わないようにいたします。 そもそも質問内容が一般的かどうかすら分かっていませんでしたので、ご回答ありがたいです。 DIやMoqを調べてみました。今からテスト対象クラスにInterfaceを設計・実装する余裕は無いので、 テスト対象クラスを実行環境から独立させて、参照エラーとなるクラスやメソッドは適当に 自作していこうと思います。 そして、この質問の目的を単体テストから、好奇心および学習目的に切り替えます。
退会済みユーザー

退会済みユーザー

2021/03/12 07:30 編集

学習目的としても、正直微妙な気がしますね。 仮に質問の内容の事が実行出来たとしても、ポインタに詳しくない、何をやってるか把握できない、という状態では役に立たないでしょうし、ドキュメントされていない箇所の処理をメモリを書き換えて無理やり弄るわけですから、バージョンが変わるだけで使えなくなる可能性もあるので、まあ実用性は皆無ですね。 自力でILや逆アセンブル出力見て処理を読み解けるようになるなら、どこかで役に立つかもしれないし、立たないかもしれません。とりあえずは、C言語の関数呼び出しの仕組みや、引数をスタックからクリーンアップする仕組み、関数ポインタの勉強辺りから始める必要があるでしょう。
退会済みユーザー

退会済みユーザー

2021/03/12 07:45

別に非難している訳ではなく、個人的には興味深い事やってるなとは思いますが、自力でILや逆アセンブル出力見て処理を読み解けるレベルの人が、高尚な遊びとして手を出すような内容だと思います。
BluOxy

2021/03/12 08:06 編集

余裕がないから目的を切り替えたように文章から読み取れます。 comet7360さんの事情はよく存じ上げませんが、もし余裕がないからと焦って現在の質問の解決を試みているのであれば、その旨を周囲にアラートしたりリスケしたりなどして余裕を作ることの方がこの質問を無理やり解決することよりも優先度が高いのではないでしょうか。 そうではなく、本当に純粋な好奇心で質問しているのであれば上記は余計なお世話なので気にしないでください。ですが、そうであったとしてもradianさんが仰っているようにこの質問に必要な前提知識(ポインタや、何をやっているか把握するのに必要な知識)を身につけてから手を出すべき(この質問をすべき)と思います。
comet7360

2021/03/12 08:19

ありがとうございます。 良く分からないものを無理して使うな、ということですね。 メモリ書き換えることで、呼び出すメソッドを変更する方法は諦めたいと思います。
Zuishin

2021/03/12 08:34

そもそも DirectoryExists のシグネチャが Directory.Exists と違いますよね。 ステップ実行でメソッドの交換が成立することを確認できなかったんですが、それが確認できるコードと確認方法を教えてください。
Zuishin

2021/03/12 08:54

確か昔試した時は、mono だと交換できたけど .NET Framework だとだめだったように記憶しています。その時は「おそらくどこかでキャッシュされているから書き換えたものが使われないんじゃないか」と結論付けて、それ以上詮索しませんでした。 *target は original.MethodHandle.GetFunctionPointer() と同じ値になるので、書き換える場所は間違っていないように思いますが、シグネチャが違っていても暴走しないところを見ると、やはりキャッシュされていそうです。 Main よりもっと早く動く静的コンストラクタでやってみてもだめでしたが、typeof(Injection).GetMethod() の時点でキャッシュされるのかもしれないし、もしかしたらそれより早くキャッシュされるのかもしれません。
comet7360

2021/03/12 09:01 編集

>Zuishinさん Main()内の以下にブレークポイントを設定し、F10押下でステップ実行していけば交換されます。 Injection.MethodInjection(original, injection); こちらの環境を記載しておりませんでした。申し訳ないです。 質問にも追記いたします。 Visual Studio 2019 Professional ver 16.9.1 .NET Core 3.0 のコンソールアプリケーションです。
Zuishin

2021/03/12 09:10

3.0 だと交換されました。5.0 ではされませんでした。 3.0 でも F10 だけだとだめで、一回 F11 を押して MethodInjection の中に入らないとだめでした。
comet7360

2021/03/12 09:16

>Zuishinさん ありがとうございます。申し訳ないです。はい、1回F11で中に入らないといけなかったです。 先ほどこちらの環境でも、5.0では駄目なことを確認いたしました。 Zuishinさんのおかげで、より一層、諦めが付けやすくなりました。ありがとうございます!
guest

回答2

0

ベストアンサー

ポインタを使った当初の手段とは離れますが、コメントにて紹介したDIを使うという手段でも

単体テストのためにライブラリ関数を自作のスタブに差し替える

という目的は解決できるので回答します。

下記コードの内、WantFunctionが注入される側のオブジェクトで、ProgramクラスのMainメソッドが注入する側のオブジェクトで、そのメソッドの内、fというFuncオブジェクトが実際に注入するオブジェクトです。

今回の場合、DebugとReleaseを構成を変えることで、注入するオブジェクトを切り替えることができます。
(今回はifプリプロセッサを使って構成変更時に注入するオブジェクトを変更していますが、変更できれば手段は何でも構いません)

このDIを活用しても自作のスタブへ差し替えることができます。

C#

1public class Program 2{ 3 public static void Main(string[] args) 4 { 5#if DEBUG 6 //NOTE: ところで、パラメータがないのは意図通りでしょうか? 7 Func<string,bool> f = dir => Injection.DirectoryExists(); 8#else 9 Func<string,bool> f = dir => Directory.Exists(dir); 10#endif 11 var injected = new WantFunction<string,bool>(f); 12 Console.WriteLine(injected.CallInjectedFunction("hoge")); 13 14 } 15} 16 17public class WantFunction<T,TResult> 18{ 19 private readonly Func<T,TResult> _func; 20 public WantFunction(Func<T, TResult> func) 21 { 22 _func = func; 23 } 24 25 public TResult CallInjectedFunction(T t) 26 { 27 return _func(t); 28 } 29} 30 31public class Injection 32{ 33 public static bool DirectoryExists() 34 { 35 Console.WriteLine("DirectoryExists\n"); 36 return true; 37 } 38}

DIに慣れてくると場合によっては注入するそれぞれのインスタンスの有効期間を変えたいことがあるように、より小回りが利くDIがしたいときもあるかもしれません。
そのときは DIコンテナ と呼ばれるフレームワークを触ってみると良いかもしれません。

投稿2021/03/12 09:33

編集2021/03/12 10:33
BluOxy

総合スコア2663

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

comet7360

2021/03/12 10:16

ありがとうございます。メモリの書き換えについては皆様のおかげで諦めがついたものの、 自身の中で、振り上げた拳のもって行き場がないような状態となっておりました。 DIについては全くの無知ですので、ご回答いただいた内容を参考に、勉強しようと思います。 また、スタブにパラメータ(引数)を持たせていなかったのは、意図がありました。 メモリの内容を書き換えることで呼び出すメソッドを変更できるならば、 引数の情報がオリジナルとは同じでなくても、問題が無いと考えていたからです。 スタブメソッドは戻り値だけ気にすればよいので、引数は持たせていませんでした。 (今となっては無駄な検討でしたが。。) ご回答いただいた実装を見ると、オリジナルとスタブは引数と戻り値が同じですね。 この方法を使用する際は、そういったところにも気を付けて実装しようと思います。 Zuishinさん、radianさんも、色々とご助言、本当にありがとうござました。
BluOxy

2021/03/16 05:35 編集

> 振り上げた拳のもって行き場がないような状態となっておりました。 これは私もよくあります。というよりも エンジニアやプログラマーあるある な気がします。 この状態に陥らないためには、もしくは陥ったときは、精神・時間・行動に余裕を作るようにし、その限られた余裕(リソース)の中から見える一番良い選択肢を選べるようにフットワークを軽くするようにしています。 ※例えば、保守し辛くて駄目な設計やコードが出来上がってしまった時に思い切って 1 から書き直すとか その方が物事うまくいくことが多いと私は思っています。 なので、comet7360さんがそう感じているかもしれないなと思いつつも、私が思うベストな手段(1つの選択肢)を提案しました。 DIはオブジェクト指向で最も重要な機能の1つといってもまったく過言ではありませんから、覚えて使いこなせた際のリターンは大きいはずです。 > ご回答いただいた実装を見ると、オリジナルとスタブは引数と戻り値が同じですね。 interfaceやfuncは戻り値や引数の型は静的に指定するものなので、当然呼び出す際も型は合わせる必要があります。 といっても、型自体をinterfaceに指定することで呼び出し側のオブジェクトの型は動的に指定する(抽象化する)ことはできます。 ※むしろ、この手法は活用できるならうまく活用するべきです。依存性逆転の原則で調べると詳しいことが書いてあります 他にも、今回だとExistsメソッドに渡す文字列が予め決まっている場合は下記のように対応することもできます。 1. WantFunction<T,TResult>をWantFunction<TResult>に 2. WantFunctionクラスのコンストラクタの引数もFunc<TResult>に変更 3. WantFunction内のあらゆるTを排除 4. Mainメソッドを下記コードのように書き換える #if DEBUG Func<bool> f = () => Injection.DirectoryExists(); #else Func<bool> f = () => Directory.Exists("hoge"); #endif var injected = new WantFunction<bool>(f); Console.WriteLine(injected.CallInjectedFunction());
guest

0

あれから色々と調べたところ、harmony.libを使用すればメソッドを交換することができました。
明記されているライブラリのサポートは.NET Core 3.1までですが、.NET 5でも動作しました。

C#

1using System; 2using System.IO; 3using System.Reflection; 4using HarmonyLib; 5 6namespace ConsoleApp1 7{ 8 class Program 9 { 10 static void Main(string[] args) 11 { 12 Console.WriteLine("Hello World!"); 13 14 var harmony = new Harmony("test.hogehoge");//←引数の値は何でもOK 15 16 // Directory.Exists()を書き換え 17 { 18 // Directory.Exists()のMethodInfo 19 MethodInfo original = typeof(Directory).GetMethod("Exists"); 20 21 // 自作メソッドのMethodInfo 22 MethodInfo injection = typeof(Injection).GetMethod("DirectoryExists"); 23 24 // Directory.Exists()の書き換え 25 harmony.Patch(original, new HarmonyMethod(injection)); 26 } 27 28 // ★ここで自作メソッドが呼ばれる★ 29 Directory.Exists("hoge"); 30 } 31 } 32 33 class Injection 34 { 35 // 自作メソッド 36 public static void DirectoryExists() 37 { 38 Console.WriteLine("DirectoryExists\n"); 39 } 40 } 41} 42

投稿2021/03/16 03:46

編集2021/03/16 03:47
comet7360

総合スコア9

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

BluOxy

2021/03/16 06:09

.NETで モンキーパッチ を行えるライブラリがあるんですね。勉強になりました (+1) 確かにそれをうまく活用すればテストが行えて質問に対する問題は解決できます。 ただし、モンキーパッチは思いのまま変更できてしまう反面、予想外の挙動になるリスクがありますから、やむを得ない場合以外の利用はお勧めできません。 ※また、このライブラリはUnityを中心としたゲームのMOD作成がメイン機能のように見えるので、単体テストに関して融通か効くかは微妙に見えます 理想(C#の場合)はMicrosoftが提供している単体テストに関するドキュメントに沿って行うべきです。 それでも、やむを得ずモンキーパッチを利用する場合はリスクに注意してください。
comet7360

2021/03/16 11:05

>BlueOxyさん コメントいただき、ありがとうございます。 はい。BlueOxyさん、Zuishinさん、radianさんに教えていただいた通り、設計の段階から DIやInterfaceを織り込んで、健全な単体テストが出来るつくりにしておくようにしたいと思います。 また、モンキーパッチという言葉があるのですね。勉強になります。ありがとうございます。 このライブラリ自体も、オリジナルメソッドの前後(PreFix, PostFix)に自作メソッドを挟み入れること が本来の使用方法らしく、対象メソッドそのものを入れ替えるのは十分注意くださいと記載ありました。 もしそういう使い方をする場合は、リスクに十分注意するようにいたします。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問