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;
Comment on lines +627 to +641
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These return defaults are a little concerning. We are basically hiding errors (which I am not sure can even happen in the first place).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not. If we arrive at any of these, we have already gotten an error from the initial Python getattr. Since we are not clearing it until the very end, this is what Python is going to see. I'll add some more tests for that.

}

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"yet" here means we can't fix it until 4.0 as it would be breaking change

if (val == null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember if BorrowedReference == null is implemented. It might be better to explicitly use IsNull

{
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;
}
Comment on lines +678 to +693
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, silent error masking


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);
Comment on lines +721 to +722
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any conflicts possible?

}

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