using static System.FormattableString;
using System;
using System.Linq;
using System.Reflection;
namespace Python.Runtime.Native
{
static class ABI
{
// GIL builds only. FT splits the refcount; Refcount uses Py_REFCNT.
public static int RefCountOffset { get; private set; }
// Added to generated TypeOffsets. The FT header is 12 bytes larger on
// 32-bit and 16 bytes larger on 64-bit builds.
public static int ObjectHeadOffset { get; private set; }
public static bool IsFreeThreaded { get; private set; }
internal static void Initialize(Version version)
{
IsFreeThreaded = DetectFreeThreaded();
if (IsFreeThreaded && version < new Version(3, 14))
{
// We need Py_REFCNT to be available as a symbol, which is only
// present in Python 3.14 and later.
throw new NotSupportedException($"Python ABI v{version} is not supported: Free-threaded builds are only supported on Python 3.14 and later.");
}
RefCountOffset = IsFreeThreaded ? -1 : ProbeRefCountOffset();
ObjectHeadOffset = IsFreeThreaded
? (IntPtr.Size == 4 ? 12 : 16)
: RefCountOffset;
var thisAssembly = Assembly.GetExecutingAssembly();
const string nativeTypeOffsetClassName = "Python.Runtime.NativeTypeOffset";
string className = Invariant($"Python.Runtime.TypeOffset{version.Major}{version.Minor}");
Type nativeOffsetsClass = thisAssembly.GetType(nativeTypeOffsetClassName, throwOnError: false);
Type typeOffsetsClass =
// Try platform native offsets first. It is only present when generated by setup.py
nativeOffsetsClass ?? thisAssembly.GetType(className, throwOnError: false);
if (typeOffsetsClass is null)
{
var types = thisAssembly.GetTypes().Select(type => type.Name).Where(name => name.StartsWith("TypeOffset"));
string message = $"Searching for {className}, found {string.Join(",", types)}.";
throw new NotSupportedException($"Python ABI v{version} is not supported: {message}");
}
var typeOffsets = (ITypeOffsets)Activator.CreateInstance(typeOffsetsClass);
TypeOffset.Use(typeOffsets, nativeOffsetsClass == null ? ObjectHeadOffset : 0);
}
///
/// True when the running interpreter is a Py_GIL_DISABLED (free-threaded) build.
///
static bool DetectFreeThreaded()
{
// sys._is_gil_enabled() was added in Python 3.13; absent means GIL build.
using var sys = Runtime.PyImport_ImportModule("sys");
if (sys.IsNull())
{
Runtime.PyErr_Clear();
return false;
}
using var func = Runtime.PyObject_GetAttrString(sys.Borrow(), "_is_gil_enabled");
if (func.IsNull())
{
Runtime.PyErr_Clear();
return false;
}
using var args = Runtime.PyTuple_New(0);
using var result = Runtime.PyObject_Call(func.Borrow(), args.Borrow(), default);
if (result.IsNull())
{
Runtime.PyErr_Clear();
return false;
}
return Runtime.PyObject_IsTrue(result.Borrow()) == 0;
}
///
/// Locates the offset of ob_refcnt inside PyObject by allocating a
/// fresh list and scanning for its refcount value of 1. GIL builds only.
///
static unsafe int ProbeRefCountOffset()
{
using var tempObject = Runtime.PyList_New(0);
IntPtr* tempPtr = (IntPtr*)tempObject.DangerousGetAddress();
int offset = 0;
while (tempPtr[offset] != (IntPtr)1)
{
offset++;
if (offset > 100)
throw new InvalidProgramException("PyObject_HEAD could not be found withing reasonable distance from the start of PyObject");
}
return offset * IntPtr.Size;
}
}
}