PEP 793 – PyModExport: C 확장 모듈을 위한 새로운 진입점
- Author:
- Petr Viktorin <encukou at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 23-May-2025
- Python-Version:
- 3.15
- Post-History:
- 14-Mar-2025, 27-May-2025
- Resolution:
- 23-Oct-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP에서는 C 확장 모듈을 위한 새로운 진입점을 제안합니다. 이 진입점을 사용하면 PyModuleDef_Slot 구조체의 배열을 감싸는 PyModuleDef 구조체 없이 그 배열만으로 모듈을 정의할 수 있습니다. 이를 통해 확장 작성자는 정적으로 할당된 PyObject를 사용하지 않아도 됩니다. 이로써 컴파일된 라이브러리 파일 하나를 CPython의 일반 빌드와 자유 스레드 빌드 모두에서 사용하는 데 가장 흔한 장애물이 사라집니다.
이를 실현하기 위해 PyModuleDef의 필드를 대체하고, 타입 객체에 사용되는 Py_tp_token과 유사한 토큰을 추가할 수 있도록 하는 새로운 모듈 슬롯 ID도 정의합니다.
또한 슬롯으로부터 모듈을 동적으로 정의하는 API도 추가합니다.
기존 API(PyInit_*)는 소프트 폐지(soft-deprecated)됩니다. (즉, 경고 없이 계속 동작하며 완전히 문서화되고 지원되지만, 새로운 기능은 추가하지 않을 계획입니다.)
배경 및 동기
Python 객체의 메모리 레이아웃은 일반 빌드와 자유 스레딩 빌드에서 서로 다릅니다. 따라서 일반 빌드와 자유 스레딩 빌드를 모두 지원하는 ABI는 현재의 PyObject 메모리 레이아웃을 포함할 수 없습니다. 기존 ABI(및 API)와의 호환성을 유지하려면 이 ABI는 정적으로 할당된 Python 객체를 지원할 수 없습니다.
대부분의 확장 모듈에 필요하고 사실상 모든 경우에 정적으로 할당되는 객체 유형이 하나 있습니다. 바로 모듈 내보내기 훅(즉, PyInit_* 함수)이 반환하는 PyModuleDef입니다.
모듈 내보내기 훅(PyInit_* 함수)은 두 종류의 객체를 반환할 수 있습니다.
- 완전히 초기화된 모듈 객체입니다(이른바 단일 단계 초기화의 경우). 3.4 이하 버전에서는 이것이 유일한 선택지였습니다. 이 방식으로 생성된 모듈은 여러 인터프리터 환경이나 반복 로딩 시 예상치 못한(그러나 하위 호환성이 있는) 동작을 보입니다. (구체적으로, 이러한 모듈의
__dict__의 내용은 해당 모듈 객체의 모든 인스턴스 간에 공유됩니다.)반환되는 모듈은 보통
PyModule_Create함수로 생성되며, 이 함수에는 정적으로 할당된(또는 적어도 수명이 긴)PyModuleDef구조체가 필요합니다.더 저수준의
PyModule_New*API를 사용하여 이를 우회할 수 있습니다. 이렇게 하면PyModuleDef가 필요 없지만, 제공되는 기능이 훨씬 적습니다. - 모듈 객체를 생성하는 방법에 대한 설명을 담고 있는
PyModuleDef객체입니다. 이 방식, 즉 다단계 초기화는 PEP 489에서 도입되었습니다. 이 방식이 존재하는 이유는 해당 PEP의 동기를 참조하십시오.
인터프리터는 내보내기 훅을 호출하기 전에는 이 두 경우를 구별할 수 없습니다.
인터프리터 전환
Python 3.12에서는 모듈이 서브인터프리터에서 로드될 수 있는지를 표시하는 방법으로 Py_mod_multiple_interpreters 슬롯을 추가했습니다. 이 슬롯을 “지원하지 않음” 값으로 설정하면 해당 확장을 메인 인터프리터에서만 로드할 수 있음을 나타냅니다.
안타깝게도 Python은 모듈 내보내기 훅을 호출해야만 이 정보를 얻을 수 있습니다. 단일 단계 모듈의 경우, 이 호출은 모듈 객체를 생성하고 임의의 초기화 코드를 실행합니다. Py_mod_multiple_interpreters를 “지원하지 않음”으로 설정한 모듈의 경우, 이 초기화는 메인 인터프리터에서 이루어져야 합니다.
이를 동작하게 하기 위해, 새 모듈이 서브인터프리터에서 로드되면 Python은 일시적으로 메인 인터프리터로 전환하여 그곳에서 내보내기 훅을 호출한 다음, 다시 전환하여 임포트를 다시 수행하거나 실패합니다.
이러한 불필요하고 취약한 추가 작업은 근본적인 설계 문제를 드러냅니다. Python은 확장이 스스로를 완전히 초기화할 가능성이 생기기 전에 그 확장에 대한 정보를 얻을 방법이 없습니다.
근거
모듈 내보내기 훅에 정적으로 할당된 PyObject*가 필요하지 않게 하는 방법으로 두 가지를 떠올릴 수 있습니다.
- 동적으로 할당된 객체를 반환하며, 그 소유권은 인터프리터로 이전됩니다. 이 구조체는 같은 데이터를 담아야 하므로 기존
PyModuleDef와 매우 비슷할 수 있습니다. 기존PyModuleDef와 달리, 이 구조체는 “자신의” 모듈보다 오래 살아남으면서도 누수되지 않도록 참조 카운트되어야 합니다. PyObject*를 반환하지 않는 새 내보내기 훅을 추가합니다.이 방안은 이미 Python 3.5 때 PEP 489에서 검토되었으나 기각되었습니다:
정의를 내보내는 데 완전히 적합하지는 않더라도 PyInit 훅 이름만 유지하는 편이 훨씬 단순한 해법이었습니다.안타깝게도 이 선택이 낳은 여파를 10년 동안 수습하고 난 지금, 그 해법은 더 이상 단순하지 않습니다.
새 훅을 통해 Python은 동기 절에서 언급한 두 번째 문제, 즉 인터프리터 전환 문제도 피할 수 있게 됩니다. 사실상 다단계 초기화에 새로운 단계가 추가되며, 이 단계에서 Python은 모듈이 호환되는지 확인할 수 있습니다.
래퍼 구조체 없이 슬롯 사용하기
기존 PyModuleDef는 몇 가지 고정 필드와 “슬롯” 배열로 이루어진 구조체입니다. 슬롯과 달리 고정 필드는 개별적으로 폐기하고 대체할 수 없습니다. 이 제안은 고정 필드를 없애고, 래퍼 구조체 없이 슬롯 배열을 직접 사용할 것을 제안합니다.
PyModuleDef_Slot 구조체에는 고정 필드에 비해 몇 가지 단점이 있습니다. 저희는 이 단점들을 고칠 수 있다고 보지만, 이는 이 PEP의 범위에서 제외합니다. (참고: 이 작업은 PEP 820에서 이루어졌으며, 역시 Python 3.15에 포함됩니다.)
토큰
정적 PyModuleDef에는 모듈을 어떻게 생성할지 기술하는 것 외에 또 다른 용도가 있습니다. 정적 PyModuleDef는 모듈 객체에 계속 연결되어 있는, 정적으로 할당된 싱글턴이므로 확장 작성자는 이를 통해 주어진 Python 모듈이 “자신의” 것인지 확인할 수 있습니다: 모듈 객체에 알려진 PyModuleDef가 있다면, 그 모듈 상태는 알려진 메모리 레이아웃을 가집니다.
타입에서는 Py_tp_token을 추가하여 비슷한 문제를 해결했습니다. 이 제안은 같은 메커니즘을 모듈에 추가합니다.
타입과 달리 임포트 메커니즘에는 토큰 값으로 적합하다고 알려진 포인터가 있는 경우가 많으며, 이런 경우에는 기본 토큰을 제공할 수 있습니다. 따라서 모듈 토큰에는 우아하지 않은 Py_TP_USE_SPEC의 변형이 필요하지 않습니다.
여러 Python 버전에 걸쳐 있는 확장을 돕기 위해 PyModuleDef 주소를 기본 토큰으로 사용하며, 합리적인 경우에는 토큰과 서로 바꿔 쓸 수 있게 합니다.
기존 내보내기 훅의 소프트 폐기
기존 확장의 작성자가 여기서 제안하는 API로 전환할 유일한 이유는 자유 스레드 빌드와 비자유 스레드 빌드 모두에 단일 모듈을 사용할 수 있다는 점입니다. Python이 이를 허용한다는 점은 중요하지만, 많은 기존 모듈의 경우 3.14 이하 버전과의 호환성을 잃을 만한 가치가 전혀 없습니다.
기존 API의 폐지를 계획하기에는 너무 이릅니다.
대신 이 PEP는 PyInit_* 방식에 새 기능을 추가하는 것을 중단할 것을 제안합니다. 결국 확장 작성자가 전환하기에 가장 좋은 때는 어차피 모듈 초기화를 수정하려 할 때입니다.
명세
내보내기 훅
확장 모듈을 임포트할 때, Python은 이제 먼저 다음과 같은 내보내기 훅을 찾습니다:
PyModuleDef_Slot *PyModExport_<NAME>(void);
Note
PEP 820에서 반환 타입을 PySlot *로 변경했습니다.
여기서 <NAME>은 모듈의 이름입니다. ASCII가 아닌 이름의 경우에는 대신 PyModExportU_<NAME>을 찾으며, 이때 <NAME>은 기존 PyInitU_* 훅과 같은 방식으로 인코딩됩니다(즉, 하이픈을 밑줄로 바꾼 punycode 인코딩입니다).
찾지 못하면 임포트는 이전 Python 버전과 같이(즉, PyInit_* 또는 PyInitU_* 함수를 찾는 방식으로) 계속 진행됩니다.
찾으면 Python은 인자 없이 훅을 호출합니다.
실패하면 내보내기 훅은 예외를 설정하고 NULL을 반환해야 합니다. 이 경우 임포트는 실패합니다. (오류가 발생해도 Python은 PyInit_*로 대체하지 않습니다.)
성공하면 훅은 PyModuleDef_Slot 구조체 배열에 대한 포인터를 반환해야 합니다. 그런 다음 Python은 아래에서 제안하는 함수인 PyModule_FromSlotsAndSpec 및 PyModule_Exec를 호출하여 주어진 슬롯을 기반으로 모듈을 생성합니다. 슬롯 배열에 대한 요구 사항은 해당 함수들의 설명을 참조하십시오.
반환된 배열과 그 배열이 (재귀적으로) 가리키는 모든 데이터는 런타임이 종료될 때까지 유효하고 변경되지 않은 상태로 유지되어야 합니다. (함수는 정적 상수 하나를 내보내거나, 예를 들어 Py_Version에 따라 선택되는 여러 상수 중 하나를 내보낼 것으로 예상합니다. 동적인 동작은 일반적으로 Py_mod_create 및 Py_mod_exec 함수에서 이루어져야 합니다.)
동적 생성
슬롯 배열로부터 모듈을 생성하는 새 함수가 추가됩니다:
PyObject *PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec)
Note
PEP 820에서 첫 번째 인자의 타입을 PySlot *로 변경했습니다.
slots 인자는 PyModuleDef_Slot 구조체 배열을 가리켜야 하며, 이 배열은 slot=0인 슬롯(C에서는 보통 {0}으로 작성합니다)으로 끝나야 합니다. Py_mod_abi 슬롯은 필수이며(PEP 803 참조), 다른 모든 슬롯은 선택 사항입니다. 따라서 슬롯 배열을 가리키는 slots는 NULL이어서는 안 됩니다.
spec 인자는 덕 타이핑된 ModuleSpec 유사 객체입니다. 즉, importlib.machinery.ModuleSpec에 정의된 모든 속성은 그에 대응하는 의미를 가집니다. name 속성은 필수이지만, 이 제한은 향후 해제될 수 있습니다. name은 Py_mod_name 슬롯 대신 사용됩니다(PyModule_FromDefAndSpec이 PyModuleDef.m_name을 무시하는 것과 마찬가지입니다).
PyModule_FromSlotsAndSpec과 새 내보내기 훅 모두 슬롯 배열에 Py_mod_exec 슬롯을 최대 하나까지만 허용합니다. PyModuleDef.m_slots의 배열에는 그보다 많이 있을 수 있으며, 이는 변경되지 않습니다. 이 제한은 쉽게 우회할 수 있으며, 여러 개의 exec 슬롯은 거의 사용되지 않습니다 [1].
PyModuleDef 없이 생성된 모듈의 경우, Py_mod_create 함수는 두 번째 인자(def)로 NULL을 받아 호출됩니다. (향후 입력 슬롯 배열을 전달해야 하는 사용 사례가 발견되면, 갱신된 시그니처를 가진 새 슬롯을 추가할 수 있습니다.)
PyModExport_* 훅과 달리, 슬롯 배열 slots는 PyModule_FromSlotsAndSpec 호출 이후에 변경되거나 파기될 수 있습니다. (즉, Python은 모든 입력 데이터의 사본을 가져야 합니다.) 예외적으로, Py_mod_methods로 주어진 PyMethodDef 배열은 정적으로 할당되어야 합니다(또는 그로부터 생성된 객체보다 오래 존속함이 다른 방식으로 보장되어야 합니다). 이 제한은 향후 해제될 수 있습니다.
모듈의 exec 슬롯을 실행하는 새 함수 PyModule_Exec가 추가됩니다. 이 함수는 PyModule_ExecDef와 비슷하게 동작하지만, 슬롯을 사용해 생성된 모듈을 지원하며 명시적인 def를 받지 않습니다:
int PyModule_Exec(PyObject *module)
모듈을 완전히 초기화하려면 이 함수를 반드시 호출해야 합니다. PyModule_FromSlotsAndSpec은 이 함수를 실행하지 않습니다(PyModule_FromDefAndSpec이 PyModule_ExecDef를 호출하지 않는 것과 마찬가지입니다).
def로부터 생성된 모듈의 경우, 이 함수를 호출하는 것은 PyModule_ExecDef(module, PyModule_GetDef(module))를 호출하는 것과 같습니다.
토큰
모듈 객체는 선택적으로 “토큰”을 저장합니다. 이는 타입의 Py_tp_token과 비슷한 void* 포인터입니다.
Note
이것은 PyType_GetModuleByDef 함수를 대체하기 위한 특수한 기능입니다. PyType_GetModuleByDef가 필요 없는 사용자는 대부분 토큰도 필요하지 않을 것입니다.
이 절에는 기술 명세가 담겨 있습니다. 의도된 사용 예시는 예제 절의 exampletype_repr을 참조하십시오.
새 Py_mod_token 슬롯을 사용해 지정하는 경우, 모듈 토큰은 다음 조건을 만족해야 합니다:
- 모듈보다 오래 존속하여 모듈이 존재하는 동안 다른 용도로 재사용되지 않아야 하며,
- 모듈이 위치한 확장 모듈에 “속하여” 다른 확장 모듈과 충돌하지 않아야 합니다.
(일반적으로 모듈 생성에 사용된 슬롯 배열이나 PyModuleDef여야 하며, 동적으로 생성되는 모듈의 경우에는 다른 정적 상수여야 합니다.)
PyModuleDef의 주소가 모듈의 토큰으로 사용될 때, 모듈은 해당 PyModuleDef로부터 생성된 것처럼 동작해야 합니다. 특히 모듈 상태는 레이아웃과 의미가 일치해야 합니다.
PyModule_FromSlotsAndSpec 또는 PyModExport_<NAME> 내보내기 훅을 사용해 생성된 모듈은 새 Py_mod_token 슬롯을 사용해 토큰을 설정할 수 있습니다.
PyModuleDef로부터 생성된 모듈은 토큰이 해당 정의로 설정됩니다. 이러한 모듈에서는 명시적인 Py_mod_token 슬롯이 거부됩니다. (이를 통해 구현이 토큰과 def의 저장 공간을 공유할 수 있습니다.)
새 내보내기 훅으로 생성된 모듈의 경우, 토큰은 기본적으로 슬롯 배열의 주소로 설정됩니다. (이는 PyModule_FromSlotsAndSpec으로 생성된 모듈에는 적용되지 않습니다. 해당 함수의 입력은 모듈보다 오래 존속하지 않을 수 있기 때문입니다.)
PyModuleType이 아닌 인스턴스에는 토큰이 설정되지 않습니다.
토큰을 얻기 위한 PyModule_GetToken 함수가 추가됩니다. 결과가 NULL일 수 있으므로 결과는 포인터를 통해 전달되며, 함수는 성공 시 0을, 실패 시 -1을 반환합니다:
int PyModule_GetToken(PyObject *, void **token_p)
새 PyType_GetModuleByToken 함수가 추가됩니다. 이 함수는 기존 PyType_GetModuleByDef와 비슷한 시그니처를 갖되 const void *token 인자를 받으며, def만이 아니라 토큰을 대조한다는 점과 강한 참조를 반환한다는 점을 제외하면 동작이 같습니다.
하위 호환성을 더 쉽게 유지할 수 있도록, 기존 PyType_GetModuleByDef도 def 인자로 토큰(PyModuleDef * 포인터로 캐스트한 것)을 허용하도록 변경됩니다. 즉, PyType_GetModuleByToken과 PyType_GetModuleByDef는 두 번째 인자의 형식적 시그니처와, 빌린 참조를 반환하는지 강한 참조를 반환하는지만 다릅니다. (PyModule_GetDef 함수는 사용자가 그 결과의 멤버에 접근할 수 있으므로 이와 비슷한 변경이 적용되지 않습니다.)
새 슬롯
PyModuleDef_HEAD_INIT에서 오는 필드를 제외한 PyModuleDef 구조체의 각 필드에 대해 새 슬롯 ID가 제공됩니다: Py_mod_name, Py_mod_doc, Py_mod_clear 등. 모듈 객체가 아닌 모듈 상태와 관련된 슬롯은 Py_mod_state_ 접두사를 사용합니다. 전체 목록은 새 API 요약를 참조하십시오.
모든 새 슬롯(이 슬롯들과 앞서 다룬 Py_tp_token)은 슬롯 배열에서 반복될 수 없으며, PyModuleDef.m_slots 배열에서 사용할 수도 없습니다. 이 슬롯들은 NULL 값을 가질 수 없습니다(대신 슬롯을 완전히 생략할 수 있습니다).
현재 spec으로부터 생성되는 모듈(즉, PyModule_FromDefAndSpec을 사용하는 경우)에서는 PyModuleDef.m_name 멤버가 무시되고 대신 spec의 이름이 사용된다는 점에 유의하십시오. 이 문서에서 제안하는 모든 API는 spec으로부터 모듈을 생성하며, 같은 방식으로 Py_mod_name을 무시합니다. 이 슬롯은 선택 사항이지만, 향후 API, 외부 도구, 디버깅, 인트로스펙션을 위해 확장 작성자에게 이 슬롯을 포함할 것을 강력히 권장합니다.
기타 사항
PyMODINIT_FUNC 매크로와 비슷하지만 반환 타입이 PySlot *인 PyMODEXPORT_FUNC 매크로가 추가됩니다.
Py_mod_state_size 또는 PyModuleDef.m_size로 설정된 크기를 조회하는 PyModule_GetStateSize 함수가 추가됩니다. 결과가 -1일 수 있으므로(단일 단계 초기화 모듈의 경우) 결과는 포인터를 통해 출력되며, 함수는 성공 시 0을, 실패 시 -1을 반환합니다:
int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);
기존 내보내기 훅의 소프트 사용 중단
PyInit_* 내보내기 훅은 소프트 폐지됩니다.
새 API 요약
Note
이 요약은 PEP 820의 변경 사항을 반영하여 갱신되었습니다.
Python은 새로운 모듈 내보내기 훅을 로드하며, 여기에는 두 가지 변형이 있습니다.
PyModuleDef_Slot *PyModExport_<NAME>(void);
PyModuleDef_Slot *PyModExportU_<ENCODED_NAME>(void);
다음 함수들이 추가됩니다:
PyObject *PyModule_FromSlotsAndSpec(const PySlot *, PyObject *spec)
int PyModule_Exec(PyObject *)
int PyModule_GetToken(PyObject *, void**)
PyObject *PyType_GetModuleByToken(PyTypeObject *type, const void *token)
int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);
새 매크로가 추가됩니다:
PyMODEXPORT_FUNC
그리고 새 슬롯 타입(작은 정수에 대해 #defined 이름)이 추가됩니다:
Py_mod_name(PyModuleDef.m_name과 동등)Py_mod_doc(PyModuleDef.m_doc과 동등)Py_mod_state_size(PyModuleDef.m_size와 동등)Py_mod_methods(PyModuleDef.m_methods와 동등)Py_mod_state_traverse(PyModuleDef.m_traverse와 동등)Py_mod_state_clear(PyModuleDef.m_clear와 동등)Py_mod_state_free(PyModuleDef.m_free와 동등)Py_mod_token(위 참조)
이 모든 것은 제한된 API에 추가됩니다.
하위 호환성
기존 모듈이 새 메커니즘을 사용하도록 이식되면, PyModule_GetDef는 해당 모듈에 대해 NULL을 반환하기 시작합니다. (이는 PyModule_GetDef의 현재 문서와 일치합니다.) 모듈이 어떻게 정의되었는지는 그 모듈의 구현 세부 사항이므로, 저희는 이것을 호환성을 깨는 변경으로 간주해서는 안 된다고 봅니다.
마찬가지로, PyType_GetModuleByDef 함수는 정의가 변경된 모듈과 더 이상 일치하지 않을 수 있습니다. 모듈 작성자는 def를 토큰(token)으로 명시적으로 설정하여 이를 피할 수 있습니다.
이제 PyType_GetModuleByDef는 모듈 토큰을 def 인자로 받아들입니다. PyModuleDef 주소를 토큰으로 사용하는 데에는 적절한 제한을 명시하며, PyModuleDef가 아닌 포인터는 이전에는 유효하지 않은 입력이었으므로 이는 하위 호환성 문제가 아닙니다.
이제 Py_mod_create 함수는 두 번째 인자로 NULL을 받아 호출될 수 있습니다. 이는 def에서 슬롯(slots)으로 이식하는 사람들이 실수하게 만들 수 있으므로, 이식 안내에 언급되어야 합니다.
상위 호환성
모듈이 새 내보내기 훅을 정의하면, 이 PEP를 구현하는 CPython 버전은 기존의 PyInit_* 훅을 무시합니다.
여러 파이썬 버전을 함께 지원하는 확장은 두 훅을 모두 정의할 것으로 예상됩니다. CPython의 각 빌드는 자신이 지원하는 것 중 가장 최신의 훅을 “선택”합니다.
이식 가이드
다음은 몇 가지 까다로운 예외 상황을 포함하여, 기존 모듈을 새 API로 변환하는 가이드입니다. 이 가이드는 문서의 HOWTO로 옮겨야 합니다.
Note
이 가이드는 Module export hook에서 볼 수 있습니다. (3.15에서는 abi3t를 함께 채택하지 않으면 PyModExport로 전환해도 이점이 없으므로, 이 가이드는 abi3t 마이그레이션 HOWTO의 일부입니다.)
이 절에는 원래의 오래된 가이드가 담겨 있습니다.
이 가이드는 직접 작성한 모듈을 위한 것입니다. 코드 생성기와 언어 래퍼의 경우에는 아래의 하위 호환성 심이 더 유용할 수 있습니다.
- 코드에서
PyModule_GetDef를 사용하는 곳을 찾으십시오. 이 함수는 새 메커니즘을 사용하는 모듈에 대해NULL을 반환합니다. 대신 다음과 같이 하십시오:- 모듈의
PyModuleDef내용을 얻으려면 C 구조체를 직접 사용하십시오. 또는 예를 들어PyModule_GetNameObject,__doc__속성,PyModule_GetStateSize를 사용하여 모듈에서 속성을 가져오십시오. (Python 코드가 모듈의 속성을 변경할 수 있다는 점에 유의하십시오.) - 모듈 객체가 “여러분의 것”인지 검사하려면 대신
PyModule_GetToken을 사용하십시오. 이 가이드의 뒷부분에서 토큰이 기존PyModuleDef구조체 그 자체가 되도록 설정합니다.
- 모듈의
- 선택적으로, 코드에서
PyType_GetModuleByDef를 사용하는 곳을 찾아PyType_GetModuleByToken으로 바꾸십시오. 이 가이드의 뒷부분에서 토큰이 기존PyModuleDef구조체 그 자체가 되도록 설정합니다.(
PyType_GetModuleByDef는 하위 호환성이 있으므로,PyType_GetModuleByToken을 노출하지 않는 Python 버전을 대상으로 하는 경우 이 단계를 건너뛰어도 됩니다.) Py_mod_create로 지정된 함수가 있다면 살펴보십시오. 그 함수는NULL로 호출되므로 두 번째 인자(PyModuleDef)를 사용하지 않는지 확인하십시오. 인자 대신 기존PyModuleDef구조체를 직접 사용하십시오.- 여러 개의
Py_mod_exec슬롯을 사용한다면 하나로 합치십시오: 함수 중 하나를 고르거나 새 함수를 작성하고, 그 함수에서 나머지 함수들을 호출하십시오.Py_mod_exec슬롯을 하나만 남기고 모두 제거하십시오. PyModuleDef의m_slots멤버가 가리키는 기존PyModuleDef_Slot배열의 복사본을 만드십시오. 기존 슬롯 배열이 없다면 다음과 같이 하나 만드십시오:static PyModuleDef_Slot module_slots[] = { {0} };
이 배열에 고유한 이름을 붙이십시오. 이후 예제에서는 이 배열의 이름을
module_slots로 지었다고 가정합니다.- 기존
PyModuleDef구조체의 모든 멤버에 대한 슬롯을 추가하십시오. 새 슬롯 목록은 새 API 요약를 참조하십시오. 예를 들어 이름과 독스트링을 추가하려면 다음과 같이 하십시오:static PyModuleDef_Slot module_slots[] = { {Py_mod_name, "mymodule"}, {Py_mod_doc, (char*)PyDoc_STR("my docstring")}, // ... (keep existing slots here) {0} };
PyModule_GetDef에서PyModule_GetToken으로 전환했거나PyType_GetModuleByDef또는PyType_GetModuleByToken을 사용한다면(또는 둘 다에 해당한다면), 기존PyModuleDef구조체를 가리키는Py_mod_token슬롯을 추가하십시오:static PyModuleDef_Slot module_slots[] = { // ... (keep existing slots here) {Py_mod_token, &your_module_def}, {0} };
- 새 익스포트 훅을 추가하십시오.
PyMODEXPORT_FUNC PyModExport_examplemodule(PyObject); PyMODEXPORT_FUNC PyModExport_examplemodule(void) { return module_slots; }
새 내보내기 훅은 Python 3.15 이상에서 사용됩니다. 모듈이 더 이상 하위 버전을 지원하지 않게 되면 다음과 같이 하십시오:
PyInit_함수를 삭제하십시오.- 기존
PyModuleDef구조체가 오직Py_mod_token및/또는PyType_GetModuleByToken에만 사용된다면,Py_mod_token줄을 제거하고 나머지 모든 곳에서&your_module_def를module_slots로 바꿔도 됩니다. - 사용하지 않는 데이터를 모두 삭제하십시오.
PyModuleDef구조체와 원래의 슬롯 배열은 사용되지 않을 가능성이 높습니다.
하위 호환성 심
여기에서 제안하는 API를 사용하여 “이전” 내보내기 훅(PyInit_)을 구현하는 범용 함수를 작성할 수 있습니다.
다음 구현은 프로젝트에 복사하여 붙여 넣을 수 있으며, PyInit_examplemodule (두 번)과 PyModExport_examplemodule 이름만 조정하면 됩니다.
Note
이 절은 PEP 820에 맞추어 갱신되었습니다.
아래의 예시과 함께 Python 3.15의 비자유 스레드 빌드에서 컴파일하면, 그 결과로 생성된 확장은 참조 구현의 자유 스레딩 빌드뿐만 아니라 3.11 이상의 비자유 스레딩 빌드와도 호환됩니다. (모듈은 examplemodule.so와 같이 버전 태그 없이 이름을 지정해야 하며, sys.path에 위치해야 합니다.)
이러한 모듈을 만드는 기능을 완전히 지원하려면 일부 새로운 API의 백포트와 빌드/설치 도구의 지원이 필요합니다. 이는 이 PEP의 범위를 벗어납니다. (특히 이 데모는 3.11에서 우연히 동작하는 제한된 API 3.15의 일부를 사용하는 “편법”을 쓰고 있으며, 몇 가지 꼼수도 포함하고 있습니다. 올바른 구현이라면 제한된 API 3.11을 사용하고, Py_mod_name 같은 새 API에 대해서는 더 깔끔한 백포트 shim을 사용할 것입니다.)
이 구현은 슬롯 배열에 몇 가지 추가 요구 사항을 둡니다:
Py_mod_slots와Py_slot_subslots는 지원되지 않습니다.Py_mod_name슬롯이 반드시 있어야 합니다.- 모든
Py_mod_token은 여기에서 정의한MOD_TOKEN으로 설정해야 합니다.
#include <Python.h>
// Hack: Restore old definition of Py_TYPE
#undef Py_TYPE
#define Py_TYPE(OBJ) (((PyObject*)OBJ)->ob_type)
// PyModuleDef, also reused as module token
static PyModuleDef module_def_and_token;
#define MOD_TOKEN (&module_def_and_token)
#include "examplemodule.c"
extern PySlot *PyModExport_examplemodule(void);
PyMODINIT_FUNC PyInit_examplemodule(void);
PyMODINIT_FUNC
PyInit_examplemodule(void)
{
if (module_def_and_token.m_name) {
// Take care to only set up the static PyModuleDef once.
// (PyModExport might theoretically return different data each time.)
return PyModuleDef_Init(&module_def_and_token);
}
static PyModuleDef_Slot module_slots[5] = {{0}};
module_def_and_token.m_slots = module_slots;
int current_m_slot = 0;
PySlot *slot = PyModExport_examplemodule();
for (/* slot set above */; slot->sl_id; slot++) {
switch (slot->sl_id) {
// Set PyModuleDef members from slots. These slots must come first.
# define COPYSLOT_CASE(SLOT, DEF_MEMBER, SL_MEMBER, TYPE) \
case SLOT: \
if (slot->sl_flags & PySlot_INTPTR) { \
module_def_and_token.DEF_MEMBER = (TYPE)(slot->sl_ptr); \
} else { \
module_def_and_token.DEF_MEMBER = (TYPE)(slot->SL_MEMBER);\
} \
break; \
///////////////////////////////////////////////////////////////////
COPYSLOT_CASE(Py_mod_name, m_name, sl_ptr, char*)
COPYSLOT_CASE(Py_mod_doc, m_doc, sl_ptr, char*)
COPYSLOT_CASE(Py_mod_state_size, m_size, sl_size, Py_ssize_t)
COPYSLOT_CASE(Py_mod_methods, m_methods, sl_ptr, PyMethodDef*)
COPYSLOT_CASE(Py_mod_state_traverse, m_traverse, sl_func, traverseproc)
COPYSLOT_CASE(Py_mod_state_clear, m_clear, sl_func, inquiry)
COPYSLOT_CASE(Py_mod_state_free, m_free, sl_func, freefunc)
COPYSLOT_CASE(Py_mod_slots, m_slots, sl_ptr, PyModuleDef_Slot*)
# undef COPYSLOT_CASE
case Py_mod_create:
case Py_mod_exec:
case Py_mod_multiple_interpreters:
case Py_mod_gil:
int old_slot_id = (int)slot->sl_id;
if (old_slot_id > 83) {
// Hack: slots were renumbered; use old IDs here
old_slot_id -= 83;
}
module_slots[current_m_slot].slot = old_slot_id;
module_slots[current_m_slot].value = slot->sl_ptr;
current_m_slot++;
if (current_m_slot >= 4) {
PyErr_SetString(PyExc_SystemError,
"Too many slots for array");
goto error;
}
break;
case Py_mod_token:
// With PyInit_, the PyModuleDef is used as the token.
if (slot->sl_ptr != &module_def_and_token) {
PyErr_SetString(PyExc_SystemError,
"Py_mod_token must be set to "
"&module_def_and_token");
goto error;
}
break;
case Py_mod_abi:
// ABI checking skipped here
break;
default:
if (!(slot->sl_flags & PySlot_OPTIONAL)) {
PyErr_Format(PyExc_SystemError,
"Unknown slot ID %d.", (int)slot->sl_id);
goto error;
}
break;
}
}
if (!module_def_and_token.m_name) {
// This function needs m_name as the "is initialized" marker.
PyErr_SetString(PyExc_SystemError, "Py_mod_name slot is required");
goto error;
}
return PyModuleDef_Init(&module_def_and_token);
error:
module_def_and_token.m_name = NULL;
return NULL;
}
보안 영향
알려진 바 없습니다.
교육 방법
일반 레퍼런스 문서 외에도 이식 가이드를 새로운 HOWTO로 추가해야 합니다.
예시
Note
이 예제는 PEP 820에 맞추어 갱신되었습니다.
/*
Example module with C-level module-global state, and
- a simple function that updates and queries the state
- a class wihose repr() queries the same module state (as an example of
PyType_GetModuleByToken)
Once compiled and renamed to not include a version tag (for example
examplemodule.so on Linux), this will run succesfully on both regular
and free-threaded builds.
Python usage:
import examplemodule
print(examplemodule.increment_value()) # 0
print(examplemodule.increment_value()) # 1
print(examplemodule.increment_value()) # 2
print(examplemodule.increment_value()) # 3
class Subclass(examplemodule.ExampleType):
pass
instance = Subclass()
print(instance) # <Subclass object; module value = 3>
*/
// Avoid CPython-version-specific ABI (inline functions & macros):
#define Py_LIMITED_API 0x030f0000 // 3.15
#include <Python.h>
typedef struct {
int value;
} examplemodule_state;
static PySlot examplemodule_slots[];
#ifndef MOD_TOKEN
// Module token: normally set to the slots array,
// but a backwards-compatibility shim will redefine it.
#define MOD_TOKEN (&examplemodule_slots)
#endif
// increment_value function
static PyObject *
increment_value(PyObject *module, PyObject *_ignored)
{
examplemodule_state *state = PyModule_GetState(module);
int result = ++(state->value);
return PyLong_FromLong(result);
}
static PyMethodDef examplemodule_methods[] = {
{"increment_value", increment_value, METH_NOARGS},
{NULL}
};
// ExampleType
static PyObject *
exampletype_repr(PyObject *self)
{
/* To get module state, we cannot use PyModule_GetState(Py_TYPE(self)),
* since Py_TYPE(self) might be a subclass defined in an unrelated module.
* So, we should use use PyType_GetModuleByToken.
* For pre-3.15 compatibility, we use PyType_GetModuleByDef instead:
* this needs a cast and returns a borrowed reference.
*/
PyObject *module = PyType_GetModuleByDef(
Py_TYPE(self), (PyModuleDef*)MOD_TOKEN);
if (!module) {
return NULL;
}
examplemodule_state *state = PyModule_GetState(module);
if (!state) {
return NULL;
}
return PyUnicode_FromFormat("<ExampleType object; module value = %d>",
state->value);
}
static PyType_Spec exampletype_spec = {
.name = "examplemodule.ExampleType",
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = (PyType_Slot[]) {
{Py_tp_repr, exampletype_repr},
{0},
},
};
// Module
static int
examplemodule_exec(PyObject *module) {
examplemodule_state *state = PyModule_GetState(module);
state->value = -1;
PyTypeObject *type = (PyTypeObject*)PyType_FromModuleAndSpec(
module, &exampletype_spec, NULL);
if (!type) {
return -1;
}
if (PyModule_AddType(module, type) < 0) {
Py_DECREF(type);
return -1;
}
Py_DECREF(type);
return 0;
}
PyDoc_STRVAR(examplemodule_doc, "Example extension.");
PyABIInfo_VAR(abi_info);
static PySlot examplemodule_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "examplemodule"),
PySlot_STATIC_DATA(Py_mod_doc, (char*)examplemodule_doc),
PySlot_STATIC_DATA(Py_mod_methods, examplemodule_methods),
PySlot_SIZE(Py_mod_state_size, sizeof(examplemodule_state)),
PySlot_FUNC(Py_mod_exec, examplemodule_exec),
PySlot_STATIC_DATA(Py_mod_token, MOD_TOKEN),
PySlot_END
};
// Avoid "implicit declaration of function" warning:
PyMODEXPORT_FUNC PyModExport_examplemodule(void);
PyMODEXPORT_FUNC
PyModExport_examplemodule(void)
{
return examplemodule_slots;
}
참조 구현
구현은 GitHub issue #140550에서 추적하고 있습니다.
초안 구현은 GitHub branch에서 확인할 수 있었습니다.
기각된 아이디어
함수 대신 데이터 포인터 내보내기
이 PEP는 정적 상수 데이터를 반환할 것으로 기대되는 새로운 모듈 내보내기 함수를 제안합니다. 그 데이터를 데이터 포인터로 직접 내보낼 수도 있습니다.
함수를 사용하면 새로운 종류의 내보내기 심볼을 다루지 않아도 됩니다.
또한 함수를 사용하면 확장이 제한적으로나마 자신의 환경을 검사할 수 있습니다 – 예를 들어, 반환하는 데이터를 현재 Python 버전에 맞게 조정할 수 있습니다.
PyModuleDef가 PyObject가 아니도록 변경하기
PyModuleDef가 더 이상 PyObject 헤더를 포함하지 않도록 변경하고, 현재의 PyInit_* 훅을 계속 사용할 수도 있습니다. 이 접근 방식에는 몇 가지 문제가 있습니다:
- 임포트 메커니즘은 서로 다른 메모리 레이아웃을 구별하기 위해 객체의 비트 패턴을 검사해야 합니다:
- 현재
abi3확장이 반환하는 “이전”PyObject기반PyModuleDef, - 새로운
PyModuleDef, - 단일 단계 초기화를 위한
PyObject기반 모듈 객체.
이는 취약하며
PyObject의 향후 변경에 제약을 가합니다. 단일 단계 초기화와 현재의 안정 ABI가 모두 더 이상 지원되지 않을 때까지 메모리 레이아웃이 구별 가능한 상태로 유지되어야 합니다. - 현재
PyModuleDef_Init은 “모듈 정의가 자신의 타입과 참조 횟수를 올바르게 보고하는, 적절히 초기화된 Python 객체임을 보장합니다”라고 문서화되어 있습니다. 이 동작은 예고 없이 변경되어야 하며, 그 결과PyModuleDef를 Python 객체로 취급하는 모든 사용자 코드가 동작하지 않게 됩니다.
향후 가능한 방향
이 아이디어들은 이 제안의 범위를 벗어납니다.
슬롯 전반의 개선
Note
이 아이디어는 PEP 820에서 구현되었습니다.
슬롯, 특히 기존의 PyModuleDef_Slot에는 몇 가지 단점이 있습니다. 가장 중요한 단점은 다음과 같습니다.
- 타입 안전성: 데이터 포인터, 함수 포인터, 작은 정수에 모두
void *가 사용되므로 캐스팅이 필요합니다. 이 캐스팅은 C에서 엄밀히 따지면 정의되지 않은 동작이지만, 실제로는 관련된 모든 아키텍처에서 동작합니다. (예를 들어Py_tp_doc은 문자열을,Py_mod_gil은 정수를 나타냅니다.) - 제한적인 상위 호환성: 확장 모듈이 현재 인터프리터가 알지 못하는 슬롯 ID를 제공하면 모듈 생성이 실패합니다. 이 때문에 “선택적” 기능, 즉 인터프리터가 지원하는 경우에만 적용되어야 하는 기능을 사용하기가 번거롭습니다. (최근 추가된 슬롯인
Py_mod_gil과Py_mod_multiple_interpreters가 좋은 예입니다.)한 가지 우회 방법은 내보내기 함수에서
Py_Version을 확인하고, 현재 인터프리터에 맞는 슬롯 배열을 반환하는 것입니다.
기본값 갱신
새로운 API를 도입하면 Py_mod_multiple_interpreters 슬롯과 Py_mod_gil 슬롯의 기본값을 갱신할 수 있습니다.
inittab
inittab에서 PyModuleDef가 없는 슬롯을 허용해야 합니다. 즉, PyImport_ExtendInittab의 새로운 변형을 추가해야 합니다. 이 작업을 이 PEP에 포함해야 합니까?
inittab은 임베딩에 사용되며, 임베딩에서는 공통/안정 ABI가 그다지 중요하지 않습니다. 따라서 이 작업은 이후의 변경으로 미루어도 괜찮을 수 있습니다.
각주
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.