Skip to content
Open
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
1 change: 1 addition & 0 deletions src/runtime/InteropConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static InteropConfiguration MakeDefault()
{
DefaultBaseTypeProvider.Instance,
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
new DynamicObjectMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.dlr"))),
},
};
}
Expand Down
47 changes: 47 additions & 0 deletions src/runtime/Mixins/DynamicObjectMixinsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Dynamic;

namespace Python.Runtime.Mixins;

class DynamicObjectMixinsProvider : IPythonBaseTypeProvider, IDisposable
{
readonly Lazy<PyObject> mixinsModule;

public DynamicObjectMixinsProvider(Lazy<PyObject> mixinsModule) =>
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));

public PyObject Mixins => mixinsModule.Value;

public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
{
if (type is null)
throw new ArgumentNullException(nameof(type));

if (existingBases is null)
throw new ArgumentNullException(nameof(existingBases));

if (!typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type))
return existingBases;

var newBases = new List<PyType>(existingBases)
{
new(Mixins.GetAttr("DynamicMetaObjectProviderMixin"))
};

if (type.IsInterface && type.BaseType is null)
{
newBases.RemoveAll(@base => PythonReferenceComparer.Instance.Equals(@base, Runtime.PyBaseObjectType));
}

return newBases;
}

public void Dispose()
{
if (this.mixinsModule.IsValueCreated)
{
this.mixinsModule.Value.Dispose();
}
}
}
16 changes: 16 additions & 0 deletions src/runtime/Mixins/dlr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Implements helpers for Dynamic Language Runtime (DLR) types.
"""

class DynamicMetaObjectProviderMixin:
def __dir__(self):
names = set(super().__dir__())

get_names = getattr(self, "GetDynamicMemberNames", None)
if callable(get_names):
try:
names.update(get_names())
except Exception:
pass

return list(sorted(names))
2 changes: 1 addition & 1 deletion src/runtime/PythonEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s

static void LoadMixins(BorrowedReference targetModuleDict)
{
foreach (string nested in new[] { "collections" })
foreach (string nested in new[] { "collections", "dlr" })
{
LoadSubmodule(targetModuleDict,
fullName: "clr._extras." + nested,
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ internal static void Initialize(bool initSigs = false)

GenericUtil.Reset();
ClassManager.Reset();
ClassBase.Reset();
ClassDerivedObject.Reset();
TypeManager.Initialize();
CLRObject.creationBlocked = false;
Expand Down Expand Up @@ -280,6 +281,7 @@ internal static void Shutdown()

NullGCHandles(ExtensionType.loadedExtensions);
ClassManager.RemoveClasses();
ClassBase.Reset();
TypeManager.RemoveTypes();
_typesInitialized = false;

Expand Down
110 changes: 110 additions & 0 deletions src/runtime/Types/ClassBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
Expand All @@ -22,6 +23,10 @@ namespace Python.Runtime
[Serializable]
internal class ClassBase : ManagedType, IDeserializationCallback
{
static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new();

internal static void Reset() => dynamicMemberAccessor.Clear();

[NonSerialized]
internal List<string> dotNetMembers = new();
internal Indexer? indexer;
Expand Down Expand Up @@ -603,6 +608,105 @@ static IEnumerable<MethodInfo> GetCallImplementations(Type type)
=> type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "__call__");

static NewReference tp_getattro_dlr(BorrowedReference ob, BorrowedReference key)
{
var attr = Runtime.PyObject_GenericGetAttr(ob, key);
if (!attr.IsNull())
{
return attr;
}

// Only run the DLR binder if the error was AttributeError, otherwise preserve the original error
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
{
return default;
}

if (!Runtime.PyString_Check(key))
{
return default;
}

if (GetManagedObject(ob) is not CLRObject co)
{
return default;
}

// Slot registration already guarantees this type supports DLR
var dynamicObject = (IDynamicMetaObjectProvider)co.inst;

string? memberName = Runtime.GetManagedString(key);
if (memberName is null)
{
return default;
}

if (!dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out object? value))
{
return default;
}

// Clear the lingering AttributeError
Runtime.PyErr_Clear();

using var pyValue = value.ToPython();
return pyValue.NewReferenceOrNull();
}

static int tp_setattro_dlr(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
{
int result = Runtime.PyObject_GenericSetAttr(ob, key, val);
if (result == 0)
{
return 0;
}

// Preserve non-attribute errors exactly as they are.
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
{
return -1;
}

// Deletion fallback is intentionally not handled by DLR binder yet.
if (val == null)
{
return -1;
}

if (!Runtime.PyString_Check(key))
{
return -1;
}

if (GetManagedObject(ob) is not CLRObject co)
{
return -1;
}

// Slot registration already guarantees this type supports DLR.
var dynamicObject = (IDynamicMetaObjectProvider)co.inst;

string? memberName = Runtime.GetManagedString(key);
if (memberName is null)
{
return -1;
}

if (!Converter.ToManaged(val, typeof(object), out object? managedValue, true))
{
return -1;
}

if (!dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue))
{
return -1;
}

// Clear the lingering AttributeError
Runtime.PyErr_Clear();
return 0;
}

public virtual void InitializeSlots(BorrowedReference pyType, SlotsHolder slotsHolder)
{
if (!this.type.Valid) return;
Expand All @@ -612,6 +716,12 @@ public virtual void InitializeSlots(BorrowedReference pyType, SlotsHolder slotsH
TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_call, new Interop.BBB_N(tp_call_impl), slotsHolder);
}

if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(this.type.Value))
{
TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr), slotsHolder);
TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr), slotsHolder);
}

if (indexer is not null)
{
if (indexer.CanGet)
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/Types/ClassDerived.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ static ClassDerivedObject()
moduleBuilders = new Dictionary<Tuple<string, string>, ModuleBuilder>();
}

public static void Reset()
public static new void Reset()
{
assemblyBuilders = new Dictionary<string, AssemblyBuilder>();
moduleBuilders = new Dictionary<Tuple<string, string>, ModuleBuilder>();
Expand Down
84 changes: 84 additions & 0 deletions src/runtime/Types/DynamicObjectMemberAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Dynamic;
using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;

namespace Python.Runtime;

class DynamicObjectMemberAccessor
{
const int MaxCacheEntries = 1000;

readonly ConcurrentLruCache<MemberKey, Func<object, object>> getters = new(MaxCacheEntries);
readonly ConcurrentLruCache<MemberKey, Action<object, object?>> setters = new(MaxCacheEntries);

static readonly CSharpArgumentInfo[] getArgumentInfo =
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
};

static readonly CSharpArgumentInfo[] setArgumentInfo =
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
};

public bool TryGetMember(IDynamicMetaObjectProvider obj, string memberName, out object? value)
{
if (obj is null)
throw new ArgumentNullException(nameof(obj));
if (memberName is null)
throw new ArgumentNullException(nameof(memberName));

var getter = getters.GetOrAdd(new MemberKey(obj.GetType(), memberName), static key =>
{
var binder = Binder.GetMember(CSharpBinderFlags.None, key.MemberName, key.Type, getArgumentInfo);
var callSite = CallSite<Func<CallSite, object, object>>.Create(binder);
return obj => callSite.Target(callSite, obj);
});

try
{
value = getter(obj);
return true;
}
catch (RuntimeBinderException)
{
value = null;
return false;
}
}

public bool TrySetMember(IDynamicMetaObjectProvider obj, string memberName, object? value)
{
if (obj is null)
throw new ArgumentNullException(nameof(obj));
if (memberName is null)
throw new ArgumentNullException(nameof(memberName));

var setter = setters.GetOrAdd(new MemberKey(obj.GetType(), memberName), static key =>
{
var binder = Binder.SetMember(CSharpBinderFlags.None, key.MemberName, key.Type, setArgumentInfo);
var callSite = CallSite<Action<CallSite, object, object?>>.Create(binder);
return (obj, value) => callSite.Target(callSite, obj, value);
});

try
{
setter(obj, value);
return true;
}
catch (RuntimeBinderException)
{
return false;
}
}

readonly record struct MemberKey(Type Type, string MemberName);

public void Clear()
{
getters.Clear();
setters.Clear();
}
}
Loading
Loading