11using System ;
2+ using System . Collections . Concurrent ;
23using System . Collections . Generic ;
4+ using System . Dynamic ;
35using System . Linq ;
46using System . Reflection ;
57using System . Runtime . InteropServices ;
68using System . Diagnostics ;
9+
710using Python . Runtime . Native ;
811using Python . Runtime . StateSerialization ;
912
@@ -37,10 +40,190 @@ internal class TypeManager
3740 "tp_clear" ,
3841 } ;
3942
43+ static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new ( ) ;
44+
45+ // tp_getattro_dlr_proxy / tp_setattro_dlr_proxy hit HasClrMember on every
46+ // attribute access; cache the reflection result per (Type, name).
47+ static readonly ConcurrentDictionary < ( Type , string ) , bool > _hasClrMemberCache = new ( ) ;
48+
49+ static bool HasClrMember ( object instance , string memberName ) =>
50+ _hasClrMemberCache . GetOrAdd (
51+ ( instance . GetType ( ) , memberName ) ,
52+ k => k . Item1 . GetMember ( k . Item2 , BindingFlags . Public | BindingFlags . Instance ) . Length > 0 ) ;
53+
54+ static bool IsPythonSpecialAttributeName ( string memberName ) =>
55+ memberName . Length > 4 && memberName . StartsWith ( "__" ) && memberName . EndsWith ( "__" ) ;
56+
57+ static bool TryGetDynamicInstance ( BorrowedReference ob , out object instance , out IDynamicMetaObjectProvider dynamicObject )
58+ {
59+ if ( ManagedType . GetManagedObject ( ob ) is CLRObject co && co . inst is IDynamicMetaObjectProvider coDynamic )
60+ {
61+ instance = co . inst ;
62+ dynamicObject = coDynamic ;
63+ return true ;
64+ }
65+
66+ if ( Converter . ToManaged ( ob , typeof ( IDynamicMetaObjectProvider ) , out object ? managedDynamic , false )
67+ && managedDynamic is IDynamicMetaObjectProvider convertedDynamic )
68+ {
69+ instance = managedDynamic ;
70+ dynamicObject = convertedDynamic ;
71+ return true ;
72+ }
73+
74+ if ( Converter . ToManaged ( ob , typeof ( object ) , out object ? managedInstance , false )
75+ && managedInstance is IDynamicMetaObjectProvider boxedDynamic )
76+ {
77+ instance = managedInstance ;
78+ dynamicObject = boxedDynamic ;
79+ return true ;
80+ }
81+
82+ instance = null ! ;
83+ dynamicObject = null ! ;
84+ return false ;
85+ }
86+
87+ public static NewReference tp_getattro_dlr_proxy ( BorrowedReference ob , BorrowedReference key )
88+ {
89+ var isDynamic = TryGetDynamicInstance ( ob , out object instance , out IDynamicMetaObjectProvider dynamicObject ) ;
90+
91+ // The whole DLR machinery only makes sense with string keys and dynamic objects
92+ if ( ! isDynamic || ! Runtime . PyString_Check ( key ) )
93+ {
94+ return Runtime . PyObject_GenericGetAttr ( ob , key ) ;
95+ }
96+
97+ string memberName = Runtime . GetManagedString ( key ) ! ;
98+
99+ // Forward requests to GetDynamicMemberNames to the mixin implementation
100+ if ( memberName == nameof ( DynamicObjectMemberAccessor . GetDynamicMemberNames )
101+ && ! HasClrMember ( instance , memberName ) )
102+ {
103+ using var pyMemberNames = new Func < IReadOnlyCollection < string > > (
104+ ( ) => dynamicMemberAccessor . GetDynamicMemberNames ( dynamicObject )
105+ ) . ToPython ( ) ;
106+ return pyMemberNames . NewReferenceOrNull ( ) ;
107+ }
108+
109+ // Now, first try to access the Python attribute
110+ var attr = Runtime . PyObject_GenericGetAttr ( ob , key ) ;
111+ if ( ! attr . IsNull ( ) )
112+ return attr ;
113+
114+ // attr is null, so an exception must be set. If that exception is not an AttributeError,
115+ // we return from this function immediately without clearing. All later returns until the
116+ // very end will lead to the AttributeError getting raised.
117+ if ( Runtime . PyErr_ExceptionMatches ( Exceptions . AttributeError ) == 0 )
118+ {
119+ return default ;
120+ }
121+
122+ if ( HasClrMember ( instance , memberName ) || IsPythonSpecialAttributeName ( memberName ) )
123+ {
124+ return default ;
125+ }
126+
127+ bool resolved = false ;
128+ object ? value = null ;
129+ try
130+ {
131+ resolved = dynamicMemberAccessor . TryGetMember ( dynamicObject , memberName , out value ) ;
132+ }
133+ catch ( Exception e )
134+ {
135+ // Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
136+ // CLR type initialisation which can re-enter this slot on the same live object,
137+ // causing infinite recursion. A plain RuntimeError with the message is safe.
138+ Runtime . PyErr_Clear ( ) ;
139+ Exceptions . SetError ( Exceptions . RuntimeError , e . Message ) ;
140+ return default ;
141+ }
142+
143+ if ( ! resolved )
144+ {
145+ return default ;
146+ }
147+
148+ Runtime . PyErr_Clear ( ) ;
149+
150+ using var pyValue = value . ToPython ( ) ;
151+ return pyValue . NewReferenceOrNull ( ) ;
152+ }
153+
154+ public static int tp_setattro_dlr_proxy ( BorrowedReference ob , BorrowedReference key , BorrowedReference val )
155+ {
156+ var isDynamic = TryGetDynamicInstance ( ob , out object instance , out IDynamicMetaObjectProvider dynamicObject ) ;
157+
158+ // The whole DLR machinery only makes sense with string keys and dynamic objects
159+ if ( ! isDynamic || ! Runtime . PyString_Check ( key ) )
160+ {
161+ return Runtime . PyObject_GenericSetAttr ( ob , key , val ) ;
162+ }
163+
164+ string memberName = Runtime . GetManagedString ( key ) ! ;
165+
166+ // For Python-derived types (IPythonDerivedType), the Python descriptor protocol
167+ // (e.g. @property setters) takes priority over DLR member storage.
168+ if ( instance is IPythonDerivedType )
169+ {
170+ int pyResult = Runtime . PyObject_GenericSetAttr ( ob , key , val ) ;
171+ if ( pyResult == 0 )
172+ return 0 ;
173+
174+ if ( Runtime . PyErr_ExceptionMatches ( Exceptions . AttributeError ) == 0 )
175+ return pyResult ;
176+
177+ Runtime . PyErr_Clear ( ) ;
178+ // Fall through to DLR fallback below
179+ }
180+
181+ if ( ! HasClrMember ( instance , memberName ) && ! IsPythonSpecialAttributeName ( memberName ) )
182+ {
183+ // Try DLR member storage first
184+ bool handled ;
185+
186+ try
187+ {
188+ if ( val . IsNull )
189+ {
190+ handled = dynamicMemberAccessor . TryDeleteMember ( dynamicObject , memberName ) ;
191+ }
192+ else
193+ {
194+ object ? managedValue = null ;
195+ if ( val != Runtime . PyNone && ! Converter . ToManaged ( val , typeof ( object ) , out managedValue , true ) )
196+ return - 1 ;
197+
198+ handled = dynamicMemberAccessor . TrySetMember ( dynamicObject , memberName , managedValue ) ;
199+ if ( ! handled )
200+ {
201+ Exceptions . SetError ( Exceptions . AttributeError , $ "'{ instance . GetType ( ) . Name } ' object has no attribute '{ memberName } '") ;
202+ return - 1 ;
203+ }
204+ }
205+ }
206+ catch ( Exception e )
207+ {
208+ // Same reasoning as the getter: avoid Converter.ToPython(e) to keep this
209+ // slot re-entry-safe on live dynamic objects.
210+ Exceptions . SetError ( Exceptions . RuntimeError , e . Message ) ;
211+ return - 1 ;
212+ }
213+
214+ if ( handled )
215+ return 0 ;
216+ }
217+
218+ // Fall back to Python attribute setting
219+ return Runtime . PyObject_GenericSetAttr ( ob , key , val ) ;
220+ }
221+
40222 internal static void Initialize ( )
41223 {
42224 Debug . Assert ( cache . Count == 0 , "Cache should be empty" ,
43225 "Some errors may occurred on last shutdown" ) ;
226+ dynamicMemberAccessor . Clear ( ) ;
44227 using ( var plainType = SlotHelper . CreateObjectType ( ) )
45228 {
46229 subtype_traverse = Util . ReadIntPtr ( plainType . Borrow ( ) , TypeOffset . tp_traverse ) ;
@@ -64,6 +247,8 @@ internal static void RemoveTypes()
64247 }
65248 }
66249
250+ dynamicMemberAccessor . Clear ( ) ;
251+
67252 foreach ( var type in cache . Values )
68253 {
69254 type . Dispose ( ) ;
@@ -313,6 +498,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
313498 throw PythonException . ThrowLastAsClrException ( ) ;
314499 }
315500
501+ if ( typeof ( IDynamicMetaObjectProvider ) . IsAssignableFrom ( clrType ) )
502+ {
503+ InitializeSlot ( type , TypeOffset . tp_getattro , new Interop . BB_N ( tp_getattro_dlr_proxy ) , slotsHolder ) ;
504+ InitializeSlot ( type , TypeOffset . tp_setattro , new Interop . BBB_I32 ( tp_setattro_dlr_proxy ) , slotsHolder ) ;
505+ Runtime . PyType_Modified ( type . Reference ) ;
506+ }
507+
316508 var dict = Util . ReadRef ( type , TypeOffset . tp_dict ) ;
317509 string mn = clrType . Namespace ?? "" ;
318510 using ( var mod = Runtime . PyString_FromString ( mn ) )
0 commit comments