I am developing a C# incremental generator to act as a wrapper between managed and unmanaged callbacks in a generic context. That wrapper generates interfaces that functionally work the same way as a delegate, with an Invoke method that supports up to 16 generic type parameters with or without a return type (named similarly to System.Action and System.Func).
I wanted to be able to add ref-qualified parameters to the Invoke method, but for the same reasons as Action and Func, there's simply no way to produce every permutation of by-value, ref, in, and out for 16 different parameters, even with a source generator. (I'm elaborating on the end-goal to avoid an XY problem.)
Considering alternative approaches, I arrived at the idea of using a ref struct with a ref field to represent any one of the possible ref "categories". I could use a normal field to store a "by-value" parameter (meaning not ref-qualified; not necessarily a ValueType), and a readonly ref readonly field to store a ref, in, or out parameter:
public enum RefCategory { None = 0, Ref, InRef, OutRef } public readonly ref struct ParamProxy { [MaybeNull] private readonly T obj; private readonly ref readonly T _ref; public readonly RefCategory RefCategory; public static implicit operator ParamProxy(T obj) => new(obj); [return: MaybeNull] public static implicit operator T(ParamProxy proxy) => proxy.Value; public ParamProxy() : this(default!) { } public ParamProxy(T obj) { this.obj = obj; _ref = ref Unsafe.NullRef(); 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 }; } } This utilizes System.Runtime.CompilerServices.Unsafe to avoid initializing the obj and/or _ref fields, based on the constructor used.
The in and out constructors have dummy parameters, because C# doesn't allow you to overload methods/constructors only by the ref category. However, with defaulted dummy parameters, the compiler is able to unambiguously resolve new(ref x), new(in x), and new(out x) from each other. (Edit: corrected constructor details)
This proxy type would allow my interfaces to define Invoke like this:
public ParamProxy Invoke(scoped ParamProxy t1, scoped ParamProxy t2, scoped ParamProxy t3); My source generator is already analyzing type information (T1, T2, T3, TResult...), and I am able to reason about the types. I would similarly be able to inspect the invocations of Invoke and issue diagnostics at compile-time if the wrong ref category is used. Usability is not the concern in question.
My question here is whether I have done something dangerous. In particular, the out parameter requires using Unsafe.AsRef to avoid a "narrower escape scope" error.
The specific context of my use case leads me to believing that this is still a reliable and safe scenario:
- The ref, in, or out parameter is passed to the constructor of ParamProxy (a ref struct)
- The ParamProxy stores the ref-qualified parameter in a ref field
- The ParamProxy object is passed to Invoke as a scoped parameter
- The Invoke method then "forwards" the ref field's value to an appropriate ref, in, or out parameter of a delegate
- The delegate is invoked immediately, before Invoke returns
The user code might then call Invoke like this:
var getIntValueFromNative = /* ...get interface instance... */; getIntValueFromNative.Invoke(new(out int value)); Which would generate (via the source generator) an Invoke implementation that would do:
public void Invoke(scoped ParamProxy param) { handler(out param.OutRef); // `handler` is a `delegate` } Sorry for the lengthy post. I've tried to be thorough in describing the scenario. My early tests show the expected results. I'm leery of inadvertently leaking memory or corrupting the stack. Thank you in advance for any feedback!
Источник: https://stackoverflow.com/questions/780 ... ning-is-th