Python

free threading을 위한 안정 ABI로 마이그레이션(abi3t)

3.15 릴리스부터 CPython은 free-threaded 파이썬을 지원하는 안정 ABI의 변체인 Stable ABI for Free-Threaded Builds(약칭 abi3t)를 지원합니다. 이 문서는 C API 확장을 free threading 지원에 맞게 조정하는 방법을 설명합니다.

이 작업을 수행하는 이유

안정 ABI를 사용하는 일반적인 이유는 라이브러리의 각 버전에 대해 빌드하고 배포해야 하는 아티팩트의 수를 줄이기 위함입니다.

안정 ABI가 없으면 지원하려는 CPython의 각 기능 버전마다 별도의 공유 라이브러리와 일반적으로는 wheel 배포본을 구축해야 합니다. 예를 들어, 다음 표의 각 태그는 개별 라이브러리/wheel을 나타냅니다.

CPython 버전

non-free-threaded

free-threaded

3.12

cpython-312

3.13

cpython-313

cpython-313t

3.14

cpython-314

cpython-314t

3.15

cpython-315

cpython-315t

3.16

cpython-316

cpython-316t

이후 버전

cpython-3XX

cpython-3XXt

특히 지원하는 플랫폼 수와 곱해지면 구축해야 할 양이 매우 많아집니다.

안정 ABI(CPython 3.2에서 도입된 abi3)를 사용하면 플랫폼당 하나의 확장이 모든 non-free-threaded CPython 빌드를 지원할 수 있습니다.

CPython 버전

non-free-threaded

free-threaded

3.12

abi3

3.13

cpython-313t

3.14

cpython-314t

3.15

cpython-315t

3.16

cpython-316t

이후 버전

cpython-3XXt

CPython 3.15에서 도입된 free-threaded 빌드용 안정 ABI(abi3t)도 free-threaded 빌드에 대해 동일한 역할을 수행합니다. 또한 이는 non-free-threaded 빌드와도 호환됩니다.

CPython 버전

non-free-threaded

free-threaded

3.12

abi3 *

3.13

cpython-313t

3.14

cpython-314t

3.15

abi3t

3.16

이후 버전

* (위와 같이, abi3 확장은 모든 non-free-threaded 빌드와 호환됩니다. 이 표에서 abi3t

이것을 수행하지 않는 이유

안정 ABI에는 두 가지 주요 단점이 있습니다.

첫째, 안정 ABI는 성능보다 호환성을 우선시하기 때문에 확장의 속도가 느려질 수 있습니다. 차이는 대개 눈에 띄지 않으며, 종종 동일한 소스를 사용하여 안정 ABI 빌드와 “티어 1” CPython 버전을 위한 몇 가지 특정 버전용 빌드를 모두 생성함으로써 완화할 수 있습니다.

둘째, 모든 C API를 사용할 수 없습니다. 확장을 안정 ABI 빌드에 맞게 이식해야 하며, 이는 어렵거나 드문 경우 불가능할 수도 있습니다.

구체적으로, abi3t 는 CPython 3.15에서 추가된 API를 필요로 합니다. 동일한 소스에서 더 오래된 버전의 CPython용으로 확장을 빌드하려는 경우 두 가지 주요 선택지가 있습니다.

  • 전처리기 조건문을 사용합니다.

    이 가이드를 따를 때, 중요하게 생각하는 CPython 버전에서 빌드가 깨지는 변경 사항을 수행해야 할 경우 #ifdef Py_TARGET_ABI3T 블록을 사용하십시오. 기존 코드는 #else 블록에 유지합니다.

    직접 작성한 C 확장의 경우, PEP 697 에서 도입된 추가 사항들로 인해 CPython 3.12까지는 이 방식이 합리적입니다. 코드 생성기(예: Cython)의 경우 3.11 이하 버전과의 호환성을 유지하는 것이 가치가 있을 수 있습니다.

  • abi3t 로 이식하지 않고, 이전 버전 지원을 중단할 수 있을 때까지 각 CPython 버전에 대해 별도의 확장을 계속 빌드합니다.

    이것은 타당한 접근 방식입니다. 모든 확장을 즉시 abi3t 로 전환할 필요는 없습니다.

전제 조건

이 가이드는 C(또는 C++)로 직접 작성되어 abi3t 로 이식하려는 확장이 있는 경우를 가정합니다.

확장에서 코드 생성기(Cython 등) 또는 언어 바인딩(PyO3 등)을 사용하는 경우, 해당 도구가 abi3t 를 지원할 때까지 기다리는 것이 좋습니다. 그러한 도구를 관리하는 경우, 여기서의 지침을 자신의 도구에 맞게 조정할 수 있습니다.

non-free-threaded 안정 ABI

확장은 안정 ABI(abi3t)를 지원해야 합니다. 그렇지 않다면 먼저 이식하거나, 이 가이드를 따르되 언급되지 않은 문제를 해결할 준비를 해야 합니다.

free-threading 지원

기술적으로 필수 전제 조건은 아니지만, abi3t 로 이식하기 전에 확장을 free threading에 맞춰 준비하는 것이 좋습니다. 자세한 지침은 Free Threading을 위한 C API 확장 지원 를 참조하십시오.

더 보기

자유 스레딩 지원을 위한 확장 모듈 이식: 확장 모듈 작성자를 위한 커뮤니티 관리형 이식 가이드입니다.

확장 모듈 격리

모듈은 multi-phase initialization 을 사용해야 하며, 격리되거나 프로세스당 최대 한 번만 로드되도록 제한되어야 합니다. 해당 사항이 아니면 먼저 확장 모듈 격리하기 를 따르십시오. (지름길은 opt-out section 을 참조하십시오.)

가변 크기 타입 방지

확장에서 가변 크기 타입( Py_tp_itemsize 또는 PyTypeObject.tp_itemsize 사용)을 정의하는 경우, 3.15의 abi3t 로 이식할 수 없습니다.

빌드 설정

빌드 도구(setuptools, meson-python, scikit-build-core 등)를 사용하는 경우 해당 문서에서 abi3t``를 선택하는 방법을 찾으십시오. 작성 시점에 모든 도구가 이를 지원하지는 않지만, 도구가 제공한다면 사용하십시오. ``#include <Python.h> 바로 뒤에 다음을 일시적으로 추가하여 올바른 플래그가 설정되었는지 확인할 수 있습니다:

#if Py_TARGET_ABI3T+0 <= 0x30f0000
#error "abi3t define is not set!"
#endif

이 경우 “abt3t 정의가 설정되지 않음”과는 다른 오류가 발생해야 합니다.

참고

빌드 도구가 아직 abi3t 를 지원하지 않는 경우, Python.h 를 포함하기 전에 다음 매크로를 설정하십시오:

#define Py_TARGET_ABI3T 0x30f0000

또는 다음과 같이 컴파일러 플래그로 지정하십시오:

-DPy_TARGET_ABI3T=0x30f0000

이 설정으로 확장이 빌드되면 CPython 3.15 이상과 호환됩니다.

이 매크로를 수동으로 설정한 경우, 나중에 결과 확장의 이름과 태그도 수동으로 지정해야 합니다. 이 내용은 아래의 태그 및 배포 에서 다룹니다.

이 가이드는 일련의 변경을 요구합니다. 각 단계 이후에 원래(non-abi3t) 설정에서도 확장이 여전히 빌드되는지 확인하고, 가급적 지원하는 모든 Python 버전에서 테스트를 실행하십시오. 이는 이식 과정에서 아무것도 망가지지 않도록 보장해 줍니다.

모듈 내보내기 훅

이 단계를 이미 수행하지 않았다면, 확장 모듈은 :samp:`PyInit_{<module_name>} 이라는 이름의 모듈 초기화 함수 를 정의하고 있습니다. 이를 CPython 3.15의 PEP 793 에서 추가된 기능인 모듈 내보내기 후크PyModExport_<module name> 로 이식해야 합니다.

기존의 초기화 함수는 다음과 같은 형태여야 합니다(<modname><moddef> 은 본인의 이름으로 대체):

PyMODINIT_FUNC
PyInit_<modname>(void)
{
    return PyModuleDef_Init(&<moddef>);
}

return 이전 단계에 코드가 있는 경우, 이를 Py_mod_create 또는 Py_mod_exec 슬롯 함수로 이동하십시오. 관련 정보는 PyInit 문서 를 참조하십시오.

이 함수는 PyModuleDef 객체(위 코드의 <moddef>)를 참조합니다. 정의는 다음과 유사해야 하며, 값은 달라지고 일부 필드는 이름이 없거나 생략될 수 있습니다.

static PyModuleDef <moddef> = {
    PyModuleDef_HEAD_INIT,
    .m_name = "my_module",
    .m_doc = "my docstring",
    .m_size = sizeof(my_state_struct),
    .m_methods = my_methods,
    .m_slots = my_slots,
    .m_traverse = my_traverse,
    .m_clear = my_clear,
    .m_free = my_free,
};

이 정의와 PyInit 함수를 제거하고(하위 호환성을 유지하려면 #ifndef Py_TARGET_ABI3T 블록에 넣으십시오), 다음 내용으로 대체하십시오.

PyABIInfo_VAR(abi_info);

static PySlot my_slot_array[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "my_module"),
    PySlot_STATIC_DATA(Py_mod_doc, "my docstring"),
    PySlot_SIZE(Py_mod_state_size, sizeof(my_state_struct)),
    PySlot_STATIC_DATA(Py_mod_methods, my_methods),
    PySlot_STATIC_DATA(Py_mod_slots, my_slots),
    PySlot_FUNC(Py_mod_state_traverse, my_traverse),
    PySlot_FUNC(Py_mod_state_clear, my_clear),
    PySlot_FUNC(Py_mod_state_free, my_free),
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_<modname>(void)
{
    return my_slot_array;
}

누락된 필드(새로운 Py_mod_abi 제외)는 생략하고 본인의 값으로 대체하십시오.

이 API에 대한 자세한 내용은 PySlot내보내기 후크 문서를 참조하십시오.

예시와 같이, PyModExport_ 함수는 정적 데이터에 대한 포인터만 반환해야 합니다. 추가 코드를 피할 수 없는 경우, PyModExport_ 문서의 주의 사항 을 참조하십시오.

기존 슬롯

Py_mod_slots 슬롯이 있는 경우, 해당 슬롯이 가리키는 배열을 확인하십시오. 이 배열은 다음과 같은 PyModuleDef_Slot 배열이어야 합니다.

static PyObject *create_module(PyObject *spec, PyModuleDef *def) { ... }
static int my_first_module_exec(PyObject *module) { ... }
static int my_second_module_exec(PyObject *module) { ... }

static PyModuleDef_Slot my_slots[] = {
   {Py_mod_gil, Py_MOD_GIL_NOT_USED},
   {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
   {Py_mod_create, my_module_create},
   {Py_mod_exec, my_first_module_exec},
   {Py_mod_exec, my_second_module_exec},
   {0, NULL}
};

py_mod_create

Py_mod_create 항목이 있는 경우, 해당 함수가 두 번째 인자로 NULL 을 받아 호출될 수 있는지 확인하십시오(제거되는 PyModuleDef 대신). 종종 이 인자는 전혀 사용되지 않으며, 이름을 변경하여 이를 확인할 수 있습니다.

static PyObject *create_module(PyObject *spec, PyModuleDef *_unused) { ... }

해당 인자가 사용되는 경우, 데이터를 전달할 다른 방법을 찾으십시오. 일반적으로 정보가 정적(static)인 경우가 많으므로 직접 참조할 수 있습니다. (여러 개의 서로 다른 모듈에서 단일 함수를 재사용하는 경우, 대신 여러 개의 함수를 정의하는 것을 고려하십시오.)

여러 개의 py_mod_exec

If you have more than one Py_mod_exec entry, consolidate them: create a new function that calls the others, and replace existing slots with it.

static int my_module_exec(PyObject *module) {
   if (my_first_module_exec(module) < 0) return -1;
   if (my_second_module_exec(module) < 0) return -1;
}

static PyModuleDef_Slot my_slots[] = {
   ...
   /* (다른 Py_mod_exec 슬롯 제거) */
   ...
   {Py_mod_exec, my_module_exec},
   {0, NULL}
};

해당 함수들이 다른 곳에서 사용되지 않는다면, 대신 함수의 본문을 결합할 수도 있습니다.

슬롯 배열 병합

선택적으로, Python 3.14와의 호환성을 끊을 때 슬롯을 PySlot 배열로 이동시키고 정의를 PySlot_DATAPySlot_FUNC 으로 변환하여 코드를 정리할 수 있습니다.

static PySlot my_slot_array[] = {
    ...
    PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
    PySlot_DATA(Py_mod_multiple_interpreters,
         Py_MOD_PER_INTERPRETER_GIL_SUPPORTED)
    PySlot_FUNC(Py_mod_create, my_module_create),
    PySlot_FUNC(Py_mod_exec, my_module_exec),
    PySlot_END
};

이렇게 하는 경우, 원래의 PyModuleDef_Slot 배열과 그 Py_mod_slots 항목을 삭제하십시오.

관련된 PyModuleDef

새로운 API는 PyModuleDef 구조를 사용하지 않으므로, 결과 모듈과 연결되는 정의가 없습니다. 이는 다음 함수들의 동작을 변경합니다:

코드에서 다음 항목들을 확인하십시오. 사용하지 않는 경우 이 섹션을 건너뛸 수 있습니다.

이 함수들은 일반적으로 두 가지 목적으로 사용됩니다.

  1. 모듈이 생성될 때 사용된 정의를 가져오는 용도입니다. 새 API에서는 더 이상 이것이 불가능합니다. 모듈이 더 이상 정의에 대한 참조를 유지하지 않으므로, 관련 데이터를 전달하기 위한 다른 방법을 찾아야 합니다.

  1. 주어진 모듈 객체가 ‘본인 것’인지 확인하는 용도입니다. 이 사례는 이제 모듈을 식별하는 불투명 포인터인 모듈 토큰 에 의해 처리됩니다. 토큰을 사용하려면 다음과 같이 고유한 정적 변수를 선언(또는 재사용)하십시오.

    static char my_token;
    

    그리고 모듈의 PySlot 배열에 새로운 항목으로 그것을 가리키는 포인터를 추가하십시오.

    static PySlot my_slot_array[] = {
       ...
       PySlot_STATIC_DATA(Py_mod_token, &my_token),
       PySlot_END
    }
    

    그 다음, 다음과 같은 PyModule_GetDef() 호출을;

    PyModuleDef *def = PyModule_GetDef(module);
    

    다음과 같이 PyModule_GetToken() 으로 교체하십시오(출력 인자를 사용하며 예외와 함께 실패할 수 있음):

    void *token;
    if (PyModule_GetToken(module, &token) < 0) {
        /* 오류 처리 */
    }
    

    그리고 다음과 같은 PyType_GetModuleByDef() 호출을;

    PyObject *module = PyType_GetModuleByDef(type, my_def);
    /* 에러 처리; module 사용 */
    

    PyType_GetModuleByToken() 으로(이 함수는 강한 참조를 반환합니다):

    PyObject *module = PyType_GetModuleByToken(type, my_token);
    /* 에러 처리; module 사용 */
    Py_XDECREF(module);
    

PyObject 불투명성

PyObjectPyVarObject 구조체는 abi3t 에서 불투명(opaque)합니다.

이들의 멤버에 접근하는 것은 금지됩니다. 이를 수행해야 하는 경우 해당 문서에 언급된 getter/setter 함수로 전환하십시오:

또한, PyObject 구조체의 크기 는 컴파일러가 알 수 없습니다. 크기는 CPython 빌드에 따라 달라질 수 있으며 실제로도 변합니다.

참고

실행 시점에 크기를 확인할 수 있지만(예: 파이썬 코드에서 sys.getsizeof(object())), 이를 사용하여 포인터 오프셋을 계산하려는 유혹을 뿌리쳐야 합니다. 객체 메모리 레이아웃은 향후 abi3t 구현에서 변경될 수 있습니다.

사용자 정의 타입 정의

PyObject 가 불투명하므로 기존의 사용자 정의 타입 정의 방식이 더 이상 작동하지 않습니다.

typedef struct {
   PyObject_HEAD  // ``PyObject ob_base;``로 확장되며, 크기를 알 수 없음

   int my_data;
} CustomObject;

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = sizeof(CustomObject),
   ...
};

대부분의 경우 모든 클래스 정의와 해당 클래스의 데이터에 액세스하는 모든 코드를 다시 작성해야 합니다. 이는 abi3t 를 지원하기 위해 필요한 가장 큰 변화일 것입니다.

이러한 각 타입에 대해, 인스턴스 전체를 위한 struct 를 정의하는 대신 특정 클래스에만 고유한 “추가” 필드들로 구성된 구조체를 정의하십시오(상위 클래스와 관련된 필드는 제외).

typedef struct {
   int my_data;
} CustomObjectData;

이름을 변경하십시오. 해당 구조체를 사용하는 거의 모든 코드를 수정해야 하며(특히 새 구조체에 대한 포인터를 PyObject*``로 캐스트할 없음), 이름을 변경하면 컴파일러 에러를 통해 사용처를 강조해 줍니다. (``typeof, C++ auto 또는 유사한 방식을 사용하여 타입 이름을 명시하지 않는 경우에는 이 방법이 작동하지 않습니다. 각별히 주의하고 정의되지 않은 동작을 감지하는 도구를 실행하는 것을 고려하십시오.)

그 다음, 클래스를 생성할 때 인스턴스의 전체 크기가 아닌 “추가” 저장 공간을 나타내기 위해 음수 값의 basicsize 를 사용하십시오.

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = -sizeof(CustomObjectData), /* 마이너스 기호 주의 */
   ...
};

Py_tp_members 를 사용하는 경우, 각 멤버에 Py_RELATIVE_OFFSET 플래그를 설정하고 새로운 구조체 기준의 상대적인 위치로 offset 을 지정하십시오.

사용자 정의 데이터 접근

이제 어려운 부분이 나옵니다. 이 구조체에 접근해야 하는 모든 코드에서 PyObject *``로부터 ``CustomObjectData * 포인터를 가져오기 위해 추가적인 PyObject_GetTypeData() 호출이 필요합니다.

PyObject *obj = ...;
CustomObjectData *data = PyObject_GetTypeData(obj, cls);

이 호출 시 클래스의 타입 객체 (cls)가 필요하다는 점에 유의하십시오.

클래스가 서브 클래스화될 수 없는 경우(즉, Py_TPFLAGS_BASETYPE 플래그를 사용하지 않는 경우), clsPy_TYPE(obj) 이 됩니다. 그렇지 않은 경우에는 PyObject_GetTypeData() 와 함께 Py_TYPE사용하지 마십시오. 관련 없는 서브 클래스를 위해 예약된 메모리를 반환할 수 있습니다! 예를 들어, 사용자가 다음과 같이 서브 클래스를 만드는 경우:

class Sub(YourCustomClass):
   __slots__ = ('a', 'b')

이때 Py_TYPE(obj)Sub 가 되며, 기본 메모리는 다음과 같이 보일 수 있습니다.

╭─ PyObject *obj
│              ╭─ the pointer you want
│              │                    ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj))
▼              ▼                    ▼
┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐
│ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │
└──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘

(말줄임표는 가능성 있는 패딩을 나타냅니다. 이 메모리 레이아웃은 보장되지 않으며, 향후 파이썬 버전에서 다른 패딩이 추가되거나 구조체 순서가 바뀔 수 있습니다.)

올바른 클래스를 가져오는 두 가지 주요 방법이 있습니다.

  • 인스턴스 메서드에서, 구현에 PyCMethod 시그니처(및 PyMethodDef.ml_flagsMETH_METHOD 비트)를 사용하고 defining_class 인자로 클래스를 가져올 수 있습니다.

  • 그렇지 않은 경우, Py_tp_token 슬롯을 사용하여 클래스에 고유한 정적 토큰을 부여하고 다음을 사용하십시오.

    PyTypeObject cls;
    if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) {
        /* 에러 처리 */
    }
    CustomObjectData *data = PyObject_GetTypeData(obj, cls);
    

    유형 토큰은 이 가이드에서 전에 다룬 모듈 토큰과 유사하게 작동합니다.

빌드 시점 조건문 방지

확장 기능을 빌드 하는 데 사용된 파이썬 버전을 식별하는 API가 코드에 있는지 확인하십시오. 이 정보는 더 이상 확장 기능이 실행되는 파이썬 버전과 일치하지 않으므로, 이 정보를 사용하는 코드는 변경이 필요한 경우가 많습니다. 확인해야 할 매크로는 다음과 같습니다.

  • PY_VERSION_HEX, PY_MAJOR_VERSION, PY_MINOR_VERSION:

    • 실행 시점 버전을 가져오려면 Py_Version 을 사용하십시오.

    • 사용 가능한 C API를 확인하려면 Py_TARGET_ABI3T 를 사용하십시오. 이 매크로는 최소 지원 버전으로 설정됩니다.

  • Py_GIL_DISABLED: abi3t 환경에서 이 매크로는 항상 정의됩니다. free-threaded Python에서 작동하는 코드는 GIL이 활성화된 경우에도 작동해야 하며(런타임에 GIL을 활성화할 수 있기 때문), 일반적으로 작동합니다(어떤 이유로든 한 번에 하나 이상의 attached thread state 를 요구하는 경우가 아니라면).

추가 코드 변경 사항

여전히 컴파일러 에러나 경고가 발생한다면 해결 방법을 찾으십시오. 안타깝게도 이 가이드는 한계가 있어 확장 모듈에 필요한 모든 가능한 코드 변경 사항을 다룰 수는 없습니다.

다른 확장 모듈 작성자가 겪을 수 있는 문제를 발견하면, 이 가이드에 대해 문제 보고 (또는 풀 리퀘스트 제출)를 고려해 주십시오.

현재 버전의 abi3t 에서는 해당 문제가 해결되지 않을 수도 있습니다. 이 경우 문제를 보고하면 다음 버전의 CPython에서 우선적으로 처리되도록 하는 데 도움이 될 수 있습니다.

태그 및 배포

abi3t 를 지원하는 빌드 도구를 사용하는 경우 확장 모듈은 준비되었으나, 올바르게 빌드되었는지 확인이 필요할 수 있습니다.

abi3t 로 빌드된 확장은 다음과 같은 확장자를 가져야 합니다:

  • Windows의 경우: .pyd (다른 모든 확장과 동일);

  • Linux, macOS 및 .so 접미사를 사용하는 기타 시스템: .abi3t.so (.cpython-315t.so 또는 .abi3.so``가 **아님**). free-threaded 빌드와 non-free-threaded 빌드 모두 ``.abi3t.so 확장을 로드합니다.

  • 기타 시스템: 배포처에 문의하고 필요한 경우 이 가이드를 업데이트하십시오.

확장 모듈을 wheel 로 배포하는 경우 다음 태그를 사용하십시오:

  • Python 태그: cp3XX, 여기서 XX 는 확장 모듈이 빌드된 최소 Python 버전입니다. (예를 들어, Py_TARGET_ABI3T0x30f0000 으로 설정하면 cp315 가 됩니다. 더 많은 값은 Stable ABI용 컴파일 을 참조하십시오.)

  • ABI 태그: abi3.abi3t. 이는 non-free-threaded와 free-threaded 빌드 모두에 대한 지원을 나타내는 압축된 태그 세트 입니다.

예를 들어, wheel 파일명은 다음과 같을 수 있습니다:

myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl

더 보기

PyPA 패키지 배포 메타데이터의 Platform Compatibility Tags.

파일명이나 태그가 올바르지 않으면 수정하십시오.

테스트

여러 버전의 CPython과 호환되는 확장을 빌드할 때, 지원하는 각 버전(예: 3.15, 3.16 등)에서 항상 테스트 해야 합니다. Stable ABI는 ABI 호환성만 보장하며, 의도적인 변경 사항(PEP 387 에서 다룸)이나 버그로 인한 동작 변화가 있을 수 있습니다.

CPython의 free-threaded 및 non-free-threaded 빌드 모두에서 테스트를 실행하십시오.

모두 통과했다면 축하합니다! abi3t 확장 모듈을 성공적으로 구축했습니다.