-
Notifications
You must be signed in to change notification settings - Fork 7
Features Utilities Reflection Helpers
- 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).
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.
| 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 |
-
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.
| 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) |
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) |
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–4 -
GetConstructor(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)
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- 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- Typed property getter
var prop = typeof(Camera).GetProperty("orthographicSize");
var getSize = ReflectionHelpers.GetPropertyGetter<Camera, float>(prop);
float size = getSize(UnityEngine.Camera.main);- Typed property setter (variant)
var prop = typeof(TestPropertyClass).GetProperty("InstanceProperty");
var set = ReflectionHelpers.GetPropertySetter<TestPropertyClass, int>(prop);
var obj = new TestPropertyClass();
set(obj, 10);- 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");- 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 });- 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}- 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);- Safe attribute scanning
bool hasObsolete = ReflectionHelpers.HasAttributeSafe<ObsoleteAttribute>(typeof(MyComponent));
var values = ReflectionHelpers.GetAllAttributeValuesSafe(typeof(MyComponent));
// e.g., values["Obsolete"] -> ObsoleteAttribute instancePerformance 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.
-
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.
- Run
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.
- 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.
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
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 strippedWhen 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.
📦 Unity Helpers | 📖 Documentation | 🐛 Issues | 📜 MIT License
- Inspector Button
- Inspector Conditional Display
- Inspector Grouping Attributes
- Inspector Inline Editor
- Inspector Overview
- Inspector Selection Attributes
- Inspector Settings
- Inspector Validation Attributes
- Utility Components
- Visual Components
- Data Structures
- Helper Utilities
- Math And Extensions
- Pooling Guide
- Random Generators
- Reflection Helpers
- Singletons