Treasure chest of Unity developer tools. Professional inspector tooling, high-performance utilities, spatial queries, and 20+ editor tools.
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
What it solves
When to use it
When not to use it
GetValue/SetValue is fine.ReflectionHelpers now partitions cached delegates by capability strategy so that expression, dynamic-IL, and reflection fallbacks never overwrite each other. Key points:
CapabilityKey<TMember> (member metadata + ReflectionDelegateStrategy). This applies to fields, properties, indexers, methods, and constructors (boxed + typed variants).ConditionalWeakTable<Delegate, StrategyHolder> so diagnostics and tests can assert the producing strategy via ReflectionHelpers.TryGetDelegateStrategy.ReflectionHelpers.OverrideReflectionCapabilities(expressions, dynamicIl) temporarily toggles expression/IL support, letting tests (or runtime feature detection) confirm that caches store independent delegates per strategy.ClearFieldGetterCache, ClearPropertyCache, ClearMethodCache, and ClearConstructorCache flush the relevant cache groups to keep unit tests deterministic.| 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.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.| 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
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)GetPropertyGetter(PropertyInfo) / GetPropertySetter(PropertyInfo) (boxed)GetPropertyGetter<TInstance, TValue>(PropertyInfo) (typed)GetStaticPropertyGetter<T>(PropertyInfo)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–4GetConstructor(ConstructorInfo) (boxed) and GetParameterlessConstructor<T>()CreateInstance<T>(params object[]) and generic type construction helpersCreateArray(Type, int); GetArrayCreator(Type)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>()GetAllLoadedAssemblies() / GetAllLoadedTypes()HasAttributeSafe, GetAttributeSafe, GetAllAttributesSafe, etc.GetIndexerGetter(PropertyInfo) and GetIndexerSetter(PropertyInfo)IsComponentEnabled<T>(T) and IsActiveAndEnabled<T>(T)Usage examples
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
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
var prop = typeof(Camera).GetProperty("orthographicSize");
var getSize = ReflectionHelpers.GetPropertyGetter<Camera, float>(prop);
float size = getSize(UnityEngine.Camera.main);
var prop = typeof(TestPropertyClass).GetProperty("InstanceProperty");
var set = ReflectionHelpers.GetPropertySetter<TestPropertyClass, int>(prop);
var obj = new TestPropertyClass();
set(obj, 10);
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");
// 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 });
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}
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);
bool hasObsolete = ReflectionHelpers.HasAttributeSafe<ObsoleteAttribute>(typeof(MyComponent));
var values = ReflectionHelpers.GetAllAttributeValuesSafe(typeof(MyComponent));
// e.g., values["Obsolete"] -> ObsoleteAttribute instance
Performance tips
GetFieldGetter<TInstance, TValue>, typed static invokers) to avoid boxing and object[] allocations.GetListCreator, GetArrayCreator) in loops to avoid reflection/Activator costs.ReflectionHelperCapabilityMatrixTests resets caches and toggles capabilities around each helper. Run these suites in both expression-enabled and expression-disabled modes when changing caching internals.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.Clear*Cache helper and call it from tests to keep scenarios isolated.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.ReflectionPerformanceTests category inside the Unity Test Runner with LogFullResults enabled; copy the markdown summary into the Reflection Performance benchmarks.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.
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 EditorCreate 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:
Type.GetType("MyType") requires link.xmltypeof() when possible - Direct type references prevent stripping without link.xmlExamples 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:
GetFieldGetter<MyClass, int>() prevents stripping of MyClass)Thread‑safety
SINGLE_THREADED build flag switches to regular dictionaries for very constrained environments.Common pitfalls
FieldInfo/PropertyInfo to static getters/setters will throw clear ArgumentExceptions.GetPropertySetter on those throws.FieldSetter<TInstance, TValue>) to mutate the original struct.ref/out parameters and throw NotSupportedException for such signatures.See also