UNPKG

com.wallstop-studios.unity-helpers

Version:

Treasure chest of Unity developer tools

356 lines (267 loc) 30 kB
## ReflectionHelpers — Fast, Safe Reflection for Hot Paths ### TL;DR — When To Use - You need reflection in performance‑sensitive code paths but want to avoid allocations and security pitfalls. - These helpers cache lookups, avoid boxing where possible, and expose safe, typed APIs. Visual ![Reflection Scan](../../images/utilities/reflection/reflection-scan.svg) ReflectionHelpers is a set of utilities for high‑performance reflection in Unity projects. It generates and caches delegates to access fields and properties, call methods and constructors, and quickly create common collections — with safe fallbacks when dynamic IL isn’t available. Why it exists - Reflection is flexible but slow when used repeatedly (per‑frame, per‑object, per‑element). - Standard reflection allocates (boxing, object[] argument arrays) and repeats costly lookups. - ReflectionHelpers compiles or emits delegates once, caches them, then reuses them to remove ongoing overhead. What it solves - Field/property access without per‑call reflection. - Fast instance/static method invocation (boxed or strongly typed variants). - Allocation‑free typed static invokers for common cases (e.g., two parameters). - Zero‑allocation collection creation helpers (array/list/hash set creators, cached by element type). - Resilient type/attribute scanning that swallows loader errors safely. When to use it - Hot paths: serialization, (de)hydration, UI/inspector tooling, ECS‑style systems, property grids. - Repeated reflective operations over the same members or types. - When you can cache and reuse delegates across many calls. When not to use it - One-off reflection (e.g., editor button pressed infrequently). Simpler `GetValue/SetValue` is fine. - If you need full runtime codegen in IL2CPP/WebGL: IL emit isn’t available there. ReflectionHelpers still works, but uses expression compilation or reflection fallback — benefits remain for caching and reduced allocations. - Setting struct instance fields using boxed setters: prefer the generic ref setter to mutate the original struct (see “Struct note” below). ### Caching Strategy Overview ReflectionHelpers now partitions cached delegates by **capability strategy** so that expression, dynamic-IL, and reflection fallbacks never overwrite each other. Key points: - **Strategy fingerprinting**: every delegate cache entry is keyed by `CapabilityKey<TMember>` (member metadata + `ReflectionDelegateStrategy`). This applies to fields, properties, indexers, methods, and constructors (boxed + typed variants). - **Per-strategy blocklists**: when a strategy cannot produce a delegate (e.g., IL emit disabled on IL2CPP), we record the failure in a per-cache blocklist so later calls skip unnecessary work. - **Delegate provenance**: created delegates are tracked in a `ConditionalWeakTable<Delegate, StrategyHolder>` so diagnostics and tests can assert the producing strategy via `ReflectionHelpers.TryGetDelegateStrategy`. - **Capability overrides**: `ReflectionHelpers.OverrideReflectionCapabilities(expressions, dynamicIl)` temporarily toggles expression/IL support, letting tests (or runtime feature detection) confirm that caches store independent delegates per strategy. - **Test hooks**: `ClearFieldGetterCache`, `ClearPropertyCache`, `ClearMethodCache`, and `ClearConstructorCache` flush the relevant cache groups to keep unit tests deterministic. - **Fallback behaviour**: if neither expressions nor dynamic IL are available, the reflection-path delegates still benefit from caching and avoid repeated argument validation/boxing. ### Current Implementation Summary | API Group | Representative methods | Primary strategy (Mono/Editor) | Fallbacks (IL2CPP/WebGL/AOT) | Caching | Notes | | -------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Field access (boxed) | `GetFieldGetter(FieldInfo)`, `GetFieldSetter(FieldInfo)` | Emit `DynamicMethod` IL (`BuildFieldGetter/SetterIL`) to cast/unbox target and box return | `CreateCompiled*` builds expression delegates; otherwise wraps `FieldInfo.GetValue/SetValue` | `FieldGetterCache`, `FieldSetterCache`, static equivalents | Supports static + instance fields; struct writes box when IL emit unavailable (IL2CPP/WebGL) | | Field access (typed) | `GetFieldGetter<TInstance,TValue>`, `GetFieldSetter<TInstance,TValue>` | Emit typed `DynamicMethod` (setters use by-ref) to avoid boxing | Falls back to `GetValue/SetValue` wrappers; setter fallback boxes then copies back | None (callers must hold returned delegate) | `TInstance` must match declaring type; fastest only where IL emit allowed | | Property access (boxed) | `GetPropertyGetter(PropertyInfo)`, `GetPropertySetter(PropertyInfo)` | Emit `DynamicMethod` (`Call`/`Callvirt`) and box value types | Expression-compiled wrapper; else `PropertyInfo.GetValue/SetValue` | `PropertyGetterCache`, `PropertySetterCache`, static equivalents | Handles non-public accessors; fallback reintroduces boxing/allocations | | Property access (typed) | `GetPropertyGetter<TInstance,TValue>`, `GetPropertySetter<TInstance,TValue>` | Emit typed `DynamicMethod` with cast/unbox guards | Direct reflection wrappers casting to `TValue` | None | Avoids boxing only on IL paths; static typed getter limited to static properties | | Method invokers (boxed) | `GetMethodInvoker`, `GetStaticMethodInvoker`, `InvokeMethod` | Emit `DynamicMethod` to unpack `object[]` args and box return | Expression wrappers; otherwise call `MethodInfo.Invoke` directly | `MethodInvokers`, `StaticMethodInvokers` | Works with private members; fallback incurs reflection cost per call | | Method invokers (typed static) | `GetStaticMethodInvoker<…>`, `GetStaticActionInvoker<…>` | Emit `DynamicMethod` per arity (0–4) for direct call | Try `MethodInfo.CreateDelegate`; else expression compile | `TypedStaticInvoker0-4`, `TypedStaticAction0-4` | Signature-checked upfront; limited to four parameters today | | Method invokers (typed instance) | `GetInstanceMethodInvoker<TInstance,…>`, `GetInstanceActionInvoker<TInstance,…>` | Emit `DynamicMethod` using `ldarga` for structs and `Callvirt` for refs | Falls back to `Delegate.CreateDelegate` / expression lambdas | `TypedInstanceInvoker0-4`, `TypedInstanceAction0-4` | Requires `TInstance` assignable to declaring type; fallback boxes structs | | Constructors & factories | `GetConstructor`, `CreateInstance`, `GetParameterlessConstructor<T>`, `GetParameterlessConstructor` | Delegate factory prefers expression lambdas, falls back to dynamic IL `newobj` and finally reflection (`ConstructorInfo.Invoke` / `Activator.CreateInstance`) | Reflection invoke (no emit) | `Constructors`, `ParameterlessConstructors`, `TypedParameterlessConstructors` | Works across Editor/IL2CPP; capability overrides let tests force fallback paths | | Indexer helpers | `GetIndexerGetter`, `GetIndexerSetter` | Expression lambdas or dynamic IL to handle struct receivers and value conversions | Reflection `PropertyInfo.Get/SetValue` with argument validation | `IndexerGetters`, `IndexerSetters` | Throws `IndexOutOfRangeException`/`InvalidCastException` when indices mismatch; respects capability overrides | | Collection creators | `CreateArray`, `GetListCreator(Type)`, `GetDictionaryWithCapacityCreator` | Emit `DynamicMethod` for `newarr`/`newobj`, plus `HashSet.Add` wrappers | Use `Array.CreateInstance`, `Activator.CreateInstance`, or reflection `Invoke` | `ArrayCreators`, `ListCreators`, `ListWithCapacityCreators`, `HashSetWithCapacityCreators`, adders | `Create*` APIs cache by element type; fallback still functional but allocates | | Type/attribute scanning | `GetAllLoadedAssemblies`, `GetTypesDerivedFrom<T>`, `HasAttributeSafe` | Direct reflection with guarded iteration; Editor uses `UnityEditor.TypeCache` shortcuts | Gracefully skips assemblies/types on error; no IL emit needed | `TypeResolutionCache`, `FieldLookup`, `PropertyLookup`, `MethodLookup` | Depends on link.xml or addressables to keep members under IL2CPP stripping | ### Current Consumers Snapshot - `Runtime/Core/Serialization/Serializer.cs` and `Runtime/Core/Serialization/JsonConverters/TypeConverter.cs` lean on static method invokers and type resolution to integrate ProtoBuf and JSON pipelines. - `Runtime/Core/Attributes` (`BaseRelationalComponentAttribute`, `RelationalComponentInitializer`, `WNotNullAttribute`) depend on field getters/setters and collection factories for relational wiring. - `Runtime/Tags` (`AttributeMetadataCache`, `AttributeUtilities`, `AttributeMetadataFilters`) use attribute scanning plus cached getters/setters to hydrate metadata tables at startup. - `Runtime/Core/Helper/StringInList.cs` and `Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs` use helper invokers for dynamic lookups during logging and formatting. - `Editor/AnimationEventEditor.cs`, `Editor/Tags/AttributeMetadataCacheGenerator.cs`, and `Editor/Utils/ScriptableObjectSingletonCreator.cs` call into the helpers for TypeCache-driven discovery and editor automation. - `Runtime/Utils/ScriptableObjectSingleton.cs` relies on safe attribute retrieval to locate singleton assets without repeating reflection calls. ### Platform Capability Matrix | Target Environment | Unity Backend | `DynamicMethod` IL Emit | `Expression.Compile` | ReflectionHelpers Behaviour | Notes | | ---------------------------------------------- | ------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | **Editor (Windows/macOS/Linux)** | Mono / JIT | ✅ Enabled (`EMIT_DYNAMIC_IL`) | ✅ Enabled (`SUPPORT_EXPRESSION_COMPILE`) | Uses IL-generated delegates for getters/setters/invokers; expression compile is a fallback if IL creation fails at runtime | Same behaviour for play mode in editor; fastest path used during authoring tools and tests. | | **Standalone Player (Mono scripting backend)** | Mono / JIT | ✅ Enabled | ✅ Enabled | Matches editor experience; cached IL delegates provide best throughput | Applies to legacy desktop Mono builds (Windows/Mac/Linux) where JIT is available. | | **Standalone / Mobile / Console (IL2CPP)** | IL2CPP / AOT | ❌ Disabled at compile time (`ENABLE_IL2CPP` blocks `EMIT_DYNAMIC_IL`) | ⚠️ Disabled (`SUPPORT_EXPRESSION_COMPILE` undefined; `CheckExpressionCompilationSupport` returns false) | Falls back to pre-built delegate wrappers or direct `Invoke`/`GetValue` with caching; still avoids repeated reflection lookups | Covers Windows/macOS/iOS/Android/Consoles when built with IL2CPP. Requires link.xml (or addressables) to preserve reflected members. | | **WebGL Player** | IL2CPP / AOT (wasm) | ❌ Disabled (`UNITY_WEBGL && !UNITY_EDITOR`) | ⚠️ Disabled | Uses expression-free reflection paths identical to IL2CPP builds; object boxing unavoidable for struct setters/invokers | WebGL disallows runtime codegen; helpers rely on cached reflection only. | | **Burst-compiled jobs** | Burst | ❌ Not permitted | ❌ Not permitted | ReflectionHelpers should not be called from Burst jobs; wrap calls on main thread or use precomputed data | Burst forbids managed reflection; guard usage with `Unity.Burst.NoAlias` patterns or pre-bake data. | | **Server builds / headless (Mono)** | Mono / JIT | ✅ Enabled | ✅ Enabled | Same as desktop Mono path; suitable for dedicated servers running on JIT | Confirm `EMIT_DYNAMIC_IL` stays enabled unless IL2CPP server build is selected. | | **Continuous Integration** | Any | Depends on selected backend | Depends on backend | Benchmarks skip doc writes when `Helpers.IsRunningInContinuousIntegration` is true, but helpers themselves behave per backend | Use automated tests to validate both IL2CPP fallback and Mono fast paths. | - `DynamicMethod` support is controlled at compile time by `#if !((UNITY_WEBGL && !UNITY_EDITOR) || ENABLE_IL2CPP)` in `ReflectionHelpers.cs`. - `Expression.Compile` support is gated by the same define; the runtime guard `CheckExpressionCompilationSupport()` prevents usage when the platform forbids JIT compilation even if the symbols are present. - `SINGLE_THREADED` builds remove `System.Collections.Concurrent` usage and swap to simple dictionaries; this is rarely needed but remains AOT-friendly for constrained platforms. Key APIs at a glance - Fields - `GetFieldGetter(FieldInfo)``Func<object, object>` - `GetFieldSetter(FieldInfo)``Action<object, object>` - `GetFieldGetter<TInstance, TValue>(FieldInfo)``Func<TInstance, TValue>` - `GetFieldSetter<TInstance, TValue>(FieldInfo)``FieldSetter<TInstance, TValue>` (ref setter) - `GetStaticFieldGetter<T>(FieldInfo)` / `GetStaticFieldSetter<T>(FieldInfo)` - Properties - `GetPropertyGetter(PropertyInfo)` / `GetPropertySetter(PropertyInfo)` (boxed) - `GetPropertyGetter<TInstance, TValue>(PropertyInfo)` (typed) - `GetStaticPropertyGetter<T>(PropertyInfo)` - Methods and constructors - `GetMethodInvoker(MethodInfo)` / `GetStaticMethodInvoker(MethodInfo)` (boxed) - `GetStaticMethodInvoker<TReturn>(MethodInfo)`, `GetStaticMethodInvoker<T1, TReturn>(MethodInfo)`, `GetStaticMethodInvoker<T1, T2, TReturn>(MethodInfo)`, `GetStaticMethodInvoker<T1, T2, T3, TReturn>(MethodInfo)`, `GetStaticMethodInvoker<T1, T2, T3, T4, TReturn>(MethodInfo)` (typed) - `GetStaticActionInvoker(...)` arities 0–4 (typed, void return) - `GetInstanceMethodInvoker<TInstance, ...>(MethodInfo)` and `GetInstanceActionInvoker<TInstance, ...>(MethodInfo)` arities 0–4 - `GetConstructor(ConstructorInfo)` (boxed) and `GetParameterlessConstructor<T>()` - `CreateInstance<T>(params object[])` and generic type construction helpers - Collections - `CreateArray(Type, int)`; `GetArrayCreator(Type)` - Typed creators: `GetArrayCreator<T>()`, `GetListCreator<T>()`, `GetListWithCapacityCreator<T>()`, `GetHashSetWithCapacityCreator<T>()` - `CreateList(Type)` / `CreateList(Type, int)`; `GetListCreator(Type)`; `GetListWithCapacityCreator(Type)` - `CreateHashSet(Type, int)`; `GetHashSetWithCapacityCreator(Type)`; `GetHashSetAdder(Type)`; typed adder `GetHashSetAdder<T>()` - `CreateDictionary(Type, Type, int)`; `GetDictionaryWithCapacityCreator(Type, Type)`; `GetDictionaryCreator<TKey, TValue>()` - Scanning and attributes - `GetAllLoadedAssemblies()` / `GetAllLoadedTypes()` - Safe attribute helpers: `HasAttributeSafe`, `GetAttributeSafe`, `GetAllAttributesSafe`, etc. - Indexers - `GetIndexerGetter(PropertyInfo)` and `GetIndexerSetter(PropertyInfo)` - Unity - `IsComponentEnabled<T>(T)` and `IsActiveAndEnabled<T>(T)` Usage examples 1. Fast field get/set (boxed) ```csharp public sealed class Player { public int Score; } FieldInfo score = typeof(Player).GetField("Score"); var getScore = ReflectionHelpers.GetFieldGetter(score); // object -> object var setScore = ReflectionHelpers.GetFieldSetter(score); // (object, object) -> void var p = new Player(); setScore(p, 42); UnityEngine.Debug.Log((int)getScore(p)); // 42 ``` 2. Struct note: use typed ref setter ```csharp public struct Stat { public int Value; } FieldInfo valueField = typeof(Stat).GetField("Value"); // Prefer typed ref setter for structs var setValue = ReflectionHelpers.GetFieldSetter<Stat, int>(valueField); Stat s = default; setValue(ref s, 100); // s.Value == 100 ``` 3. Typed property getter ```csharp var prop = typeof(Camera).GetProperty("orthographicSize"); var getSize = ReflectionHelpers.GetPropertyGetter<Camera, float>(prop); float size = getSize(UnityEngine.Camera.main); ``` 4. Typed property setter (variant) ```csharp var prop = typeof(TestPropertyClass).GetProperty("InstanceProperty"); var set = ReflectionHelpers.GetPropertySetter<TestPropertyClass, int>(prop); var obj = new TestPropertyClass(); set(obj, 10); ``` 5. Fast static method invoker (two params, typed) ```csharp MethodInfo concat = typeof(string).GetMethod( nameof(string.Concat), new[] { typeof(string), typeof(string) } ); var concat2 = ReflectionHelpers.GetStaticMethodInvoker<string, string, string>(concat); string joined = concat2("Hello ", "World"); ``` 6. Low‑allocation constructors ```csharp // Parameterless constructor var newList = ReflectionHelpers.GetParameterlessConstructor<List<int>>(); List<int> list = newList(); // Constructor via ConstructorInfo ConstructorInfo ci = typeof(Dictionary<string, int>) .GetConstructor(new[] { typeof(int) }); var ctor = ReflectionHelpers.GetConstructor(ci); var dict = (Dictionary<string, int>)ctor(new object[] { 128 }); ``` 7. Collection creators and HashSet adder ```csharp var makeArray = ReflectionHelpers.GetArrayCreator(typeof(Vector3)); Array positions = makeArray(256); // Vector3[256] IList names = ReflectionHelpers.CreateList(typeof(string), 64); // List<string> object set = ReflectionHelpers.CreateHashSet(typeof(int), 0); // HashSet<int> var add = ReflectionHelpers.GetHashSetAdder(typeof(int)); add(set, 1); add(set, 1); add(set, 2); // set contains {1, 2} ``` 8. Typed collection creators ```csharp var makeArrayT = ReflectionHelpers.GetArrayCreator<int>(); int[] ints = makeArrayT(128); var makeListT = ReflectionHelpers.GetListCreator<string>(); IList strings = makeListT(); var makeSetT = ReflectionHelpers.GetHashSetWithCapacityCreator<int>(); HashSet<int> intsSet = makeSetT(64); var addT = ReflectionHelpers.GetHashSetAdder<int>(); addT(intsSet, 5); ``` 9. Safe attribute scanning ```csharp bool hasObsolete = ReflectionHelpers.HasAttributeSafe<ObsoleteAttribute>(typeof(MyComponent)); var values = ReflectionHelpers.GetAllAttributeValuesSafe(typeof(MyComponent)); // e.g., values["Obsolete"] -> ObsoleteAttribute instance ``` Performance tips - Cache delegates (getters/setters/invokers) once and reuse them. - Prefer typed APIs (`GetFieldGetter<TInstance, TValue>`, typed static invokers) to avoid boxing and object[] allocations. - Use creators (`GetListCreator`, `GetArrayCreator`) in loops to avoid reflection/Activator costs. ### Benchmarking & Verification - **Unit coverage**: `ReflectionHelperCapabilityMatrixTests` resets caches and toggles capabilities around each helper. Run these suites in both expression-enabled and expression-disabled modes when changing caching internals. - **Micro-benchmarks**: Use `Tests/Runtime/Performance/ReflectionPerformanceTests` to capture before/after numbers for getters, setters, method invokers, and constructors (now including expression vs. dynamic IL comparisons). Record results with each `ReflectionDelegateStrategy` forced via `OverrideReflectionCapabilities` so regressions are easy to spot. - **Cache hygiene**: when adding new delegate families, update the appropriate `Clear*Cache` helper and call it from tests to keep scenarios isolated. - **Documentation updates**: note the Unity version, scripting backend, and OS whenever you refresh timing data, and sync any tables in the [Reflection Performance docs](../../performance/reflection-performance.md) so contributors can compare against baseline numbers. - **Execution recipe**: 1. Run `Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests` twice—once normally and once with `REFLECTION_HELPERS_FORCE_REFLECTION=1` (or by wrapping the suite in `OverrideReflectionCapabilities(false, false)`) to cover accelerated and fallback paths. 2. Export raw benchmark data by running the `ReflectionPerformanceTests` category inside the Unity Test Runner with `LogFullResults` enabled; copy the markdown summary into the [Reflection Performance benchmarks](../../performance/reflection-performance.md). 3. Validate editor/runtime builds (Mono + IL2CPP) to ensure blocklists behave consistently across backends. ### Testing fallback behaviour When you need to validate the pure-reflection paths (for example, to mimic IL2CPP/WebGL behaviour), override the runtime capability probes inside a `using` scope: ```csharp using (ReflectionHelpers.OverrideReflectionCapabilities(expressions: false, dynamicIl: false)) { // Force expression + IL emit to be unavailable Func<TestConstructorClass> ctor = ReflectionHelpers.GetParameterlessConstructor<TestConstructorClass>(); TestConstructorClass instance = ctor(); // Uses reflection fallback PropertyInfo indexer = typeof(IndexerClass).GetProperty("Item"); var getter = ReflectionHelpers.GetIndexerGetter(indexer); var setter = ReflectionHelpers.GetIndexerSetter(indexer); setter(new IndexerClass(), 42, new object[] { 0 }); // reflection-based path } ``` The helper restores the original capability state when disposed, so nested overrides remain safe. Runtime regression tests now cover constructors and indexers in both accelerated and fallback modes. ### IL2CPP/WebGL notes - Dynamic IL emit is disabled on IL2CPP/WebGL; ReflectionHelpers automatically falls back to expression compilation or direct reflection where necessary. - Caching still reduces overhead significantly, even without IL emit. ### ⚠️ IL2CPP Code Stripping Considerations **Important for IL2CPP builds (WebGL, iOS, Android, Consoles):** While ReflectionHelpers itself is IL2CPP-safe, Unity's managed code stripping may remove types or members you're trying to access via reflection. This affects **any** reflection-based code, not just ReflectionHelpers. **Symptoms of stripping issues:** - `TypeLoadException` or `NullReferenceException` when calling `Type.GetType()` - `FieldInfo` or `MethodInfo` returns null for members that exist in the Editor - "Type not found" or "Member not found" errors in IL2CPP builds - Works in Editor/Development, fails in Release builds #### Solution: Use link.xml to preserve reflected types Create a `link.xml` file in your `Assets` folder: ```xml <linker> <!-- Preserve types you access via reflection --> <assembly fullname="Assembly-CSharp"> <!-- Preserve entire type and all members --> <type fullname="MyNamespace.MyReflectedClass" preserve="all"/> <!-- Or preserve specific members --> <type fullname="MyNamespace.AnotherClass"> <method signature="System.Void DoSomething()" /> <field name="importantField" /> <property name="ImportantProperty" /> </type> <!-- Preserve all types in a namespace --> <namespace fullname="MyNamespace.ReflectedTypes" preserve="all"/> </assembly> </linker> ``` **Best practices:** -**Test IL2CPP builds regularly** - Stripping only occurs in Release builds -**Preserve all types accessed via string names** - `Type.GetType("MyType")` requires link.xml -**Check build logs** - Unity logs which types are stripped during the build -**Use `typeof()` when possible** - Direct type references prevent stripping without link.xml -**Test on target platform** - Stripping behavior differs across platforms **Examples of code that needs link.xml:** ```csharp // ❌ Requires link.xml: Type accessed by name Type t = Type.GetType("MyNamespace.MyClass"); // ✅ Safer: Direct type reference Type t = typeof(MyClass); // ❌ Requires link.xml: Field accessed by name FieldInfo field = typeof(MyClass).GetField("myField", BindingFlags.NonPublic); // ✅ Safer: If field is definitely there, link.xml ensures it won't be stripped ``` **When ReflectionHelpers doesn't need link.xml:** - Accessing Unity built-in types (they're never stripped) - Using generic type parameters (`GetFieldGetter<MyClass, int>()` prevents stripping of MyClass) - Accessing types that are directly referenced elsewhere in code Thread‑safety - Caches use thread‑safe dictionaries by default. A `SINGLE_THREADED` build flag switches to regular dictionaries for very constrained environments. Common pitfalls - Passing a non‑static `FieldInfo`/`PropertyInfo` to static getters/setters will throw clear `ArgumentException`s. - Read‑only properties do not have setters; using `GetPropertySetter` on those throws. - Struct instance field writes require the generic ref setter (`FieldSetter<TInstance, TValue>`) to mutate the original struct. - Typed method invokers do not support `ref`/`out` parameters and throw `NotSupportedException` for such signatures. See also - Runtime/Core/Helper/ReflectionHelpers.cs for full XML docs and additional examples.