【问题标题】:Could the CLR support a "function pointer" value type?CLR 能否支持“函数指针”值类型?
【发布时间】:2011-10-28 16:42:46
【问题描述】:

几天前,我问why delegates are reference types,基于我的错误观念,即委托只需要两个引用:一个指向对象,一个指向函数。我完全忽略了(不是因为我不知道,只是因为我忘记了)在 .NET 中,委托至少部分到位以支持事件作为 @ 的内置实现987654322@,这意味着每个委托都通过调用列表支持多个订阅者。

这让我想到,委托在 .NET 世界中确实扮演着两种不同的角色。一种是不起眼的函数指针,如:

Action<string> writeLine = Console.WriteLine;

other 是 observable 的:

textBox.TextChanged += HandleTextChanged;

调用列表的存在似乎专门用于第二个角色,就像上面简单的writeLine 示例一样,您通常甚至考虑订阅者。

真的,在我看来可能有两种不同的“类型”委托:“函数指针”类型和“可观察”类型。在我看来,前者可能是一种值类型。

现在,我并不是说如果可能的话,应该就是这种情况。我确信在常规委托和多播委托之间进行这种区分会有很多缺点,例如如果委托是值类型,可能会出现高频率的装箱,可能需要引入一个新关键字(multicast?),不可避免的开发人员困惑等等。我真正想知道的是,从 CLR 的角度来看,是否有可能具有可以充当函数指针的值类型。

我想问这个问题的另一种方式是:是System.Delegate,它的调用列表等等,基本上是一个基本的CLR类型;还是它是对任何 CLR 语言都没有公开的更简单的“函数引用”类型的封装?

我为我使用的所有非正式术语道歉,这些术语可能使一些受过良好教育的开发人员感到困惑。

【问题讨论】:

    标签: .net delegates clr


    【解决方案1】:

    在 CLR 的早期,System.Delegate(类似函数指针)和 System.MulticastDelegate(类似事件)之间存在区别。这在 .NET 1.0 发布之前已被废弃,无法创建派生自 Delegate 的委托类型的实例。只是没有必要。 MulticastDelegate 已优化为仅在有多个订阅者时创建其调用列表。 System.Delegate 因某种原因被保留,可能需要做太多工作才能将其删除。

    【讨论】:

    • 这是否意味着原来的System.Delegate 类型可能是一个值类型,或者更确切地说,可能已经被值类型子类化 ,很像System.Enum? (大概它仍然不会有,但是,原因可能比我能想到的更多?)
    • 呃,难,它是MulticastDelegate的基类。很难对从未真正存在过的事物进行推理。
    【解决方案2】:

    在我看来,在绝大多数情况下,.NET 开发人员发现对单播/多播和封闭/开放实例的“统一”支持非常值得所涉及的少量开销。如果你陷入少数情况,那是不幸的,但如果你不介意打破成语并重新发明很多轮子,那么有办法绕过它。稍后会介绍更多内容。

    实际上,这个问题的意图并不是那么清楚,但我会尝试解决个别问题。

    我真正想知道的是,如果有可能, 从 CLR 的角度来看,有一个值类型可以充当 函数指针。

    当然。实际上,委托是使用本机 int 大小的值类型函数指针(在托管世界中为IntPtr)构建的。所有类型安全的花里胡哨都是之上构建的。

    Action&lt;string&gt; writeLine = Console.WriteLine; 示例的 IL 如下所示:

    // Push null-reference onto stack.
    // (Console.WriteLine is a static method)
    ldnull
    
    // Push unmanaged pointer to desired function onto stack.
    ldftn void [mscorlib]System.Console::WriteLine(string)
    
    // Create delegate and push reference to it onto stack.
    instance void [mscorlib]System.Action`1<string>::.ctor(object, native int)
    
    // Pop delegate-reference from top of the stack and store in local.
    stloc.0 
    

    Action&lt;T&gt; 构造函数非常方便地声明为:

    // First arg is 'this' for closed-instance delegates.
    // Second arg is pointer to function.
    public Action(object @object, IntPtr method);
    

    我想问这个问题的另一种方式是:是 System.Delegate,与 它的调用列表和所有,基本上是一个基本的 CLR 类型;或者是 它是一个简单的“函数引用”类型的包装器 没有被任何 CLR 语言公开?

    嗯,它是一个基本的 CLR 类型(在某种意义上,执行引擎知道它并对其进行特殊处理)一个“包装器”围绕“函数引用”-System.Delegate 将指向函数的指针存储为字段(以及封闭实例委托的对象引用)。多播支持,通过其子类System.MulticastDelegate,显然要复杂得多,因为需要存储多播调用列表。统一是通过这样一个事实实现的,即所有的委托类型都必须继承自 System.MulticastDelegate

    而且我认为“函数引用” 暴露给 CLR 语言 - 可以通过委托的 Target 属性获得代表方法的 MethodInfo,并从那里获得相关的方法句柄和函数指针。


    但是您可能已经知道所有这些。在我看来,您的真实问题是:

    我将如何构建一个轻量级、类型安全、类似委托的值类型 只存储指向 .NET 中托管函数的指针?

    鉴于我到目前为止提到的所有内容,这真的很容易:

    // Cool but mostly useless lightweight (in space, not time)
    // type-safe delegate-like value-type. Doesn't support closed-instance scenarios 
    // in the interests of space, but trivial to put in if desired.
    public struct LeanDelegate<TDelegate>
    {
        // The only storage required.
        private readonly IntPtr _functionPointer;
    
        public LeanDelegate(TDelegate source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
    
            var del = source as Delegate;
    
            if (del == null)
                throw new ArgumentException("Argument is not a delegate", "source");
    
            if (del.Target != null)
                throw new ArgumentException("Delegate is a closed-instance delegate.", "source");
    
            if (del.GetInvocationList().Length > 1)
                throw new ArgumentException("Delegate is a multicast delegate.", "source");
    
             // Retrieve and store pointer to the delegate's target function.
            _functionPointer = del.Method.MethodHandle.GetFunctionPointer();
        }
    
        // Creates delegate-instance on demand.
        public TDelegate Delegate
        {
            get
            {
                if (_functionPointer == IntPtr.Zero)
                    throw new InvalidOperationException("Uninitialized LeanDelegate instance.");
    
                // Use the aforementioned compiler-generated constructor 
                // to generate the delegate instance.
                return (TDelegate)Activator.CreateInstance
                                  (typeof(TDelegate), null, _functionPointer);
            }
        }
    }
    

    然后你可以这样做:

     var del = new LeanDelegate<Action<string>>(Console.WriteLine);
     del.Delegate("Hello world");
    

    你得到了什么? 存储指向任意静态方法(或实例方法,如果委托是开放实例)的指针并以类型安全的方式执行它的能力,所有这些都在机器指针大小的空间中(不包括临时分配)。

    你失去了什么? 封闭实例能力。多播。与许多其他 API 直接兼容。速度(几乎可以肯定),尽管可以想到变通方法。将使用您的 API 的开发人员的爱。

    【讨论】:

    • 请注意,结构可以只存储MethodInfo。由于方法信息被缓存,这应该提供类似的空间配置文件。但你需要一个真实的、诚实的函数指针。
    • 我希望 Delegate 是一个通用接口类型家族。这将允许任何只需要为具有特定签名的方法创建“委托”的类将自身用作调用该方法的委托。这对于闭包之类的东西特别有用。否则,Delegate 必须是一个类而不是一个结构,以允许原子分配,但是当大多数委托只有一个目标时,我认为委托没有理由为调用列表提供额外的字段。
    • 也许我有偏见,因为我真的不喜欢在观察者模式中使用 MulticastDelegates。许多事件订阅和取消订阅代码不是线程安全的,这意味着对外部代码发布的事件的订阅不能在异步上下文中安全地取消订阅。
    【解决方案3】:

    这里的一个问题是赋值的原子性。如果您想到一个包含目标和指向方法的指针的结构。这个结构有两个指针的大小。 CLR 只保证对指针大小的变量进行原子写入,因此这需要一些特殊的机制来保持目标和方法的同步。但是,如果攻击者设法在类型不匹配的对象上调用函数,他可能能够绕过沙箱。

    【讨论】:

    • 哇,好点子!我没有考虑过这个问题。
    • 这是唯一的原因 Delegate 需要是一个值类型,但这已经足够了。没有理由只有一个函数和一个目标的类型不足以支持多播委托,但非原子更新将是一个杀手。
    • @supercat “这是 Delegate 需要成为值类型的唯一原因”您的意思是“需要成为引用类型”吗?
    • 确实如此。我可能一直在“......需要成为引用类型”和“......不能是值类型”之间进行辩论,并将这两个概念混合在一起,非常糟糕。谢谢。要点是对多播委托的支持不需要委托本身有任何额外的字段。可以通过定义一个 MultiDelegateInvoker 类来加入多个委托,该类包含多个委托的方法和目标,然后创建一个新委托,其目标是 MultiDelegateInvoker 对象,其方法运行 MultiDelegateInvoker 中的所有委托。
    • 这可能不是处理事件订阅的最有效方式,但由于多播委托无论如何都不是正确的方法,我不认为这是一种损失。无论如何,其他处理多播委托的方法会更有效。
    【解决方案4】:

    System.Delegate,有它的调用列表等等,基本上是一个基本的CLR类型;还是它是对任何 CLR 语言都没有公开的更简单的“函数引用”类型的封装?

    我认为都没有。首先,虽然System.Delegate 有一些由CLR 实现的方法,但它的实例并没有特别处理,仅在C# 和其他语言中(它只是一个具有Invoke 方法和其他一些方法的类)。其次,有一个非常特殊的 CLR 类型,称为 fnptr,表示一个函数指针(参见 this question)。但是,这种类型是可以访问的,尽管不是在 C# 中,而是在 CIL (method void *()) 或 C++/CLI (void (*foo)()) 中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-12-10
      • 1970-01-01
      • 2016-12-26
      • 2013-09-09
      • 2015-01-05
      相关资源
      最近更新 更多