From 1994a9e85122f03796793a2e8bcbfab757c9ab11 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Tue, 21 Jul 2026 00:02:43 +0300 Subject: [PATCH] Precompute per-parameter interop binding flags Interop argument binding re-derived three per-parameter-type facts on every call: typeof(JsValue).IsAssignableFrom(parameterType), the generic-shape test hidden inside IsGenericParameter (which forced a boxing argument.ToObject() detour for every argument, including plain int parameters), and the JsValue[] params-signature check. MethodDescriptor now classifies each parameter once into InteropParameterFlags; MethodInfoFunction.TryCall and MethodDescriptor.Call branch on the cached flags. Resolved generic methods re-classify their own parameters (their ParameterInfos differ from the descriptor's). The general conversion branch computes the boxed argument lazily now that the generic probe no longer produces it as a side effect. Conversion semantics are unchanged - the same conversions run, only constant-per-method reflection checks and the needless pre-boxing are gone. Co-Authored-By: Claude Fable 5 --- Jint/Runtime/Interop/MethodDescriptor.cs | 53 +++++++++++++++++++++- Jint/Runtime/Interop/MethodInfoFunction.cs | 20 +++++--- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Jint/Runtime/Interop/MethodDescriptor.cs b/Jint/Runtime/Interop/MethodDescriptor.cs index 4d9853ac3..b025e9265 100644 --- a/Jint/Runtime/Interop/MethodDescriptor.cs +++ b/Jint/Runtime/Interop/MethodDescriptor.cs @@ -29,6 +29,12 @@ internal MethodDescriptor(MethodBase method) ParameterDefaultValuesCount++; } } + + ParameterFlags = Parameters.Length == 0 ? [] : new InteropParameterFlags[Parameters.Length]; + for (var i = 0; i < Parameters.Length; i++) + { + ParameterFlags[i] = ComputeParameterFlags(Parameters[i].ParameterType); + } } public MethodBase Method { get; } @@ -37,6 +43,32 @@ internal MethodDescriptor(MethodBase method) public int ParameterDefaultValuesCount { get; } public bool IsExtensionMethod { get; } + /// + /// Facts about each parameter type that argument binding consults on every call — computed + /// once so the per-call loop avoids reflection checks (and the boxing ToObject detour that + /// only generic-shaped parameters need). Valid for only; a resolved + /// generic method's parameters must be re-classified with . + /// + public InteropParameterFlags[] ParameterFlags { get; } + + public static InteropParameterFlags ComputeParameterFlags(Type parameterType) + { + var flags = InteropParameterFlags.None; + if (typeof(JsValue).IsAssignableFrom(parameterType)) + { + flags |= InteropParameterFlags.JsValueAssignable; + } + if (parameterType.IsGenericParameter || parameterType.IsGenericType) + { + flags |= InteropParameterFlags.GenericLike; + } + if (parameterType == typeof(JsValue[])) + { + flags |= InteropParameterFlags.JsValueArray; + } + return flags; + } + #if NET8_0_OR_GREATER // lazily initialized fast invokers, benign race - last writer wins private MethodInvoker? _methodInvoker; @@ -282,6 +314,7 @@ public JsValue Call(Engine engine, object? instance, JsCallArguments arguments) { object?[] parameters = arguments.Length == 0 ? [] : new object?[arguments.Length]; var methodParameters = Parameters; + var parameterFlags = ParameterFlags; var valueCoercionType = engine.Options.Interop.ValueCoercion; try @@ -293,7 +326,7 @@ public JsValue Call(Engine engine, object? instance, JsCallArguments arguments) var value = arguments[i]; object? converted; - if (typeof(JsValue).IsAssignableFrom(parameterType)) + if ((parameterFlags[i] & InteropParameterFlags.JsValueAssignable) != InteropParameterFlags.None) { converted = value; } @@ -327,3 +360,21 @@ public JsValue Call(Engine engine, object? instance, JsCallArguments arguments) } } } + +/// +/// Per-parameter-type facts consulted by interop argument binding on every call. +/// +[Flags] +internal enum InteropParameterFlags : byte +{ + None = 0, + + /// The parameter accepts a JsValue directly, no conversion needed. + JsValueAssignable = 1, + + /// Generic parameter or generic type — the only shapes the generic-binding probe can match. + GenericLike = 2, + + /// The parameter is exactly JsValue[] (the params JsValue[] host signature). + JsValueArray = 4, +} diff --git a/Jint/Runtime/Interop/MethodInfoFunction.cs b/Jint/Runtime/Interop/MethodInfoFunction.cs index 60bed1239..9b07730cb 100644 --- a/Jint/Runtime/Interop/MethodInfoFunction.cs +++ b/Jint/Runtime/Interop/MethodInfoFunction.cs @@ -248,10 +248,17 @@ private bool TryCall( var resolvedMethod = ResolveMethod(method.Method, methodParameters, arguments); // We only need to call GetParameters it if this ends up being a generic method (i.e. they will be different in that scenario) var isGenericDefinition = false; + var parameterFlags = method.ParameterFlags; if (resolvedMethod.IsGenericMethod) { + // the resolved parameters differ from the descriptor's, re-classify them methodParameters = resolvedMethod.GetParameters(); isGenericDefinition = method.Method is MethodInfo { IsGenericMethodDefinition: true }; + parameterFlags = new InteropParameterFlags[methodParameters.Length]; + for (var i = 0; i < methodParameters.Length; i++) + { + parameterFlags[i] = MethodDescriptor.ComputeParameterFlags(methodParameters[i].ParameterType); + } } // NOTE: pooling this buffer via ArrayPool was measured slower than allocation (tiny @@ -263,10 +270,11 @@ private bool TryCall( { var methodParameter = methodParameters[i]; var parameterType = methodParameter.ParameterType; + var flags = parameterFlags[i]; var argument = arguments.Length > i ? arguments[i] : null; object? argumentObject = null; - if (typeof(JsValue).IsAssignableFrom(parameterType)) + if ((flags & InteropParameterFlags.JsValueAssignable) != InteropParameterFlags.None) { parameters[i] = argument; } @@ -275,13 +283,13 @@ private bool TryCall( // optional parameters[i] = System.Type.Missing; } - else if (IsGenericParameter(argumentObject = argument.ToObject(), parameterType)) // don't think we need the condition preface of (argument == null) because of earlier condition + else if ((flags & InteropParameterFlags.GenericLike) != InteropParameterFlags.None && IsGenericParameter(argumentObject = argument.ToObject(), parameterType)) { - // ^ this is the best way I could think of to only eval argument.ToObject() when needed. - // But it's very cursed, I'm sorry. + // only generic-shaped parameter types can match the probe, so the boxing + // ToObject() detour is skipped entirely for plain parameter types parameters[i] = argumentObject; } - else if (parameterType == typeof(JsValue[]) && argument.IsArray()) + else if ((flags & InteropParameterFlags.JsValueArray) != InteropParameterFlags.None && argument.IsArray()) { // Handle specific case of F(params JsValue[]) var arrayInstance = argument.AsArray(); @@ -301,7 +309,7 @@ private bool TryCall( else { if (!ReflectionExtensions.TryConvertViaTypeCoercion(parameterType, _engine.Options.Interop.ValueCoercion, argument, out parameters[i]) - && !converter.TryConvert(argumentObject, parameterType, CultureInfo.InvariantCulture, out parameters[i])) + && !converter.TryConvert(argumentObject ?? argument.ToObject(), parameterType, CultureInfo.InvariantCulture, out parameters[i])) { // arguments don't match this method return false;