Skip to content

Commit c3ace5b

Browse files
committed
added support for byref parameters when overriding .NET methods from Python
new code is emitted to 1. unpack the tuple returned from Python to extract new values for byref parameters and modify args array correspondingly 2. marshal those new values from the args array back into arguments in IL fixes #1481
1 parent 88850f5 commit c3ace5b

File tree

6 files changed

+160
-37
lines changed

6 files changed

+160
-37
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1313
- Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]).
1414
- Add GetPythonThreadID and Interrupt methods in PythonEngine
1515
- Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355])
16+
- Ability to override .NET methods that have `out` or `ref` in Pyhton by returning the modified parameter values in a tuple. ([#1481][i1481])
1617
- `PyType` - a wrapper for Python type objects, that also permits creating new heap types from `TypeSpec`
1718
- Improved exception handling:
1819
* exceptions can now be converted with codecs
@@ -879,3 +880,4 @@ This version improves performance on benchmarks significantly compared to 2.3.
879880
[i449]: https://github.com/pythonnet/pythonnet/issues/449
880881
[i1342]: https://github.com/pythonnet/pythonnet/issues/1342
881882
[i238]: https://github.com/pythonnet/pythonnet/issues/238
883+
[i1481]: https://github.com/pythonnet/pythonnet/issues/1481

src/runtime/classderived.cs

+94-15
Original file line numberDiff line numberDiff line change
@@ -429,24 +429,33 @@ private static void AddVirtualMethod(MethodInfo method, Type baseType, TypeBuild
429429
il.Emit(OpCodes.Ldloc_0);
430430
il.Emit(OpCodes.Ldc_I4, i);
431431
il.Emit(OpCodes.Ldarg, i + 1);
432-
if (parameterTypes[i].IsValueType)
432+
var type = parameterTypes[i];
433+
if (type.IsByRef)
433434
{
434-
il.Emit(OpCodes.Box, parameterTypes[i]);
435+
type = type.GetElementType();
436+
il.Emit(OpCodes.Ldobj, type);
437+
}
438+
if (type.IsValueType)
439+
{
440+
il.Emit(OpCodes.Box, type);
435441
}
436442
il.Emit(OpCodes.Stelem, typeof(object));
437443
}
438444
il.Emit(OpCodes.Ldloc_0);
445+
446+
il.Emit(OpCodes.Ldtoken, method);
439447
#pragma warning disable CS0618 // PythonDerivedType is for internal use only
440448
if (method.ReturnType == typeof(void))
441449
{
442-
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod("InvokeMethodVoid"));
450+
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(InvokeMethodVoid)));
443451
}
444452
else
445453
{
446454
il.Emit(OpCodes.Call,
447-
typeof(PythonDerivedType).GetMethod("InvokeMethod").MakeGenericMethod(method.ReturnType));
455+
typeof(PythonDerivedType).GetMethod(nameof(InvokeMethod)).MakeGenericMethod(method.ReturnType));
448456
}
449457
#pragma warning restore CS0618 // PythonDerivedType is for internal use only
458+
CodeGenerator.GenerateMarshalByRefsBack(il, parameterTypes);
450459
il.Emit(OpCodes.Ret);
451460
}
452461

@@ -500,35 +509,65 @@ private static void AddPythonMethod(string methodName, PyObject func, TypeBuilde
500509
argTypes.ToArray());
501510

502511
ILGenerator il = methodBuilder.GetILGenerator();
512+
503513
il.DeclareLocal(typeof(object[]));
514+
il.DeclareLocal(typeof(RuntimeMethodHandle));
515+
516+
// this
504517
il.Emit(OpCodes.Ldarg_0);
518+
519+
// Python method to call
505520
il.Emit(OpCodes.Ldstr, methodName);
521+
522+
// original method name
506523
il.Emit(OpCodes.Ldnull); // don't fall back to the base type's method
524+
525+
// create args array
507526
il.Emit(OpCodes.Ldc_I4, argTypes.Count);
508527
il.Emit(OpCodes.Newarr, typeof(object));
509528
il.Emit(OpCodes.Stloc_0);
529+
530+
// fill args array
510531
for (var i = 0; i < argTypes.Count; ++i)
511532
{
512533
il.Emit(OpCodes.Ldloc_0);
513534
il.Emit(OpCodes.Ldc_I4, i);
514535
il.Emit(OpCodes.Ldarg, i + 1);
515-
if (argTypes[i].IsValueType)
536+
var type = argTypes[i];
537+
if (type.IsByRef)
516538
{
517-
il.Emit(OpCodes.Box, argTypes[i]);
539+
type = type.GetElementType();
540+
il.Emit(OpCodes.Ldobj, type);
541+
}
542+
if (type.IsValueType)
543+
{
544+
il.Emit(OpCodes.Box, type);
518545
}
519546
il.Emit(OpCodes.Stelem, typeof(object));
520547
}
548+
549+
// args array
521550
il.Emit(OpCodes.Ldloc_0);
551+
552+
// method handle for the base method is null
553+
il.Emit(OpCodes.Ldloca_S, 1);
554+
il.Emit(OpCodes.Initobj, typeof(RuntimeMethodHandle));
555+
il.Emit(OpCodes.Ldloc_1);
522556
#pragma warning disable CS0618 // PythonDerivedType is for internal use only
557+
558+
// invoke the method
523559
if (returnType == typeof(void))
524560
{
525-
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod("InvokeMethodVoid"));
561+
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(InvokeMethodVoid)));
526562
}
527563
else
528564
{
529565
il.Emit(OpCodes.Call,
530-
typeof(PythonDerivedType).GetMethod("InvokeMethod").MakeGenericMethod(returnType));
566+
typeof(PythonDerivedType).GetMethod(nameof(InvokeMethod)).MakeGenericMethod(returnType));
531567
}
568+
569+
CodeGenerator.GenerateMarshalByRefsBack(il, argTypes);
570+
532571
#pragma warning restore CS0618 // PythonDerivedType is for internal use only
533572
il.Emit(OpCodes.Ret);
534573
}
@@ -672,7 +711,8 @@ public class PythonDerivedType
672711
/// method binding (i.e. it has been overridden in the derived python
673712
/// class) it calls it, otherwise it calls the base method.
674713
/// </summary>
675-
public static T? InvokeMethod<T>(IPythonDerivedType obj, string methodName, string origMethodName, object[] args)
714+
public static T? InvokeMethod<T>(IPythonDerivedType obj, string methodName, string origMethodName,
715+
object[] args, RuntimeMethodHandle methodHandle)
676716
{
677717
var self = GetPyObj(obj);
678718

@@ -698,8 +738,10 @@ public class PythonDerivedType
698738
}
699739

700740
PyObject py_result = method.Invoke(pyargs);
701-
disposeList.Add(py_result);
702-
return py_result.As<T>();
741+
PyTuple? result_tuple = MarshalByRefsBack(args, methodHandle, py_result, outsOffset: 1);
742+
return result_tuple is not null
743+
? result_tuple[0].As<T>()
744+
: py_result.As<T>();
703745
}
704746
}
705747
}
@@ -726,7 +768,7 @@ public class PythonDerivedType
726768
}
727769

728770
public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, string origMethodName,
729-
object[] args)
771+
object?[] args, RuntimeMethodHandle methodHandle)
730772
{
731773
var self = GetPyObj(obj);
732774
if (null != self.Ref)
@@ -736,8 +778,7 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
736778
try
737779
{
738780
using var pyself = new PyObject(self.CheckRun());
739-
PyObject method = pyself.GetAttr(methodName, Runtime.None);
740-
disposeList.Add(method);
781+
using PyObject method = pyself.GetAttr(methodName, Runtime.None);
741782
if (method.Reference != Runtime.None)
742783
{
743784
// if the method hasn't been overridden then it will be a managed object
@@ -752,7 +793,7 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
752793
}
753794

754795
PyObject py_result = method.Invoke(pyargs);
755-
disposeList.Add(py_result);
796+
MarshalByRefsBack(args, methodHandle, py_result, outsOffset: 0);
756797
return;
757798
}
758799
}
@@ -779,6 +820,44 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
779820
args);
780821
}
781822

823+
/// <summary>
824+
/// If the method has byref arguments, reinterprets Python return value
825+
/// as a tuple of new values for those arguments, and updates corresponding
826+
/// elements of <paramref name="args"/> array.
827+
/// </summary>
828+
private static PyTuple? MarshalByRefsBack(object?[] args, RuntimeMethodHandle methodHandle, PyObject pyResult, int outsOffset)
829+
{
830+
if (methodHandle == default) return null;
831+
832+
var originalMethod = MethodBase.GetMethodFromHandle(methodHandle);
833+
var parameters = originalMethod.GetParameters();
834+
PyTuple? outs = null;
835+
int byrefIndex = 0;
836+
for (int i = 0; i < parameters.Length; ++i)
837+
{
838+
Type type = parameters[i].ParameterType;
839+
if (!type.IsByRef)
840+
{
841+
continue;
842+
}
843+
844+
type = type.GetElementType();
845+
846+
if (outs is null)
847+
{
848+
outs = new PyTuple(pyResult);
849+
pyResult.Dispose();
850+
}
851+
852+
args[i] = outs[byrefIndex + outsOffset].AsManagedObject(type);
853+
byrefIndex++;
854+
}
855+
if (byrefIndex > 0 && outs!.Length() > byrefIndex + outsOffset)
856+
throw new ArgumentException("Too many output parameters");
857+
858+
return outs;
859+
}
860+
782861
public static T? InvokeGetProperty<T>(IPythonDerivedType obj, string propertyName)
783862
{
784863
var self = GetPyObj(obj);

src/runtime/codegenerator.cs

+35
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Reflection;
34
using System.Reflection.Emit;
45
using System.Threading;
@@ -42,5 +43,39 @@ internal TypeBuilder DefineType(string name, Type basetype)
4243
var attrs = TypeAttributes.Public;
4344
return mBuilder.DefineType(name, attrs, basetype);
4445
}
46+
47+
/// <summary>
48+
/// Generates code, that copies potentially modified objects in args array
49+
/// back to the corresponding byref arguments
50+
/// </summary>
51+
internal static void GenerateMarshalByRefsBack(ILGenerator il, IReadOnlyList<Type> argTypes)
52+
{
53+
// assumes argument array is in loc_0
54+
for (int i = 0; i < argTypes.Count; ++i)
55+
{
56+
var type = argTypes[i];
57+
if (type.IsByRef)
58+
{
59+
type = type.GetElementType();
60+
61+
il.Emit(OpCodes.Ldarg, i + 1); // for stobj/stind later at the end
62+
63+
il.Emit(OpCodes.Ldloc_0);
64+
il.Emit(OpCodes.Ldc_I4, i);
65+
il.Emit(OpCodes.Ldelem_Ref);
66+
67+
if (type.IsValueType)
68+
{
69+
il.Emit(OpCodes.Unbox_Any, type);
70+
il.Emit(OpCodes.Stobj, type);
71+
}
72+
else
73+
{
74+
il.Emit(OpCodes.Castclass, type);
75+
il.Emit(OpCodes.Stind_Ref);
76+
}
77+
}
78+
}
79+
}
4580
}
4681
}

src/runtime/delegatemanager.cs

+1-22
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,7 @@ private Type GetDispatcher(Type dtype)
135135
if (anyByRef)
136136
{
137137
// Dispatch() will have modified elements of the args list that correspond to out parameters.
138-
for (var c = 0; c < signature.Length; c++)
139-
{
140-
Type t = signature[c];
141-
if (t.IsByRef)
142-
{
143-
t = t.GetElementType();
144-
// *arg = args[c]
145-
il.Emit(OpCodes.Ldarg_S, (byte)(c + 1));
146-
il.Emit(OpCodes.Ldloc_0);
147-
il.Emit(OpCodes.Ldc_I4, c);
148-
il.Emit(OpCodes.Ldelem_Ref);
149-
if (t.IsValueType)
150-
{
151-
il.Emit(OpCodes.Unbox_Any, t);
152-
il.Emit(OpCodes.Stobj, t);
153-
}
154-
else
155-
{
156-
il.Emit(OpCodes.Stind_Ref);
157-
}
158-
}
159-
}
138+
CodeGenerator.GenerateMarshalByRefsBack(il, signature);
160139
}
161140

162141
if (method.ReturnType == voidtype)

src/testing/interfacetest.cs

+14
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,18 @@ private interface IPrivate
7979
{
8080
}
8181
}
82+
83+
public interface IOutArg
84+
{
85+
string MyMethod_Out(string name, out int index);
86+
}
87+
88+
public class OutArgCaller
89+
{
90+
public static int CallMyMethod_Out(IOutArg myInterface)
91+
{
92+
myInterface.MyMethod_Out("myclient", out int index);
93+
return index;
94+
}
95+
}
8296
}

tests/test_interface.py

+14
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ def test_interface_object_returned_through_out_param():
9393

9494
assert hello2.SayHello() == 'hello 2'
9595

96+
def test_interface_out_param_python_impl():
97+
from Python.Test import IOutArg, OutArgCaller
98+
99+
class MyOutImpl(IOutArg):
100+
__namespace__ = "Python.Test"
101+
102+
def MyMethod_Out(self, name, index):
103+
other_index = 101
104+
return ('MyName', other_index)
105+
106+
py_impl = MyOutImpl()
107+
108+
assert 101 == OutArgCaller.CallMyMethod_Out(py_impl)
109+
96110

97111
def test_null_interface_object_returned():
98112
"""Test None is used also for methods with interface return types"""

0 commit comments

Comments
 (0)