Skip to content

Commit bf389cf

Browse files
committed
Fixed issue calling multiple levels of derived classes with some defining a method and some not.
Also fixed an issue causing the finalizer to be called twice. Added unit tests to verify the fix.
1 parent 0826fc0 commit bf389cf

File tree

3 files changed

+82
-21
lines changed

3 files changed

+82
-21
lines changed

src/runtime/Types/ClassDerived.cs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,15 @@ internal static Type CreateDerivedType(string name,
198198

199199
// Override any properties explicitly overridden in python
200200
var pyProperties = new HashSet<string>();
201+
var dictKeys = new HashSet<string>();
201202
if (py_dict != null && Runtime.PyDict_Check(py_dict))
202203
{
203204
using var dict = new PyDict(py_dict);
204205
using var keys = dict.Keys();
205206
foreach (PyObject pyKey in keys)
206207
{
208+
var keyString = pyKey.As<string>();
209+
dictKeys.Add(keyString);
207210
using var value = dict[pyKey];
208211
if (value.HasAttr("_clr_property_type_"))
209212
{
@@ -239,11 +242,18 @@ internal static Type CreateDerivedType(string name,
239242
continue;
240243
}
241244

245+
// if the name of the method is not in the dict keys, then the method is not explicitly
246+
// declared in the python code and we dont need to add it here.
247+
bool isDeclared = dictKeys.Contains(method.Name);
248+
if (!isDeclared)
249+
continue;
250+
242251
// keep track of the virtual methods redirected to the python instance
243252
virtualMethods.Add(method.Name);
244253

254+
245255
// override the virtual method to call out to the python method, if there is one.
246-
AddVirtualMethod(method, baseType, typeBuilder);
256+
AddVirtualMethod(method, baseType, typeBuilder, isDeclared);
247257
}
248258

249259
// Add any additional methods and properties explicitly exposed from Python.
@@ -271,35 +281,43 @@ internal static Type CreateDerivedType(string name,
271281
}
272282
}
273283

274-
// add the destructor so the python object created in the constructor gets destroyed
275-
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Finalize",
276-
MethodAttributes.Family |
277-
MethodAttributes.Virtual |
278-
MethodAttributes.HideBySig,
279-
CallingConventions.Standard,
280-
typeof(void),
281-
Type.EmptyTypes);
282-
ILGenerator il = methodBuilder.GetILGenerator();
283-
il.Emit(OpCodes.Ldarg_0);
284+
285+
// only add finalizer if it has not allready been added on a base type.
286+
// otherwise PyFinalize will be called multiple times for the same object,
287+
// causing an access violation exception on some platforms.
288+
// to see if this is the case, we can check if the base type is a IPythonDerivedType if so, it already
289+
// has the finalizer.
290+
if (typeof(IPythonDerivedType).IsAssignableFrom(baseType) == false)
291+
{
292+
// add the destructor so the python object created in the constructor gets destroyed
293+
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Finalize",
294+
MethodAttributes.Family |
295+
MethodAttributes.Virtual |
296+
MethodAttributes.HideBySig,
297+
CallingConventions.Standard,
298+
typeof(void),
299+
Type.EmptyTypes);
300+
ILGenerator il = methodBuilder.GetILGenerator();
301+
il.Emit(OpCodes.Ldarg_0);
284302
#pragma warning disable CS0618 // PythonDerivedType is for internal use only
285-
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(PyFinalize)));
303+
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(PyFinalize)));
286304
#pragma warning restore CS0618 // PythonDerivedType is for internal use only
287-
il.Emit(OpCodes.Ldarg_0);
288-
il.Emit(OpCodes.Call, baseClass.GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance));
289-
il.Emit(OpCodes.Ret);
305+
il.Emit(OpCodes.Ldarg_0);
306+
il.Emit(OpCodes.Call, baseClass.GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance));
307+
il.Emit(OpCodes.Ret);
308+
}
290309

291310
Type type = typeBuilder.CreateType();
292311

293-
// scan the assembly so the newly added class can be imported
312+
// scan the assembly so the newly added class can be imported.
294313
Assembly assembly = Assembly.GetAssembly(type);
295314
AssemblyManager.ScanAssembly(assembly);
296315

297-
// FIXME: assemblyBuilder not used
298-
AssemblyBuilder assemblyBuilder = assemblyBuilders[assemblyName];
299-
300316
return type;
301317
}
302318

319+
320+
303321
/// <summary>
304322
/// Add a constructor override that calls the python ctor after calling the base type constructor.
305323
/// </summary>
@@ -368,7 +386,8 @@ private static void AddConstructor(ConstructorInfo ctor, Type baseType, TypeBuil
368386
/// <param name="method">virtual method to be overridden</param>
369387
/// <param name="baseType">Python callable object</param>
370388
/// <param name="typeBuilder">TypeBuilder for the new type the method is to be added to</param>
371-
private static void AddVirtualMethod(MethodInfo method, Type baseType, TypeBuilder typeBuilder)
389+
/// <param name="isDeclared"></param>
390+
private static void AddVirtualMethod(MethodInfo method, Type baseType, TypeBuilder typeBuilder, bool isDeclared)
372391
{
373392
ParameterInfo[] parameters = method.GetParameters();
374393
Type[] parameterTypes = (from param in parameters select param.ParameterType).ToArray();
@@ -720,12 +739,15 @@ public class PythonDerivedType
720739
{
721740
var disposeList = new List<PyObject>();
722741
PyGILState gs = Runtime.PyGILState_Ensure();
742+
743+
723744
try
724745
{
725746
using var pyself = new PyObject(self.CheckRun());
726747
using PyObject method = pyself.GetAttr(methodName, Runtime.None);
727748
if (method.Reference != Runtime.PyNone)
728749
{
750+
729751
// if the method hasn't been overridden then it will be a managed object
730752
ManagedType? managedMethod = ManagedType.GetManagedObject(method.Reference);
731753
if (null == managedMethod)

src/testing/classtest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,10 @@ public static void TestObject(object obj)
7070
throw new Exception("Expected ISayHello and SimpleClass instance");
7171
}
7272
}
73+
74+
public virtual string SayGoodbye()
75+
{
76+
return "!";
77+
}
7378
}
7479
}

tests/test_subclass.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,42 @@ class OverloadingSubclass2(OverloadingSubclass):
339339
assert obj.VirtMethod[int](5) == 5
340340

341341
def test_implement_interface_and_class():
342+
import clr
343+
class DualSubClass0(ISayHello1, SimpleClass):
344+
__namespace__ = "Test0"
345+
def SayHello(self):
346+
return "hello"
347+
348+
@clr.clrmethod(str, [])
349+
def SayGoodbye(self):
350+
return "bye" + super().SayGoodbye()
351+
352+
def test_multi_level_subclass():
353+
"""
354+
Test multi levels of subclassing. This has shown verious issues, like stack overflow
355+
exception if a method was not implemented in the middle of the tree.
356+
"""
357+
import clr
342358
class DualSubClass0(ISayHello1, SimpleClass):
343359
__namespace__ = "Test"
344360
def SayHello(self):
345361
return "hello"
346-
obj = DualSubClass0()
362+
def SayGoodbye(self):
363+
return "bye" + super().SayGoodbye()
364+
class DualSubClass1(DualSubClass0):
365+
__namespace__ = "Test"
366+
def SayHello(self):
367+
return super().SayHello() + " hi1"
368+
class DualSubClass2(DualSubClass1):
369+
__namespace__ = "Test"
370+
class DualSubClass3(DualSubClass2):
371+
__namespace__ = "Test"
372+
def SayHello(self):
373+
return super().SayHello() + " hi3"
374+
def SayGoodbye(self):
375+
return super().SayGoodbye() + "!"
376+
obj = DualSubClass3()
377+
helloResult = obj.SayHello()
378+
goodByeResult = obj.SayGoodbye()
379+
assert goodByeResult =="bye!!"
380+
assert helloResult == "hello hi1 hi3"

0 commit comments

Comments
 (0)