diff --git a/AUTHORS.md b/AUTHORS.md index 18435671c..a24b4be0d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -56,6 +56,7 @@ - Mohamed Koubaa ([@koubaa](https://github.com/koubaa)) - Patrick Stewart ([@patstew](https://github.com/patstew)) - Peter Kese ([@pkese](https://github.com/pkese)) +- Ramin Dalkouhi ([@Romout](https://github.com/Romout)) - Raphael Nestler ([@rnestler](https://github.com/rnestler)) - Rickard Holmberg ([@rickardraysearch](https://github.com/rickardraysearch)) - Sam Winstanley ([@swinstanley](https://github.com/swinstanley)) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cc52d72..893c49f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added +- Added a decoder to ease use of C# Func<> method arguments. + - Added `ToPythonAs()` extension method to allow for explicit conversion using a specific type. ([#2311][i2311]) - Added `IComparable` and `IEquatable` implementations to `PyInt`, `PyFloat`, and `PyString` diff --git a/src/embed_tests/Codecs.cs b/src/embed_tests/Codecs.cs index c8b8ecb6e..8e71d03e7 100644 --- a/src/embed_tests/Codecs.cs +++ b/src/embed_tests/Codecs.cs @@ -92,6 +92,25 @@ static void TupleRoundtripGeneric() static PyObject GetPythonIterable() => PythonEngine.Eval("map(lambda x: x, [1,2,3])"); + [Test] + public void FuncDecoderTest() + { + IPyObjectDecoder decoder = FuncDecoder.Instance; + var scope = Py.CreateScope(); + var func = scope.Eval("lambda x: x + 30"); + var funcType = func.GetPythonType(); + var wrongType = new PyList(new PyObject[0]).GetPythonType(); + // Check some good conversions... + Assert.IsTrue(decoder.CanDecode(funcType, typeof(Func))); + Assert.IsTrue(decoder.CanDecode(funcType, typeof(Func))); + // ...and some bad + Assert.IsFalse(decoder.CanDecode(funcType, typeof(List))); + Assert.IsFalse(decoder.CanDecode(wrongType, typeof(Func))); + // Create a wrapper for the lambda function above and invoke it + Assert.IsTrue(decoder.TryDecode(func, out Func handler)); + Assert.AreEqual(handler(12), 42); + } + [Test] public void ListDecoderTest() { diff --git a/src/runtime/Codecs/FuncDecoder.cs b/src/runtime/Codecs/FuncDecoder.cs new file mode 100644 index 000000000..28bda78c9 --- /dev/null +++ b/src/runtime/Codecs/FuncDecoder.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Reflection; +using System.Text; + +namespace Python.Runtime.Codecs +{ + public class FuncDecoder : IPyObjectDecoder + { + private ModuleBuilder _moduleBuilder; + private Dictionary _typeCache = new Dictionary(); + private static FuncDecoder? _instance; + FuncDecoder() + { + AssemblyName assemblyName = new AssemblyName("decoded"); + AssemblyBuilder builder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + _moduleBuilder = builder.DefineDynamicModule("MainModule"); + } + + public static FuncDecoder Instance + { + get + { + if (_instance == null) + _instance = new FuncDecoder(); + return _instance; + } + } + + bool IPyObjectDecoder.CanDecode(PyType objectType, Type targetType) + { + if (objectType.Name == "function" && targetType.Name.StartsWith("Func`") && targetType.Namespace == typeof(Func).Namespace) + return true; + return false; + } + + static int counter = 0; + bool IPyObjectDecoder.TryDecode(PyObject pyObj, out T1 value) + { + bool retVal = TryDecode(pyObj, typeof(T1), out Delegate handler); + value = (T1)Convert.ChangeType(handler, typeof(T1)); + return retVal; + } + + MethodInfo? FindMethod(Type type, string name, BindingFlags flags, params Type[] expectedTypes) + { + foreach (var method in type.GetMethods(flags)) + { + if (method.Name == name) + { + var parameters = method.GetParameters(); + if (parameters.Length == expectedTypes.Length) + { + bool found = true; + for (int i = 0; i < expectedTypes.Length; ++i) + { + if (parameters[i].ParameterType != expectedTypes[i]) + { + found = false; + break; + } + } + if (found) + return method; + } + } + } + return null; + } + + private (Type, MethodBuilder) CreateType(Type funcType) + { + TypeBuilder typeBuilder = _moduleBuilder.DefineType($"narf{counter++}", TypeAttributes.Public | TypeAttributes.AnsiClass | TypeAttributes.AutoClass | TypeAttributes.AutoLayout | TypeAttributes.BeforeFieldInit | TypeAttributes.Class, null); + + var field = typeBuilder.DefineField("_pyObject", typeof(PyObject), FieldAttributes.Private); + var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(PyObject) }); + var generator = constructor.GetILGenerator(); + // Invoke Base Class Constructor + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); + // Load this and argument of constructor, apply it to this.field + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Stfld, field); + // Return + generator.Emit(OpCodes.Ret); + + Type[] arguments = funcType.GetGenericArguments(); + Type[] parameters = new Type[0]; + if (arguments.Length > 1) + parameters = arguments.Take(arguments.Length - 1).ToArray(); + Type returnType = arguments.Last(); + + var invokeMethod = typeBuilder.DefineMethod("Invoke", MethodAttributes.Public, returnType, parameters); + generator = invokeMethod.GetILGenerator(); + // Define local variables + generator.DeclareLocal(typeof(Py.GILState)); + generator.DeclareLocal(typeof(bool)); + // Invoke Py.GIL and store state to local-0 + generator.Emit(OpCodes.Call, typeof(Py).GetMethod("GIL", BindingFlags.Static | BindingFlags.Public)); + generator.Emit(OpCodes.Stloc_0); + + // Try {... + var exceptionBlock = generator.BeginExceptionBlock(); + + // load field _pyObject from this - we need this for the invoke right after we have created and set the arguments + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldfld, field); + // Create argument array for invoke call with proper number of arguments + generator.Emit(OpCodes.Ldc_I4, parameters.Length); + generator.Emit(OpCodes.Newarr, typeof(PyObject)); + // Set parameters by invoking ToPython on each of them + for (int i = 0; i < parameters.Length; ++i) + { + // Duplicate array as the reference gets lost otherwise + generator.Emit(OpCodes.Dup); + // Take constant index i as index into argument array + generator.Emit(OpCodes.Ldc_I4, i); + // use the n'th + 1 argument passed into the function as arg0 == this + generator.Emit(OpCodes.Ldarg, i + 1); + // Box if argument is value type + if (parameters[i].IsValueType) + generator.Emit(OpCodes.Box, parameters[i]); + generator.Emit(OpCodes.Call, typeof(ConverterExtension).GetMethod(nameof(ConverterExtension.ToPython), BindingFlags.Public | BindingFlags.Static)); + // Store result into argument array + generator.Emit(OpCodes.Stelem_Ref); + } + // Invoke Invoke-method of _pyObject + generator.Emit(OpCodes.Callvirt, FindMethod(typeof(PyObject), nameof(PyObject.Invoke), BindingFlags.Public | BindingFlags.Instance, typeof(PyObject[]))); + // As was hard, this is a try for AsManagedObject but also not convenient + generator.Emit(OpCodes.Ldtoken, returnType); + generator.Emit(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), BindingFlags.Public | BindingFlags.Static)); + generator.Emit(OpCodes.Callvirt, typeof(PyObject).GetMethod(nameof(PyObject.AsManagedObject), BindingFlags.Public | BindingFlags.Instance)); + generator.Emit(OpCodes.Unbox_Any, returnType); + // Store Return-Value + generator.Emit(OpCodes.Stloc_1); + // End Try } + generator.Emit(OpCodes.Leave_S, exceptionBlock); + generator.BeginFinallyBlock(); + + generator.Emit(OpCodes.Ldloc_0); + // Not sure if dispose can be found like that + generator.Emit(OpCodes.Callvirt, typeof(Py.GILState).GetMethod(nameof(Py.GILState.Dispose), BindingFlags.Public | BindingFlags.Instance)); + // End exception bloc + generator.EndExceptionBlock(); + + // Return result + generator.Emit(OpCodes.Ldloc_1); + generator.Emit(OpCodes.Ret); + + for (int i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + invokeMethod.DefineParameter(i + 1, ParameterAttributes.None, $"p{i}"); + } + + return (typeBuilder.CreateType(), invokeMethod); + } + + private bool TryDecode(PyObject pyObj, Type funcType, out Delegate value) + { + if (!_typeCache.TryGetValue(funcType, out (Type type, MethodBuilder methodBuilder) wrapper)) + { + wrapper = CreateType(funcType); + _typeCache[funcType] = wrapper; + } + + object instance = Activator.CreateInstance(wrapper.type, pyObj); + value = Delegate.CreateDelegate(funcType, instance, wrapper.type.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance)); + + return true; + } + } +} 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