Skip to content

Support for byref parameters when overriding .NET methods from Python #1663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 7, 2022
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]).
- Add GetPythonThreadID and Interrupt methods in PythonEngine
- Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355])
- Ability to override .NET methods that have `out` or `ref` in Python by returning the modified parameter values in a tuple. ([#1481][i1481])
- `PyType` - a wrapper for Python type objects, that also permits creating new heap types from `TypeSpec`
- Improved exception handling:
* exceptions can now be converted with codecs
Expand Down Expand Up @@ -879,3 +880,4 @@ This version improves performance on benchmarks significantly compared to 2.3.
[i449]: https://github.com/pythonnet/pythonnet/issues/449
[i1342]: https://github.com/pythonnet/pythonnet/issues/1342
[i238]: https://github.com/pythonnet/pythonnet/issues/238
[i1481]: https://github.com/pythonnet/pythonnet/issues/1481
109 changes: 94 additions & 15 deletions src/runtime/classderived.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,24 +429,33 @@ private static void AddVirtualMethod(MethodInfo method, Type baseType, TypeBuild
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldarg, i + 1);
if (parameterTypes[i].IsValueType)
var type = parameterTypes[i];
if (type.IsByRef)
{
il.Emit(OpCodes.Box, parameterTypes[i]);
type = type.GetElementType();
il.Emit(OpCodes.Ldobj, type);
}
if (type.IsValueType)
{
il.Emit(OpCodes.Box, type);
}
il.Emit(OpCodes.Stelem, typeof(object));
}
il.Emit(OpCodes.Ldloc_0);

il.Emit(OpCodes.Ldtoken, method);
#pragma warning disable CS0618 // PythonDerivedType is for internal use only
if (method.ReturnType == typeof(void))
{
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod("InvokeMethodVoid"));
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(InvokeMethodVoid)));
}
else
{
il.Emit(OpCodes.Call,
typeof(PythonDerivedType).GetMethod("InvokeMethod").MakeGenericMethod(method.ReturnType));
typeof(PythonDerivedType).GetMethod(nameof(InvokeMethod)).MakeGenericMethod(method.ReturnType));
}
#pragma warning restore CS0618 // PythonDerivedType is for internal use only
CodeGenerator.GenerateMarshalByRefsBack(il, parameterTypes);
il.Emit(OpCodes.Ret);
}

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

ILGenerator il = methodBuilder.GetILGenerator();

il.DeclareLocal(typeof(object[]));
il.DeclareLocal(typeof(RuntimeMethodHandle));

// this
il.Emit(OpCodes.Ldarg_0);

// Python method to call
il.Emit(OpCodes.Ldstr, methodName);

// original method name
il.Emit(OpCodes.Ldnull); // don't fall back to the base type's method

// create args array
il.Emit(OpCodes.Ldc_I4, argTypes.Count);
il.Emit(OpCodes.Newarr, typeof(object));
il.Emit(OpCodes.Stloc_0);

// fill args array
for (var i = 0; i < argTypes.Count; ++i)
{
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldarg, i + 1);
if (argTypes[i].IsValueType)
var type = argTypes[i];
if (type.IsByRef)
{
il.Emit(OpCodes.Box, argTypes[i]);
type = type.GetElementType();
il.Emit(OpCodes.Ldobj, type);
}
if (type.IsValueType)
{
il.Emit(OpCodes.Box, type);
}
il.Emit(OpCodes.Stelem, typeof(object));
}

// args array
il.Emit(OpCodes.Ldloc_0);

// method handle for the base method is null
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Initobj, typeof(RuntimeMethodHandle));
il.Emit(OpCodes.Ldloc_1);
#pragma warning disable CS0618 // PythonDerivedType is for internal use only

// invoke the method
if (returnType == typeof(void))
{
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod("InvokeMethodVoid"));
il.Emit(OpCodes.Call, typeof(PythonDerivedType).GetMethod(nameof(InvokeMethodVoid)));
}
else
{
il.Emit(OpCodes.Call,
typeof(PythonDerivedType).GetMethod("InvokeMethod").MakeGenericMethod(returnType));
typeof(PythonDerivedType).GetMethod(nameof(InvokeMethod)).MakeGenericMethod(returnType));
}

CodeGenerator.GenerateMarshalByRefsBack(il, argTypes);

#pragma warning restore CS0618 // PythonDerivedType is for internal use only
il.Emit(OpCodes.Ret);
}
Expand Down Expand Up @@ -672,7 +711,8 @@ public class PythonDerivedType
/// method binding (i.e. it has been overridden in the derived python
/// class) it calls it, otherwise it calls the base method.
/// </summary>
public static T? InvokeMethod<T>(IPythonDerivedType obj, string methodName, string origMethodName, object[] args)
public static T? InvokeMethod<T>(IPythonDerivedType obj, string methodName, string origMethodName,
object[] args, RuntimeMethodHandle methodHandle)
{
var self = GetPyObj(obj);

Expand All @@ -698,8 +738,10 @@ public class PythonDerivedType
}

PyObject py_result = method.Invoke(pyargs);
disposeList.Add(py_result);
return py_result.As<T>();
PyTuple? result_tuple = MarshalByRefsBack(args, methodHandle, py_result, outsOffset: 1);
return result_tuple is not null
? result_tuple[0].As<T>()
: py_result.As<T>();
}
}
}
Expand All @@ -726,7 +768,7 @@ public class PythonDerivedType
}

public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, string origMethodName,
object[] args)
object?[] args, RuntimeMethodHandle methodHandle)
{
var self = GetPyObj(obj);
if (null != self.Ref)
Expand All @@ -736,8 +778,7 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
try
{
using var pyself = new PyObject(self.CheckRun());
PyObject method = pyself.GetAttr(methodName, Runtime.None);
disposeList.Add(method);
using PyObject method = pyself.GetAttr(methodName, Runtime.None);
if (method.Reference != Runtime.None)
{
// if the method hasn't been overridden then it will be a managed object
Expand All @@ -752,7 +793,7 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
}

PyObject py_result = method.Invoke(pyargs);
disposeList.Add(py_result);
MarshalByRefsBack(args, methodHandle, py_result, outsOffset: 0);
return;
}
}
Expand All @@ -779,6 +820,44 @@ public static void InvokeMethodVoid(IPythonDerivedType obj, string methodName, s
args);
}

/// <summary>
/// If the method has byref arguments, reinterprets Python return value
/// as a tuple of new values for those arguments, and updates corresponding
/// elements of <paramref name="args"/> array.
/// </summary>
private static PyTuple? MarshalByRefsBack(object?[] args, RuntimeMethodHandle methodHandle, PyObject pyResult, int outsOffset)
{
if (methodHandle == default) return null;

var originalMethod = MethodBase.GetMethodFromHandle(methodHandle);
var parameters = originalMethod.GetParameters();
PyTuple? outs = null;
int byrefIndex = 0;
for (int i = 0; i < parameters.Length; ++i)
{
Type type = parameters[i].ParameterType;
if (!type.IsByRef)
{
continue;
}

type = type.GetElementType();

if (outs is null)
{
outs = new PyTuple(pyResult);
pyResult.Dispose();
}

args[i] = outs[byrefIndex + outsOffset].AsManagedObject(type);
byrefIndex++;
}
if (byrefIndex > 0 && outs!.Length() > byrefIndex + outsOffset)
throw new ArgumentException("Too many output parameters");

return outs;
}

public static T? InvokeGetProperty<T>(IPythonDerivedType obj, string propertyName)
{
var self = GetPyObj(obj);
Expand Down
35 changes: 35 additions & 0 deletions src/runtime/codegenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
Expand Down Expand Up @@ -42,5 +43,39 @@ internal TypeBuilder DefineType(string name, Type basetype)
var attrs = TypeAttributes.Public;
return mBuilder.DefineType(name, attrs, basetype);
}

/// <summary>
/// Generates code, that copies potentially modified objects in args array
/// back to the corresponding byref arguments
/// </summary>
internal static void GenerateMarshalByRefsBack(ILGenerator il, IReadOnlyList<Type> argTypes)
{
// assumes argument array is in loc_0
for (int i = 0; i < argTypes.Count; ++i)
{
var type = argTypes[i];
if (type.IsByRef)
{
type = type.GetElementType();

il.Emit(OpCodes.Ldarg, i + 1); // for stobj/stind later at the end

il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldelem_Ref);

if (type.IsValueType)
{
il.Emit(OpCodes.Unbox_Any, type);
il.Emit(OpCodes.Stobj, type);
}
else
{
il.Emit(OpCodes.Castclass, type);
il.Emit(OpCodes.Stind_Ref);
}
}
}
}
}
}
23 changes: 1 addition & 22 deletions src/runtime/delegatemanager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,7 @@ private Type GetDispatcher(Type dtype)
if (anyByRef)
{
// Dispatch() will have modified elements of the args list that correspond to out parameters.
for (var c = 0; c < signature.Length; c++)
{
Type t = signature[c];
if (t.IsByRef)
{
t = t.GetElementType();
// *arg = args[c]
il.Emit(OpCodes.Ldarg_S, (byte)(c + 1));
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldc_I4, c);
il.Emit(OpCodes.Ldelem_Ref);
if (t.IsValueType)
{
il.Emit(OpCodes.Unbox_Any, t);
il.Emit(OpCodes.Stobj, t);
}
else
{
il.Emit(OpCodes.Stind_Ref);
}
}
}
CodeGenerator.GenerateMarshalByRefsBack(il, signature);
}

if (method.ReturnType == voidtype)
Expand Down
14 changes: 14 additions & 0 deletions src/testing/interfacetest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,18 @@ private interface IPrivate
{
}
}

public interface IOutArg
{
string MyMethod_Out(string name, out int index);
}

public class OutArgCaller
{
public static int CallMyMethod_Out(IOutArg myInterface)
{
myInterface.MyMethod_Out("myclient", out int index);
return index;
}
}
}
14 changes: 14 additions & 0 deletions tests/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ def test_interface_object_returned_through_out_param():

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

def test_interface_out_param_python_impl():
from Python.Test import IOutArg, OutArgCaller

class MyOutImpl(IOutArg):
__namespace__ = "Python.Test"

def MyMethod_Out(self, name, index):
other_index = 101
return ('MyName', other_index)

py_impl = MyOutImpl()

assert 101 == OutArgCaller.CallMyMethod_Out(py_impl)


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