Table of Contents

Object Layout

WARNING: This is an extremely advanced subject, and is not recommended for users unfamiliar with unsafe code or C++.

All classes that inherit from UnityEngine.Object are actually wrappers for a native object on the C++ side of the engine. The address of the native object is stored in the class, as seen in the layout below:

class Object
{
    // Cached pointer to the native object
    IntPtr m_CachedPtr;
 
    // Cached instance ID from native object
    int m_InstanceID;
 
    // True purpose unknown
    string m_UnityRuntimeErrorString;
}

The cached pointer can be accessed through reflection in two ways:

Once you have retrieved the pointer, you can access the native object by defining the appropriate structs and using unsafe code.

The C++ Class Hierarchy

The C++ class hierarchy is as follows: NonCopyable → Object → EditorExtension → NamedObject

You can define this from C# using the following set of types (for the sake of brevity, these are simplified definitions):

public struct NonCopyable
{
    public IntPtr MethodTable;
}
 
public struct AllocationRootWithSalt
{
    public uint m_Salt;
    public uint m_RootReferenceIndex;
}
 
public struct MemLabelId
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
    public AllocationRootWithSalt m_RootReferenceWithSalt;
#endif
 
    public int identifier;
}
 
public struct ScriptingGCHandle
{
    public IntPtr m_Handle;
    public int m_Weakness;
    public IntPtr m_Object;
}
 
// Note that this struct layout differs between Unity versions.
// This is correct for Unity 2020, but hasn't been tested with
// other engine versions.
//
// The type has been renamed to NativeObject in order to avoid
// conflicts with UnityEngine.Object.
//
public struct NativeObject
{
    public NonCopyable Base;
    public int m_InstanceID;
 
    // This is represented on the C++ side as several bit-fields.
    public int m_BitFields;
 
    public IntPtr m_EventIndex;
 
#if UNITY_EDITOR || DEVELOPMENT_BUILD
    public MemLabelId m_FullMemoryLabel;
#endif
 
    public ScriptingGCHandle m_MonoReference;
 
#if UNITY_EDITOR
    public uint m_DirtyIndex;
    public ulong m_FileIDHint;
    public bool m_IsPreviewSceneObject;
#endif
 
#if UNITY_EDITOR || DEVELOPMENT_BUILD
    public int m_ObjectProfilerListIndex;
#endif
}
 
public struct PPtr<T>
    where T : unmanaged
{
    public int m_InstanceID;
}
 
// Renamed to NativePrefab to avoid name clashes
public struct NativePrefab
{
    public NativeObject Base;
    // ...
}
 
// Renamed to NativePrefabInstance to avoid name clashes
public struct NativePrefabInstance
{
    public NativeObject Base;
    // ...
}
 
public struct EditorExtension
{
    public NativeObject Base;
 
#if UNITY_EDITOR
    public PPtr<EditorExtension> m_CorrespondingSourceObject;
    public PPtr<EditorExtension> m_DeprecatedExtensionPtr;
    public PPtr<NativePrefab> m_PrefabAsset;
    public PPtr<NativePrefabInstance> m_PrefabInstance;
    public bool m_IsClonedFromPrefabObject;
#endif
}
 
public struct ConstantString
{
    public unsafe byte* m_Buffer;
}
 
public struct NamedObject
{
    public EditorExtension Base;
    public ConstantString m_Name;
}

All of the above information was obtained from inspecting the .pdb files that are included with Unity installations. Some versions of Unity contain more information in these files than others.

Advanced Use Cases

Once you have access to native objects, you can perform many different actions that are usually only possible in the editor via SerializedObject and SerializedProperty. For example, changing the m_LineSpacing value on a Font object:

// Since these structures are accessed through pointers,
// we don't need to define the entire structure in order
// to use it. Fonts have several more fields in them that
// are not particularly useful to access in this way.
public struct NativeFont
{
    public NamedObject Base;
    public float m_LineSpacing;
    public int m_FontSize;
}
 
// Make a new Font and access its native object.
Font f = new Font();
var fp = (NativeFont*)f.GetCachedPtr();
 
// Now we can simply set m_LineSpacing on it
fp->m_LineSpacing = 12f;
 
// This should print 12
Debug.Log(f.lineHeight);