diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fd2b1dcf..9febc7974 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- Added function that sets Py_NoSiteFlag to 1.
- Added support for Jetson Nano.
- Added support for __len__ for .NET classes that implement ICollection
+- Added `IPyArgumentConverter` interface and `PyArgConverter` attribute, that control custom argument marshaling from Python to .NET (#835)
### Changed
diff --git a/src/embed_tests/TestCustomArgMarshal.cs b/src/embed_tests/TestCustomArgMarshal.cs
new file mode 100644
index 000000000..d9f22ef9d
--- /dev/null
+++ b/src/embed_tests/TestCustomArgMarshal.cs
@@ -0,0 +1,88 @@
+using System;
+using NUnit.Framework;
+using Python.Runtime;
+
+namespace Python.EmbeddingTest
+{
+ class TestCustomArgMarshal
+ {
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ PythonEngine.Initialize();
+ }
+
+ [OneTimeTearDown]
+ public void Dispose()
+ {
+ PythonEngine.Shutdown();
+ }
+
+ [Test]
+ public void CustomArgMarshaller()
+ {
+ var obj = new CustomArgMarshaling();
+ using (Py.GIL()) {
+ dynamic callWithInt = PythonEngine.Eval("lambda o: o.CallWithInt('42')");
+ callWithInt(obj.ToPython());
+ }
+ Assert.AreEqual(expected: 42, actual: obj.LastArgument);
+ }
+
+ [Test]
+ public void MarshallerOverride() {
+ var obj = new DerivedMarshaling();
+ using (Py.GIL()) {
+ dynamic callWithInt = PythonEngine.Eval("lambda o: o.CallWithInt({ 'value': 42 })");
+ callWithInt(obj.ToPython());
+ }
+ Assert.AreEqual(expected: 42, actual: obj.LastArgument);
+ }
+ }
+
+ [PyArgConverter(typeof(CustomArgConverter))]
+ class CustomArgMarshaling {
+ public object LastArgument { get; protected set; }
+ public virtual void CallWithInt(int value) => this.LastArgument = value;
+ }
+
+ // this should override original custom marshaling behavior for any new methods
+ [PyArgConverter(typeof(CustomArgConverter2))]
+ class DerivedMarshaling : CustomArgMarshaling {
+ public override void CallWithInt(int value) {
+ base.CallWithInt(value);
+ }
+ }
+
+ class CustomArgConverter : DefaultPyArgumentConverter {
+ public override bool TryConvertArgument(IntPtr pyarg, Type parameterType, bool needsResolution,
+ out object arg, out bool isOut) {
+ if (parameterType != typeof(int))
+ return base.TryConvertArgument(pyarg, parameterType, needsResolution, out arg, out isOut);
+
+ bool isString = base.TryConvertArgument(pyarg, typeof(string), needsResolution,
+ out arg, out isOut);
+ if (!isString) return false;
+
+ int number;
+ if (!int.TryParse((string)arg, out number)) return false;
+ arg = number;
+ return true;
+ }
+ }
+
+ class CustomArgConverter2 : DefaultPyArgumentConverter {
+ public override bool TryConvertArgument(IntPtr pyarg, Type parameterType, bool needsResolution,
+ out object arg, out bool isOut) {
+ if (parameterType != typeof(int))
+ return base.TryConvertArgument(pyarg, parameterType, needsResolution, out arg, out isOut);
+ bool isPyObject = base.TryConvertArgument(pyarg, typeof(PyObject), needsResolution,
+ out arg, out isOut);
+ if (!isPyObject) return false;
+ var dict = new PyDict((PyObject)arg);
+ int number = (dynamic)dict["value"];
+ arg = number;
+ return true;
+ }
+ }
+}
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index 0c2f912de..4d959e6ab 100644
--- a/src/runtime/Python.Runtime.csproj
+++ b/src/runtime/Python.Runtime.csproj
@@ -94,6 +94,7 @@
+
@@ -122,6 +123,7 @@
+
diff --git a/src/runtime/defaultpyargconverter.cs b/src/runtime/defaultpyargconverter.cs
new file mode 100644
index 000000000..b2dd5d236
--- /dev/null
+++ b/src/runtime/defaultpyargconverter.cs
@@ -0,0 +1,111 @@
+namespace Python.Runtime {
+ using System;
+
+ ///
+ /// The implementation of used by default
+ ///
+ public class DefaultPyArgumentConverter: IPyArgumentConverter
+ {
+ ///
+ /// Gets the singleton instance.
+ ///
+ public static DefaultPyArgumentConverter Instance { get; } = new DefaultPyArgumentConverter();
+
+ ///
+ ///
+ /// Attempts to convert an argument passed by Python to the specified parameter type.
+ ///
+ /// Unmanaged pointer to the Python argument value
+ /// The expected type of the parameter
+ /// true if the method is overloaded
+ /// This parameter will receive the converted value, matching the specified type
+ /// This parameter will be set to true,
+ /// if the final type needs to be marshaled as an out argument.
+ /// true, if the object matches requested type,
+ /// and conversion was successful, otherwise false
+ public virtual bool TryConvertArgument(
+ IntPtr pyarg, Type parameterType, bool needsResolution,
+ out object arg, out bool isOut)
+ {
+ arg = null;
+ isOut = false;
+ Type clrType = TryComputeClrArgumentType(parameterType, pyarg, needsResolution: needsResolution);
+ if (clrType == null)
+ {
+ return false;
+ }
+
+ if (!Converter.ToManaged(pyarg, clrType, out arg, false))
+ {
+ Exceptions.Clear();
+ return false;
+ }
+
+ isOut = clrType.IsByRef;
+ return true;
+ }
+
+ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool needsResolution)
+ {
+ // this logic below handles cases when multiple overloading methods
+ // are ambiguous, hence comparison between Python and CLR types
+ // is necessary
+ Type clrType = null;
+ IntPtr pyArgType;
+ if (needsResolution)
+ {
+ // HACK: each overload should be weighted in some way instead
+ pyArgType = Runtime.PyObject_Type(argument);
+ Exceptions.Clear();
+ if (pyArgType != IntPtr.Zero)
+ {
+ clrType = Converter.GetTypeByAlias(pyArgType);
+ }
+ Runtime.XDecref(pyArgType);
+ }
+
+ if (clrType != null)
+ {
+ if ((parameterType != typeof(object)) && (parameterType != clrType))
+ {
+ IntPtr pyParamType = Converter.GetPythonTypeByAlias(parameterType);
+ pyArgType = Runtime.PyObject_Type(argument);
+ Exceptions.Clear();
+
+ bool typeMatch = false;
+ if (pyArgType != IntPtr.Zero && pyParamType == pyArgType)
+ {
+ typeMatch = true;
+ clrType = parameterType;
+ }
+ if (!typeMatch)
+ {
+ // this takes care of enum values
+ TypeCode argTypeCode = Type.GetTypeCode(parameterType);
+ TypeCode paramTypeCode = Type.GetTypeCode(clrType);
+ if (argTypeCode == paramTypeCode)
+ {
+ typeMatch = true;
+ clrType = parameterType;
+ }
+ }
+ Runtime.XDecref(pyArgType);
+ if (!typeMatch)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ clrType = parameterType;
+ }
+ }
+ else
+ {
+ clrType = parameterType;
+ }
+
+ return clrType;
+ }
+ }
+}
diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs
index 8a7fc1930..ca69a2ba4 100644
--- a/src/runtime/methodbinder.cs
+++ b/src/runtime/methodbinder.cs
@@ -1,5 +1,9 @@
using System;
using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
@@ -19,15 +23,16 @@ internal class MethodBinder
public MethodBase[] methods;
public bool init = false;
public bool allow_threads = true;
+ IPyArgumentConverter pyArgumentConverter;
internal MethodBinder()
{
list = new ArrayList();
}
- internal MethodBinder(MethodInfo mi)
+ internal MethodBinder(MethodInfo mi): this()
{
- list = new ArrayList { mi };
+ this.AddMethod(mi);
}
public int Count
@@ -37,6 +42,7 @@ public int Count
internal void AddMethod(MethodBase m)
{
+ Debug.Assert(!init);
list.Add(m);
}
@@ -164,11 +170,50 @@ internal MethodBase[] GetMethods()
// I'm sure this could be made more efficient.
list.Sort(new MethodSorter());
methods = (MethodBase[])list.ToArray(typeof(MethodBase));
+ pyArgumentConverter = GetArgumentConverter(this.methods);
init = true;
}
return methods;
}
+ static IPyArgumentConverter GetArgumentConverter(IEnumerable methods) {
+ IPyArgumentConverter converter = null;
+ Type converterType = null;
+ foreach (MethodBase method in methods)
+ {
+ PyArgConverterAttribute attribute = TryGetArgConverter(method.DeclaringType);
+ if (converterType == null)
+ {
+ if (attribute == null) continue;
+
+ converterType = attribute.ConverterType;
+ converter = attribute.Converter;
+ } else if (converterType != attribute?.ConverterType)
+ {
+ throw new NotSupportedException("All methods must have the same IPyArgumentConverter");
+ }
+ }
+
+ return converter ?? DefaultPyArgumentConverter.Instance;
+ }
+
+ static readonly ConcurrentDictionary ArgConverterCache =
+ new ConcurrentDictionary();
+ static PyArgConverterAttribute TryGetArgConverter(Type type) {
+ if (type == null) return null;
+
+ return ArgConverterCache.GetOrAdd(type, declaringType =>
+ declaringType
+ .GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: true)
+ .OfType()
+ .SingleOrDefault()
+ ?? declaringType.Assembly
+ .GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: true)
+ .OfType()
+ .SingleOrDefault()
+ );
+ }
+
///
/// Precedence algorithm largely lifted from Jython - the concerns are
/// generally the same so we'll start with this and tweak as necessary.
@@ -300,14 +345,17 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
var pynargs = (int)Runtime.PyTuple_Size(args);
var isGeneric = false;
+ IPyArgumentConverter argumentConverter;
if (info != null)
{
_methods = new MethodBase[1];
_methods.SetValue(info, 0);
+ argumentConverter = GetArgumentConverter(_methods);
}
else
{
_methods = GetMethods();
+ argumentConverter = this.pyArgumentConverter;
}
// TODO: Clean up
@@ -326,7 +374,8 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
continue;
}
var outs = 0;
- var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList,
+ var margs = TryConvertArguments(pi, paramsArray, argumentConverter,
+ args, pynargs, kwargDict, defaultArgList,
needsResolution: _methods.Length > 1,
outs: out outs);
@@ -383,6 +432,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
/// Returns number of output parameters
/// An array of .NET arguments, that can be passed to a method.
static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
+ IPyArgumentConverter argumentConverter,
IntPtr args, int pyArgCount,
Dictionary kwargDict,
ArrayList defaultArgList,
@@ -423,7 +473,9 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
}
bool isOut;
- if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut))
+ if (!argumentConverter.TryConvertArgument(
+ op, parameter.ParameterType, needsResolution,
+ out margs[paramIndex], out isOut))
{
return null;
}
@@ -445,97 +497,6 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
return margs;
}
- static bool TryConvertArgument(IntPtr op, Type parameterType, bool needsResolution,
- out object arg, out bool isOut)
- {
- arg = null;
- isOut = false;
- var clrtype = TryComputeClrArgumentType(parameterType, op, needsResolution: needsResolution);
- if (clrtype == null)
- {
- return false;
- }
-
- if (!Converter.ToManaged(op, clrtype, out arg, false))
- {
- Exceptions.Clear();
- return false;
- }
-
- isOut = clrtype.IsByRef;
- return true;
- }
-
- static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool needsResolution)
- {
- // this logic below handles cases when multiple overloading methods
- // are ambiguous, hence comparison between Python and CLR types
- // is necessary
- Type clrtype = null;
- IntPtr pyoptype;
- if (needsResolution)
- {
- // HACK: each overload should be weighted in some way instead
- pyoptype = Runtime.PyObject_Type(argument);
- Exceptions.Clear();
- if (pyoptype != IntPtr.Zero)
- {
- clrtype = Converter.GetTypeByAlias(pyoptype);
- }
- Runtime.XDecref(pyoptype);
- }
-
- if (clrtype != null)
- {
- var typematch = false;
- if ((parameterType != typeof(object)) && (parameterType != clrtype))
- {
- IntPtr pytype = Converter.GetPythonTypeByAlias(parameterType);
- pyoptype = Runtime.PyObject_Type(argument);
- Exceptions.Clear();
- if (pyoptype != IntPtr.Zero)
- {
- if (pytype != pyoptype)
- {
- typematch = false;
- }
- else
- {
- typematch = true;
- clrtype = parameterType;
- }
- }
- if (!typematch)
- {
- // this takes care of enum values
- TypeCode argtypecode = Type.GetTypeCode(parameterType);
- TypeCode paramtypecode = Type.GetTypeCode(clrtype);
- if (argtypecode == paramtypecode)
- {
- typematch = true;
- clrtype = parameterType;
- }
- }
- Runtime.XDecref(pyoptype);
- if (!typematch)
- {
- return null;
- }
- }
- else
- {
- typematch = true;
- clrtype = parameterType;
- }
- }
- else
- {
- clrtype = parameterType;
- }
-
- return clrtype;
- }
-
static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters,
Dictionary kwargDict,
out bool paramsArray,
diff --git a/src/runtime/pyargconverter.cs b/src/runtime/pyargconverter.cs
new file mode 100644
index 000000000..cf6be7b6d
--- /dev/null
+++ b/src/runtime/pyargconverter.cs
@@ -0,0 +1,59 @@
+namespace Python.Runtime {
+ using System;
+
+ ///
+ /// Specifies how to convert Python objects, passed to .NET functions to the expected CLR types.
+ ///
+ public interface IPyArgumentConverter
+ {
+ ///
+ /// Attempts to convert an argument passed by Python to the specified parameter type.
+ ///
+ /// Unmanaged pointer to the Python argument value
+ /// The expected type of the parameter
+ /// true if the method is overloaded
+ /// This parameter will receive the converted value, matching the specified type
+ /// This parameter will be set to true,
+ /// if the final type needs to be marshaled as an out argument.
+ /// true, if the object matches requested type,
+ /// and conversion was successful, otherwise false
+ bool TryConvertArgument(IntPtr pyarg, Type parameterType,
+ bool needsResolution, out object arg, out bool isOut);
+ }
+
+ ///
+ /// Specifies an argument converter to be used, when methods in this class/assembly are called from Python.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct)]
+ public class PyArgConverterAttribute : Attribute
+ {
+ static readonly Type[] EmptyArgTypeList = new Type[0];
+ static readonly object[] EmptyArgList = new object[0];
+
+ ///
+ /// Gets the instance of the converter, that will be used when calling methods
+ /// of this class/assembly from Python
+ ///
+ public IPyArgumentConverter Converter { get; }
+ ///
+ /// Gets the type of the converter, that will be used when calling methods
+ /// of this class/assembly from Python
+ ///
+ public Type ConverterType { get; }
+
+ ///
+ /// Specifies an argument converter to be used, when methods
+ /// in this class/assembly are called from Python.
+ ///
+ /// Type of the converter to use.
+ /// Must implement .
+ public PyArgConverterAttribute(Type converterType)
+ {
+ if (converterType == null) throw new ArgumentNullException(nameof(converterType));
+ var ctor = converterType.GetConstructor(EmptyArgTypeList);
+ if (ctor == null) throw new ArgumentException("Specified converter must have public parameterless constructor");
+ this.Converter = (IPyArgumentConverter)ctor.Invoke(EmptyArgList);
+ this.ConverterType = converterType;
+ }
+ }
+}