Skip to content

Features Utilities Reflection Helpers

github-actions[bot] edited this page Jan 26, 2026 · 5 revisions

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

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)
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
  1. Struct note: use typed ref setter
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
  1. Typed property getter
var prop = typeof(Camera).GetProperty("orthographicSize");
var getSize = ReflectionHelpers.GetPropertyGetter<Camera, float>(prop);
float size = getSize(UnityEngine.Camera.main);
  1. Typed property setter (variant)
var prop = typeof(TestPropertyClass).GetProperty("InstanceProperty");
var set = ReflectionHelpers.GetPropertySetter<TestPropertyClass, int>(prop);
var obj = new TestPropertyClass();
set(obj, 10);
  1. Fast static method invoker (two params, typed)
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");
  1. Low‑allocation constructors
// 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 });
  1. Collection creators and HashSet adder
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}
  1. Typed collection creators
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);
  1. Safe attribute scanning
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 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.
    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:

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:

<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:

// ❌ 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 ArgumentExceptions.
  • 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.

Clone this wiki locally