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
55 changes: 55 additions & 0 deletions Jint.Tests/Runtime/InteropCompiledInvokerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,59 @@ public bool TryConvert(Engine engine, object value, [System.Diagnostics.CodeAnal
return false;
}
}

[Fact]
public void CustomTypeConverterStillIntercepts()
{
// a user-installed ITypeConverter participates in some exact-type argument conversions on
// the slow path (e.g. bool); the fast lane must decline so it keeps being consulted
var engine = new Engine(options => options.SetTypeConverter(e => new BoolVetoingTypeConverter(e)));
engine.SetValue("host", new Host());

var ex = Assert.Throws<Jint.Runtime.JavaScriptException>(() => engine.Evaluate("host.And(true, true)"));
Assert.Contains("No public methods", ex.Message);

// conversions the veto does not touch keep working
Assert.Equal(5, engine.Evaluate("host.AddInt(2, 3)").AsNumber());
}

private sealed class BoolVetoingTypeConverter : Jint.Runtime.Interop.DefaultTypeConverter
{
public BoolVetoingTypeConverter(Engine engine) : base(engine)
{
}

public override bool TryConvert(object? value, Type type, IFormatProvider formatProvider, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? converted)
{
if (type == typeof(bool))
{
converted = null;
return false;
}

return base.TryConvert(value, type, formatProvider, out converted);
}
}

[Fact]
public void WrongTypedThisSurfacesReflectionPathException()
{
// an extracted method invoked with a foreign receiver must decline the fast lane so the
// host observes the same TargetException the reflection path always produced (not the
// compiled cast's InvalidCastException) — ExceptionHandler predicates key on the type
var engine = new Engine(options => options.Interop.ExceptionHandler = _ => false);
engine.SetValue("host", new Host());
engine.SetValue("other", new OtherHost());

var ex = Assert.ThrowsAny<Exception>(() => engine.Evaluate("var f = host.TimesTwo; f.call(other, 21)"));
Assert.IsAssignableFrom<System.Reflection.TargetException>(ex);

// a correctly-typed extracted call still uses the fast lane and works
Assert.Equal(42, engine.Evaluate("var g = host.TimesTwo; g.call(host, 21)").AsNumber());
}

public sealed class OtherHost
{
public int Unrelated => 1;
}
}
11 changes: 9 additions & 2 deletions Jint/Runtime/Interop/MethodInfoFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,15 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments jsA
#if NET8_0_OR_GREATER
// exact-type fast lane: a compiled delegate binds and invokes without the object?[]
// parameter array, argument boxes, boxed return, and return-mapper lookup. Skipped
// when custom object converters are registered because those must see return values.
if (_engine._objectConverters is null && method.GetCompiledInvoker() is { } compiledInvoker)
// when custom object converters are registered because those must see return values,
// and when a custom ITypeConverter is installed because the slow path consults it for
// some exact-type conversions (e.g. bool) that the compiled lane performs directly.
// A wrong-typed receiver (extracted method invoked via .call on a foreign this) also
// declines so the reflection path surfaces the same TargetException it always did.
if (_engine._objectConverters is null
&& _engine.TypeConverter.GetType() == typeof(DefaultTypeConverter)
&& method.GetCompiledInvoker() is { } compiledInvoker
&& (method.Method.IsStatic || method.Method.DeclaringType?.IsInstanceOfType(thisObj) == true))
{
JsValue compiledResult = null!;
bool handled;
Expand Down