1+ using static System . FormattableString ;
2+ using System ;
3+ using System . Linq ;
4+ using System . Reflection ;
5+
6+
17namespace Python . Runtime . Native
28{
3- using System ;
4- using System . Globalization ;
5- using System . Linq ;
6- using System . Reflection ;
7-
89 static class ABI
910 {
1011 // GIL builds only. FT splits the refcount; Refcount uses Py_REFCNT.
@@ -19,22 +20,28 @@ static class ABI
1920 internal static void Initialize ( Version version )
2021 {
2122 IsFreeThreaded = DetectFreeThreaded ( ) ;
23+ if ( IsFreeThreaded && version < new Version ( 3 , 14 ) )
24+ {
25+ // We need Py_REFCNT to be available as a symbol, which is only
26+ // present in Python 3.14 and later.
27+ throw new NotSupportedException ( $ "Python ABI v{ version } is not supported: Free-threaded builds are only supported on Python 3.14 and later.") ;
28+ }
29+
2230 RefCountOffset = IsFreeThreaded ? - 1 : ProbeRefCountOffset ( ) ;
2331 ObjectHeadOffset = IsFreeThreaded
2432 ? ( IntPtr . Size == 4 ? 12 : 16 )
2533 : RefCountOffset ;
2634
27- string offsetsClassSuffix = string . Format ( CultureInfo . InvariantCulture ,
28- "{0}{1}" , version . Major , version . Minor ) ;
29-
3035 var thisAssembly = Assembly . GetExecutingAssembly ( ) ;
3136
3237 const string nativeTypeOffsetClassName = "Python.Runtime.NativeTypeOffset" ;
33- string className = "Python.Runtime.TypeOffset" + offsetsClassSuffix ;
38+ string className = Invariant ( $ "Python.Runtime.TypeOffset{ version . Major } { version . Minor } ") ;
39+
3440 Type nativeOffsetsClass = thisAssembly . GetType ( nativeTypeOffsetClassName , throwOnError : false ) ;
3541 Type typeOffsetsClass =
3642 // Try platform native offsets first. It is only present when generated by setup.py
3743 nativeOffsetsClass ?? thisAssembly . GetType ( className , throwOnError : false ) ;
44+
3845 if ( typeOffsetsClass is null )
3946 {
4047 var types = thisAssembly . GetTypes ( ) . Select ( type => type . Name ) . Where ( name => name . StartsWith ( "TypeOffset" ) ) ;
@@ -53,12 +60,30 @@ static bool DetectFreeThreaded()
5360 {
5461 // sys._is_gil_enabled() was added in Python 3.13; absent means GIL build.
5562 using var sys = Runtime . PyImport_ImportModule ( "sys" ) ;
56- if ( sys . IsNull ( ) ) { Runtime . PyErr_Clear ( ) ; return false ; }
63+
64+ if ( sys . IsNull ( ) )
65+ {
66+ Runtime . PyErr_Clear ( ) ;
67+ return false ;
68+ }
69+
5770 using var func = Runtime . PyObject_GetAttrString ( sys . Borrow ( ) , "_is_gil_enabled" ) ;
58- if ( func . IsNull ( ) ) { Runtime . PyErr_Clear ( ) ; return false ; }
71+
72+ if ( func . IsNull ( ) )
73+ {
74+ Runtime . PyErr_Clear ( ) ;
75+ return false ;
76+ }
77+
5978 using var args = Runtime . PyTuple_New ( 0 ) ;
6079 using var result = Runtime . PyObject_Call ( func . Borrow ( ) , args . Borrow ( ) , default ) ;
61- if ( result . IsNull ( ) ) { Runtime . PyErr_Clear ( ) ; return false ; }
80+
81+ if ( result . IsNull ( ) )
82+ {
83+ Runtime . PyErr_Clear ( ) ;
84+ return false ;
85+ }
86+
6287 return Runtime . PyObject_IsTrue ( result . Borrow ( ) ) == 0 ;
6388 }
6489
@@ -71,7 +96,7 @@ static unsafe int ProbeRefCountOffset()
7196 using var tempObject = Runtime . PyList_New ( 0 ) ;
7297 IntPtr * tempPtr = ( IntPtr * ) tempObject . DangerousGetAddress ( ) ;
7398 int offset = 0 ;
74- while ( tempPtr [ offset ] != ( IntPtr ) 1 )
99+ while ( tempPtr [ offset ] != ( IntPtr ) 1 )
75100 {
76101 offset ++ ;
77102 if ( offset > 100 )
0 commit comments