|
@@ -3,11 +3,12 @@ using System.Collections;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Globalization;
|
|
|
using System.Reflection;
|
|
|
+using System.Threading;
|
|
|
using Jint.Native;
|
|
|
using Jint.Native.Object;
|
|
|
using Jint.Native.Symbol;
|
|
|
using Jint.Runtime.Descriptors;
|
|
|
-using Jint.Runtime.Descriptors.Specialized;
|
|
|
+using Jint.Runtime.Interop.Reflection;
|
|
|
|
|
|
namespace Jint.Runtime.Interop
|
|
|
{
|
|
@@ -16,59 +17,60 @@ namespace Jint.Runtime.Interop
|
|
|
/// </summary>
|
|
|
public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper
|
|
|
{
|
|
|
+ private readonly TypeDescriptor _typeDescriptor;
|
|
|
+
|
|
|
public ObjectWrapper(Engine engine, object obj)
|
|
|
: base(engine)
|
|
|
{
|
|
|
Target = obj;
|
|
|
- var type = obj.GetType();
|
|
|
- if (ObjectIsArrayLikeClrCollection(type))
|
|
|
+ _typeDescriptor = TypeDescriptor.Get(obj.GetType());
|
|
|
+ if (_typeDescriptor.IsArrayLike)
|
|
|
{
|
|
|
// create a forwarder to produce length from Count or Length if one of them is present
|
|
|
- var lengthProperty = type.GetProperty("Count") ?? type.GetProperty("Length");
|
|
|
+ var lengthProperty = obj.GetType().GetProperty("Count") ?? obj.GetType().GetProperty("Length");
|
|
|
if (lengthProperty is null)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
- IsArrayLike = true;
|
|
|
- IsIntegerIndexedArray = typeof(IList).IsAssignableFrom(type);
|
|
|
-
|
|
|
- var functionInstance = new ClrFunctionInstance(engine, "length", (thisObj, arguments) => JsNumber.Create((int) lengthProperty.GetValue(obj)));
|
|
|
+ var functionInstance = new ClrFunctionInstance(engine, "length", (_, _) => JsNumber.Create((int) lengthProperty.GetValue(obj)));
|
|
|
var descriptor = new GetSetPropertyDescriptor(functionInstance, Undefined, PropertyFlag.Configurable);
|
|
|
SetProperty(KnownKeys.Length, descriptor);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static bool ObjectIsArrayLikeClrCollection(Type type)
|
|
|
+
|
|
|
+ public object Target { get; }
|
|
|
+
|
|
|
+ public override bool IsArrayLike => _typeDescriptor.IsArrayLike;
|
|
|
+
|
|
|
+ internal override bool IsIntegerIndexedArray => _typeDescriptor.IsIntegerIndexedArray;
|
|
|
+
|
|
|
+ public override bool Set(JsValue property, JsValue value, JsValue receiver)
|
|
|
{
|
|
|
- if (typeof(ICollection).IsAssignableFrom(type))
|
|
|
+ // check if we can take shortcuts for empty object, no need to generate properties
|
|
|
+ if (property is JsString stringKey)
|
|
|
{
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- foreach (var interfaceType in type.GetInterfaces())
|
|
|
- {
|
|
|
- if (!interfaceType.IsGenericType)
|
|
|
- {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (interfaceType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>)
|
|
|
- || interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>))
|
|
|
+ var member = stringKey.ToString();
|
|
|
+ if (_properties is null || !_properties.ContainsKey(member))
|
|
|
{
|
|
|
+ // can try utilize fast path
|
|
|
+ var accessor = GetAccessor(_engine, Target.GetType(), member);
|
|
|
+
|
|
|
+ // CanPut logic
|
|
|
+ if (!accessor.Writable || !_engine.Options._IsClrWriteAllowed)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ accessor.SetValue(_engine, Target, value);
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return false;
|
|
|
+ return SetSlow(property, value);
|
|
|
}
|
|
|
|
|
|
- public object Target { get; }
|
|
|
-
|
|
|
- public override bool IsArrayLike { get; }
|
|
|
-
|
|
|
- internal override bool IsIntegerIndexedArray { get; }
|
|
|
-
|
|
|
- public override bool Set(JsValue property, JsValue value, JsValue receiver)
|
|
|
+ private bool SetSlow(JsValue property, JsValue value)
|
|
|
{
|
|
|
if (!CanPut(property))
|
|
|
{
|
|
@@ -88,16 +90,37 @@ namespace Jint.Runtime.Interop
|
|
|
|
|
|
public override JsValue Get(JsValue property, JsValue receiver)
|
|
|
{
|
|
|
- if (property.IsSymbol())
|
|
|
+ if (property.IsInteger() && Target is IList list)
|
|
|
+ {
|
|
|
+ var index = (int) ((JsNumber) property)._value;
|
|
|
+ return (uint) index < list.Count ? FromObject(_engine, list[index]) : Undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (property.IsSymbol() && property != GlobalSymbolRegistry.Iterator)
|
|
|
{
|
|
|
// wrapped objects cannot have symbol properties
|
|
|
return Undefined;
|
|
|
}
|
|
|
|
|
|
- if (property.IsInteger() && Target is IList list)
|
|
|
+ if (property is JsString stringKey)
|
|
|
{
|
|
|
- var index = (int) ((JsNumber) property)._value;
|
|
|
- return (uint) index < list.Count ? FromObject(_engine, list[index]) : Undefined;
|
|
|
+ var member = stringKey.ToString();
|
|
|
+ var result = Engine.Options._MemberAccessor?.Invoke(Engine, Target, member);
|
|
|
+ if (result is not null)
|
|
|
+ {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_properties is null || !_properties.ContainsKey(member))
|
|
|
+ {
|
|
|
+ // can try utilize fast path
|
|
|
+ var accessor = GetAccessor(_engine, Target.GetType(), member);
|
|
|
+ var value = accessor.GetValue(_engine, Target);
|
|
|
+ if (value is not null)
|
|
|
+ {
|
|
|
+ return FromObject(_engine, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
return base.Get(property, receiver);
|
|
@@ -188,96 +211,57 @@ namespace Jint.Runtime.Interop
|
|
|
return iteratorProperty;
|
|
|
}
|
|
|
|
|
|
- var memberAccessor = Engine.Options._MemberAccessor;
|
|
|
-
|
|
|
- if (memberAccessor != null)
|
|
|
- {
|
|
|
- var result = memberAccessor.Invoke(Engine, Target, property.ToString());
|
|
|
-
|
|
|
- if (result != null)
|
|
|
- {
|
|
|
- return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var type = Target.GetType();
|
|
|
- var key = new ClrPropertyDescriptorFactoriesKey(type, property.ToString());
|
|
|
-
|
|
|
- if (!_engine.ClrPropertyDescriptorFactories.TryGetValue(key, out var factory))
|
|
|
+ var member = property.ToString();
|
|
|
+ var result = Engine.Options._MemberAccessor?.Invoke(Engine, Target, member);
|
|
|
+ if (result is not null)
|
|
|
{
|
|
|
- factory = ResolveProperty(type, property.ToString());
|
|
|
- _engine.ClrPropertyDescriptorFactories[key] = factory;
|
|
|
+ return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
|
|
|
}
|
|
|
|
|
|
- var descriptor = factory(_engine, Target);
|
|
|
- AddProperty(property, descriptor);
|
|
|
+ var accessor = GetAccessor(_engine, Target.GetType(), member);
|
|
|
+ var descriptor = accessor.CreatePropertyDescriptor(_engine, Target);
|
|
|
+ SetProperty(member, descriptor);
|
|
|
return descriptor;
|
|
|
}
|
|
|
-
|
|
|
- private Func<Engine, object, PropertyDescriptor> ResolveProperty(Type type, string propertyName)
|
|
|
+
|
|
|
+ private static ReflectionAccessor GetAccessor(Engine engine, Type type, string member)
|
|
|
{
|
|
|
- var isNumber = uint.TryParse(propertyName, out _);
|
|
|
+ var key = new ClrPropertyDescriptorFactoriesKey(type, member);
|
|
|
|
|
|
- // properties and fields cannot be numbers
|
|
|
- if (!isNumber)
|
|
|
+ var factories = Engine.ReflectionAccessors;
|
|
|
+ if (factories.TryGetValue(key, out var accessor))
|
|
|
{
|
|
|
- // look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
|
|
|
- PropertyInfo property = null;
|
|
|
- foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
- {
|
|
|
- // only if it's not an indexer, we can do case-ignoring matches
|
|
|
- var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
|
|
|
- if (!isStandardIndexer && EqualsIgnoreCasing(p.Name, propertyName))
|
|
|
- {
|
|
|
- property = p;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (property != null)
|
|
|
- {
|
|
|
- return (engine, target) => new PropertyInfoDescriptor(engine, property, target);
|
|
|
- }
|
|
|
+ return accessor;
|
|
|
+ }
|
|
|
|
|
|
- // look for a field
|
|
|
- FieldInfo field = null;
|
|
|
- foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
+ var factory = ResolvePropertyDescriptorFactory(engine, type, member);
|
|
|
+ // racy, we don't care, worst case we'll catch up later
|
|
|
+ Interlocked.CompareExchange(ref Engine.ReflectionAccessors,
|
|
|
+ new Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor>(factories)
|
|
|
{
|
|
|
- if (EqualsIgnoreCasing(f.Name, propertyName))
|
|
|
- {
|
|
|
- field = f;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ [key] = factory
|
|
|
+ }, factories);
|
|
|
|
|
|
- if (field != null)
|
|
|
- {
|
|
|
- return (engine, target) => new FieldInfoDescriptor(engine, field, target);
|
|
|
- }
|
|
|
-
|
|
|
- // if no properties were found then look for a method
|
|
|
- List<MethodInfo> methods = null;
|
|
|
- foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
- {
|
|
|
- if (EqualsIgnoreCasing(m.Name, propertyName))
|
|
|
- {
|
|
|
- methods ??= new List<MethodInfo>();
|
|
|
- methods.Add(m);
|
|
|
- }
|
|
|
- }
|
|
|
+ return factory;
|
|
|
+ }
|
|
|
|
|
|
- if (methods?.Count > 0)
|
|
|
- {
|
|
|
- var array = methods.ToArray();
|
|
|
- return (engine, target) => new PropertyDescriptor(new MethodInfoFunctionInstance(engine, array), PropertyFlag.OnlyEnumerable);
|
|
|
- }
|
|
|
+ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine, Type type, string memberName)
|
|
|
+ {
|
|
|
+ var isNumber = uint.TryParse(memberName, out _);
|
|
|
|
|
|
+ // we can always check indexer if there's one, and then fall back to properties if indexer returns null
|
|
|
+ IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);
|
|
|
+
|
|
|
+ // properties and fields cannot be numbers
|
|
|
+ if (!isNumber && TryFindStringPropertyAccessor(type, memberName, indexer, out var temp))
|
|
|
+ {
|
|
|
+ return temp;
|
|
|
}
|
|
|
|
|
|
// if no methods are found check if target implemented indexing
|
|
|
- if (IndexDescriptor.TryFindIndexer(_engine, type, propertyName, out var indexerFactory))
|
|
|
+ if (indexerAccessor != null)
|
|
|
{
|
|
|
- return (engine, target) => indexerFactory(target);
|
|
|
+ return indexerAccessor;
|
|
|
}
|
|
|
|
|
|
// try to find a single explicit property implementation
|
|
@@ -286,7 +270,7 @@ namespace Jint.Runtime.Interop
|
|
|
{
|
|
|
foreach (var iprop in iface.GetProperties())
|
|
|
{
|
|
|
- if (EqualsIgnoreCasing(iprop.Name, propertyName))
|
|
|
+ if (EqualsIgnoreCasing(iprop.Name, memberName))
|
|
|
{
|
|
|
list ??= new List<PropertyInfo>();
|
|
|
list.Add(iprop);
|
|
@@ -296,7 +280,7 @@ namespace Jint.Runtime.Interop
|
|
|
|
|
|
if (list?.Count == 1)
|
|
|
{
|
|
|
- return (engine, target) => new PropertyInfoDescriptor(engine, list[0], target);
|
|
|
+ return new PropertyAccessor(memberName, list[0]);
|
|
|
}
|
|
|
|
|
|
// try to find explicit method implementations
|
|
@@ -305,7 +289,7 @@ namespace Jint.Runtime.Interop
|
|
|
{
|
|
|
foreach (var imethod in iface.GetMethods())
|
|
|
{
|
|
|
- if (EqualsIgnoreCasing(imethod.Name, propertyName))
|
|
|
+ if (EqualsIgnoreCasing(imethod.Name, memberName))
|
|
|
{
|
|
|
explicitMethods ??= new List<MethodInfo>();
|
|
|
explicitMethods.Add(imethod);
|
|
@@ -315,36 +299,105 @@ namespace Jint.Runtime.Interop
|
|
|
|
|
|
if (explicitMethods?.Count > 0)
|
|
|
{
|
|
|
- var array = explicitMethods.ToArray();
|
|
|
- return (engine, target) => new PropertyDescriptor(new MethodInfoFunctionInstance(engine, array), PropertyFlag.OnlyEnumerable);
|
|
|
+ return new MethodAccessor(MethodDescriptor.Build(explicitMethods));
|
|
|
}
|
|
|
|
|
|
// try to find explicit indexer implementations
|
|
|
foreach (var interfaceType in type.GetInterfaces())
|
|
|
{
|
|
|
- if (IndexDescriptor.TryFindIndexer(_engine, interfaceType, propertyName, out var interfaceIndexerFactory))
|
|
|
+ if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
|
|
|
{
|
|
|
- return (engine, target) => interfaceIndexerFactory(target);
|
|
|
+ return accessor;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return (engine, target) => PropertyDescriptor.Undefined;
|
|
|
+ return ConstantValueAccessor.NullAccessor;
|
|
|
}
|
|
|
|
|
|
- private static bool EqualsIgnoreCasing(string s1, string s2)
|
|
|
+ private static bool TryFindStringPropertyAccessor(
|
|
|
+ Type type,
|
|
|
+ string memberName,
|
|
|
+ PropertyInfo indexerToTry,
|
|
|
+ out ReflectionAccessor wrapper)
|
|
|
{
|
|
|
- bool equals = false;
|
|
|
- if (s1.Length == s2.Length)
|
|
|
+ // look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
|
|
|
+ PropertyInfo property = null;
|
|
|
+ foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
+ {
|
|
|
+ // only if it's not an indexer, we can do case-ignoring matches
|
|
|
+ var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
|
|
|
+ if (!isStandardIndexer && EqualsIgnoreCasing(p.Name, memberName))
|
|
|
+ {
|
|
|
+ property = p;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (property != null)
|
|
|
+ {
|
|
|
+ wrapper = new PropertyAccessor(memberName, property, indexerToTry);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // look for a field
|
|
|
+ FieldInfo field = null;
|
|
|
+ foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
{
|
|
|
- if (s1.Length > 0)
|
|
|
+ if (EqualsIgnoreCasing(f.Name, memberName))
|
|
|
{
|
|
|
- equals = char.ToLowerInvariant(s1[0]) == char.ToLowerInvariant(s2[0]);
|
|
|
+ field = f;
|
|
|
+ break;
|
|
|
}
|
|
|
- if (equals && s1.Length > 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (field != null)
|
|
|
+ {
|
|
|
+ wrapper = new FieldAccessor(field, memberName, indexerToTry);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // if no properties were found then look for a method
|
|
|
+ List<MethodInfo> methods = null;
|
|
|
+ foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
|
|
|
+ {
|
|
|
+ if (EqualsIgnoreCasing(m.Name, memberName))
|
|
|
{
|
|
|
- equals = s1.Substring(1) == s2.Substring(1);
|
|
|
+ methods ??= new List<MethodInfo>();
|
|
|
+ methods.Add(m);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ if (methods?.Count > 0)
|
|
|
+ {
|
|
|
+ wrapper = new MethodAccessor(MethodDescriptor.Build(methods));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ wrapper = default;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool EqualsIgnoreCasing(string s1, string s2)
|
|
|
+ {
|
|
|
+ if (s1.Length != s2.Length)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ var equals = false;
|
|
|
+ if (s1.Length > 0)
|
|
|
+ {
|
|
|
+ equals = char.ToLowerInvariant(s1[0]) == char.ToLowerInvariant(s2[0]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (@equals && s1.Length > 1)
|
|
|
+ {
|
|
|
+#if NETSTANDARD2_1
|
|
|
+ equals = s1.AsSpan(1).SequenceEqual(s2.AsSpan(1));
|
|
|
+#else
|
|
|
+ equals = s1.Substring(1) == s2.Substring(1);
|
|
|
+#endif
|
|
|
+ }
|
|
|
return equals;
|
|
|
}
|
|
|
}
|