표준 라이브러리 확장 모듈

이 절에서는 C extension module을 포함하는 CPython 프로젝트를 구성하고 컴파일하는 방법을 설명합니다. C 확장 모듈을 작성하는 방법은 설명하지 않으며, 대신 좋은 문서를 읽을 수 있는 몇 가지 링크를 소개합니다:

datetime 또는 pickle 같은 표준 라이브러리의 일부 모듈은 C와 Python으로 된 동일한 구현을 가지고 있습니다. C 구현을 사용할 수 있으면 성능이 향상될 것으로 기대됩니다(이러한 확장 모듈을 흔히 가속기 모듈이라고 부릅니다).

주로 Python으로 구현된 다른 모듈은 구현 세부 사항을 제공하는 C 도우미 확장을 임포트할 수 있습니다(예를 들어 csv 모듈은 Modules/_csv.c에 정의된 내부 _csv 모듈을 사용합니다).

확장 모듈 분류하기

확장 모듈은 두 가지 범주로 분류할 수 있습니다:

  • 내장 확장 모듈은 Python 인터프리터와 함께 빌드되고 배포되는 모듈입니다. 내장 모듈은 인터프리터에 정적으로 링크되므로 __file__ 어트리뷰트가 없습니다.

    더 보기

    sys.builtin_module_names — 내장 모듈의 이름입니다.

    내장 모듈은 Py_BUILD_CORE_BUILTIN 매크로가 정의된 상태로 빌드됩니다.

  • 공유 (또는 동적) 확장 모듈은 공유 라이브러리(.so 또는 .dll 파일)로 빌드되며 인터프리터에 동적으로 링크됩니다.

    특히 모듈의 __file__ 어트리뷰트에는 .so 또는 .dll 파일의 경로가 들어 있습니다.

    공유 모듈은 Py_BUILD_CORE_MODULE 매크로가 정의된 상태로 빌드됩니다. 대신 Py_BUILD_CORE_BUILTIN 매크로를 사용하면 모듈을 임포트할 때 ImportError가 발생합니다.

참고

비공식적으로 말하면, 내장 확장 모듈은 필수로 간주할 수 있고, 공유 확장 모듈은 외부에서 제공되거나 재정의되거나 비활성화될 수 있다는 의미에서 선택 사항으로 간주할 수 있습니다.

일반적으로 가속기 모듈은 공유 확장 모듈로 빌드되며, 이미 순수 Python 구현이 있는 경우에는 특히 그렇습니다.

PEP 399에 따르면 운영 위원회의 특별 허가가 없는 한 새로운 확장 모듈은 동작하며 테스트를 거친 순수 Python 구현을 반드시 제공해야 합니다.

CPython에 확장 모듈 추가하기

표준 라이브러리에 다음과 같은 foo.greet() 함수가 있는 순수 Python 모듈 foo가 있다고 가정합시다:

Lib/foo.py
def greet():
    return "Hello World!"

foo.greet()의 Python 구현을 사용하는 대신, _foo 모듈에 공개된 대응 C 확장 구현을 사용하려고 합니다. 이상적으로는 Lib/foo.py를 다음과 같이 수정하려고 합니다:

Lib/foo.py
try:
    # use the C implementation if possible
    from _foo import greet
except ImportError:
    # fallback to the pure Python implementation
    def greet():
        return "Hello World!"

참고

가속기 모듈은 절대로 직접 임포트해서는 안 됩니다. 관례적으로 가속기 모듈에는 밑줄 접두사를 붙여 비공개 구현 세부 사항임을 표시합니다(이 예에서는 _foo).

가속기 모듈을 통합하려면 다음을 결정해야 합니다:

  • 확장 모듈 소스 코드를 CPython 프로젝트 트리의 어디에 반영할지,

  • CPython 프로젝트를 구성하고 컴파일하기 위해 어떤 파일을 수정할지, 그리고

  • 마지막에 어떤 Makefile 규칙을 호출할지입니다.

CPython 프로젝트 트리 갱신하기

일반적으로 가속기 모듈은 CPython 프로젝트의 Modules 디렉터리에 추가합니다. 확장 모듈에 파일이 두 개 이상 필요하다면 Modules 안에 하위 디렉터리를 만드는 편이 더 편리합니다.

확장 모듈이 파일 하나로 구성된 가장 간단한 경우에는 Modules에 Modules/_foomodule.c로 둘 수 있습니다. 확장 모듈 _foo의 간단하지 않은 예로, 다음 작업 트리를 생각해 봅시다:

관례에 따라 확장 모듈 구현을 담은 소스 파일의 이름은 <NAME>module.c이며, 여기서 <NAME>은 나중에 임포트할 모듈의 이름입니다(이 경우 _foo). 또한 구현을 담은 디렉터리의 이름도 이와 비슷하게 지어야 합니다.

Modules/_foo/helper.h
#ifndef _FOO_HELPER_H
#define _FOO_HELPER_H

#include "Python.h"

typedef struct {
    /* ... */
} foomodule_state;

static inline foomodule_state *
get_foomodule_state(PyObject *module)
{
    void *state = PyModule_GetState(module);
    assert(state != NULL);
    return (foomodule_state *)state;
}

/* Helper used in Modules/_foo/_foomodule.c
 * but implemented in Modules/_foo/helper.c.
 */
extern PyObject *
_Py_greet_fast(void);

#endif // _FOO_HELPER_H

팁

여러 C 소스 파일 간에 공유할 필요가 없는 함수나 데이터는 libpython에서 해당 심볼이 내보내지지 않도록 static으로 선언해야 합니다.

심볼을 내보내야 한다면 그 이름은 Py 또는 _Py로 시작해야 합니다. 이는 make smelly로 확인할 수 있습니다. 자세한 내용은 Python의 C API 변경하기 절을 참고하십시오.

Modules/_foo/helper.c
#include "_foomodule.h"

PyObject *_Py_greet_fast(void) {
    return PyUnicode_FromString("Hello World!");
}
Modules/_foo/_foomodule.c
#include "helper.h"
#include "clinic/_foomodule.c.h"

/* Functions for the extension module's state */
static int
foomodule_exec(PyObject *module)
{
    // imports, static attributes, exported classes, etc
    return 0;
}

static int
foomodule_traverse(PyObject *m, visitproc visit, void *arg)
{
    foomodule_state *st = get_foomodule_state(m);
    // call Py_VISIT() on the state attributes
    return 0;
}

static int
foomodule_clear(PyObject *m)
{
    foomodule_state *st = get_foomodule_state(m);
    // call Py_CLEAR() on the state attributes
    return 0;
}

static void
foomodule_free(void *m) {
    (void)foomodule_clear((PyObject *)m);
}

/* Implementation of publicly exported functions. */

/*[clinic input]
module foo
[clinic start generated code]*/
/*[clinic end generated code: output=... input=...]*/

/*[clinic input]
foo.greet -> object

[clinic start generated code]*/

static PyObject *
foo_greet_impl(PyObject *module)
/*[clinic end generated code: output=... input=...]*/
{
    return _Py_greet_fast();
}

/* Exported module's data */

static PyMethodDef foomodule_methods[] = {
    // macro in 'clinic/_foomodule.c.h' after running 'make clinic'
    FOO_GREET_METHODDEF
    {NULL, NULL}
};

static struct PyModuleDef_Slot foomodule_slots[] = {
    // 'foomodule_exec' may be NULL if the state is trivial
    {Py_mod_exec, foomodule_exec},
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
    {0, NULL},
};

static struct PyModuleDef foomodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "_foo",
    .m_doc = "some doc",               // or NULL if not needed
    .m_size = sizeof(foomodule_state),
    .m_methods = foomodule_methods,
    .m_slots = foomodule_slots,
    .m_traverse = foomodule_traverse,  // or NULL if the state is trivial
    .m_clear = foomodule_clear,        // or NULL if the state is trivial
    .m_free = foomodule_free,          // or NULL if the state is trivial
};

PyMODINIT_FUNC
PyInit__foo(void)
{
    return PyModuleDef_Init(&foomodule);
}

팁

PyInit_<NAME> 함수의 이름은 import 문에서 사용하는 모듈 이름 <NAME>(여기서는 _foo)으로 끝나야 하며, 이 이름은 보통 PyModuleDef.m_name 구성원과 일치한다는 점을 기억하십시오.

Argument Clinic 입력에 사용하는 식별자 등 다른 식별자에는 이러한 이름 규칙이 없습니다.

CPython 프로젝트 구성하기

이제 확장 모듈을 CPython 소스 트리에 추가했으므로, 여러 플랫폼에서 CPython 프로젝트를 컴파일할 수 있도록 몇몇 구성 파일을 갱신해야 합니다.

Modules/Setup.{bootstrap,stdlib}.in 갱신하기

확장 모듈이 정상적으로 동작하는 인터프리터를 얻는 데 필요한지 여부에 따라 Modules/Setup.bootstrap.in 또는 Modules/Setup.stdlib.in을 갱신합니다. 전자의 경우 확장 모듈은 반드시 내장 확장 모듈로 빌드됩니다.

팁

가속기 모듈에는 Modules/Setup.bootstrap.in보다 Modules/Setup.stdlib.in을 우선해서 사용해야 합니다.

내장 확장 모듈의 경우 *static* 마커 뒤에 다음 줄을 추가하여 Modules/Setup.bootstrap.in을 갱신합니다:

*static*
...
_foo _foo/_foomodule.c _foo/helper.c
...

구문은 <NAME> <SOURCES>입니다. 여기서 <NAME>은 import 키워드 문에서 사용하는 모듈 이름이고, <SOURCES>는 공백으로 구분된 소스 파일 목록입니다.

그 밖의 확장 모듈의 경우 *@MODULE_BUILDTYPE@* 마커 뒤, *shared* 마커 앞에 다음 줄을 추가하여 Modules/Setup.stdlib.in을 갱신합니다:

*@MODULE_BUILDTYPE@*
...
@MODULE__FOO_TRUE@_foo _foo/_foomodule.c _foo/helper.c
...
*shared*

@MODULE_<NAME_UPPER>_TRUE@<NAME> 마커에서 <NAME_UPPER>는 <NAME>의 대문자 형태여야 하며, <NAME>은 앞에서와 같은 의미입니다(이 경우 <NAME_UPPER>와 <NAME>은 각각 _FOO와 _foo입니다). 마커 뒤에는 소스 파일 목록이 옵니다.

확장 모듈을 공유 모듈로 빌드해야 한다면 @MODULE__FOO_TRUE@_foo 줄을 *shared* 마커 뒤에 두십시오:

...
*shared*
...
@MODULE__FOO_TRUE@_foo _foo/_foomodule.c _foo/helper.c

configure.ac 갱신하기

  • SRCDIRS 변수를 찾아 다음 줄을 추가하십시오:

    AC_SUBST([SRCDIRS])
    SRCDIRS="\
    ...
    Modules/_foo \
    ..."
    

    참고

    이 단계는 CPython 프로젝트에 새 소스 디렉터리를 추가할 때만 필요합니다.

  • PY_STDLIB_MOD과 PY_STDLIB_MOD_SIMPLE을 사용하는 부분이 있는 절을 찾아 다음 줄을 추가하십시오:

    dnl always enabled extension modules
    ...
    PY_STDLIB_MOD_SIMPLE([_foo], [-I\$(srcdir)/Modules/_foo], [])
    ...
    

    PY_STDLIB_MOD_SIMPLE 매크로는 다음을 인자로 받습니다:

    • import 키워드 문에서 사용하는 모듈 이름 <NAME>,

    • 컴파일러 플래그(CFLAGS),

    • 링커 플래그(LDFLAGS).

    호스트 구성에 따라 확장 모듈이 활성화되지 않거나 지원되지 않을 수 있다면, 대신 PY_STDLIB_MOD 매크로를 사용하십시오. 이 매크로는 다음을 인자로 받습니다:

    • import 키워드 문에서 사용하는 모듈 이름 <NAME>,

    • 확장이 활성화되었는지 여부를 나타내는 불리언,

    • 확장이 지원되는지 여부를 나타내는 불리언,

    • 컴파일러 플래그(CFLAGS),

    • 링커 플래그(LDFLAGS).

    예를 들어 Linux 플랫폼에서 _foo 확장을 활성화하되 32비트 아키텍처만 지원하도록 하려면 다음과 같이 합니다:

    PY_STDLIB_MOD([_foo],
                  [test "$ac_sys_system" = "Linux"],
                  [test "$ARCH_RUN_32BIT" = "true"],
                  [-I\$(srcdir)/Modules/_foo], [])
    

    더 일반적으로, 호스트에서 확장의 구성 상태는 다음과 같이 결정됩니다:

    활성화

    지원

    상태

    true

    true

    yes

    true

    false

    missing

    false

    true or false

    disabled

    확장이 PY_STDLIB_MOD_SET_NA 매크로로 사용 불가로 표시되면 확장 상태는 n/a입니다. 확장을 사용 불가로 표시하려면 configure.ac에서 PY_STDLIB_MOD_SET_NA가 사용된 곳을 찾아 다음 줄을 추가하십시오:

    dnl Modules that are not available on some platforms
    AS_CASE([$ac_sys_system],
        ...
        [PLATFORM_NAME], [PY_STDLIB_MOD_SET_NA([_foo])],
        ...
    )
    

팁

외부 빌드 의존성이 필요한 확장 모듈에 새 의존성을 추가하는 방법은 configure.ac에 있는 기존 모듈의 주석과 구성을 읽고 참고하십시오.

Makefile.pre.in 갱신하기

필요하다면 모듈 의존성 섹션에 다음 줄을 추가하십시오:

##########################################################################
# Module dependencies and platform-specific files
...
MODULE__FOO_DEPS=$(srcdir)/Modules/_foo/helper.h
...

MODULE_<NAME_UPPER>_DEPS 변수는 @MODULE_<NAME_UPPER>_TRUE@<NAME> 마커와 같은 명명 요구 사항을 따릅니다.

MSVC 프로젝트 파일 갱신하기

여기서는 Windows에서 MSVC로 컴파일하기 위한 최소한의 단계를 설명합니다.

  • PC/config.c를 갱신하십시오:

    ...
    // add the entry point prototype
    extern PyObject* PyInit__foo(void);
    ...
    // update the entry points table
    struct _inittab _PyImport_Inittab[] = {
       ...
       {"_foo", PyInit__foo},
       ...
       {0, 0}
    };
    ...
    

    _PyImport_Inittab의 각 항목은 임포트할 모듈 이름(여기서는 _foo)과 올바른 접미사가 붙은 대응 PyInit_* 함수로 구성됩니다.

  • PCbuild/pythoncore.vcxproj를 갱신하십시오:

    <!-- group with header files ..\Modules\<MODULE>.h -->
    <ItemGroup>
      ...
      <ClInclude Include="..\Modules\_foo\helper.h" />
      ...
    </ItemGroup>
    
    <!-- group with source files ..\Modules\<MODULE>.c -->
    <ItemGroup>
      ...
      <ClCompile Include="..\Modules\_foo\_foomodule.c" />
      <ClCompile Include="..\Modules\_foo\helper.c" />
      ...
    </ItemGroup>
    
  • PCbuild/pythoncore.vcxproj.filters를 갱신하십시오:

    <!-- group with header files ..\Modules\<MODULE>.h -->
    <ItemGroup>
      ...
      <ClInclude Include="..\Modules\_foo\helper.h">
        <Filter>Modules\_foo</Filter>
      </ClInclude>
      ...
    </ItemGroup>
    
    <!-- group with source files ..\Modules\<MODULE>.c -->
    <ItemGroup>
      ...
      <ClCompile Include="..\Modules\_foo\_foomodule.c">
        <Filter>Modules\_foo</Filter>
      </ClCompile>
      <ClCompile Include="..\Modules\_foo\helper.c">
        <Filter>Modules\_foo</Filter>
      </ClCompile>
      ...
    <ItemGroup>
    

팁

헤더 파일은 <ClInclude> 태그를 사용하고, 소스 파일은 <ClCompile> 태그를 사용합니다.

CPython 프로젝트 컴파일하기

이제 구성이 갖추어졌으므로 프로젝트를 컴파일하는 일만 남았습니다:

make regen-configure
./configure
make regen-all
make regen-stdlib-module-names
make

팁

make -jN을 사용하면 가능한 한 많은 CPU 코어를 활용해 컴파일 속도를 높일 수 있습니다. 여기서 N은 할애하려는 (그리고 메모리가 감당할 수 있는) CPU 코어 수입니다. 인자 없이 make -j를 사용할 때는 주의하십시오. 이렇게 하면 작업 수에 제한이 없어지며, 컴파일 과정에서 때때로 많은 메모리를 사용할 수 있습니다(LTO로 빌드할 때처럼).

  • make regen-configure는 configure 스크립트를 갱신합니다.

    configure 스크립트는 특정 버전의 autoconf를 사용하여 생성해야 합니다. 이를 위해 regen-configure 규칙의 기반이 되는 Tools/build/regen-configure.sh 스크립트에는 Docker 또는 Podman이 필요하며, 기본적으로 Podman을 사용한다고 가정합니다.

    팁

    Docker 대신 Podman을 설치할 것을 권장합니다. Podman은 백그라운드 서비스가 필요하지 않으며, 경우에 따라 root 사용자 소유의 파일이 생성되는 것을 방지합니다.

  • make regen-all은 헤더 파일을 다시 생성하고 Argument Clinic 같은 다른 스크립트를 호출하는 역할을 합니다. 어떤 파일을 갱신해야 할지 모르겠다면 이 규칙을 실행하십시오.

  • make regen-stdlib-module-names는 표준 모듈 이름을 갱신하여 _foo를 찾을 수 있게 하고 import _foo로 임포트할 수 있게 합니다.

  • 앞선 make 호출에서 프로젝트 전체가 다시 빌드될 수 있으므로 마지막 make 단계는 일반적으로 필요하지 않지만, 일부 특정한 경우에는 필요할 수 있습니다.

문제 해결

이 절에서는 확장 모듈을 추가하는 이 예제를 따라 할 때 마주칠 수 있는 흔한 문제를 다룹니다.

regen-configure 타깃을 만들 규칙이 없음

이 문제는 보통 make distclean을 실행한 후에 발생합니다(이 명령은 Makefile을 제거합니다). 해결 방법은 다음과 같이 configure 스크립트를 다시 생성하는 것입니다:

./configure            # for creating the 'Makefile' file
make regen-configure   # for updating the 'configure' script
./configure            # for updating the 'Makefile' file

configure 스크립트가 없다면 Tools/build/regen-configure.sh를 실행하여 다시 생성할 수 있습니다:

./Tools/build/regen-configure.sh     # create an up-to-date 'configure'
./configure                          # create an up-to-date 'Makefile'

make regen-configure와 Docker의 권한 부족

Docker가 권한 부족을 알린다면 다음 Stack Overflow 게시물이 문제 해결에 도움이 될 수 있습니다: How to fix docker: permission denied. 또는 Podman을 사용해 볼 수도 있습니다.

내부 헤더를 사용할 때 Py_BUILD_CORE 정의 누락

기본적으로 CPython 안정 ABI는 #include "Python.h"를 통해 노출됩니다. 경우에 따라 이것만으로는 충분하지 않아 Include/internal의 내부 헤더가 필요할 수 있습니다. 특히 이 헤더들을 사용하려면 Py_BUILD_CORE 매크로가 정의되어 있어야 합니다.

이를 위해 확장 모듈이 내장 모듈인지 공유 모듈인지에 따라 Py_BUILD_CORE_BUILTIN 또는 Py_BUILD_CORE_MODULE 매크로를 정의해야 합니다. 두 매크로 중 어느 것을 사용하든 Py_BUILD_CORE가 함께 적용되며 CPython 내부에 접근할 수 있습니다:

Py_BUILD_CORE_BUILTIN 매크로의 정의
#ifndef Py_BUILD_CORE_MODULE
#  define Py_BUILD_CORE_BUILTIN 1
#endif
Py_BUILD_CORE_MODULE 매크로의 정의
#ifndef Py_BUILD_CORE_BUILTIN
#  define Py_BUILD_CORE_MODULE 1
#endif

팁

이 절에서는 표준 라이브러리에 포함될 확장 모듈의 품질을 높이기 위한 몇 가지 팁을 제공합니다.

제한된 API로 한정하기

CPython이 아닌 구현도 새 확장 모듈의 혜택을 누릴 수 있도록 제한된 API를 사용할 것을 권장합니다. 안정 ABI 전체를 노출하는 대신 #include "Python.h" 지시문 앞에 Py_LIMITED_API 매크로를 정의하십시오:

3.13 제한된 API를 사용합니다.
#include "pyconfig.h"    // Py_GIL_DISABLED
#ifndef Py_GIL_DISABLED
#  define Py_LIMITED_API 0x030d0000
#endif

#include "Python.h"

이렇게 하면 CPython 내부 구현에 대한 의존성이 제거되어, 확장 모듈을 CPython이 아닌 구현에서도 쉽게 사용할 수 있습니다.