通过具有“不安全”引用双关语的引用结构转发引用参数 - 这安全吗?

问题描述 投票:0回答:1

我正在开发一个 C# 增量生成器,充当通用上下文中托管回调和非托管回调之间的包装器。该包装器生成 interface

,其功能与 
delegate
 的工作方式相同,具有支持最多 16 个带或不带返回类型的泛型类型参数的 
Invoke
 方法(命名类似于 
System.Action
System.Func
) ).

我希望能够将

ref

 限定参数添加到 
Invoke
 方法中,但出于与 
Action
Func
 相同的原因,根本没有办法生成 
by-value
ref 的每个排列
in
out
 适用于 16 个不同的参数,即使使用源生成器也是如此。 (
我正在详细阐述最终目标以避免 XY 问题。

考虑替代方法,我想到了使用

ref struct

ref
 字段来表示任何一个可能的 
ref
“类别”。我可以使用普通字段来存储“
by-value
”参数(意味着
不是ref
限定
;不是
必然a ValueType
),以及一个
readonly ref readonly
字段来存储
ref
in
,或
out
参数:

public enum RefCategory { None = 0, Ref, InRef, OutRef } public readonly ref struct ParamProxy<T> { [MaybeNull] private readonly T obj; private readonly ref readonly T _ref; public readonly RefCategory RefCategory; public static implicit operator ParamProxy<T>(T obj) => new(obj); [return: MaybeNull] public static implicit operator T(ParamProxy<T> proxy) => proxy.Value; public ParamProxy() : this(default!) { } public ParamProxy(T obj) { this.obj = obj; _ref = ref Unsafe.NullRef<T>(); RefCategory = RefCategory.None; } public ParamProxy(ref T @ref) { Unsafe.SkipInit(out obj); _ref = ref @ref; RefCategory = RefCategory.Ref; } public ParamProxy(in T inRef, object? _ = null) { Unsafe.SkipInit(out obj); _ref = ref inRef; RefCategory = RefCategory.InRef; } public ParamProxy(out T outRef, int _ = 0) { Unsafe.SkipInit(out obj); Unsafe.SkipInit(out outRef); _ref = ref Unsafe.AsRef(in outRef); RefCategory = RefCategory.OutRef; } private readonly ref T GetRef(RefCategory category) { switch (category) { case RefCategory.None: throw new InvalidOperationException("Parameter is not a by-ref parameter"); case RefCategory.Ref: if (RefCategory != RefCategory.Ref) { throw new InvalidOperationException("Parameter is not a `ref` parameter"); } break; case RefCategory.InRef: if ((RefCategory != RefCategory.InRef) && (RefCategory != RefCategory.Ref)) { throw new InvalidOperationException("Parameter is not an `in` or `ref` parameter"); } break; case RefCategory.OutRef: if ((RefCategory != RefCategory.OutRef) && (RefCategory != RefCategory.Ref)) { throw new InvalidOperationException("Parameter is not an `out` or `ref` parameter"); } break; default: throw new UnreachableException(); } return ref Unsafe.AsRef(in _ref); } public readonly ref readonly T InRef { get => ref GetRef(RefCategory.InRef); } public readonly ref T OutRef { get => ref GetRef(RefCategory.OutRef); } public readonly ref T Ref { get => ref GetRef(RefCategory.Ref); } [MaybeNull] public readonly T Value { get => RefCategory switch { RefCategory.None => obj, _ => Unsafe.IsNullRef(in _ref) ? default : _ref }; } }
这利用 

System.Runtime.CompilerServices.Unsafe 来避免根据所使用的构造函数初始化 obj

 和/或 
_ref
 字段。

in

out
 构造函数具有虚拟参数,因为 C# 不允许您仅通过 
ref
 类别重载方法/构造函数。但是,使用默认的虚拟参数,编译器能够明确地相互解析 
new(ref x)
new(in x)
new(out x)
。 (
编辑:更正了构造函数详细信息)

此代理类型将允许我的

interface

定义
Invoke
,如下所示:

public ParamProxy<TResult> Invoke(scoped ParamProxy<T1> t1, scoped ParamProxy<T2> t2, scoped ParamProxy<T3> t3);
我的源生成器已经在分析类型信息(

T1

T2
T3
TResult
...),并且我能够推理类型。如果使用了错误的 
Invoke
 类别,我同样能够检查 
ref
 的调用并在编译时发出诊断。可用性不是我们所关心的问题。

我的问题是我是否做了一些

危险的事情。特别是,out参数需要使用

Unsafe.AsRef
来避免“更窄的转义范围”错误。
我的用例的

特定上下文

让我相信这仍然是一个可靠且安全的场景:

    ref
  • in
    out
    参数传递给
    ParamProxy<T>
    的构造函数(a
    ref struct
  • ParamProxy<T>
  • ref
    限定参数存储在
    ref
    字段中
  • ParamProxy<T>
  • 对象作为
    Invoke
    参数传递给
    scoped
     
  • Invoke
  • 方法然后将
    ref
    字段的值“转发”到适当的
    ref
    in
    out
    delegate
     参数
    
  • delegate
  • 返回之前,立即调用
    Invoke
    
    
  • 用户代码可能会像这样调用
Invoke

var getIntValueFromNative = /* ...get interface instance... */;
getIntValueFromNative.Invoke(new(out int value));

这将生成(通过源生成器)一个 
Invoke

实现:

public void Invoke(scoped ParamProxy<int> param)
{
    handler(out param.OutRef); // `handler` is a `delegate`
}

抱歉帖子太长。我试图彻底描述这个场景。我的早期测试显示了预期的结果。我担心无意中泄漏内存或损坏堆栈。预先感谢您的任何反馈!

c# parameters field ref-struct
1个回答
0
投票
scoped

关键字的更多信息时找到了答案:

低级结构改进 - 更改输出参数的行为
虽然我仍然相当确定在

预期

用例中,out参数对象不会超出范围,但

可以想象
有人可能会滥用它,这就是为什么我不得不这样做仅在 Unsafe.AsRef 构造函数中使用
out
更重要的是(根据上面的链接),这种用法在语言中是

明确禁止

的(对于out参数)。我现在可能得到了正确的结果,但没有什么可以保证运行时或语言中未来的实现更改不会导致此问题。

AFAICT,

ref

in
构造函数是有效的,特别是
因为
我没有设置构造函数参数scoped。如果参数
scoped,那么将它们存储在
ref
字段中将违反它们的“转义范围”。

out

构造函数应该被完全删除,但是用户可以通过声明和

default
初始化一个局部变量,然后通过
ref
传递它来达到相同的效果。就源生成器而言,
ref
out
参数的有效参数,因此这仍然有效。
var getIntValueFromNative = /* ...get interface instance... */;
int value = default;
getIntValueFromNative.Invoke(new(ref value)); // `value` is `out` parameter in `handler`

我仍然欢迎对此的任何其他反馈,但我会将其标记为已接受的答案。

© www.soinside.com 2019 - 2024. All rights reserved.