我正在为我的 c# 应用程序开发一个不干涉日志机制。
这是我希望它看起来的样子:
functiona(arg1, arg2, arg 3.....)
调用 function b(arg4,arg5,arg6....)
,后者又调用log()
which 能够检测堆栈跟踪(这可以通过 来完成Environment.StackTrace
)以及调用堆栈跟踪中的每个函数(例如a
和b
)的值。
我希望它在调试和发布模式下工作(或者,至少在调试模式下)。
这可以在.net中完成吗?
我正在为我的 c# 应用程序开发一个不干涉日志机制。
这是我希望它看起来的样子:
functiona(arg1, arg2, arg 3.....)
调用 function b(arg4,arg5,arg6....)
,后者又调用log()
which 能够检测堆栈跟踪(这可以通过 来完成Environment.StackTrace
)以及调用堆栈跟踪中的每个函数(例如a
和b
)的值。
我希望它在调试和发布模式下工作(或者,至少在调试模式下)。
这可以在.net中完成吗?
可证明不可能:
到b
调用时,a 使用的堆栈空间arg1
(IL 堆栈,因此它可能从未放入堆栈,但已在调用中注册)不能保证仍由arg1
.
通过扩展, ifarg1
是一个引用类型,如果在调用b
.
编辑:
更详细一点,因为您的评论表明您没有理解这一点并且仍然认为它应该是可能的。
抖动使用的调用约定未在任何相关标准的规范中指定,这使实施者可以自由地进行改进。它们确实在 32 位和 64 位版本以及不同的版本之间存在差异。
但是,MS 人员的文章建议使用的约定类似于__fastcall约定。在您对 的调用中a
,arg1
将被放入arg2
运行代码的核心的 ECX 寄存器*和 EDX 寄存器中(我通过假设 32 位 x86 来简化,并且注册了 amd64 甚至更多的参数)。arg3
将被推入堆栈并且确实存在于内存中。
请注意,在这一点上,并没有存在的内存位置arg1
,arg2
它们仅在 CPU 寄存器中。
在执行方法本身的过程中,根据需要使用寄存器和存储器。并且b
被称为。
现在,如果a
需要arg1
,或者arg2
它必须在它调用之前推送它b
。但如果它没有,那么它就不会 - 甚至可能会重新排序以减少这种需求。相反,此时这些寄存器可能已经被用于其他用途 - 抖动并不愚蠢,因此如果它需要一个寄存器或堆栈上的一个插槽,并且在方法的其余部分有一个未使用,它会继续重用那个空间。(就此而言,在此之上的级别,C# 编译器将重用 IL 生成的虚拟堆栈中使用的插槽)。
因此,当b
被调用时,arg4
将被放入寄存器 ECX 中,arg5
放入 EDX 并arg6
压入堆栈。在这一点上,arg1
不arg2
存在,你再也找不到它们是什么,就像你读一本书被回收并变成卫生纸之后一样。
(有趣的是,一个方法在相同位置调用另一个具有相同参数的方法是很常见的,在这种情况下,ECX 和 EDX 可以单独使用)。
然后,b
根据大小,返回,将其返回值放入 EAX 寄存器或 EDX:EAX 对或 EAX 指向它的内存中,在将其返回值放入该寄存器a
之前做一些更多的工作,依此类推。
现在,这是假设没有进行任何优化。实际上,可能b
根本没有调用它,而是它的代码是内联的。在这种情况下,无论值在寄存器中还是在堆栈上 - 在后一种情况下,它们在堆栈上的位置,不再与b
's 的签名有关,也与相关值在a
's期间的位置有关执行,并且在另一个“调用”到b
的情况下,甚至在另一个“调用”到b
from的情况下,它会有所不同,因为包括其调用a
的整个调用可能已经内联在一种情况下,而不是内联另一个,并且在另一个中以不同的方式内联。例如,如果a
b
arg4
直接来自另一个调用返回的值,此时它可能在 EAX 寄存器中,而在arg5
ECX 中,因为它与.arg1
arg6
a
另一种可能性是b
调用是被消除的尾调用:因为调用b
也将立即返回其返回值a
(或其他一些可能性),而不是推入堆栈,而是使用正在使用的值bya
被就地替换,并且返回地址发生了变化,因此 return fromb
跳转回调用的方法a
,跳过了一些工作(并减少了内存使用,以至于一些会溢出堆栈的函数式方法改为工作并且确实运作良好)。在这种情况下,在调用 的过程中b
,参数 toa
很可能完全消失了,即使是那些已经在堆栈上的参数。
最后一种情况是否应该被认为是一种优化,这是非常值得商榷的。有些语言严重依赖于它的完成,因为它们提供了良好的性能,而如果它们甚至可以工作(而不是溢出堆栈),它们的性能却很糟糕。
可以有各种各样的其他优化。应该有各种各样的其他优化——如果 .NET 团队或 Mono 团队做了一些事情,使我的代码更快或使用更少的内存,但在其他方面表现相同,而无需我做任何事情,我不会抱怨!
这是假设首先编写 C# 的人从未更改过参数的值,这肯定不会是真的。考虑这段代码:
IEnumerable<T> RepeatedlyInvoke(Func<T> factory, int count)
{
if(count < 0)
throw new ArgumentOutOfRangeException();
while(count-- != 0)
yield return factory();
}
即使 C# 编译器和抖动的设计非常浪费,您可以保证不会以上述方式更改参数,但您怎么能知道count
在 的调用中已经发生了factory
什么?即使在第一次调用时它也不同,而且上面的代码也不是奇怪的。
所以,总结一下:
从所有这些来看,到底怎么可能知道是什么arg1
?
现在,添加垃圾收集的存在。想象一下,如果我们能神奇地知道arg1
无论如何,尽管如此。如果它是对堆上对象的引用,它可能仍然对我们没有好处,因为如果以上所有都意味着堆栈上没有更多活动引用 - 应该很清楚,这肯定会发生 -并且 GC 启动,然后对象可能已被收集。所以我们能神奇地得到的只是对不再存在的东西的引用——实际上很可能是堆中的一个区域现在被用于其他东西,砰地一声,整个框架的整个类型安全!
它与获得 IL 的反射没有丝毫可比性,因为:
MethodBody
该方法的一个,那么它通常内联的事实是无关紧要的。其他答案中关于分析、AOP 和拦截的建议与您将获得的一样接近。
*实际上,this
是实例成员的真正第一个参数。让我们假装一切都是静态的,所以我们不必一直指出这一点。
这在.net 中是不可能的。在运行时,JITter 可能决定使用 CPU 寄存器而不是堆栈来存储方法参数,甚至重写堆栈中的初始(传递)值。因此,.net 允许在源代码中的任何位置记录参数将非常耗费性能。
据我所知,您通常可以做到的唯一方法是使用 .net CLR 分析 API。(例如 Typemock 框架能够做这样的事情,它使用 CLR 分析 API)
如果您只需要拦截虚拟函数/属性(包括接口方法/属性)调用,您可以使用任何拦截框架(例如 Unity 或 Castle)。
有一些关于 .net 分析 API 的信息:
如果没有类型模拟或一些 ICorDebug 魔法,可能不会发生。甚至StackFrame类也只列出了允许您获取有关源信息的成员,而不是参数。
但是,您所追求的功能作为带有方法日志记录的 IntelliTrace 存在。您可以过滤您需要查看的内容。
这在 C# 中是不可能的,您应该使用 AOP 方法并在调用每个方法时执行方法参数记录。通过这种方式,您可以集中您的日志记录代码,使其可重用,然后您只需要标记哪些方法需要参数日志记录。
我相信这可以使用PostSharp之类的 AOP 框架轻松实现。