Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Defining extension modules.

×

See PEP 1 for how to propose changes.

초록

이 PEP에서는 바깥쪽의 PyModuleDef 구조체 없이 PyModuleDef_Slot 구조체 배열을 사용하여 모듈을 정의할 수 있는 C 확장 모듈용 새로운 진입점을 제안합니다. 이를 통해 확장 모듈 작성자는 정적으로 할당된 PyObject를 사용하지 않아도 되며, 하나의 컴파일된 라이브러리 파일을 CPython의 일반 빌드와 자유 스레드 빌드 모두에서 사용할 수 있게 만드는 데 가장 일반적인 장애물을 제거합니다.

이를 실현하기 위해 PyModuleDef의 필드를 대체하는 새로운 모듈 슬롯 ID와 타입 객체에 사용되는 Py_tp_token과 유사한 토큰을 추가할 수 있도록 하는 방법도 지정합니다.

또한 슬롯에서 동적으로 모듈을 정의하는 API를 추가합니다.

기존 API(PyInit_*)는 소프트 폐기되었습니다. (즉, 경고 없이 계속 작동하며 완전히 문서화되고 지원되지만, 여기에 새로운 기능을 추가할 계획은 없습니다.)

배경 및 동기

Python 객체의 메모리 레이아웃은 일반 빌드와 자유 스레드 빌드 사이에서 다릅니다. 따라서 일반 빌드와 자유 스레드 빌드를 모두 지원하는 ABI에는 현재 PyObject 메모리 레이아웃을 포함할 수 없습니다. 기존 ABI 및 API와의 호환성을 유지하려면 정적으로 할당된 Python 객체를 지원할 수 없습니다.

대부분의 확장 모듈에 필요하며 사실상 모든 경우에 정적으로 할당되는 객체 유형이 하나 있습니다. 바로 모듈 내보내기 훅(즉, PyInit_* 함수)이 반환하는 PyModuleDef입니다.

모듈 내보내기 훅(PyInit_* 함수)은 두 종류의 객체를 반환할 수 있습니다.

  1. 완전히 초기화된 모듈 객체입니다(이른바 단일 단계 초기화). 이는 3.4 이하에서 유일한 선택지였습니다. 이러한 방식으로 생성된 모듈은 여러 인터프리터 또는 반복 로딩과 관련하여 놀라운(하지만 하위 호환성을 유지하는) 동작을 보입니다. (구체적으로는 이러한 모듈의 __dict__내용이 모듈 객체의 모든 인스턴스 사이에서 공유됩니다.)

    반환되는 모듈은 일반적으로 PyModule_Create 함수로 생성되며, 이 함수에는 정적으로 할당된(또는 적어도 수명이 긴) PyModuleDef 구조체가 필요합니다.

    더 낮은 수준의 PyModule_New* API를 사용하여 이를 우회할 수 있습니다. 이렇게 하면 PyModuleDef가 필요하지 않지만 기능은 훨씬 적습니다.

  2. 모듈 객체를 생성하는 방법에 대한 설명을 포함하는 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이 Motivation에서 언급된 두 번째 문제인 인터프리터 전환도 피할 수 있습니다. 실제로 이는 다단계 초기화에 새로운 단계를 추가하여, Python이 모듈이 호환되는지 확인할 수 있도록 합니다.

래퍼 구조체 없이 슬롯 사용하기

기존의 PyModuleDef는 일부 고정 필드와 “슬롯” 배열을 포함하는 구조체입니다. 슬롯과 달리 고정 필드는 개별적으로 사용 중단하고 대체할 수 없습니다. 이 제안은 고정 필드를 없애고 래퍼 구조체 없이 슬롯 배열을 직접 사용할 것을 제안합니다.

PyModuleDef_Slot 구조체에는 고정 필드와 비교할 때 몇 가지 단점이 있습니다. 이러한 문제는 해결할 수 있다고 생각하지만, 이 PEP의 범위에서는 제외합니다. (참고: 이는 Python 3.15에서도 여전히 PEP 820에서 수행되었습니다.)

토큰

정적 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은 인자 없이 해당 후크를 호출합니다.

실패하면 export 후크는 예외가 설정된 상태로 NULL을 반환해야 합니다. 그러면 가져오기가 실패합니다(Python은 오류 발생 시 PyInit_*로 대체하지 않습니다).

성공하면 후크는 PyModuleDef_Slot 구조체 배열을 가리키는 포인터를 반환해야 합니다. 그러면 Python은 아래에서 제안하는 함수인 PyModule_FromSlotsAndSpecPyModule_Exec을 호출하여 지정된 슬롯을 기반으로 모듈을 생성합니다. 슬롯 배열에 필요한 사항은 해당 함수의 설명을 참조하십시오.

반환된 배열과 배열이 재귀적으로 가리키는 모든 데이터는 런타임이 종료될 때까지 유효하고 변경되지 않은 상태로 유지되어야 합니다. (함수는 정적 상수를 내보내거나, 예를 들어 Py_Version에 따라 선택되는 여러 상수 중 하나를 내보낼 것으로 예상합니다. 동적 동작은 일반적으로 Py_mod_createPy_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 참조), 다른 모든 슬롯은 선택 사항입니다. 따라서 slotsNULL이어서는 안 됩니다.

spec인자는 덕 타이핑된 ModuleSpec 유사 객체이며, importlib.machinery.ModuleSpec에 정의된 모든 속성이 동일한 의미를 가집니다. name 속성은 필수이지만, 이 제한은 향후 해제될 수 있습니다. namePy_mod_name슬롯 대신 사용됩니다(PyModule_FromDefAndSpecPyModuleDef.m_name을 무시하는 것과 마찬가지입니다).

PyModule_FromSlotsAndSpec 및 새 export 후크 모두의 슬롯 배열에서는 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_FromDefAndSpecPyModule_ExecDef를 호출하지 않습니다).

def에서 생성된 모듈의 경우 이를 호출하는 것은 PyModule_ExecDef(module, PyModule_GetDef(module))을 호출하는 것과 같습니다.

토큰

모듈 객체는 선택적으로 타입의 Py_tp_token과 유사한 void* 포인터인 “토큰”을 저장합니다.

Note

이는 PyType_GetModuleByDef 함수를 대체하기 위한 특수 기능입니다. PyType_GetModuleByDef이 필요하지 않은 사용자는 토큰도 필요하지 않을 가능성이 큽니다.

이 절에는 기술 사양이 포함되어 있습니다. 의도된 사용 예는 Example sectionexampletype_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_GetModuleByDef와 유사한 시그니처를 가지되 const void *token 인자를 사용하고, def만이 아니라 토큰을 일치시키는 것을 제외하면 동일하게 동작하며 강한 참조를 반환하는 새 PyType_GetModuleByToken 함수가 추가됩니다.

하위 호환성을 더욱 쉽게 유지하기 위해 기존 PyType_GetModuleByDef를 변경하여 토큰을 (PyModuleDef * 포인터로 캐스팅하여) def 인자로 사용할 수도 있게 합니다. 즉, PyType_GetModuleByTokenPyType_GetModuleByDef는 두 번째 인자의 형식적 시그니처와 빌린 참조와 강한 참조 중 무엇을 반환하는지만 다릅니다. (사용자가 결과의 멤버에 접근할 수 있으므로 PyModule_GetDef 함수에는 이와 유사한 변경이 적용되지 않습니다.)

새 슬롯

PyModuleDef_HEAD_INIT에서 가져온 필드를 제외한 PyModuleDef 구조체의 각 필드에 대해 Py_mod_name, Py_mod_doc, Py_mod_clear 등의 새 슬롯 ID가 제공됩니다. 모듈 객체가 아니라 모듈 상태와 관련된 슬롯에는 Py_mod_state_ 접두사가 사용됩니다. 전체 목록은 새 API 요약를 참조하십시오.

새 슬롯은 모두(위에서 논의한 Py_tp_token 포함) 슬롯 배열에 반복해서 사용할 수 없으며, PyModuleDef.m_slots 배열에서 사용할 수도 없습니다. 이러한 슬롯에는 NULL 값을 지정할 수 없습니다. (대신 슬롯 자체를 완전히 생략할 수 있습니다.)

현재 spec (즉, PyModule_FromDefAndSpec를 사용하여)에서 생성된 모듈에서는 PyModuleDef.m_name 멤버가 무시되고 spec의 이름이 대신 사용된다는 점에 유의하십시오. 이 문서에서 제안하는 모든 API는 spec에서 모듈을 생성하며, 동일한 방식으로 Py_mod_name을 무시합니다. 이 슬롯은 선택 사항이지만, 확장 모듈 작성자는 향후 API, 외부 도구, 디버깅 및 인트로스펙션에 도움이 되도록 이를 포함할 것을 강력히 권장합니다.

기타 사항

반환 형식으로 PySlot *를 사용하지만 PyMODINIT_FUNC 매크로와 유사한 PyMODEXPORT_FUNC 매크로가 추가됩니다.

Py_mod_state_size 또는 PyModuleDef.m_size로 설정된 크기를 가져오기 위한 PyModule_GetStateSize 함수가 추가됩니다. 결과가 -1일 수 있으므로(단계별 초기화 모듈의 경우) 포인터를 통해 출력하며, 함수는 성공 시 0을, 실패 시 -1을 반환합니다:

int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);

기존 내보내기 훅의 소프트 사용 중단

PyInit_* 내보내기 훅은 soft-deprecated됩니다.

새 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 (위 참조)

이 모든 것이 Limited API에 추가됩니다.

하위 호환성

기존 모듈을 새로운 메커니즘을 사용하도록 이식하면 PyModule_GetDef은 해당 모듈에 대해 NULL을 반환하기 시작합니다. (이는 PyModule_GetDef의 현재 문서와 일치합니다.) 모듈이 어떻게 정의되었는지는 해당 모듈의 구현 세부 사항이라고 주장하므로, 이를 호환성을 깨뜨리는 변경으로 간주해서는 안 됩니다.

마찬가지로 PyType_GetModuleByDef 함수는 정의가 변경된 모듈과의 일치를 중단할 수 있습니다. 모듈 작성자는 deftoken으로 명시적으로 설정하여 이를 방지할 수 있습니다.

이제 PyType_GetModuleByDef은 모듈 토큰을 def 인자로 허용합니다. PyModuleDef 주소를 토큰으로 사용하는 것에 대한 적절한 제한을 지정하며, PyModuleDef가 아닌 포인터는 이전에도 유효하지 않은 입력이었으므로 이는 하위 호환성 문제가 아닙니다.

이제 Py_mod_create 함수는 두 번째 인자로 NULL을 사용하여 호출할 수 있습니다. 이로 인해 def에서 slots로 이식하는 사람들이 문제를 겪을 수 있으므로, 이 내용은 이식 참고 사항에 언급해야 합니다.

상위 호환성

모듈이 새로운 내보내기 훅을 정의하면 이 PEP를 구현하는 CPython 버전은 기존의 PyInit_* 훅을 무시합니다.

여러 Python 버전에 걸쳐 사용되는 확장 기능은 두 훅을 모두 정의해야 하며, CPython의 각 빌드는 지원하는 것 중 가장 최신 버전을 “선택”합니다.

이식 안내서

다음은 까다로운 일부 예외 사례를 포함하여 기존 모듈을 새로운 API로 변환하는 안내서입니다. 이 내용은 문서의 HOWTO로 옮겨야 합니다.

Note

안내서는 Module export hook에서 확인할 수 있습니다. (PyModExport로 전환해도 abi3t를 함께 채택하지 않는다면 3.15에서는 이점이 없으므로, 이는 abi3t 마이그레이션 HOWTO의 일부입니다.)

이 절에는 원래의 오래된 안내서가 포함되어 있습니다.

이 안내서는 손으로 작성한 모듈을 대상으로 합니다. 코드 생성기와 언어 래퍼에는 아래의 하위 호환성 심이 더 유용할 수 있습니다.

  1. 코드에서 PyModule_GetDef사용을 검색하십시오. 이 함수는 새로운 메커니즘을 사용하는 모듈에 대해 NULL을 반환합니다. 대신:
    • 모듈의 PyModuleDef내용을 가져오려면 C 구조체를 직접 사용하십시오. 또는 PyModule_GetNameObject, __doc__속성 및 PyModule_GetStateSize등을 사용하여 모듈에서 속성을 가져오십시오. (Python 코드가 모듈의 속성을 변경할 수 있다는 점에 유의하십시오.)
    • 모듈 객체가 자신의 것인지 테스트하려면 대신 PyModule_GetToken을 사용하십시오. 이 안내서의 뒷부분에서 토큰이 기존 PyModuleDef구조체가 되도록 설정합니다.
  2. 선택적으로 코드에서 PyType_GetModuleByDef사용을 검색하고, 이를 PyType_GetModuleByToken으로 대체하십시오. 이 안내서의 뒷부분에서 토큰이 기존 PyModuleDef구조체가 되도록 설정합니다.

    PyType_GetModuleByToken을 제공하지 않는 Python 버전을 대상으로 하는 경우에는 이 단계를 건너뛰어도 됩니다. PyType_GetModuleByDef는 하위 호환성을 가지기 때문입니다.

  3. Py_mod_create로 식별되는 함수가 있는 경우 이를 살펴보십시오. 해당 함수가 두 번째 인자인 (PyModuleDef)을 사용하지 않는지 확인하십시오. 해당 함수는 NULL로 호출되기 때문입니다. 인자 대신 기존 PyModuleDef구조체를 직접 사용하십시오.
  4. 여러 Py_mod_exec슬롯을 사용하는 경우 이를 통합하십시오. 함수 중 하나를 선택하거나 새 함수를 작성하고, 그 함수에서 나머지 함수를 호출하십시오. Py_mod_exec슬롯을 하나만 남기고 모두 제거하십시오.
  5. PyModuleDefm_slots멤버가 가리키는 기존 PyModuleDef_Slot배열을 복사하십시오. 기존 슬롯 배열이 없다면 다음과 같이 하나를 만드십시오:
    static PyModuleDef_Slot module_slots[] = {
        {0}
    };
    

    이 배열에 고유한 이름을 지정하십시오. 이후 예제에서는 이 배열의 이름을 module_slots로 지정했다고 가정합니다.

  6. 기존 PyModuleDef구조체의 모든 멤버에 대한 슬롯을 추가하십시오. 새 슬롯 목록은 새 API 요약를 참조하십시오. 예를 들어 이름과 독스트링을 추가하려면:
    static PyModuleDef_Slot module_slots[] = {
        {Py_mod_name, "mymodule"},
        {Py_mod_doc, (char*)PyDoc_STR("my docstring")},
        // ... (keep existing slots here)
        {0}
    };
    
  7. 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}
    };
    
  8. 새 내보내기 훅을 추가하십시오.
    PyMODEXPORT_FUNC PyModExport_examplemodule(PyObject);
    
    PyMODEXPORT_FUNC
    PyModExport_examplemodule(void)
    {
        return module_slots;
    }
    

새 내보내기 훅은 Python 3.15 이상에서 사용됩니다. 모듈이 더 낮은 버전을 더 이상 지원하지 않게 되면:

  1. PyInit_ 함수를 삭제하십시오.
  2. 기존 PyModuleDef구조체가 Py_mod_token및/또는 PyType_GetModuleByToken대해서만 사용되는 경우에는 Py_mod_token행을 제거하고 다른 모든 곳에서 &your_module_defmodule_slots로 바꾸어도 됩니다.
  3. 사용되지 않는 데이터를 삭제하십시오. PyModuleDef구조체와 원래의 슬롯 배열은 사용되지 않을 가능성이 높습니다.

하위 호환성 심

여기서 제안하는 API를 사용하여 “기존” 내보내기 훅(PyInit_)을 구현하는 일반 함수를 작성할 수 있습니다.

다음 구현은 프로젝트에 복사하여 붙여 넣을 수 있으며, 이름 PyInit_examplemodule (두 번)과 PyModExport_examplemodule만 수정하면 됩니다.

Note

이 섹션은 PEP 820에 맞게 업데이트되었습니다.

아래의 예제과 함께 Python 3.15의 비 자유 스레드 빌드에서 컴파일하면, 그 결과로 생성되는 확장 기능은 참조 구현의 자유 스레딩 빌드뿐만 아니라 비 자유 스레딩 3.11 이상 빌드와도 호환됩니다. (모듈 이름은 버전 태그 없이 지정해야 합니다(예: examplemodule.so). 또한 sys.path에 배치해야 합니다.)

이러한 모듈을 생성하는 기능을 완전히 지원하려면 일부 새 API의 백포트와 빌드/설치 도구의 지원이 필요합니다. 이는 이 PEP의 범위를 벗어납니다. (특히 이 데모는 3.11에서 happens to work하는 Limited API 3.15의 일부를 사용하고 몇 가지 해킹을 포함하여 “편법을 사용합니다”. 적절한 구현에서는 새 API인 Py_mod_name과 같은 항목에 더 깔끔한 백포트 심을 제공하면서 Limited API 3.11을 사용합니다.)

이 구현은 슬롯 배열에 몇 가지 추가 요구 사항을 적용합니다.

  • Py_mod_slotsPy_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에서 제공되었습니다.

거부된 아이디어

함수 대신 데이터 포인터 내보내기

이는 정적 상수 데이터를 반환할 것으로 예상되는 새 모듈 내보내기 function을 제안합니다. 해당 데이터는 데이터 포인터로 직접 내보낼 수 있습니다.

함수를 사용하면 새로운 종류의 내보낸 심볼을 처리하지 않아도 됩니다.

함수는 확장 기능이 제한된 방식으로 자신의 환경을 검사할 수 있도록 합니다 – 예를 들어 현재 Python 버전에 맞게 반환 데이터를 조정할 수 있습니다.

PyModuleDefPyObject가 아니도록 변경하기

PyModuleDef가 더 이상 PyObject 헤더를 포함하지 않도록 변경하고 현재 PyInit_* 후크를 계속 사용하는 것이 가능합니다. 이 접근 방식에는 몇 가지 문제가 있습니다.

  • 가져오기 메커니즘은 서로 다른 메모리 레이아웃을 구별하기 위해 객체의 비트 패턴을 검사해야 합니다.
    • 현재 abi3 확장 기능이 반환하는 “이전” PyObject 기반 PyModuleDef,
    • 새로운 PyModuleDef,
    • 단일 단계 초기화를 위한 PyObject 기반 모듈 객체.

    이는 취약하며, 향후 PyObject를 변경하는 데 제약을 둡니다. 메모리 레이아웃은 단일 단계 초기화와 현재 Stable ABI가 모두 더 이상 지원되지 않을 때까지 구별 가능한 상태로 유지되어야 합니다.

  • PyModuleDef_Init은 “모듈 정의가 적절히 초기화된 Python 객체이며 해당 형식과 참조 횟수를 올바르게 보고하도록 보장합니다.”라고 문서화되어 있습니다. 이는 경고 없이 변경되어야 하므로, PyModuleDefs를 Python 객체로 취급하는 사용자 코드를 모두 손상시킬 수 있습니다.

가능한 향후 방향

이러한 아이디어는 제안의 범위에 포함되지 않습니다.

일반적인 슬롯 개선

Note

이 아이디어는 PEP 820에서 구현되었습니다.

슬롯, 특히 기존의 PyModuleDef_Slot에는 몇 가지 단점이 있습니다. 가장 중요한 사항은 다음과 같습니다.

  • 형식 안전성: 데이터 포인터, 함수 포인터 및 작은 정수에 void *를 사용하므로 캐스팅이 필요합니다. 이는 C에서 기술적으로 정의되지 않은 동작이지만, 관련된 모든 아키텍처에서 실제로는 작동합니다. (예를 들어 Py_tp_doc은 문자열을 나타내고, Py_mod_gil은 정수를 나타냅니다.)
  • 제한적인 순방향 호환성: 확장 모듈이 현재 인터프리터에서 알 수 없는 슬롯 ID를 제공하면 모듈 생성이 실패합니다. 이로 인해 “선택적” 기능, 즉 인터프리터가 지원하는 경우에만 적용되어야 하는 기능을 사용하기가 번거로워집니다. (최근 추가된 Py_mod_gilPy_mod_multiple_interpreters슬롯이 좋은 예입니다.)

    한 가지 해결 방법은 내보내기 함수에서 Py_Version을 확인하고, 현재 인터프리터에 적합한 슬롯 배열을 반환하는 것입니다.

기본값 업데이트

새 API를 사용하면 Py_mod_multiple_interpretersPy_mod_gil슬롯의 기본값을 업데이트할 수 있습니다.

inittab

inittab에서 PyModuleDef가 없는 슬롯을 허용해야 합니다. 즉, PyImport_ExtendInittab의 새로운 변형을 추가해야 합니다. 이것을 이 PEP의 일부로 포함해야 합니까?

inittab은 임베딩에 사용되며, 이 경우 공통/안정 ABI는 그다지 중요하지 않습니다. 따라서 이는 이후 변경 사항으로 남겨 두어도 괜찮을 수 있습니다.

각주