diff --git a/src/embed_tests/TestConversionErrors.cs b/src/embed_tests/TestConversionErrors.cs new file mode 100644 index 000000000..a3ee43e7d --- /dev/null +++ b/src/embed_tests/TestConversionErrors.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + /// + /// A failure while converting a CLR value for Python has to surface as a + /// Python exception that Python code can catch, rather than as a CLR exception + /// unwinding through the interpreter. + /// + public class TestConversionErrors + { + internal const string Failure = "conversion failed on purpose"; + + [SetUp] + public void SetUp() => PyObjectConversions.RegisterEncoder(new FailingConversionEncoder()); + + [TearDown] + public void TearDown() => PyObjectConversions.Reset(); + + [TestCase("host.Method()", TestName = "MethodReturn")] + [TestCase("host.OutParam(None)", TestName = "OutParameterAndReturn")] + [TestCase("host.OutOnly(None)", TestName = "OutParameterOnly")] + [TestCase("list(host.Iterate())", TestName = "IteratorItem")] + [TestCase("host.Array1[0]", TestName = "ArrayItem")] + [TestCase("host.Array2[0, 0]", TestName = "MultidimensionalArrayItem")] + [TestCase("host[0]", TestName = "IndexerResult")] + public void ConversionFailureIsCatchableInPython(string expression) + { + Assert.That(CaughtInPython(expression), Does.Contain(Failure)); + } + + [Test] + public void ThrowingCurrentIsCatchableInPython() + { + Assert.That( + CaughtInPython("list(host.ThrowingEnumerable())"), + Does.Contain(ThrowingCurrentEnumerator.Message)); + } + + /// + /// Evaluates inside a Python try/except and + /// returns what Python caught. If the failure escapes Python instead, the + /// call throws and the test fails. + /// + static string CaughtInPython(string expression) + { + using var scope = Py.CreateScope(); + scope.Set("host", new ConversionErrorHost()); + scope.Exec($@" +def attempt(): + try: + {expression} + except Exception as e: + return str(e) + return 'nothing raised' +"); + return scope.Eval("attempt()"); + } + } + + public class UnconvertibleValue { } + + class FailingConversionEncoder : IPyObjectEncoder + { + public bool CanEncode(Type type) => type == typeof(UnconvertibleValue); + + public PyObject TryEncode(object value) => + throw new InvalidOperationException(TestConversionErrors.Failure); + } + + /// Hands an unconvertible value to Python through each boundary. + public class ConversionErrorHost + { + public UnconvertibleValue Method() => new(); + public int OutParam(out UnconvertibleValue value) { value = new(); return 1; } + public void OutOnly(out UnconvertibleValue value) => value = new(); + public IEnumerable Iterate() { yield return new(); } + public UnconvertibleValue[] Array1 = { new() }; + public UnconvertibleValue[,] Array2 = { { new() } }; + public UnconvertibleValue this[int index] => new(); + public IEnumerable ThrowingEnumerable() => new ThrowingCurrentEnumerable(); + } + + class ThrowingCurrentEnumerable : IEnumerable + { + public IEnumerator GetEnumerator() => new ThrowingCurrentEnumerator(); + } + + class ThrowingCurrentEnumerator : IEnumerator + { + public const string Message = "Current failed on purpose"; + bool _moved; + + public object Current => throw new InvalidOperationException(Message); + public bool MoveNext() => !_moved && (_moved = true); + public void Reset() => _moved = false; + } +} diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 383d0bf49..9beb9a056 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -964,43 +964,54 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType; - if (binding.outs > 0) + // Converting the results can fail too - a type refused by an + // IClrTypeFilter, for one - and that must become a Python exception + // rather than a CLR exception unwinding through the interpreter + try { - ParameterInfo[] pi = binding.info.GetParameters(); - int c = pi.Length; - var n = 0; - - bool isVoid = returnType == typeof(void); - int tupleSize = binding.outs + (isVoid ? 0 : 1); - using var t = Runtime.PyTuple_New(tupleSize); - if (!isVoid) + if (binding.outs > 0) { - using var v = Converter.ToPython(result, returnType); - Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); - n++; - } - - for (var i = 0; i < c; i++) - { - Type pt = pi[i].ParameterType; - if (pt.IsByRef) + ParameterInfo[] pi = binding.info.GetParameters(); + int c = pi.Length; + var n = 0; + + bool isVoid = returnType == typeof(void); + int tupleSize = binding.outs + (isVoid ? 0 : 1); + using var t = Runtime.PyTuple_New(tupleSize); + if (!isVoid) { - using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); + using var v = Converter.ToPython(result, returnType); Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); n++; } - } - if (binding.outs == 1 && returnType == typeof(void)) - { - BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); - return new NewReference(item); + for (var i = 0; i < c; i++) + { + Type pt = pi[i].ParameterType; + if (pt.IsByRef) + { + using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); + Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); + n++; + } + } + + if (binding.outs == 1 && returnType == typeof(void)) + { + BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); + return new NewReference(item); + } + + return new NewReference(t.Borrow()); } - return new NewReference(t.Borrow()); + return Converter.ToPython(result, returnType); + } + catch (Exception e) + { + Exceptions.SetError(e); + return default; } - - return Converter.ToPython(result, returnType); } } diff --git a/src/runtime/Types/ArrayObject.cs b/src/runtime/Types/ArrayObject.cs index f220d53fb..54b6208ea 100644 --- a/src/runtime/Types/ArrayObject.cs +++ b/src/runtime/Types/ArrayObject.cs @@ -191,7 +191,7 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference value = items.GetValue(index); - return Converter.ToPython(value, itemType); + return ItemToPython(value, itemType); } // Multi-dimensional arrays can be indexed a la: list[1, 2, 3]. @@ -238,7 +238,22 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference value = items.GetValue(indices); - return Converter.ToPython(value, itemType); + return ItemToPython(value, itemType); + } + + // The conversion can throw - for a type refused by an IClrTypeFilter, say - + // and that must become a Python exception rather than unwinding through it + static NewReference ItemToPython(object? value, Type itemType) + { + try + { + return Converter.ToPython(value, itemType); + } + catch (Exception e) + { + Exceptions.SetError(e); + return default; + } } diff --git a/src/runtime/Types/Iterator.cs b/src/runtime/Types/Iterator.cs index 49145d2c3..e009ca841 100644 --- a/src/runtime/Types/Iterator.cs +++ b/src/runtime/Types/Iterator.cs @@ -42,8 +42,18 @@ public static NewReference tp_iternext(BorrowedReference ob) Exceptions.SetError(e); return default; } - object item = self.iter.Current; - return Converter.ToPython(item, self.elemType); + // Current and the conversion can both throw - the latter for a type + // refused by an IClrTypeFilter - and must not unwind through Python + try + { + object item = self.iter.Current; + return Converter.ToPython(item, self.elemType); + } + catch (Exception e) + { + Exceptions.SetError(e); + return default; + } } public static NewReference tp_iter(BorrowedReference ob) => new (ob);