unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
dotnet/runtime
senior8/10
refactoringapi-designalgorithmic-optimization

JIT/Interp: Remove MAYBENULL TypeHandle -> RuntimeType Helper uses

Project: dotnet/runtime | Language: C# | Concepts: refactoring, api-design, algorithmic-optimization | Difficulty: senior
Link: https://github.com/dotnet/runtime/pull/132539

The problem

The runtime had special helper methods (with the _MAYBENULL suffix) to convert a possibly-null TypeHandle to a RuntimeType or RuntimeTypeHandle. These helpers were used in both the JIT and interpreter, but their existence complicated the codebase and introduced unnecessary indirection. The goal was to remove these helpers and instead handle possible nulls with explicit, local logic.

Why it's non-trivial

A naive fix would simply remove the _MAYBENULL helpers and replace their uses with the non-nullable versions. However, this would break scenarios where the handle could actually be null, leading to incorrect behavior or exceptions. The fix requires careful replacement with explicit null checks at each call site, ensuring correct semantics and performance. Additionally, the interpreter needed a new opcode to efficiently handle this case.

The approach

1. Remove the _MAYBENULL helper methods and their registrations

In the managed code:

-        internal static RuntimeType? GetRuntimeTypeFromHandleMaybeNull(IntPtr handle)
-        {
-            if (handle == IntPtr.Zero)
-            {
-                return null;
-            }
-
-            return GetRuntimeTypeFromHandle(handle);
-        }

This deletes the helper that handled the maybe-null case by checking for zero and returning null, or else delegating to the regular helper.

In the JIT helper registration:

-    DYNAMICJITHELPER(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE_MAYBENULL,  NULL,   METHOD__RT_TYPE_HANDLE__GETRUNTIMETYPEFROMHANDLEMAYBENULL)
...
-    DYNAMICJITHELPER(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPEHANDLE_MAYBENULL,    NULL,   METHOD__RT_TYPE_HANDLE__GETRUNTIMETYPEFROMHANDLEMAYBENULL)

Removes the registration of the _MAYBENULL helpers from the JIT infrastructure.

2. Replace helper uses with explicit null checks

In managed code:

-        public static RuntimeTypeHandle FromIntPtr(IntPtr value) =>
-            new RuntimeTypeHandle(GetRuntimeTypeFromHandleMaybeNull(value));
+        public static RuntimeTypeHandle FromIntPtr(IntPtr value) =>
+            new RuntimeTypeHandle(value != IntPtr.Zero ? GetRuntimeTypeFromHandle(value) : null);

Replaces the helper call with an explicit null check and conditional operator.

In the JIT importer:

-                // Convert native TypeHandle to RuntimeTypeHandle.
-                op1 = gtNewHelperCallNode(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPEHANDLE_MAYBENULL, TYP_STRUCT, op1);
+                unsigned handleTemp = lvaGrabTemp(true DEBUGARG("spill result of REFANYTYPE for null check"));
+                impStoreToTemp(handleTemp, op1, CHECK_SPILL_ALL);
+                GenTree* helperCall = gtNewHelperCallNode(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPEHANDLE, TYP_STRUCT,
+                                                          gtNewLclVarNode(handleTemp, TYP_BYREF));
+                // ... set up return type, etc.
+                unsigned resultTmp = lvaGrabTemp(true DEBUGARG("result of REFANYTYPE"));
+                lvaSetStruct(resultTmp, classHandle, false);
+                GenTree* storeResult  = gtNewStoreLclVarNode(resultTmp, helperCall);
+                GenTree* storeDefault = gtNewStoreLclVarNode(resultTmp, gtNewIconNode(0));
+                GenTree* cond =
+                    gtNewOperNode(GT_NE, TYP_INT, gtNewLclVarNode(handleTemp, TYP_BYREF), gtNewZeroConNode(TYP_BYREF));
+                GenTree* qmark = gtNewQmarkNode(TYP_VOID, cond, gtNewColonNode(TYP_VOID, storeResult, storeDefault));
+                impAppendTree(qmark, CHECK_SPILL_ALL, impCurStmtDI);
+                op1 = gtNewLclVarNode(resultTmp, TYP_STRUCT);

This sequence introduces a temporary to hold the handle, emits a conditional (ternary) node that checks for null and either assigns the result of the helper or a default value, and then pushes the result.

3. Update the interpreter to use a new, explicit opcode

In the interpreter compiler:

-                        AddIns(INTOP_CALL_HELPER_P_S);
-                        m_pLastNewIns->data[0] = GetDataForHelperFtn(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPEHANDLE_MAYBENULL);
+                        AddIns(INTOP_GET_RUNTIME_TYPE_FROM_HANDLE);
                         m_pLastNewIns->SetSVar(classHandleVar);
                         PushInterpType(InterpTypeVT, m_compHnd->getBuiltinClass(CLASSID_TYPE_HANDLE));
                         m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);

Instead of calling the generic helper, the interpreter now emits a specialized opcode that will perform the null check and conversion directly.

Add the new opcode:

+OPDEF(INTOP_GET_RUNTIME_TYPE_FROM_HANDLE, "get.runtimetype.from.handle", 3, 1, 1, InterpOpNoArgs)

Defines the new interpreter opcode for this operation.

4. Remove references to the deleted helpers in JIT and interpreter logic

In various places:

-    return call->IsHelperCall(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE) ||
-           call->IsHelperCall(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE_MAYBENULL);
+    return call->IsHelperCall(CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE);

Removes logic that considered the _MAYBENULL helpers.

From the review thread

EgorBo: Should this PR physically remove _MAYBENULL everywhere?

adamperlin: It's still used in the interpreter, so I thought maybe removing it in the interpreter could be a separate change? I'm happy to add that to this PR though!

jkotas: I think it would be nice to do it all on one PR


[review] jkotas: This method is only used from 3 places. We may want to inline the null check in the 3 places it is used and delete it.


[review] jkotas: Instead this opcode, we may want to add an opcode that gets System.Type from handle directly (with null check) - the implementation can call TypeHandle::GetManagedObject that is a lot more efficient than going through generic managed helper infrastructure.

Takeaways

  • Replace generic helpers with explicit, local logic when possible. Removing special-case helpers in favor of direct null checks and conditional logic at call sites can simplify code and improve clarity.
  • Inline checks can improve performance and maintainability. By handling nullability explicitly at the usage site, the code avoids unnecessary indirection and makes the logic more transparent.
  • Interpreter and JIT should have specialized opcodes for common patterns. Introducing a dedicated opcode for a frequent operation (like converting a possibly-null handle) can both clarify intent and enable more efficient execution.