From 198f4f73c4c143b118ae1c4e1dfe93e04d869d44 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 1 Apr 2024 12:15:45 +0300 Subject: [PATCH] Support interop against System.Text.Json types on NET8+ (#1826) --- .../InteropTests.SystemTextJson.cs | 16 ++- .../Jint.Tests.PublicInterface.csproj | 2 +- Jint/Options.cs | 5 + .../Runtime/Interop/DefaultObjectConverter.cs | 26 +++++ Jint/Runtime/Interop/DefaultTypeConverter.cs | 3 +- Jint/Runtime/Interop/MethodInfoFunction.cs | 4 +- Jint/Runtime/Interop/ObjectWrapper.cs | 20 +++- .../Interop/Reflection/IndexerAccessor.cs | 97 ++++++++++++------- 8 files changed, 127 insertions(+), 46 deletions(-) diff --git a/Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs b/Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs index ef36ef549b..83aaa54405 100644 --- a/Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs +++ b/Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Text.Json.Nodes; using Jint.Native; using Jint.Runtime.Interop; @@ -8,6 +7,12 @@ namespace Jint.Tests.PublicInterface; public sealed class SystemTextJsonValueConverter : IObjectConverter { + public static readonly SystemTextJsonValueConverter Instance = new(); + + private SystemTextJsonValueConverter() + { + } + public bool TryConvert(Engine engine, object value, out JsValue result) { if (value is JsonValue jsonValue) @@ -90,6 +95,9 @@ public void AccessingJsonNodeShouldWork() var engine = new Engine(options => { +#if !NET8_0_OR_GREATER + // Jint doesn't know about the types statically as they are not part of the out-of-the-box experience + // make JsonArray behave like JS array options.Interop.WrapObjectHandler = static (e, target, type) => { @@ -103,13 +111,14 @@ public void AccessingJsonNodeShouldWork() return ObjectWrapper.Create(e, target); }; - options.AddObjectConverter(new SystemTextJsonValueConverter()); + options.AddObjectConverter(SystemTextJsonValueConverter.Instance); + // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw options.Interop.TypeResolver = new TypeResolver { MemberFilter = static info => { - if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is PropertyInfo p) + if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is System.Reflection.PropertyInfo p) { var parameters = p.GetIndexParameters(); return parameters.Length != 1 || parameters[0].ParameterType != typeof(string); @@ -118,6 +127,7 @@ public void AccessingJsonNodeShouldWork() return true; } }; +#endif }); engine diff --git a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj index c9a9500625..d355919d43 100644 --- a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj +++ b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj @@ -27,7 +27,7 @@ - + diff --git a/Jint/Options.cs b/Jint/Options.cs index e4ab735e8c..b1079d1232 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -355,6 +355,11 @@ public class InteropOptions /// Defaults to . /// public DateTimeKind DateTimeKind { get; set; } = DateTimeKind.Utc; + + /// + /// Should the Array prototype be attached instead of Object prototype to the wrapped interop objects when type looks suitable. Defaults to true. + /// + public bool AttachArrayPrototype { get; set; } = true; } public class ConstraintOptions diff --git a/Jint/Runtime/Interop/DefaultObjectConverter.cs b/Jint/Runtime/Interop/DefaultObjectConverter.cs index 4eb726c339..2b0be92e3a 100644 --- a/Jint/Runtime/Interop/DefaultObjectConverter.cs +++ b/Jint/Runtime/Interop/DefaultObjectConverter.cs @@ -86,6 +86,14 @@ public static bool TryConvert(Engine engine, object value, Type? type, [NotNullW } #endif +#if NET8_0_OR_GREATER + if (value is System.Text.Json.Nodes.JsonValue jsonValue) + { + result = ConvertSystemTextJsonValue(engine, jsonValue); + return result is not null; + } +#endif + var t = value.GetType(); if (!engine.Options.Interop.AllowSystemReflection @@ -148,6 +156,24 @@ public static bool TryConvert(Engine engine, object value, Type? type, [NotNullW return result is not null; } +#if NET8_0_OR_GREATER + private static JsValue? ConvertSystemTextJsonValue(Engine engine, System.Text.Json.Nodes.JsonValue value) + { + return value.GetValueKind() switch + { + System.Text.Json.JsonValueKind.Object => JsValue.FromObject(engine, value), + System.Text.Json.JsonValueKind.Array => JsValue.FromObject(engine, value), + System.Text.Json.JsonValueKind.String => JsString.Create(value.ToString()), + System.Text.Json.JsonValueKind.Number => value.TryGetValue(out var doubleValue) ? JsNumber.Create(doubleValue) : JsValue.Undefined, + System.Text.Json.JsonValueKind.True => JsBoolean.True, + System.Text.Json.JsonValueKind.False => JsBoolean.False, + System.Text.Json.JsonValueKind.Undefined => JsValue.Undefined, + System.Text.Json.JsonValueKind.Null => JsValue.Null, + _ => null + }; + } +#endif + private static bool TryConvertConvertible(Engine engine, IConvertible convertible, [NotNullWhen(true)] out JsValue? result) { result = convertible.GetTypeCode() switch diff --git a/Jint/Runtime/Interop/DefaultTypeConverter.cs b/Jint/Runtime/Interop/DefaultTypeConverter.cs index 6ddd7cfb86..7e1a376790 100644 --- a/Jint/Runtime/Interop/DefaultTypeConverter.cs +++ b/Jint/Runtime/Interop/DefaultTypeConverter.cs @@ -56,7 +56,8 @@ public DefaultTypeConverter(Engine engine) return converted; } - public virtual bool TryConvert(object? value, + public virtual bool TryConvert( + object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type, IFormatProvider formatProvider, [NotNullWhen(true)] out object? converted) diff --git a/Jint/Runtime/Interop/MethodInfoFunction.cs b/Jint/Runtime/Interop/MethodInfoFunction.cs index f1b1da7a94..9e7218f33b 100644 --- a/Jint/Runtime/Interop/MethodInfoFunction.cs +++ b/Jint/Runtime/Interop/MethodInfoFunction.cs @@ -276,9 +276,7 @@ private JsValue[] ProcessParamsArrays(JsValue[] jsArguments, MethodDescriptor me return jsArguments; } - var jsArray = Engine.Realm.Intrinsics.Array.Construct(Arguments.Empty); - Engine.Realm.Intrinsics.Array.PrototypeObject.Push(jsArray, argsToTransform); - + var jsArray = new JsArray(_engine, argsToTransform); var newArgumentsCollection = new JsValue[nonParamsArgumentsCount + 1]; for (var j = 0; j < nonParamsArgumentsCount; ++j) { diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index 17b05c6100..9ea5025d30 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -31,12 +31,19 @@ public ObjectWrapper( Target = obj; ClrType = GetClrType(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 var functionInstance = new ClrFunction(engine, "length", GetLength); var descriptor = new GetSetPropertyDescriptor(functionInstance, Undefined, PropertyFlag.Configurable); SetProperty(KnownKeys.Length, descriptor); + + if (_typeDescriptor.IsArrayLike && engine.Options.Interop.AttachArrayPrototype) + { + // if we have array-like object, we can attach array prototype + SetPrototypeOf(engine.Intrinsics.Array.PrototypeObject); + } } } @@ -45,7 +52,18 @@ public ObjectWrapper( /// public static ObjectInstance Create(Engine engine, object obj, Type? type = null) #pragma warning disable CS0618 // Type or member is obsolete - => new ObjectWrapper(engine, obj, type); + { + +#if NET8_0_OR_GREATER + if (type == typeof(System.Text.Json.Nodes.JsonNode)) + { + // we need to always expose the actual type instead of the type nodes provide + type = obj.GetType(); + } +#endif + + return new ObjectWrapper(engine, obj, type); + } #pragma warning restore CS0618 // Type or member is obsolete public object Target { get; } diff --git a/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs b/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs index 5a86c5bf1a..d64670af12 100644 --- a/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs +++ b/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs @@ -48,46 +48,13 @@ internal static bool TryFindIndexer( integerKey = intKeyTemp; } - IndexerAccessor? ComposeIndexerFactory(PropertyInfo candidate, Type paramType) - { - object? key = null; - // int key is quite common case - if (paramType == typeof(int) && integerKey is not null) - { - key = integerKey; - } - else - { - engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key); - } - - if (key is not null) - { - // the key can be converted for this indexer - var indexerProperty = candidate; - // get contains key method to avoid index exception being thrown in dictionaries - paramTypeArray[0] = paramType; - var containsKeyMethod = targetType.GetMethod(nameof(IDictionary.ContainsKey), paramTypeArray); - if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary))) - { - paramTypeArray[0] = typeof(object); - containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray); - } - - return new IndexerAccessor(indexerProperty, containsKeyMethod, key); - } - - // the key type doesn't work for this indexer - return null; - } - var filter = new Func(m => engine.Options.Interop.TypeResolver.Filter(engine, m)); // default indexer wins var descriptor = TypeDescriptor.Get(targetType); - if (descriptor.IntegerIndexerProperty is not null && filter(descriptor.IntegerIndexerProperty)) + if (descriptor.IntegerIndexerProperty is not null && !filter(descriptor.IntegerIndexerProperty)) { - indexerAccessor = ComposeIndexerFactory(descriptor.IntegerIndexerProperty, typeof(int)); + indexerAccessor = ComposeIndexerFactory(engine, targetType, descriptor.IntegerIndexerProperty, paramType: typeof(int), propertyName, integerKey, paramTypeArray); if (indexerAccessor != null) { indexer = descriptor.IntegerIndexerProperty; @@ -113,7 +80,7 @@ internal static bool TryFindIndexer( if (candidate.GetGetMethod() != null || candidate.GetSetMethod() != null) { var paramType = indexParameters[0].ParameterType; - indexerAccessor = ComposeIndexerFactory(candidate, paramType); + indexerAccessor = ComposeIndexerFactory(engine, targetType, candidate, paramType, propertyName, integerKey, paramTypeArray); if (indexerAccessor != null) { if (paramType != typeof(string) || integerKey is null) @@ -145,6 +112,62 @@ internal static bool TryFindIndexer( return false; } + private static IndexerAccessor? ComposeIndexerFactory( + Engine engine, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType, + PropertyInfo candidate, + Type paramType, + string propertyName, + int? integerKey, + Type[] paramTypeArray) + { + // check for known incompatible types +#if NET8_0_OR_GREATER + if (typeof(System.Text.Json.Nodes.JsonNode).IsAssignableFrom(targetType) + && (targetType != typeof(System.Text.Json.Nodes.JsonArray) || paramType != typeof(int)) + && (targetType != typeof(System.Text.Json.Nodes.JsonObject) || paramType != typeof(string))) + { + // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw + // we cannot access this[int] with anything else than JsonArray, otherwise itw will throw + return null; + } +#endif + + object? key = null; + // int key is quite common case + if (paramType == typeof(int)) + { + if (integerKey is not null) + { + key = integerKey; + } + } + else + { + engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key); + } + + if (key is not null) + { + // the key can be converted for this indexer + var indexerProperty = candidate; + // get contains key method to avoid index exception being thrown in dictionaries + paramTypeArray[0] = paramType; + var containsKeyMethod = targetType.GetMethod(nameof(IDictionary.ContainsKey), paramTypeArray); + if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary))) + { + paramTypeArray[0] = typeof(object); + containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray); + } + + return new IndexerAccessor(indexerProperty, containsKeyMethod, key); + } + + // the key type doesn't work for this indexer + return null; + } + + public override bool Readable => _indexer.CanRead; public override bool Writable => _indexer.CanWrite; @@ -157,7 +180,7 @@ internal static bool TryFindIndexer( return null; } - object[] parameters = { _key }; + object[] parameters = [_key]; if (_containsKey != null) {