通过含unsafe引用转义的ref struct转发ref参数是否安全?
我正在开发一个C#增量生成器,作为泛型上下文下托管与非托管回调之间的包装器。这个工具会生成功能与delegate一致的接口,其Invoke方法支持最多16个带或不带返回值的泛型参数(命名逻辑和System.Action、System.Func一致)。
我希望给Invoke方法添加ref限定参数,但和Action/Func的情况一样,哪怕用源生成器,也没法为16个参数生成by-value、ref、in、out的所有排列组合(特意说明最终目标避免XY问题)。
考虑替代方案后,我想到用带ref字段的ref struct来表示任意一种ref分类:用普通字段存储by-value参数(指非ref限定,不一定是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)。
用这个代理类型,我的接口可以这样定义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
(ref struct)的构造函数 - ParamProxy
将ref限定参数存储在ref字段中 - ParamProxy
对象作为scoped参数传入Invoke方法 - Invoke方法将ref字段的值转发给delegate对应的ref、in或out参数
- 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` }
早期测试结果符合预期,但我担心会无意中造成内存泄漏或栈损坏,希望得到反馈!
内容的提问来源于stack exchange,提问作者monkey0506

