Skip to content

Support for BigInteger (C#) <-> PyInt #1710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- .NET collection types now implement standard Python collection interfaces from `collections.abc`.
See [Mixins/collections.py](src/runtime/Mixins/collections.py).
- .NET arrays implement Python buffer protocol
- Python integer interoperability with `System.Numerics.BigInteger`
- Python.NET will correctly resolve .NET methods, that accept `PyList`, `PyInt`,
and other `PyObject` derived types when called from Python.
- .NET classes, that have `__call__` method are callable from Python
Expand Down
20 changes: 20 additions & 0 deletions src/embed_tests/TestConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

using NUnit.Framework;

Expand Down Expand Up @@ -131,6 +132,25 @@ public void ToNullable()
Assert.AreEqual(Const, ni);
}

[Test]
public void BigIntExplicit()
{
BigInteger val = 42;
var i = new PyInt(val);
var ni = i.As<BigInteger>();
Assert.AreEqual(val, ni);
var nullable = i.As<BigInteger?>();
Assert.AreEqual(val, nullable);
}

[Test]
public void PyIntImplicit()
{
var i = new PyInt(1);
var ni = (PyObject)i.As<object>();
Assert.AreEqual(i.rawPtr, ni.rawPtr);
}

[Test]
public void ToPyList()
{
Expand Down
34 changes: 34 additions & 0 deletions src/embed_tests/TestPyInt.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System;
using System.Globalization;
using System.Linq;
using System.Numerics;

using NUnit.Framework;
using Python.Runtime;

Expand Down Expand Up @@ -179,5 +183,35 @@ public void TestConvertToInt64()
Assert.IsInstanceOf(typeof(long), a.ToInt64());
Assert.AreEqual(val, a.ToInt64());
}

[Test]
public void ToBigInteger()
{
int[] simpleValues =
{
0, 1, 2,
0x10,
0x123,
0x1234,
};
simpleValues = simpleValues.Concat(simpleValues.Select(v => -v)).ToArray();

foreach (var val in simpleValues)
{
var pyInt = new PyInt(val);
Assert.AreEqual((BigInteger)val, pyInt.ToBigInteger());
}
}

[Test]
public void ToBigIntegerLarge()
{
BigInteger val = BigInteger.Pow(2, 1024) + 3;
var pyInt = new PyInt(val);
Assert.AreEqual(val, pyInt.ToBigInteger());
val = -val;
pyInt = new PyInt(val);
Assert.AreEqual(val, pyInt.ToBigInteger());
}
}
}
8 changes: 8 additions & 0 deletions src/runtime/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,14 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
}
}

if (obType == typeof(System.Numerics.BigInteger)
&& Runtime.PyInt_Check(value))
{
using var pyInt = new PyInt(value);
result = pyInt.ToBigInteger();
return true;
}

return ToPrimitive(value, obType, out result, setError);
}

Expand Down
44 changes: 38 additions & 6 deletions src/runtime/PythonTypes/PyInt.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Runtime.Serialization;

namespace Python.Runtime
{
/// <summary>
/// Represents a Python integer object. See the documentation at
/// PY2: https://docs.python.org/2/c-api/int.html
/// PY3: No equivalent
/// for details.
/// Represents a Python integer object.
/// See the documentation at https://docs.python.org/3/c-api/long.html
/// </summary>
public class PyInt : PyNumber
public class PyInt : PyNumber, IFormattable
{
internal PyInt(in StolenReference ptr) : base(ptr)
{
}

internal PyInt(BorrowedReference reference): base(reference)
internal PyInt(BorrowedReference reference) : base(reference)
{
if (!Runtime.PyInt_Check(reference)) throw new ArgumentException("object is not an int");
}
Expand Down Expand Up @@ -135,6 +135,8 @@ public PyInt(string value) : base(Runtime.PyLong_FromString(value, 0).StealOrThr
{
}

public PyInt(BigInteger value) : this(value.ToString(CultureInfo.InvariantCulture)) { }

protected PyInt(SerializationInfo info, StreamingContext context)
: base(info, context) { }

Expand Down Expand Up @@ -198,5 +200,35 @@ public long ToInt64()
}
return val.Value;
}

public BigInteger ToBigInteger()
{
using var pyHex = Runtime.HexCallable.Invoke(this);
string hex = pyHex.As<string>();
int offset = 0;
bool neg = false;
if (hex[0] == '-')
{
offset++;
neg = true;
}
byte[] littleEndianBytes = new byte[(hex.Length - offset + 1) / 2];
for (; offset < hex.Length; offset++)
{
int littleEndianHexIndex = hex.Length - 1 - offset;
int byteIndex = littleEndianHexIndex / 2;
int isByteTopHalf = littleEndianHexIndex & 1;
int valueShift = isByteTopHalf * 4;
littleEndianBytes[byteIndex] += (byte)(Util.HexToInt(hex[offset]) << valueShift);
}
var result = new BigInteger(littleEndianBytes);
return neg ? -result : result;
}

public string ToString(string format, IFormatProvider formatProvider)
{
using var _ = Py.GIL();
return ToBigInteger().ToString(format, formatProvider);
}
}
}
16 changes: 11 additions & 5 deletions src/runtime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ internal static void Initialize(bool initSigs = false)

clrInterop = GetModuleLazy("clr.interop");
inspect = GetModuleLazy("inspect");
hexCallable = new(() => new PyString("%x").GetAttr("__mod__"));
}

static void NewRun()
Expand Down Expand Up @@ -279,8 +280,9 @@ internal static void Shutdown()

Exceptions.Shutdown();
PythonEngine.InteropConfiguration.Dispose();
DisposeLazyModule(clrInterop);
DisposeLazyModule(inspect);
DisposeLazyObject(clrInterop);
DisposeLazyObject(inspect);
DisposeLazyObject(hexCallable);
PyObjectConversions.Reset();

PyGC_Collect();
Expand Down Expand Up @@ -352,11 +354,11 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops)
public static bool TryCollectingGarbage(int runs)
=> TryCollectingGarbage(runs, forceBreakLoops: false);

static void DisposeLazyModule(Lazy<PyObject> module)
static void DisposeLazyObject(Lazy<PyObject> pyObject)
{
if (module.IsValueCreated)
if (pyObject.IsValueCreated)
{
module.Value.Dispose();
pyObject.Value.Dispose();
}
}

Expand Down Expand Up @@ -489,8 +491,12 @@ private static void NullGCHandles(IEnumerable<IntPtr> objects)

private static Lazy<PyObject> inspect;
internal static PyObject InspectModule => inspect.Value;

private static Lazy<PyObject> clrInterop;
internal static PyObject InteropModule => clrInterop.Value;

private static Lazy<PyObject> hexCallable;
internal static PyObject HexCallable => hexCallable.Value;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

internal static BorrowedReference CLRMetaType => PyCLRMetaType;
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/Util/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ internal static string ReadStringResource(this System.Reflection.Assembly assemb
return reader.ReadToEnd();
}

public static int HexToInt(char hex) => hex switch
{
>= '0' and <= '9' => hex - '0',
>= 'a' and <= 'f' => hex - 'a' + 10,
_ => throw new ArgumentOutOfRangeException(nameof(hex)),
};

public static IEnumerator<T> GetEnumerator<T>(this IEnumerator<T> enumerator) => enumerator;

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
Expand Down