Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion Jint/Runtime/Interop/MethodDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -37,6 +43,32 @@ internal MethodDescriptor(MethodBase method)
public int ParameterDefaultValuesCount { get; }
public bool IsExtensionMethod { get; }

/// <summary>
/// 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 <see cref="Parameters"/> only; a resolved
/// generic method's parameters must be re-classified with <see cref="ComputeParameterFlags"/>.
/// </summary>
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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -327,3 +360,21 @@ public JsValue Call(Engine engine, object? instance, JsCallArguments arguments)
}
}
}

/// <summary>
/// Per-parameter-type facts consulted by interop argument binding on every call.
/// </summary>
[Flags]
internal enum InteropParameterFlags : byte
{
None = 0,

/// <summary>The parameter accepts a JsValue directly, no conversion needed.</summary>
JsValueAssignable = 1,

/// <summary>Generic parameter or generic type — the only shapes the generic-binding probe can match.</summary>
GenericLike = 2,

/// <summary>The parameter is exactly JsValue[] (the params JsValue[] host signature).</summary>
JsValueArray = 4,
}
20 changes: 14 additions & 6 deletions Jint/Runtime/Interop/MethodInfoFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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();
Expand All @@ -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;
Expand Down