8000 Added the ability to implement delegates with `ref` and `out` paramet… · pythonnet/pythonnet@063a674 · GitHub
[go: up one dir, main page]

Skip to content

Commit 063a674

Browse files
tminkalostmsu
authored andcommitted
Added the ability to implement delegates with ref and out parameters in Python, by returning the modified parameter values in a tuple.
BREAKING: MethodBinder omits a void return type when returning a tuple of out parameters. DelegateManager unpacks a tuple of out parameters from Python (reversing the logic in MethodBinder) and sets the out parameters of the delegate.
1 parent 92932fd commit 063a674

File tree

8 files changed

+471
-57
lines changed

8 files changed

+471
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1212
- Ability to instantiate new .NET arrays using `Array[T](dim1, dim2, ...)` syntax
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
15+
- Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355])
1516

1617
### Changed
1718
- Drop support for Python 2, 3.4, and 3.5
@@ -32,6 +33,7 @@ details about the cause of the failure
3233
- floating point values passed from Python are no longer silently truncated
3334
when .NET expects an integer [#1342][i1342]
3435
- More specific error messages for method argument mismatch
36+
- BREAKING: Methods with `ref` or `out` parameters and void return type return a tuple of only the `ref` and `out` parameters.
3537

3638
### Fixed
3739

@@ -48,8 +50,8 @@ when .NET expects an integer [#1342][i1342]
4850
- Fixed issue when calling PythonException.Format where another exception would be raise for unnormalized exceptions
4951
- Made it possible to call `ToString`, `GetHashCode`, and `GetType` on inteface objects
5052
- Fixed objects returned by enumerating `PyObject` being disposed too soon
51-
- Incorrectly using a non-generic type with type parameters now produces a helpful Python error instead of throwing NullReferenceException
52-
- `import` may now raise errors with more detail than "No module named X"
53+
- Incorrectly using a non-generic type with type parameters now produces a helpful Python error instead of throwing NullReferenceException ([#1325][i1325])
54+
- `import` may now raise errors with more detail than "No module named X"
5355
- Providing an invalid type parameter to a generic type or method produces a helpful Python error
5456

5557
### Removed

src/runtime/converter.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -338,23 +338,23 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
338338

339339
if (mt != null)
340340
{
341-
if (mt is CLRObject)
341+
if (mt is CLRObject co)
342342
{
343-
object tmp = ((CLRObject)mt).inst;
343+
object tmp = co.inst;
344344
if (obType.IsInstanceOfType(tmp))
345345
{
346346
result = tmp;
347347
return true;
348348
}
349349
if (setError)
350350
{
351-
Exceptions.SetError(Exceptions.TypeError, $"value cannot be converted to {obType}");
351+
string typeString = tmp is null ? "null" : tmp.GetType().ToString();
352+
Exceptions.SetError(Exceptions.TypeError, $"{typeString} value cannot be converted to {obType}");
352353
}
353354
return false;
354355
}
355-
if (mt is ClassBase)
356+
if (mt is ClassBase cb)
356357
{
357-
var cb = (ClassBase)mt;
358358
if (!cb.type.Valid)
359359
{
360360
Exceptions.SetError(Exceptions.TypeError, cb.type.DeletedMessage);

src/runtime/delegatemanager.cs

Lines changed: 166 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2-
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Reflection;
45
using System.Reflection.Emit;
6+
using System.Text;
57

68
namespace Python.Runtime
79
{
@@ -11,23 +13,20 @@ namespace Python.Runtime
1113
/// </summary>
1214
internal class DelegateManager
1315
{
14-
private Hashtable cache;
15-
private Type basetype;
16-
private Type listtype;
17-
private Type voidtype;
18-
private Type typetype;
19-
private Type ptrtype;
20-
private CodeGenerator codeGenerator;
16+
private readonly Dictionary<Type,Type> cache = new Dictionary<Type, Type>();
17+
private readonly Type basetype = typeof(Dispatcher);
18+
private readonly Type arrayType = typeof(object[]);
19+
private readonly Type voidtype = typeof(void);
20+
private readonly Type typetype = typeof(Type);
21+
private readonly Type ptrtype = typeof(IntPtr);
22+
private readonly CodeGenerator codeGenerator = new CodeGenerator();
23+
private readonly ConstructorInfo arrayCtor;
24+
private readonly MethodInfo dispatch;
2125

2226
public DelegateManager()
2327
{
24-
basetype = typeof(Dispatcher);
25-
listtype = typeof(ArrayList);
26-
voidtype = typeof(void);
27-
typetype = typeof(Type);
28-
ptrtype = typeof(IntPtr);
29-
cache = new Hashtable();
30-
codeGenerator = new CodeGenerator();
28+
arrayCtor = arrayType.GetConstructor(new[] { typeof(int) });
29+
dispatch = basetype.GetMethod("Dispatch");
3130
}
3231

3332
/// <summary>
@@ -58,10 +57,9 @@ private Type GetDispatcher(Type dtype)
5857
// unique signatures rather than delegate types, since multiple
5958
// delegate types with the same sig could use the same dispatcher.
6059

61-
object item = cache[dtype];
62-
if (item != null)
60+
if (cache.TryGetValue(dtype, out Type item))
6361
{
64-
return (Type)item;
62+
return item;
6563
}
6664

6765
string name = $"__{dtype.FullName}Dispatcher";
@@ -103,34 +101,77 @@ private Type GetDispatcher(Type dtype)
103101

104102
MethodBuilder mb = tb.DefineMethod("Invoke", MethodAttributes.Public, method.ReturnType, signature);
105103

106-
ConstructorInfo ctor = listtype.GetConstructor(Type.EmptyTypes);
107-
MethodInfo dispatch = basetype.GetMethod("Dispatch");
108-
MethodInfo add = listtype.GetMethod("Add");
109-
110104
il = mb.GetILGenerator();
111-
il.DeclareLocal(listtype);
112-
il.Emit(OpCodes.Newobj, ctor);
105+
// loc_0 = new object[pi.Length]
106+
il.DeclareLocal(arrayType);
107+
il.Emit(OpCodes.Ldc_I4, pi.Length);
108+
il.Emit(OpCodes.Newobj, arrayCtor);
113109
il.Emit(OpCodes.Stloc_0);
114110

111+
bool anyByRef = false;
112+
115113
for (var c = 0; c < signature.Length; c++)
116114
{
117115
Type t = signature[c];
118116
il.Emit(OpCodes.Ldloc_0);
117+
il.Emit(OpCodes.Ldc_I4, c);
119118
il.Emit(OpCodes.Ldarg_S, (byte)(c + 1));
120119

120+
if (t.IsByRef)
121+
{
122+
// The argument is a pointer. We must dereference the pointer to get the value or object it points to.
123+
t = t.GetElementType();
124+
if (t.IsValueType)
125+
{
126+
il.Emit(OpCodes.Ldobj, t);
127+
}
128+
else
129+
{
130+
il.Emit(OpCodes.Ldind_Ref);
131+
}
132+
anyByRef = true;
133+
}
134+
121135
if (t.IsValueType)
122136
{
123137
il.Emit(OpCodes.Box, t);
124138
}
125139

126-
il.Emit(OpCodes.Callvirt, add);
127-
il.Emit(OpCodes.Pop);
140+
// args[c] = arg
141+
il.Emit(OpCodes.Stelem_Ref);
128142
}
129143

130144
il.Emit(OpCodes.Ldarg_0);
131145
il.Emit(OpCodes.Ldloc_0);
132146
il.Emit(OpCodes.Call, dispatch);
133147

148+
if (anyByRef)
149+
{
150+
// Dispatch() will have modified elements of the args list that correspond to out parameters.
151+
for (var c = 0; c < signature.Length; c++)
152+
{
153+
Type t = signature[c];
154+
if (t.IsByRef)
155+
{
156+
t = t.GetElementType();
157+
// *arg = args[c]
158+
il.Emit(OpCodes.Ldarg_S, (byte)(c + 1));
159+
il.Emit(OpCodes.Ldloc_0);
160+
il.Emit(OpCodes.Ldc_I4, c);
161+
il.Emit(OpCodes.Ldelem_Ref);
162+
if (t.IsValueType)
163+
{
164+
il.Emit(OpCodes.Unbox_Any, t);
165+
il.Emit(OpCodes.Stobj, t);
166+
}
167+
else
168+
{
169+
il.Emit(OpCodes.Stind_Ref);
170+
}
171+
}
172+
}
173+
}
174+
134175
if (method.ReturnType == voidtype)
135176
{
136177
il.Emit(OpCodes.Pop);
@@ -218,7 +259,7 @@ public void Dispose()
218259
GC.SuppressFinalize(this);
219260
}
220261

221-
public object Dispatch(ArrayList args)
262+
public object Dispatch(object[] args)
222263
{
223264
IntPtr gs = PythonEngine.AcquireLock();
224265
object ob;
@@ -235,7 +276,7 @@ public object Dispatch(ArrayList args)
235276
return ob;
236277
}
237278

238-
public object TrueDispatch(ArrayList args)
279+
private object TrueDispatch(object[] args)
239280
{
240281
MethodInfo method = dtype.GetMethod("Invoke");
241282
ParameterInfo[] pi = method.GetParameters();
@@ -259,20 +300,108 @@ public object TrueDispatch(ArrayList args)
259300
throw e;
260301
}
261302

262-
if (rtype == typeof(void))
303+
try
263304
{
264-
return null;
265-
}
305+
int byRefCount = pi.Count(parameterInfo => parameterInfo.ParameterType.IsByRef);
306+
if (byRefCount > 0)
307+
{
308+
// By symmetry with MethodBinder.Invoke, when there are out
309+
// parameters we expect to receive a tuple containing
310+
// the result, if any, followed by the out parameters. If there is only
311+
// one out parameter and the return type of the method is void,
312+
// we instead receive the out parameter as the result from Python.
313+
314+
bool isVoid = rtype == typeof(void);
315+
int tupleSize = byRefCount + (isVoid ? 0 : 1);
316+
if (isVoid && byRefCount == 1)
317+
{
10000 318+
// The return type is void and there is a single out parameter.
319+
for (int i = 0; i < pi.Length; i++)
320+
{
321+
Type t = pi[i].ParameterType;
322+
if (t.IsByRef)
323+
{
324+
if (!Converter.ToManaged(op, t, out object newArg, true))
325+
{
326+
Exceptions.RaiseTypeError($"The Python function did not return {t.GetElementType()} (the out parameter type)");
327+
throw new PythonException();
328+
}
329+
args[i] = newArg;
330+
break;
331+
}
332+
}
333+
return null;
334+
}
335+
else if (Runtime.PyTuple_Check(op) && Runtime.PyTuple_Size(op) == tupleSize)
336+
{
337+
int index = isVoid ? 0 : 1;
338+
for (int i = 0; i < pi.Length; i++)
339+
{
340+
Type t = pi[i].ParameterType;
341+
if (t.IsByRef)
342+
{
343+
IntPtr item = Runtime.PyTuple_GetItem(op, index++);
344+
if (!Converter.ToManaged(item, t, out object newArg, true))
345+
{
346+
Exceptions.RaiseTypeError($"The Python function returned a tuple where element {i} was not {t.GetElementType()} (the out parameter type)");
347+
throw new PythonException();
348+
}
349+
args[i] = newArg;
350+
}
351+
}
352+
if (isVoid)
353+
{
354+
return null;
355+
}
356+
IntPtr item0 = Runtime.PyTuple_GetItem(op, 0);
357+
if (!Converter.ToManaged(item0, rtype, out object result0, true))
358+
{
359+
Exceptions.RaiseTypeError($"The Python function returned a tuple where element 0 was not {rtype} (the return type)");
360+
throw new PythonException();
361+
}
362+
return result0;
363+
}
364+
else
365+
{
366+
string tpName = Runtime.PyObject_GetTypeName(op);
367+
if (Runtime.PyTuple_Check(op))
368+
{
369+
tpName += $" of size {Runtime.PyTuple_Size(op)}";
370+
}
371+
StringBuilder sb = new StringBuilder();
372+
if (!isVoid) sb.Append(rtype.FullName);
373+
for (int i = 0; i < pi.Length; i++)
374+
{
375+
Type t = pi[i].ParameterType;
376+
if (t.IsByRef)
377+
{
378+
if (sb.Length > 0) sb.Append(",");
379+
sb.Append(t.GetElementType().FullName);
380+
}
381+
}
382+
string returnValueString = isVoid ? "" : "the return value and ";
383+
Exceptions.RaiseTypeError($"Expected a tuple ({sb}) of {returnValueString}the values for out and ref parameters, got {tpName}.");
384+
throw new PythonException();
385+
}
386+
}
387+
388+
if (rtype == typeof(void))
389+
{
390+
return null;
391+
}
266392

267-
object result;
268-
if (!Converter.ToManaged(op, rtype, out result, true))
393+
object result;
394+
if (!Converter.ToManaged(op, rtype, out result, true))
395+
{
396+
throw new PythonException();
397+
}
398+
399+
return result;
400+
}
401+
finally
269402
{
270403
Runtime.XDecref(op);
271-
throw new PythonException();
272404
}
273-
274-
Runtime.XDecref(op);
275-
return result;
276405
}
277406
}
278407
}

src/runtime/methodbinder.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -960,42 +960,43 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i
960960
}
961961

962962
// If there are out parameters, we return a tuple containing
963-
// the result followed by the out parameters. If there is only
963+
// the result, if any, followed by the out parameters. If there is only
964964
// one out parameter and the return type of the method is void,
965965
// we return the out parameter as the result to Python (for
966966
// code compatibility with ironpython).
967967

968968
var mi = (MethodInfo)binding.info;
969969

970-
if (binding.outs == 1 && mi.ReturnType == typeof(void))
971-
{
972-
}
973-
974970
if (binding.outs > 0)
975971
{
976972
ParameterInfo[] pi = mi.GetParameters();
977973
int c = pi.Length;
978974
var n = 0;
979975

980-
IntPtr t = Runtime.PyTuple_New(binding.outs + 1);
981-
IntPtr v = Converter.ToPython(result, mi.ReturnType);
982-
Runtime.PyTuple_SetItem(t, n, v);
983-
n++;
976+
bool isVoid = mi.ReturnType == typeof(void);
977+
int tupleSize = binding.outs + (isVoid ? 0 : 1);
978+
IntPtr t = Runtime.PyTuple_New(tupleSize);
979+
if (!isVoid)
980+
{
981+
IntPtr v = Converter.ToPython(result, mi.ReturnType);
982+
Runtime.PyTuple_SetItem(t, n, v);
983+
n++;
984+
}
984985

985986
for (var i = 0; i < c; i++)
986987
{
987988
Type pt = pi[i].ParameterType;
988-
if (pi[i].IsOut || pt.IsByRef)
989+
if (pt.IsByRef)
989990
{
990-
v = Converter.ToPython(binding.args[i], pt.GetElementType());
991+
IntPtr v = Converter.ToPython(binding.args[i], pt.GetElementType());
991992
Runtime.PyTuple_SetItem(t, n, v);
992993
n++;
993994
}
994995
}
995996

996997
if (binding.outs == 1 && mi.ReturnType == typeof(void))
997998
{
998-
v = Runtime.PyTuple_GetItem(t, 1);
999+
IntPtr v = Runtime.PyTuple_GetItem(t, 0);
9991000
Runtime.XIncref(v);
10001001
Runtime.XDecref(t);
10011002
return v;

0 commit comments

Comments
 (0)
0