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; + } + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy