PEP 809 – 미래를 위한 안정 ABI
- Author:
- Steve Dower <steve.dower at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Requires:
- 703, 793, 697
- Created:
- 19-Sep-2025
- Python-Version:
- 3.15
- Post-History:
- 30-Sep-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
abi3로서의 안정 ABI는 더 이상 유지할 수 없으며, 대체가 필요합니다. abi2026이 첫 번째 대체로서 현재 알려진 비호환성을 해결하며, 최소 10년 후에 퇴역할 예정입니다. 다음 ABI(예를 들어 abi2031)는 이전 ABI와 최소 5년간 겹치게 됩니다.
장기적인 안정성은 런타임 ABI 탐색 메커니즘을 통해 확보되며, 이를 통해 같은 ABI를 지원하는 이전 릴리스에서도 확장을 실행할 수 있습니다. ABI의 수명 동안 이루어지는 변경과 추가는 인터페이스로 추가할 수 있으며, 이를 런타임에 탐색할 수 있으므로 호출자가 적절한 대체 동작을 선택할 수 있습니다. 현재는 이러한 추가 때문에 이전 런타임에서 확장이 아예 로드되지 않습니다.
abi3 ABI는 GIL 활성 빌드에서 최소 5년간 유지되며, 그 이후에는 퇴역할 수 있습니다(이 경우 abi2026 및 그 이후 ABI만 사용할 수 있습니다). 그 전에 GIL 활성 빌드 자체가 완전히 퇴역할 가능성도 있습니다. 자유 스레드 빌드에는 abi3가 없으므로, 이들의 첫 번째 안정 ABI는 abi2026이 됩니다.
용어
이 PEP에서는 “GIL 활성 빌드”를 “자유 스레드 빌드”의 반대말로, 즉 Py_GIL_DISABLED 없이 빌드된 인터프리터나 확장을 가리키는 말로 사용합니다.
동기
현재 자유 스레드 빌드에서는 안정 ABI를 사용할 수 없습니다. Py_LIMITED_API 매크로가 정의되어 있으면 확장의 빌드가 실패합니다. 마찬가지로, CPython의 GIL 활성 빌드용으로 빌드된 확장은 자유 스레드 빌드에서 로드에 실패합니다(또는 크래시가 발생합니다).
PEP 779에 대한 수락 게시물에서 운영 위원회는 “자유 스레딩을 위한 안정 ABI가 Python 3.15에 맞춰 준비되고 정의될 것을 기대합니다”라고 밝혔습니다.
이 PEP는 3.15 이상의 모든 변형과 호환되는 안정 ABI를 제안하며, 이를 통해 패키지 개발자는 확장을 단 하나의 빌드로 만들 수 있습니다.
관련 PEP
PEP 803은 이 제안의 대안이며, 이 PEP에서 사용한 배경 설명의 상당 부분은 의도적으로 동일하게 작성되었습니다. 글을 사용할 수 있게 해 주신 PEP 803 저자에게 감사드립니다.
배경
PEP 384와 PEP 652에 정의된 Python의 안정 ABI는 CPython 인터프리터의 여러 마이너 버전에서 로드할 수 있는 확장 모듈을 컴파일하는 방법을 제공합니다. 여러 프로젝트가 이를 이용해 릴리스마다 빌드하고 배포해야 하는 휠(바이너리 산출물)의 수를 줄이거나, Python의 사전 릴리스 버전으로 테스트하기 쉽게 만들거나, 혹은 둘 다를 수행합니다.
자유 스레딩 빌드(PEP 703)가 결국 기본값이 되는 방향(PEP 779)으로 나아가고 있으므로, 이러한 빌드에서도 안정 ABI를 사용할 수 있게 하는 방법이 필요합니다.
안정 ABI를 대상으로 빌드하려면 확장은 제한된 API를, 즉 CPython이 노출하는 함수, 구조체 등의 일부분만을 사용해야 합니다. 제한된 API에는 버전이 있으며, 제한된 API 3.X를 대상으로 빌드하면 CPython 3.X 및 그 이후의 모든 버전과 ABI 호환되는 확장이 만들어집니다(다만 실제로는 CPython의 버그로 인해 비호환성이 생기는 경우가 있습니다). 또한 제한된 API는 “안정적”이지 않습니다. 새 버전에서는 이전 버전에서 사용할 수 있던 API 항목이 제거될 수 있습니다.
이 PEP는 제한된 API와 안정 ABI 모두의 버전 관리에 대한 중대한 변경을 제안합니다. 목표는 안정성과 호환성을 장기적으로 관리할 수 있게 하는 동시에, 제한된 부분 집합의 사용자도 이후 Python 릴리스의 혁신을 누릴 수 있게 하는 것입니다.
근거
이 PEP의 설계는 다음과 같은 몇 가지 가정을 전제로 합니다:
- 단일 ABI
- 컴파일된 확장 모듈 하나가 자유 스레드 빌드와 GIL 활성화 빌드를 모두 지원해야 합니다.
- 하위 호환성 없음
- 새로운 제한된 API는 CPython 3.14 이하에서 지원되지 않습니다. 이러한 지원이 필요한 프로젝트는 3.14 자유 스레드 인터프리터용 확장과 이전 안정 ABI 버전용 확장을 각각 따로 빌드할 수 있습니다.
- API 변경 허용
- 새로운 제한된 API로 인해 확장 작성자가 코드를 크게 변경해야 할 수도 있습니다. (아직) 그렇게 할 수 없는 프로젝트는 제한된 API 3.14를 계속 사용할 수 있으며, 이 경우 GIL이 활성화된 빌드와만 호환되는 확장이 만들어집니다.
- 추가 설정 없음
- 사용 가능한 API와 ABI의 호환 대상에 영향을 주는 새로운 “조정 장치”는 도입하지 않습니다.
명세
명세의 상당 부분은 PEP 803과 동일하므로, 자세한 내용은 해당 제안을 참고하십시오. ABI 안정성, 빌드 시점 매크로, 인터페이스 API 절은 이 제안에만 있는 내용입니다.
ABI 안정성
안정 ABI는 최소 10년 동안 동결됩니다. 안정 ABI의 새 버전이 동결되면 기존 버전은 최소 5년 동안 계속 지원됩니다. 이를 통해 패키지 유지 관리자(및 기타 사용자)가 지원하는 모든 릴리스 범위를 동시에 마이그레이션할 수 있도록 충분한 마이그레이션 시간을 확보할 수 있습니다. 다만 Python 코어 개발 팀이 현재의 안정 ABI를 대체할 이유가 없다고 판단하면, 새 버전의 동결은 연기될 수 있습니다.
새 안정 ABI는 PEP 절차를 통해 정의되며, 그 이름에는 해당 ABI를 지원하는 첫 런타임의 출시 연도가 반영됩니다.
안정 ABI가 동결되면 그 연도가 ABI의 이름이 됩니다. 예를 들어 이 방식에 따른 첫 ABI는 abi2026이 될 것으로 예상하며, 이 ABI는 최소 2036년까지 모든 릴리스에서 지원됩니다. 2036년에 지원이 중단되면 abi2031이 이전 대상이 되며, 이에 따라 패키지 개발자는 자신이 이전한 후에도 최소 5년 치 릴리스를 지원할 수 있습니다.
동결 기간에는 ABI 변경이 일절 허용되지 않습니다. 추가도, 제거나 수정, 급격한 의미 변경도 허용되지 않습니다. 결정적으로, 특정 ABI를 대상으로 컴파일된 확장 모듈은 해당 ABI를 지원하는 Python 버전이라면 이전 버전이든 이후 버전이든 어느 버전에서나 성공적으로 로드되어야 합니다(즉, 지원되는 모든 플랫폼에서 임포트하는 모든 심볼이 충족되어야 합니다).
기존의 호환 ABI를 통해 런타임에 감지할 수 없는 의미 변경은 허용되지 않습니다. 즉, 현재 Python 릴리스에서 특정 동작이 예상되는지를 감지하는 API는 해당 ABI를 지원하는 모든 이전 릴리스에서 사용할 수 있었어야 합니다.
불투명한 PyObject
제한된 API 버전 3.15에서는 여러 구조체를 불투명하게 만들어, 사용자가 그 크기나 레이아웃에 대해 어떠한 가정도 할 수 없게 합니다. 자세한 내용은 PEP 803에서 확인할 수 있으며, 이 제안의 내용도 동일합니다.
새 내보내기 훅(PEP 793)
이 PEP를 구현하려면 확장 모듈을 정의하기 위한 새로운 “내보내기 훅”을 제공하는 PEP 793 (PyModExport: C 확장 모듈을 위한 새로운 진입점)이 채택되어야 합니다. 제한된 API 3.15에서는 새 훅의 사용이 필수가 됩니다.
이 제안은 PEP 803의 제안과 동일합니다.
런타임 ABI 검사
자세한 내용은 PEP 803을 참조하십시오. 이 제안은 동일합니다.
빌드 시점 매크로
Py_LIMITED_API 매크로는 0x03ff_YYYY로 정의되어야 합니다. 즉, 상위 워드는 상수 0x03ff이고 하위 워드는 ABI 이름(연도)을 16진수 값으로 나타낸 것입니다. 이렇게 하면 십진수 값이 연도와 같지 않게 되지만, 이 값은 임의의 레이블이며 계산된 값보다는 (cc 명령줄에서) 상수로 지정될 가능성이 높으므로 이는 중요하지 않다고 판단합니다.
상수로 0x03ff를 사용하는 것은 이전 런타임과의 호환성을 허용하기 위한 것입니다. 같은 상수를 abi3만 지원하는 헤더와 함께 사용하면 해당 릴리스에서 사용할 수 있는 “가장 완전한” 버전의 ABI3이 선택됩니다. 예를 들어, 3.15 이상에서 0x03ff2026을 사용하면 abi2026이 선택되고, 3.10에서는 3.10–3.14에서 동작하는 버전의 ABI3이 선택됩니다.
휠 태그
휠에는 ABI 태그 abi2026을 지정해야 합니다. Python 태그나 플랫폼 태그는 변경할 필요가 없습니다. 참고로 cp314 이하로 태그된 릴리스에는 abi2026이 존재하지 않았으므로 이들 릴리스는 결코 abi2026과 호환되지 않으며, 따라서 py3-abi2026-<plat>로 태그된 휠 때문에 새로운 안정 ABI를 사용하는 휠이 이전 릴리스에서 로드되는 일은 없습니다.
새 API
이 PEP를 구현하면 자유 스레드 Python에서 성공적으로 로드될 수 있는 확장을 빌드할 수 있게 되지만, 그 확장이 반드시 GIL 없이도 스레드 안전한 것은 아닙니다.
GIL 없이 스레드 안전성을 확보하기 위한 제한된 API(아마도 PyMutex, PyCriticalSection 및 이와 유사한 것들)는 C API 작업 그룹을 통해, 또는 후속 PEP에서 추가될 것입니다.
인터페이스 API
Python과 새로운 제한된 API에 새로운 인터페이스 API가 추가됩니다. 이 API는 위의 ABI 안정성 절에서 제시한 “의미상의 변경을 모든 릴리스에서 감지할 수 있어야 한다”는 요구 사항을 충족하기 위한 것입니다. 즉, 소비자 [1]는 새 API를 즉시 도입하고, 최신 릴리스로 제한된 API를 대상으로 컴파일하면서도, 해당 ABI를 지원하는 모든 릴리스에 대해 바이너리 호환성을 유지할 수 있습니다.
요컨대, 기본 API는 PyObject_GetInterface()이며, 이 함수는 새로운 네이티브 전용 타입 슬롯에 위임하여 데이터 또는 함수 포인터를 담은 C 구조체를 채웁니다. C 구조체 정의는 런타임에 얻어지는 것이 아니라 확장에 내장되므로, 확장 모듈은 해당 구조체를 제공하지 않는 Python 릴리스에서 실행되는 동안에도 이후에 추가된 구조체를 인지할 수 있습니다.
PyObject_GetInterface 호출이 현재 버전에서 사용할 수 없거나 주어진 객체에 대해 사용할 수 없는 구조체를 요청하면, 호출은 안전하게 실패합니다. 그러면 호출자는 선호에 따라 대체 로직(예를 들어, 추상 Python API 사용)을 사용하거나 중단할 수 있습니다.
예를 들어, abi2026의 수명 동안 int 객체의 내부 데이터에 더 효율적으로 접근할 수 있게 해 주는 새로운 API를 추가하게 된다면, 새로운 API를 추가하는 대신 새로운 인터페이스를 만들 것입니다. 이 인터페이스는 데이터를 새로운 위치로 복사하는 함수 포인터를 담은 구조체와, 해당 인터페이스를 위해 이전에 사용되지 않은 인덱스/이름으로 이루어집니다. 호출자는 먼저 PyObject_GetInterface(int_object, &intf_struct)를 호출할 수 있습니다. 호출이 성공하면 (가상의) (*intf_struct.copy_bits)(&intf_struct, dest, sizeof(dest))를 호출하고, 실패하면 PyObject_CallMethod(int_object, "to_bytes", ...)를 사용하여 같은 작업을 덜 효율적으로 수행할 수 있습니다. 이 예의 최종 결과는 abi2026을 지원하는 모든 릴리스와 바이너리 호환되면서도 더 새로운 Python 릴리스에서 실행될 때는 더 효율적인 단일 확장 모듈입니다.
개요는 이것으로 마치고, 다음은 각 새로운 API의 전체 명세입니다:
// Abstract API to request an interface for an object (or type).
PyAPI_FUNC(int) PyObject_GetInterface(PyObject *obj, void *intf);
// API to release an interface.
PyAPI_FUNC(int) PyInterface_Release(void *intf);
// Expected layout of the start of each interface. Actual interface structs
// will add additional function pointers or data.
typedef struct PyInterface_Base {
// sizeof(self), for additional validation that the caller is passing
// the correct structure.
Py_ssize_t size;
// Unique identifier for the struct. Details below.
uint64_t name;
// Function to release the struct (e.g. to decref any PyObject fields).
// Should only be invoked by PyInterface_Release(), not directly.
int (*release)(struct PyInterface_Base *intf);
} PyInterface_Base;
// Type slot definition for PyTypeObject field.
typedef int (*Py_getinterfacefunc)(PyObject *o, PyInterface_Base *intf);
구조체의 고유 식별자는 매크로로 정의된 64비트 정수입니다(컴파일된 확장 모듈이 런타임에 값을 알아내려 하지 않고 그 값을 내장하도록 하기 위함입니다). 상위 32비트는 네임스페이스이며, 자체 구조체를 정의하는 구현자는 자신만의 고유한 값을 선택해야 합니다. 0은 CPython용으로 예약되어 있습니다.
인터페이스 이름은 구조체 레이아웃을 식별하기 위한 것이므로, 정의된 모든 객체는 구조체가 일치하기만 하면 다른 네임스페이스의 인터페이스 이름을 재사용할 수 있습니다. 이는 의도된 것으로, 서드파티 타입이 구현을 공유하는 데 의존하지 않고도 코어 타입과 같은 인터페이스를 구현할 수 있게 해 줍니다. 분명히 하자면, CPython용으로 정의된 인터페이스는 이름이나 그 이름의 네임스페이스를 바꾸지 않고도 다른 확장 모듈에서 사용할 수 있습니다.
예를 들어, PyDict_GetItemString()을 구현하기 위한 가상의 인터페이스를 생각해 봅시다. 코어 dict 타입은 문자열 키로 항목을 찾기 위해 내부 최적화를 할 수 있고, 외부 타입은 같은 인터페이스를 사용하여 자체적인 최적화를 할 수 있습니다. 호출자에게는 같은 인터페이스를 사용하는 것으로 보이므로, 호출자는 (예를 들어) CPython의 구체 객체 API를 사용하는 경우보다 더 넓은 범위의 타입과 호환됩니다.
인터페이스 이름은 언제든 헤더에서 제거할 수 없으며, 구조체 정의는 이를 지원하는 모든 안정 ABI 버전이 완전히 퇴역한 경우에만 제거할 수 있습니다. 그러나 객체는 특정 인터페이스가 더 이상 권장되지 않거나 신뢰할 수 없게 되면, 이전 릴리스에서 반환했더라도 그 인터페이스를 더 이상 반환하지 않을 수 있습니다. 적절한 경우 런타임 폐지 경고를 사용할 수 있으며, 특별한 규칙은 정해져 있지 않습니다.
인터페이스 구조체는 고정되어 있으며 변경할 수 없습니다. 변경이 필요하면 새 이름으로 새 인터페이스를 정의해야 합니다. 인터페이스를 위해 구조체에 추가된 필드는 공개 API이며 문서화해야 합니다. 직접 사용하도록 의도되지 않은 필드는 밑줄로 시작해야 하지만, 그 밖의 방법으로 “비공개”로 만들 수는 없습니다. 인터페이스는 데이터와 함수 포인터를 섞어서 제공하거나, 경쟁 조건을 피하기 위해 강한 PyObject * 참조를 사용할 수 있습니다.
인터페이스를 가져온 후에는 객체에 대한 참조가 해제되더라도 인터페이스가 해제될 때까지 유효하게 유지되어야 합니다. 인터페이스의 동작은 하부 객체의 변경을 적절한 어떤 방식으로든 처리할 수 있지만, 그 선택을 문서화하는 것이 좋을 것입니다. 이러한 종류의 변경을 서로 다르게 처리하는 두 개의 유사한 인터페이스를 두는 것도 불합리하지 않습니다(예를 들어, 한 인터페이스는 인터페이스의 수명 동안 객체에 잠금을 걸고, 다른 인터페이스는 잠금을 걸지 않는 경우).
새로운 제한된 API를 추가하는 절차는 다소 달라집니다. 릴리스마다 ABI가 커지는 대신, 새 API는 제한된 API를 사용하지 않는 경우를 위해서는 실제 함수로 추가할 수 있지만, 제한된 API를 위해서는 static inline 함수로 추가해야 합니다. 이 정적 인라인 함수는 인터페이스를 사용하여 런타임에 기능을 감지해야 하며, 추상 대체 구현이나 적절한 예외를 포함해야 합니다.
즉, 사용자는 새 API를 즉시 도입하고, 최신 릴리스로 제한된 API용 컴파일을 하면서도 같은 안정 ABI를 지원하는 모든 릴리스에 대해 바이너리 호환성을 유지할 수 있습니다.
다음 안정 ABI 동결 시점에 해당 API는 실제 함수로서 새 안정 ABI/제한된 API로 승격되거나, 인터페이스로 유지될 수 있습니다.
하위 호환성
제한된 API 3.15는 제거된 구조체와 함수 때문에 이전 CPython 릴리스와 하위 호환되지 않습니다.
전환할 수 없는 확장 작성자는 GIL이 활성화된 빌드에서 사용하기 위해 제한된 API 3.14 이하를 계속 사용할 수 있습니다.
GIL 활성화 빌드의 abi3에는 아무런 변경도 이루어지지 않으며, 기존의 모든 심볼은 새 Stable ABI에서는 더 이상 사용할 수 없더라도 계속 사용할 수 있습니다.
자유 스레드 빌드를 CPython의 기본/유일한 릴리스로 만드는 것은 하위 호환되지 않는 변경이 될 것이며, 확장 작성자는 그 전에 마이그레이션을 마쳐야 합니다.
보안 영향
알려진 바 없습니다.
교육 방법
Python의 네이티브 ABI는 다른 언어와 마찬가지로 연도로 식별되며 주기적으로 갱신되는 표준 또는 명세라고 할 수 있습니다. 모든 확장 모듈은 이 ABI를 사용할 수 있으며, 자신이 기대하는 ABI를 배포 정보에 명시합니다. 모든 Python 구현은 특정 ABI 버전을 지원할지 선택할 수 있으며, 같은 버전을 지원하는 확장이라면 어떤 것이든 사용할 수 있어야 합니다.
abi3에서 새 ABI로 옮겨 가려면 소스 코드를 변경해야 할 수도 있지만, 한 번만 하면 되는 작업으로 볼 수 있습니다. 대부분까지는 아니더라도 많은 경우에 소스 코드는 abi3과 새 ABI 모두와 호환되므로, 이전 릴리스용 빌드와 현재 릴리스용 빌드를 만들기가 간단해집니다. 일반적으로 abi3 빌드는 지원되는 가장 오래된 CPython 런타임으로 빌드해야 하고, 새 ABI 빌드는 최신 CPython 런타임(또는 호환되는 다른 런타임)으로 빌드해야 합니다.
한 ABI(예: abi2026)에서 다음 ABI(예: abi2031)로 옮겨 가는 작업은 수동으로 해야 합니다. ABI 업데이트 간에 겹치는 부분이 충분히 많으므로 대부분의 프로젝트는 한 번에 하나만 지원하면 되고, 자체 지원 매트릭스가 허용한다면 모든 빌드를 한꺼번에 업데이트할 수 있습니다. 패키지 유지 관리자가 새 ABI가 나올 때마다 즉시 지원해야 한다고 기대하지는 않습니다.
상위 호환성과 하위 호환성은 동적 인터페이스 탐지로 보장됩니다. 최근 추가된 제한된 API 함수를 사용하는 코드도 이전 릴리스에서 실행됩니다. 다만 성능이 더 낮을 수 있습니다. 제한된 API에만 해당하는 세부 사항은 새 함수의 문서를 참조하십시오.
C가 아닌 언어의 호출자는 인터페이스 메커니즘을 직접 사용해야 합니다. 그래야 호환 범위를 굳이 새 릴리스로 좁히지 않고도 새 기능을 사용할 수 있습니다. 인터페이스의 이름과 구조체 레이아웃은 영구적으로 안정성이 보장됩니다. 하지만 어떤 인터페이스가 영구적으로 제공된다고 가정해서는 안 되며, 적절한 대체 코드(대안 구현이나 오류 처리)를 포함해야 합니다.
참조 구현
해당 PEP에서 이어받은 부분의 참조 구현 링크는 PEP 803을 참조하십시오.
인터페이스의 참조 구현은 zooba/cpython#44입니다.
기각된 아이디어
[지금은 논의를 참조하십시오.]
미해결 문제
[지금은 논의를 참조하십시오.]
각주
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.