Skip to content

Commit e840a4b

Browse files
committed
enable expanding set of marshaling conversions via PyObjectConversions
added sample TupleCodec (only supporting ValueTuple)
1 parent f0e9c38 commit e840a4b

10 files changed

+447
-9
lines changed

src/embed_tests/Codecs.cs

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
namespace Python.EmbeddingTest {
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using NUnit.Framework;
6+
using Python.Runtime;
7+
using Python.Runtime.Codecs;
8+
9+
public class Codecs {
10+
[SetUp]
11+
public void SetUp() {
12+
PythonEngine.Initialize();
13+
}
14+
15+
[TearDown]
16+
public void Dispose() {
17+
PythonEngine.Shutdown();
18+
}
19+
20+
[Test]
21+
public void ConversionsGeneric() {
22+
ConversionsGeneric<ValueTuple<int, string, object>, ValueTuple>();
23+
}
24+
25+
static void ConversionsGeneric<T, TTuple>() {
26+
TupleCodec<TTuple>.Register();
27+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
28+
T restored = default;
29+
using (Py.GIL())
30+
using (var scope = Py.CreateScope()) {
31+
void Accept(T value) => restored = value;
32+
var accept = new Action<T>(Accept).ToPython();
33+
scope.Set(nameof(tuple), tuple);
34+
scope.Set(nameof(accept), accept);
35+
scope.Exec($"{nameof(accept)}({nameof(tuple)})");
36+
Assert.AreEqual(expected: tuple, actual: restored);
37+
}
38+
}
39+
40+
[Test]
41+
public void ConversionsObject() {
42+
ConversionsObject<ValueTuple<int, string, object>, ValueTuple>();
43+
}
44+
static void ConversionsObject<T, TTuple>() {
45+
TupleCodec<TTuple>.Register();
46+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
47+
T restored = default;
48+
using (Py.GIL())
49+
using (var scope = Py.CreateScope()) {
50+
void Accept(object value) => restored = (T)value;
51+
var accept = new Action<object>(Accept).ToPython();
52+
scope.Set(nameof(tuple), tuple);
53+
scope.Set(nameof(accept), accept);
54+
scope.Exec($"{nameof(accept)}({nameof(tuple)})");
55+
Assert.AreEqual(expected: tuple, actual: restored);
56+
}
57+
}
58+
59+
[Test]
60+
public void TupleRoundtripObject() {
61+
TupleRoundtripObject<ValueTuple<int, string, object>, ValueTuple>();
62+
}
63+
static void TupleRoundtripObject<T, TTuple>() {
64+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
65+
using (Py.GIL()) {
66+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
67+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out object restored));
68+
Assert.AreEqual(expected: tuple, actual: restored);
69+
}
70+
}
71+
72+
[Test]
73+
public void TupleRoundtripGeneric() {
74+
TupleRoundtripGeneric<ValueTuple<int, string, object>, ValueTuple>();
75+
}
76+
77+
static void TupleRoundtripGeneric<T, TTuple>() {
78+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
79+
using (Py.GIL()) {
80+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
81+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out T restored));
82+
Assert.AreEqual(expected: tuple, actual: restored);
83+
}
84+
}
85+
}
86+
}

src/embed_tests/Python.EmbeddingTest.15.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
2424
<PythonBuildDir Condition="'$(TargetFramework)'=='net40' AND '$(PythonBuildDir)' == ''">$(SolutionDir)\bin\</PythonBuildDir>
2525
<PublishDir Condition="'$(TargetFramework)'!='net40'">$(OutputPath)\$(TargetFramework)_publish</PublishDir>
26-
<LangVersion>6</LangVersion>
26+
<LangVersion>7.3</LangVersion>
2727
<ErrorReport>prompt</ErrorReport>
2828
<CustomDefineConstants Condition="'$(CustomDefineConstants)' == ''">$(PYTHONNET_DEFINE_CONSTANTS)</CustomDefineConstants>
2929
<BaseDefineConstants>XPLAT</BaseDefineConstants>
@@ -81,6 +81,7 @@
8181
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
8282
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
8383
<PackageReference Include="NUnitLite" Version="3.7.2" />
84+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
8485
</ItemGroup>
8586
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.0'">
8687
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />

src/embed_tests/Python.EmbeddingTest.csproj

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
33
<PropertyGroup>
44
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -80,6 +80,7 @@
8080
<None Include="packages.config" />
8181
</ItemGroup>
8282
<ItemGroup>
83+
<Compile Include="Codecs.cs" />
8384
<Compile Include="dynamic.cs" />
8485
<Compile Include="pyimport.cs" />
8586
<Compile Include="pyinitialize.cs" />
@@ -126,4 +127,4 @@
126127
<Copy SourceFiles="$(TargetAssembly)" DestinationFolder="$(PythonBuildDir)" />
127128
<!--Copy SourceFiles="$(TargetAssemblyPdb)" Condition="Exists('$(TargetAssemblyPdb)')" DestinationFolder="$(PythonBuildDir)" /-->
128129
</Target>
129-
</Project>
130+
</Project>

src/embed_tests/packages.config

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
33
<package id="NUnit" version="3.7.1" targetFramework="net40" />
44
<package id="NUnit.ConsoleRunner" version="3.7.0" targetFramework="net40" />
5-
</packages>
5+
<package id="System.ValueTuple" version="4.5.0" targetFramework="net40" />
6+
</packages>

src/runtime/Codecs/TupleCodecs.cs

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
namespace Python.Runtime.Codecs
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
7+
8+
public sealed class TupleCodec<TTuple> : IPyObjectEncoder, IPyObjectDecoder
9+
{
10+
TupleCodec() { }
11+
public static TupleCodec<TTuple> Instance { get; } = new TupleCodec<TTuple>();
12+
13+
public bool CanEncode(Type type)
14+
=> type.Namespace == typeof(TTuple).Namespace && type.Name.StartsWith(typeof(TTuple).Name + '`')
15+
|| type == typeof(object) || type == typeof(TTuple);
16+
17+
public PyObject TryEncode(object value)
18+
{
19+
if (value == null) return null;
20+
21+
var tupleType = value.GetType();
22+
if (tupleType == typeof(object)) return null;
23+
if (!this.CanEncode(tupleType)) return null;
24+
if (tupleType == typeof(TTuple)) return new PyTuple();
25+
26+
long fieldCount = tupleType.GetGenericArguments().Length;
27+
var tuple = Runtime.PyTuple_New(fieldCount);
28+
Exceptions.ErrorCheck(tuple);
29+
int fieldIndex = 0;
30+
foreach (FieldInfo field in tupleType.GetFields())
31+
{
32+
var item = field.GetValue(value);
33+
IntPtr pyItem = Converter.ToPython(item);
34+
Runtime.XIncref(pyItem);
35+
Runtime.PyTuple_SetItem(tuple, fieldIndex, pyItem);
36+
fieldIndex++;
37+
}
38+
return new PyTuple(Runtime.SelfIncRef(tuple));
39+
}
40+
41+
public bool CanDecode(PyObject objectType, Type targetType)
42+
=> objectType.Handle == Runtime.PyTuple && this.CanEncode(targetType);
43+
44+
public bool TryDecode<T>(PyObject pyObj, out T value)
45+
{
46+
if (pyObj == null) throw new ArgumentNullException(nameof(pyObj));
47+
48+
value = default;
49+
50+
if (!Runtime.PyTuple_Check(pyObj.Handle)) return false;
51+
52+
if (typeof(T) == typeof(object))
53+
{
54+
bool converted = Decode(pyObj, out object result);
55+
if (converted)
56+
{
57+
value = (T)result;
58+
return true;
59+
}
60+
61+
return false;
62+
}
63+
64+
var itemTypes = typeof(T).GetGenericArguments();
65+
long itemCount = Runtime.PyTuple_Size(pyObj.Handle);
66+
if (itemTypes.Length != itemCount) return false;
67+
68+
if (itemCount == 0)
69+
{
70+
value = (T)EmptyTuple;
71+
return true;
72+
}
73+
74+
var elements = new object[itemCount];
75+
for (int itemIndex = 0; itemIndex < itemTypes.Length; itemIndex++)
76+
{
77+
IntPtr pyItem = Runtime.PyTuple_GetItem(pyObj.Handle, itemIndex);
78+
if (!Converter.ToManaged(pyItem, itemTypes[itemIndex], out elements[itemIndex], setError: false))
79+
{
80+
return false;
81+
}
82+
}
83+
var factory = tupleCreate[itemCount].MakeGenericMethod(itemTypes);
84+
value = (T)factory.Invoke(null, elements);
85+
return true;
86+
}
87+
88+
static bool Decode(PyObject tuple, out object value)
89+
{
90+
long itemCount = Runtime.PyTuple_Size(tuple.Handle);
91+
if (itemCount == 0)
92+
{
93+
value = EmptyTuple;
94+
return true;
95+
}
96+
var elements = new object[itemCount];
97+
var itemTypes = new Type[itemCount];
98+
value = null;
99+
for (int itemIndex = 0; itemIndex < elements.Length; itemIndex++)
100+
{
101+
var pyItem = Runtime.PyTuple_GetItem(tuple.Handle, itemIndex);
102+
if (!Converter.ToManaged(pyItem, typeof(object), out elements[itemIndex], setError: false))
103+
{
104+
return false;
105+
}
106+
107+
itemTypes[itemIndex] = elements[itemIndex]?.GetType() ?? typeof(object);
108+
}
109+
110+
var factory = tupleCreate[itemCount].MakeGenericMethod(itemTypes);
111+
value = factory.Invoke(null, elements);
112+
return true;
113+
}
114+
115+
static readonly MethodInfo[] tupleCreate =
116+
typeof(TTuple).GetMethods(BindingFlags.Public | BindingFlags.Static)
117+
.Where(m => m.Name == nameof(Tuple.Create))
118+
.OrderBy(m => m.GetParameters().Length)
119+
.ToArray();
120+
121+
static readonly object EmptyTuple = tupleCreate[0].Invoke(null, parameters: new object[0]);
122+
123+
public static void Register()
124+
{
125+
PyObjectConversions.RegisterEncoder(Instance);
126+
PyObjectConversions.RegisterDecoder(Instance);
127+
}
128+
}
129+
}

src/runtime/Python.Runtime.csproj

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
33
<PropertyGroup>
44
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -76,6 +76,8 @@
7676
<Reference Include="System" />
7777
</ItemGroup>
7878
<ItemGroup>
79+
<Compile Include="Codecs\TupleCodecs.cs" />
80+
<Compile Include="converterextensions.cs" />
7981
<Compile Include="finalizer.cs" />
8082
<Compile Include="Properties\AssemblyInfo.cs" />
8183
<Compile Include="..\SharedAssemblyInfo.cs">
@@ -172,4 +174,4 @@
172174
<Copy SourceFiles="$(TargetAssembly)" DestinationFolder="$(PythonBuildDir)" />
173175
<!--Copy SourceFiles="$(TargetAssemblyPdb)" Condition="Exists('$(TargetAssemblyPdb)')" DestinationFolder="$(PythonBuildDir)" /-->
174176
</Target>
175-
</Project>
177+
</Project>

src/runtime/converter.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,16 @@ internal static IntPtr ToPython(object value, Type type)
135135
return result;
136136
}
137137

138-
if (value is IList && !(value is INotifyPropertyChanged) && value.GetType().IsGenericType)
139-
{
138+
if (Type.GetTypeCode(type) == TypeCode.Object && value.GetType() != typeof(object)) {
139+
var encoded = PyObjectConversions.TryEncode(value, type);
140+
if (encoded != null) {
141+
Runtime.XIncref(encoded.Handle);
142+
return encoded.Handle;
143+
}
144+
}
145+
146+
if (value is IList && !(value is INotifyPropertyChanged) && value.GetType().IsGenericType)
147+
{
140148
using (var resultlist = new PyList())
141149
{
142150
foreach (object o in (IEnumerable)value)
@@ -437,9 +445,21 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
437445
return false;
438446
}
439447

448+
TypeCode typeCode = Type.GetTypeCode(obType);
449+
if (typeCode == TypeCode.Object)
450+
{
451+
IntPtr pyType = Runtime.PyObject_TYPE(value);
452+
if (PyObjectConversions.TryDecode(value, pyType, obType, out result))
453+
{
454+
return true;
455+
}
456+
}
457+
440458
return ToPrimitive(value, obType, out result, setError);
441459
}
442460

461+
internal delegate bool TryConvertFromPythonDelegate(IntPtr pyObj, out object result);
462+
443463
/// <summary>
444464
/// Convert a Python value to an instance of a primitive managed type.
445465
/// </summary>

0 commit comments

Comments
 (0)