Reflection Helpers
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
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/SetValueis 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 viaReflectionHelpers.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, andClearConstructorCacheflush 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.csandRuntime/Core/Serialization/JsonConverters/TypeConverter.cslean 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.csandRuntime/Core/Helper/Logging/UnityLogTagFormatter.csuse helper invokers for dynamic lookups during logging and formatting.Editor/AnimationEventEditor.cs,Editor/Tags/AttributeMetadataCacheGenerator.cs, andEditor/Utils/ScriptableObjectSingletonCreator.cscall into the helpers for TypeCache-driven discovery and editor automation.Runtime/Utils/ScriptableObjectSingleton.csrelies 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. |
DynamicMethodsupport is controlled at compile time by#if !((UNITY_WEBGL && !UNITY_EDITOR) || ENABLE_IL2CPP)inReflectionHelpers.cs.Expression.Compilesupport is gated by the same define; the runtime guardCheckExpressionCompilationSupport()prevents usage when the platform forbids JIT compilation even if the symbols are present.SINGLE_THREADEDbuilds removeSystem.Collections.Concurrentusage 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)andGetInstanceActionInvoker<TInstance, ...>(MethodInfo)arities 0–4GetConstructor(ConstructorInfo)(boxed) andGetParameterlessConstructor<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 adderGetHashSetAdder<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)andGetIndexerSetter(PropertyInfo)- Unity
IsComponentEnabled<T>(T)andIsActiveAndEnabled<T>(T)
Usage examples
- Fast field get/set (boxed)
- Struct note: use typed ref setter
| C# | |
|---|---|
- Typed property getter
| C# | |
|---|---|
- Typed property setter (variant)
| C# | |
|---|---|
- Fast static method invoker (two params, typed)
| C# | |
|---|---|
- Low‑allocation constructors
- Collection creators and HashSet adder
- Typed collection creators
- Safe attribute scanning
| C# | |
|---|---|
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:
ReflectionHelperCapabilityMatrixTestsresets 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/ReflectionPerformanceTeststo capture before/after numbers for getters, setters, method invokers, and constructors (now including expression vs. dynamic IL comparisons). Record results with eachReflectionDelegateStrategyforced viaOverrideReflectionCapabilitiesso regressions are easy to spot. - Cache hygiene: when adding new delegate families, update the appropriate
Clear*Cachehelper 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:
- Run
Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTeststwice—once normally and once withREFLECTION_HELPERS_FORCE_REFLECTION=1(or by wrapping the suite inOverrideReflectionCapabilities(false, false)) to cover accelerated and fallback paths. - Export raw benchmark data by running the
ReflectionPerformanceTestscategory inside the Unity Test Runner withLogFullResultsenabled; copy the markdown summary into the Reflection Performance benchmarks. - 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:
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:
TypeLoadExceptionorNullReferenceExceptionwhen callingType.GetType()FieldInfoorMethodInforeturns 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:
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:
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_THREADEDbuild flag switches to regular dictionaries for very constrained environments.
Common pitfalls
- Passing a non‑static
FieldInfo/PropertyInfoto static getters/setters will throw clearArgumentExceptions. - Read‑only properties do not have setters; using
GetPropertySetteron 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/outparameters and throwNotSupportedExceptionfor such signatures.
See also
- Runtime/Core/Helper/ReflectionHelpers.cs for full XML docs and additional examples.