diff --git a/Jint.Tests/Runtime/InteropExplicitTypeTests.cs b/Jint.Tests/Runtime/InteropExplicitTypeTests.cs new file mode 100644 index 0000000000..9aeaea3494 --- /dev/null +++ b/Jint.Tests/Runtime/InteropExplicitTypeTests.cs @@ -0,0 +1,186 @@ +using System.Reflection; + +namespace Jint.Tests.Runtime +{ + public class InteropExplicitTypeTests + { + public interface I1 + { + string Name { get; } + } + + public class Super + { + public string Name { get; } = "Super"; + } + + public class CI1 : Super, I1 + { + public new string Name { get; } = "CI1"; + + string I1.Name { get; } = "CI1 as I1"; + } + + public class Indexer + { + private readonly T t; + public Indexer(T t) + { + this.t = t; + } + public T this[int index] + { + get { return t; } + } + } + + public class InterfaceHolder + { + public InterfaceHolder() + { + var ci1 = new CI1(); + this.ci1 = ci1; + this.i1 = ci1; + this.super = ci1; + + this.IndexerCI1 = new Indexer(ci1); + this.IndexerI1 = new Indexer(ci1); + this.IndexerSuper = new Indexer(ci1); + } + + public readonly CI1 ci1; + public readonly I1 i1; + public readonly Super super; + + public CI1 CI1 { get => ci1; } + public I1 I1 { get => i1; } + public Super Super { get => super; } + + public CI1 GetCI1() => ci1; + public I1 GetI1() => i1; + public Super GetSuper() => super; + + public Indexer IndexerCI1 { get; } + public Indexer IndexerI1 { get; } + public Indexer IndexerSuper { get; } + + } + + private readonly Engine _engine; + private readonly InterfaceHolder holder; + + public InteropExplicitTypeTests() + { + holder = new InterfaceHolder(); + _engine = new Engine(cfg => cfg.AllowClr( + typeof(Console).GetTypeInfo().Assembly, + typeof(File).GetTypeInfo().Assembly)) + .SetValue("log", new Action(Console.WriteLine)) + .SetValue("assert", new Action(Assert.True)) + .SetValue("equal", new Action(Assert.Equal)) + .SetValue("holder", holder) + ; + } + [Fact] + public void EqualTest() + { + Assert.Equal(_engine.Evaluate("holder.I1"), _engine.Evaluate("holder.i1")); + Assert.NotEqual(_engine.Evaluate("holder.I1"), _engine.Evaluate("holder.ci1")); + + Assert.Equal(_engine.Evaluate("holder.Super"), _engine.Evaluate("holder.super")); + Assert.NotEqual(_engine.Evaluate("holder.Super"), _engine.Evaluate("holder.ci1")); + } + + [Fact] + public void ExplicitInterfaceFromField() + { + Assert.Equal(holder.i1.Name, _engine.Evaluate("holder.i1.Name")); + Assert.NotEqual(holder.i1.Name, _engine.Evaluate("holder.ci1.Name")); + } + + [Fact] + public void ExplicitInterfaceFromProperty() + { + Assert.Equal(holder.I1.Name, _engine.Evaluate("holder.I1.Name")); + Assert.NotEqual(holder.I1.Name, _engine.Evaluate("holder.CI1.Name")); + } + + [Fact] + public void ExplicitInterfaceFromMethod() + { + Assert.Equal(holder.GetI1().Name, _engine.Evaluate("holder.GetI1().Name")); + Assert.NotEqual(holder.GetI1().Name, _engine.Evaluate("holder.GetCI1().Name")); + } + + [Fact] + public void ExplicitInterfaceFromIndexer() + { + Assert.Equal(holder.IndexerI1[0].Name, _engine.Evaluate("holder.IndexerI1[0].Name")); + } + + + [Fact] + public void SuperClassFromField() + { + Assert.Equal(holder.super.Name, _engine.Evaluate("holder.super.Name")); + Assert.NotEqual(holder.super.Name, _engine.Evaluate("holder.ci1.Name")); + } + + [Fact] + public void SuperClassFromProperty() + { + Assert.Equal(holder.Super.Name, _engine.Evaluate("holder.Super.Name")); + Assert.NotEqual(holder.Super.Name, _engine.Evaluate("holder.CI1.Name")); + } + + [Fact] + public void SuperClassFromMethod() + { + Assert.Equal(holder.GetSuper().Name, _engine.Evaluate("holder.GetSuper().Name")); + Assert.NotEqual(holder.GetSuper().Name, _engine.Evaluate("holder.GetCI1().Name")); + } + + [Fact] + public void SuperClassFromIndexer() + { + Assert.Equal(holder.IndexerSuper[0].Name, _engine.Evaluate("holder.IndexerSuper[0].Name")); + } + + public struct NullabeStruct: I1 + { + public NullabeStruct() + { + } + public string name = "NullabeStruct"; + + public string Name { get => name; } + + string I1.Name { get => "NullabeStruct as I1"; } + } + + public class NullableHolder + { + public I1? I1 { get; set; } + public NullabeStruct? NullabeStruct { get; set; } + } + + [Fact] + public void TestNullable() + { + var nullableHolder = new NullableHolder(); + _engine.SetValue("nullableHolder", nullableHolder); + _engine.SetValue("nullabeStruct", new NullabeStruct()); + + Assert.Equal(_engine.Evaluate("nullableHolder.NullabeStruct"), Native.JsValue.Null); + _engine.Evaluate("nullableHolder.NullabeStruct = nullabeStruct"); + Assert.Equal(_engine.Evaluate("nullableHolder.NullabeStruct.Name"), nullableHolder.NullabeStruct?.Name); + } + + [Fact] + public void TestUnwrapClr() + { + Assert.NotEqual(holder.CI1.Name, _engine.Evaluate("holder.I1.Name")); + Assert.Equal(holder.CI1.Name, _engine.Evaluate("unwrapClr(holder.I1).Name")); + } + } +} diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index 771eab8833..07f345a3c5 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -849,7 +849,7 @@ public void CanAddArrayPrototypeForArrayLikeClrObjects() { var e = new Engine(cfg => cfg .AllowClr(typeof(Person).Assembly) - .SetWrapObjectHandler((engine, target) => + .SetWrapObjectHandler((engine, target, type) => { var instance = new ObjectWrapper(engine, target); if (instance.IsArrayLike) @@ -884,7 +884,7 @@ public void CanSetIsConcatSpreadableForArrays() { var engine = new Engine(opt => { - opt.SetWrapObjectHandler((eng, obj) => + opt.SetWrapObjectHandler((eng, obj, type) => { var wrapper = new ObjectWrapper(eng, obj); if (wrapper.IsArrayLike) diff --git a/Jint/Native/JsValue.cs b/Jint/Native/JsValue.cs index 0a2067b100..6499e0d58d 100644 --- a/Jint/Native/JsValue.cs +++ b/Jint/Native/JsValue.cs @@ -110,6 +110,14 @@ public Types Type /// Creates a valid instance from any instance /// public static JsValue FromObject(Engine engine, object? value) + { + return FromObjectWithType(engine, value, null); + } + + /// + /// Creates a valid instance from any instance, with a type + /// + public static JsValue FromObjectWithType(Engine engine, object? value, Type? type) { if (value is null) { @@ -132,7 +140,7 @@ public static JsValue FromObject(Engine engine, object? value) } } - if (DefaultObjectConverter.TryConvert(engine, value, out var defaultConversion)) + if (DefaultObjectConverter.TryConvert(engine, value, type, out var defaultConversion)) { return defaultConversion; } diff --git a/Jint/Options.cs b/Jint/Options.cs index 157cf4247e..63f57f2769 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -15,7 +15,7 @@ namespace Jint { public delegate JsValue? MemberAccessorDelegate(Engine engine, object target, string member); - public delegate ObjectInstance? WrapObjectDelegate(Engine engine, object target); + public delegate ObjectInstance? WrapObjectDelegate(Engine engine, object target, Type? type); public delegate bool ExceptionHandlerDelegate(Exception exception); @@ -120,6 +120,22 @@ internal void Apply(Engine engine) (thisObj, arguments) => new NamespaceReference(engine, TypeConverter.ToString(arguments.At(0)))), PropertyFlag.AllForbidden)); + engine.Realm.GlobalObject.SetProperty("unwrapClr", new PropertyDescriptor(new ClrFunctionInstance( + engine, + "unwrapClr", + (thisObj, arguments) => + { + var arg = arguments.At(0); + if (arg is ObjectWrapper obj) + { + return new ObjectWrapper(engine, obj.Target); + } + else + { + return arg; + } + }), + PropertyFlag.AllForbidden)); } if (Interop.ExtensionMethodTypes.Count > 0) @@ -282,7 +298,7 @@ public class InteropOptions /// ObjectInstance using class ObjectWrapper. This function can be used to /// change the behavior. /// - public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target) => new ObjectWrapper(engine, target); + public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => new ObjectWrapper(engine, target, type); /// /// diff --git a/Jint/Runtime/Descriptors/Specialized/ReflectionDescriptor.cs b/Jint/Runtime/Descriptors/Specialized/ReflectionDescriptor.cs index d35d4eef7b..4885b4f52f 100644 --- a/Jint/Runtime/Descriptors/Specialized/ReflectionDescriptor.cs +++ b/Jint/Runtime/Descriptors/Specialized/ReflectionDescriptor.cs @@ -29,7 +29,8 @@ protected internal override JsValue? CustomValue get { var value = _reflectionAccessor.GetValue(_engine, _target); - return JsValue.FromObject(_engine, value); + var type = _reflectionAccessor.MemberType; + return JsValue.FromObjectWithType(_engine, value, type); } set { diff --git a/Jint/Runtime/Interop/DefaultObjectConverter.cs b/Jint/Runtime/Interop/DefaultObjectConverter.cs index 81ba34cc5e..8c3a647389 100644 --- a/Jint/Runtime/Interop/DefaultObjectConverter.cs +++ b/Jint/Runtime/Interop/DefaultObjectConverter.cs @@ -34,10 +34,10 @@ internal static class DefaultObjectConverter } }; - public static bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result) + public static bool TryConvert(Engine engine, object value, Type? type, [NotNullWhen(true)] out JsValue? result) { result = null; - var valueType = value.GetType(); + Type valueType = ObjectWrapper.ClrType(value, type); var typeMappers = _typeMappers; @@ -109,7 +109,7 @@ public static bool TryConvert(Engine engine, object value, [NotNullWhen(true)] o } else { - var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value); + var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value, type); result = wrapped; if (engine.Options.Interop.TrackObjectWrapperIdentity && wrapped is not null) diff --git a/Jint/Runtime/Interop/MethodInfoFunctionInstance.cs b/Jint/Runtime/Interop/MethodInfoFunctionInstance.cs index 3ad902ac52..81deb326ee 100644 --- a/Jint/Runtime/Interop/MethodInfoFunctionInstance.cs +++ b/Jint/Runtime/Interop/MethodInfoFunctionInstance.cs @@ -220,6 +220,12 @@ JsValue[] ArgumentProvider(MethodDescriptor method) continue; } + Type? returnType = null; + if (method.Method is MethodInfo methodInfo) + { + returnType = methodInfo.ReturnType; + } + // todo: cache method info try { @@ -227,10 +233,10 @@ JsValue[] ArgumentProvider(MethodDescriptor method) { var genericMethodInfo = resolvedMethod; var result = genericMethodInfo.Invoke(thisObj, parameters); - return FromObject(Engine, result); + return FromObjectWithType(Engine, result, returnType); } - return FromObject(Engine, method.Method.Invoke(thisObj, parameters)); + return FromObjectWithType(Engine, method.Method.Invoke(thisObj, parameters), returnType); } catch (TargetInvocationException exception) { diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index f6f5ad67cb..74eb62a65e 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Globalization; using System.Reflection; @@ -10,18 +11,20 @@ namespace Jint.Runtime.Interop { - /// - /// Wraps a CLR instance - /// - public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable + /// + /// Wraps a CLR instance + /// + public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable { private readonly TypeDescriptor _typeDescriptor; + private readonly Type _clrType; - public ObjectWrapper(Engine engine, object obj) + public ObjectWrapper(Engine engine, object obj, Type? type = null) : base(engine) { Target = obj; - _typeDescriptor = TypeDescriptor.Get(obj.GetType()); + _clrType = ClrType(obj, type); + _typeDescriptor = TypeDescriptor.Get(_clrType); if (_typeDescriptor.LengthProperty is not null) { // create a forwarder to produce length from Count or Length if one of them is present @@ -48,7 +51,7 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) if (_properties is null || !_properties.ContainsKey(member)) { // can try utilize fast path - var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member, forWrite: true); + var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, _clrType, member, forWrite: true); if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor)) { @@ -160,7 +163,7 @@ private IEnumerable EnumerateOwnPropertyKeys(Types types) else if (includeStrings) { // we take public properties and fields - var type = Target.GetType(); + var type = _clrType; foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public)) { var indexParameters = p.GetIndexParameters(); @@ -232,7 +235,7 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable); } - var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member); + var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, _clrType, member); var descriptor = accessor.CreatePropertyDescriptor(_engine, Target, enumerable: !isDictionary); if (!isDictionary && !ReferenceEquals(descriptor, PropertyDescriptor.Undefined)) { @@ -259,6 +262,26 @@ public static PropertyDescriptor GetPropertyDescriptor(Engine engine, object tar return engine.Options.Interop.TypeResolver.GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target); } + public static Type ClrType(object obj, Type? type) + { + if (type is null || type == typeof(object)) + { + return obj.GetType(); + } + else + { + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType is not null) + { + return underlyingType; + } + else + { + return type; + } + } + } + private static JsValue Iterator(JsValue thisObject, JsValue[] arguments) { var wrapper = (ObjectWrapper) thisObject; @@ -296,12 +319,15 @@ public bool Equals(ObjectWrapper? other) return true; } - return Equals(Target, other.Target); + return Equals(Target, other.Target) && Equals(_clrType, other._clrType); } public override int GetHashCode() { - return Target?.GetHashCode() ?? 0; + var hashCode = -1468639730; + hashCode = hashCode * -1521134295 + Target.GetHashCode(); + hashCode = hashCode * -1521134295 + _clrType.GetHashCode(); + return hashCode; } private sealed class DictionaryIterator : IteratorInstance diff --git a/Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs b/Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs index d626bc90f2..ea28ec602e 100644 --- a/Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs +++ b/Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs @@ -16,6 +16,8 @@ internal abstract class ReflectionAccessor private readonly object? _memberName; private readonly PropertyInfo? _indexer; + public Type MemberType => _memberType; + protected ReflectionAccessor( Type memberType, object? memberName,