Python

첫 번째 C API 확장 모듈

이 튜토리얼에서는 C 또는 C++로 작성된 간단한 파이썬 확장 모듈을 생성해 보겠습니다.

저희는 낮은 수준의 Python C API를 직접 사용할 것입니다. 확장 모듈을 더 쉽게 만드는 방법은 :ref:`권장되는 제삼자 도구 <c-api-tools>`를 참조하세요.

이 튜토리얼은 파이썬에 대한 기본 지식을 가정합니다. C로 작성하기 전에 파이썬 코드에서 함수를 정의할 수 있어야 합니다. 파이썬 자체에 대한 소개는 :ref:`tutorial-index`를 참조하십시오.

이 튜토리얼은 기본적인 C 라이브러리를 작성할 수 있는 누구나 접근할 수 있도록 구성되었습니다. 저희는 static 함수나 링크 선언과 같이 C 초보자가 알 것으로 예상되지 않는 개념들을 언급할 것이지만, 성공하는 데는 이러한 개념을 이해할 필요는 없습니다.

저희는 파이썬 C API가 어떤 느낌인지에 초점을 맞출 것입니다. 오류 처리 및 참조 카운팅과 같은 중요한 개념은 나중 장에서 다루므로, 이 부분은 가르치지 않습니다.

Unix와 같은 시스템(macOS 및 Linux 포함) 또는 Windows를 사용한다고 가정합니다. 다른 시스템에서는 일부 세부 사항(예: 시스템 명령 이름)을 조정해야 할 수도 있습니다.

적절한 C 컴파일러와 Python 개발 헤더가 설치되어 있어야 합니다. Linux에서는 헤더가 종종 python3-dev 또는 python3-devel 과 같은 패키지에 포함되어 있습니다.

파이썬 패키지를 설치할 수 있어야 합니다. 이 튜토리얼은 pip (pip install)를 사용하지만, uv (uv pip install)와 같이 pyproject.toml 기반 프로젝트를 빌드하고 설치할 수 있는 모든 도구로 대체할 수 있습니다. 가급적 :ref:`가상 환경 <venv-def>`이 활성화되어 있어야 합니다.

참고

이 튜토리얼은 CPython 3.15에 추가된 API를 사용합니다. CPython의 이전 버전을 지원하는 확장을 작성하려면, 이 문서의 이전 버전을 참조하십시오.

이 튜토리얼은 C11 및 C++20에 추가된 C 구문을 사용합니다. 확장 기능이 이전 표준과 호환되어야 하는 경우, Python 3.14 이하 문서의 튜토리얼을 따르십시오.

무엇을 할 것인가

spam 이라는 이름의 확장 모듈을 만들겠습니다 [1]. 이 모듈은 C 표준 라이브러리 함수 system() 에 대한 Python 인터페이스를 포함합니다. 이 함수는 stdlib.h 에 정의되어 있습니다. C 문자열을 인수로 받아 인수를 시스템 명령으로 실행하고, 정수형으로 결과 값을 반환합니다. system() 의 매뉴얼 페이지는 다음과 같이 요약할 수 있습니다:

#include <stdlib.h>
int system(const char *command);

참고로, C 표준 라이브러리의 많은 함수들과 마찬가지로, 이 함수는 이미 Python에 노출되어 있습니다. 프로덕션 환경에서는 여기에 작성할 모듈 대신 os.system() 또는 :py:func:`subprocess.run`을 사용하십시오.

이 함수가 다음과 같이 Python에서 호출되도록 하고 싶습니다:

>>> import spam
>>> status = spam.system("whoami")
User Name
>>> status
0

참고

시스템 명령 whoami 는 사용자 이름을 출력합니다. 이 예제에서는 유닉스와 윈도우 모두에서 이름이 같기 때문에 유용합니다.

헤더 파일부터 시작합니다

이 튜토리얼을 위한 디렉터리를 생성하고 명령줄에서 해당 디렉터리로 전환하십시오. 그런 다음 디렉터리에 :file:`spammodule.c`라는 이름의 파일을 만드십시오. [2]

이 파일에서 두 개의 헤더를 포함할 것입니다: Python C API의 모든 선언을 가져오기 위한 Python.h`와 :c:func:`system 함수를 위한 :file:`stdlib.h`입니다. [3]

:file:`spammodule.c`에 다음 줄을 추가하십시오:

#include <Python.h>
#include <stdlib.h>     // system()을 위해

Python.h 파일 뒤에 stdlib.h 및 기타 표준 라이브러리 포함 파일을 넣는 것을 잊지 마십시오. 일부 시스템에서는 Python이 표준 헤더에 영향을 미치는 일부 전처리기 정의를 정의할 수 있습니다.

빌드 도구 실행

포함 구문만으로는 확장 기능이 아무것도 하지 않을 것입니다. 그럼에도 불구하고, 컴파일하여 가져오도록 시도하기에 좋은 시간입니다. 이렇게 하면 빌드 도구가 작동하는지 확인할 수 있어, 이후의 텍스트를 따르면서 증분적인 변경 사항을 만들고 테스트할 수 있습니다.

CPython itself does not come with a tool to build extension modules; it is recommended to use a third-party project for this. In this tutorial, we’ll use meson-python. (If you want to use another one, see 부록: 다른 빌드 도구들.)

meson-python 은 “프로젝트”를 정의하기 위해 두 개의 추가 파일이 필요합니다.

먼저, 다음 내용으로 pyproject.toml 을 추가하십시오:

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[project]
# 플레이스홀더 프로젝트 정보
# (모듈 배포 전에 변경해야 함)
name = 'sampleproject'
version = '0'

다음 내용을 포함하는 meson.build 를 생성하십시오:

project('sampleproject', 'c')

py = import('python').find_installation(pure: false)

py.extension_module(
   'spam',          # 가져올 수 있는 Python 모듈 이름
   'spammodule.c',  # C 소스 파일
   install: true,
)

참고

구성 세부 정보는 `meson-python documentation <meson-python>`을 참조하십시오.

이제, pip 를 사용하여 현재 디렉터리 (. )에 프로젝트를 빌드 및 설치하십시오:

python -m pip -v install .

-v (--verbose) 옵션은 pip 가 컴파일러의 출력을 표시하도록 하여, 개발 중 유용할 때가 많습니다.

pip``가 설치되어 있지 않다면, :ref:`virtual environment <venv-def>`에 있는 것이 가장 좋으며, ``python -m ensurepip``을 실행하십시오. (또는, ``pyproject.toml 기반 프로젝트를 빌드 및 설치할 수 있는 다른 도구를 선호한다면 그것을 사용하십시오.)

확장 기능을 변경할 때마다 이 명령을 다시 실행해야 한다는 점에 유의하십시오. Python과 달리 C는 명시적인 컴파일 단계가 있습니다.

확장 기능이 컴파일되고 설치되면, Python을 시작하고 가져오기를 시도하십시오. 그러면 다음과 같은 예외가 발생하여 실패해야 합니다:

>>> import spam
Traceback (most recent call last):
   ...
ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)

모듈 내보내기 훅

모듈을 가져오려고 할 때 얻은 예외는 Python이 “모듈 내보내기 함수”, 즉 :ref:`module export hook <extension-export-hook>`를 찾고 있다고 알려줍니다. 하나를 정의해 봅시다.

먼저, #include 줄 아래에 프로토타입을 추가하십시오:


PyMODEXPORT_FUNC PyModExport_spam(void);

프로토타입은 엄격하게필수는 아니지만, 일부 최신 컴파일러는 이것 없이 경고를 발생시킵니다. 경고를 비활성화하는 것보다 프로토타입을 추가하는 것이 일반적으로 더 좋습니다.

PyMODEXPORT_FUNC 매크로는 함수의 반환형을 선언하고, CPython이 로드할 때 함수를 보이게 하고 사용할 수 있도록 필요한 특수한 링크 선언을 추가합니다.

프로토타입 다음에 함수 자체를 추가하십시오. 일단 NULL 을 반환하도록 만드세요:

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return NULL;
}

모듈을 다시 컴파일하고 로드하십시오. 이번에는 다른 오류가 발생할 것입니다.

>>> import spam
Traceback (most recent call last):
   ...
SystemError: module export hook for module 'spam' failed without setting an exception

단순히 NULL 을 반환하는 것은 내보내기 훅(export hook)에 적절한 동작이 아니며, CPython이 이를 지적합니다. 괜찮습니다. 즉, CPython이 함수를 발견했다는 뜻입니다! 이제 유용한 작업을 하도록 수정해 보겠습니다.

슬롯 테이블

NULL 대신, 내보내기 훅은 모듈을 생성하는 데 필요한 정보를 반환해야 합니다. 가장 기본적인 것부터 시작해 보겠습니다: 이름과 독스트링입니다.

정보는 PySlot 항목 배열에 정의되어야 합니다. 이는 본질적으로 키-값 쌍과 같습니다. 이 배열을 내보내기 훅 바로 전에 정의하십시오:

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
   PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
   PySlot_STATIC_DATA(Py_mod_name, "spam"),
   PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
   PySlot_END
};

PySlot_STATIC_DATA 매크로는 슬롯의 값(여기서: &abi_info, "spam", 및 독스트링)이 상수, 정적으로 할당된 데이터에 대한 포인터인 경우에 사용됩니다.

PyABIInfo_VAR(abi_info); 매크로와 Py_mod_abi 슬롯은 확장 모듈이 다른 버전의 Python을 위해 컴파일되었을 때 인터프리터를 충돌하는 것을 방지하는 데 도움이 되는 다소 번거로운 부분입니다.

Py_mod_namePy_mod_doc 모두 값은 C 문자열입니다. 즉, NUL로 종료되고 UTF-8로 인코딩된 바이트 배열입니다.

끝 부분의 PySlot_END 센티넬 항목에 유의하십시오. 이것이 배열의 끝을 표시합니다. 이것을 잊으면 정의되지 않은 동작을 유발할 것입니다.

배열은 static 으로 정의되어야 합니다. 즉, 이 .c 파일 외부에서는 보이지 않습니다. 이것은 일반적인 주제가 될 것입니다. CPython은 내보내기 훅에만 접근할 필요가 있으며, 모든 전역 변수와 다른 모든 함수는 일반적으로 static 이어야 합니다. 그래야 다른 확장 모듈과 충돌하지 않습니다.

NULL 대신 이 배열을 내보내기 훅에서 반환하십시오:

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

이제 다시 컴파일하고 실행해 보십시오:

>>> import spam
>>> print(spam)
<module 'spam' from '/home/encukou/dev/cpython/spam.so'>

확장 모듈이 생겼습니다! 독스트링을 보려면 help(spam) 을 시도해 보세요.

다음 단계는 함수를 추가하는 것입니다.

함수 노출

system() C 함수를 Python에 직접 노출하려면, Python 객체에서 C 값으로 인수를 변환하고 C 반환 값을 다시 Python으로 변환하는 레이어의 접착 코드(glue code)를 작성해야 합니다.

접착 코드를 작성하는 가장 간단한 방법 중 하나는 두 개의 Python 객체를 받아 하나를 반환하는 “METH_O” 함수입니다. 모든 Python 객체는 – Python 타입에 관계없이 – C에서 PyObject 구조체에 대한 포인터로 표현됩니다.

이러한 함수를 슬롯 배열 위에 추가하십시오:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   Py_RETURN_NONE;
}

지금은 인수를 무시하고, Python의 None 객체를 적절하게 반환하는 return 문으로 확장되는 Py_RETURN_NONE 매크로를 사용합니다.

구문 오류가 없는지 확인하기 위해 확장 모듈을 다시 컴파일하십시오. 아직 spam_system 을 모듈에 추가하지 않았으므로, spam_system 이 사용되지 않았다는 경고가 표시될 수 있습니다.

메서드 정의

C 함수를 Python에 노출하려면, PyMethodDef 구조체라는 구조체에 여러 정보 조각을 제공해야 합니다 [4]:

  • ml_name: Python 함수의 이름;

  • ml_doc: 독스트링;

  • ml_meth: 호출될 C 함수; 그리고

  • ml_flags: Python 인수가 C 함수로 어떻게 전달되는지 같은 세부 정보를 설명하는 플래그 집합. 여기서는 우리의 spam_system 함수의 시그니처에 맞는 플래그인 :c:data:`METH_O`를 사용하겠습니다.

모듈은 일반적으로 여러 함수를 생성하므로, 이러한 정의들은 끝에 0으로 채워진 센티넬과 함께 배열에 수집되어야 합니다. 이 배열을 spam_system 함수 바로 아래에 추가하십시오:

static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="실행할 셸 명령어.",
    },
    {NULL, NULL, 0, NULL}        /* 센티넬 */
};

모듈 슬롯과 마찬가지로, 0으로 채워진 센티넬이 배열의 끝을 표시합니다.

다음으로, 메서드를 모듈에 추가할 것입니다. PyMethodDef 배열에 Py_mod_methods 슬롯을 추가하십시오:


PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
    PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
    PySlot_END
};

확장 모듈을 다시 컴파일하고 테스트하십시오. import spam 이 모듈의 새 버전을 가져오도록 파이썬 인터프리터를 재시작하는 것을 잊지 마십시오.

이제 다음과 같이 함수를 호출할 수 있어야 합니다:

>>> import spam
>>> print(spam.system)
<built-in function system>
>>> print(spam.system('whoami'))
None

우리의 spam.system 은 아직 whoami 명령을 실행하지 않습니다. 단순히 None 을 반환하기만 합니다.

METH_O 플래그가 지정하는 대로, 함수가 정확히 하나의 인자를 받는지 확인하십시오:

>>> print(spam.system('too', 'many', 'arguments'))
Traceback (most recent call last):
   ...
TypeError: spam.system() takes exactly one argument (3 given)

정수

이제 반환 값을 들여다봅시다. None 대신, spam.system``이 숫자인, Python의 :py:type:`int` 객체를 반환하도록 하고 싶습니다. 결국 이것은 시스템 명령어의 종료 코드가 되겠지만, 일단은 ``3 같은 고정 값으로 시작해 봅시다.

Python C API는 C int 값으로부터 Python int 객체를 생성하는 함수를 제공합니다: PyLong_FromLong(). [5]

호출하려면, Py_RETURN_NONE 를 다음 3줄로 대체하십시오:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

재컴파일하고 파이썬 인터프리터를 다시 시작한 다음, 함수가 이제 3을 반환하는지 확인하십시오:

>>> import spam
>>> spam.system('whoami')
3

문자열 받기

마지막으로, 함수 인자를 처리해 봅시다.

우리의 C 함수 spam_system`은 개의 인자를 받습니다. 번째 인자인 ``PyObject *self``는 ``spam`() 모듈 객체로 설정됩니다. 이것은 저희 경우에 유용하지 않으므로, 무시합니다.

다른 하나인 PyObject *arg``는 사용자가 Python에서 전달한 객체로 설정됩니다. 이것은 Python 문자열이어야 한다고 예상합니다. 안에 있는 정보를 사용하려면, 이를 C 값(이 경우, C 문자열 ``const char *)으로 변환할 필요가 있습니다.

여기서 가벼운 타입 불일치가 있습니다: Python의 str 객체는 유니코드 텍스트를 저장하지만, C 문자열은 바이트 배열입니다. 따라서 데이터를 인코딩 해야 하며, 여기서는 UTF-8 인코딩을 사용합니다. (UTF-8이 시스템 명령어에 항상 올바르지 않을 수도 있지만, str.encode() 가 기본적으로 사용하는 방식이며, C API는 이에 대한 특별한 지원이 있습니다.)

Python 문자열을 UTF-8 버퍼로 인코딩하는 함수는 :c:func:`PyUnicode_AsUTF8AndSize`라는 이름입니다 [6]. 다음과 같이 호출합니다:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

PyUnicode_AsUTF8AndSize() 가 성공하면, command 는 결과 C 문자열, 즉 널 종료 바이트 배열을 가리킵니다 [7]. 이 버퍼는 arg 객체에 의해 관리되므로 해제할 필요는 없지만, 몇 가지 규칙을 따라야 합니다:

  • 이 버퍼는 spam_system 함수 내부에서만 사용해야 합니다. spam_system 이 반환한 후, arg 와 이가 관리하는 버퍼는 가비지 컬렉션될 수 있습니다.

  • 우리는 이것을 수정해서는 안 됩니다. 이것이 우리가 const 를 사용하는 이유입니다.

PyUnicode_AsUTF8AndSize()성공하지 하면, NULL 포인터를 반환합니다. 어떤 Python C API를 호출할 때든, 우리는 항상 이러한 오류 사례를 처리해야 합니다. 일반적인 방법은 이 문서의 나중에 나오는 장에서 다룹니다. 현재로서는, 우리가 PyLong_FromLong() 에서 오는 오류를 올바르게 처리하고 있다는 것을 확신하십시오.

PyUnicode_AsUTF8AndSize() 호출에 대해, 오류를 처리하는 올바른 방법은 spam_system 에서 NULL 을 반환하는 것입니다. 이 구문을 추가하십시오:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg);
   if (command == NULL) {
      return NULL;
   }
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

오류 처리가 작동하는지 테스트하려면, 다시 컴파일하고 파이썬을 재시작하여 import spam 이 모듈의 새 버전을 가져오게 한 다음, 함수에 문자열이 아닌 값을 전달해 보십시오:

>>> import spam
>>> spam.system(3)
Traceback (most recent call last):
   ...
TypeError: bad argument type for built-in operation

이제 남은 것은 C 라이브러리 함수 system()char * 버퍼와 함께 호출하고, 3 대신 그 결과를 사용하는 것입니다:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg);
   if (command == NULL) {
      return NULL;
   }
   int status = system(command);
   PyObject *result = PyLong_FromLong(status);
   return result;
}

모듈을 컴파일하고 파이썬을 재시작한 다음 테스트하십시오. 이번에는 사용자의 사용자 이름, 즉 whoami 시스템 명령어의 출력을 볼 수 있어야 합니다:

>>> import spam
>>> result = spam.system('whoami')
User Name
>>> result
0

또한, ls, dir 또는 존재하지 않는 명령어와 같은 다른 명령어로도 테스트할 수 있습니다:

>>> import spam
>>> result = spam.system('nonexistent-command')
sh: line 1: nonexistent-command: command not found
>>> result
32512

결과

축하합니다! 완벽한 Python C API 확장 모듈을 작성했고, 이 튜토리얼을 마쳤습니다!

편의를 위해 전체 소스 파일을 여기에 첨부합니다:

/// 포함
#include <Python.h>
#include <stdlib.h>     // system()용

/// spam.system 구현

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
   if (command == NULL) {
      return NULL;
   }
   int status = system(command);
   PyObject *result = PyLong_FromLong(status);
   return result;
}

/// 모듈 메서드 테이블

static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="셸 명령을 실행합니다.",
    },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

/// 모듈 슬롯 테이블

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_STATIC_DATA(Py_mod_doc, "예제 함수가 포함된 멋진 모듈"),
    PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
    PySlot_END
};

/// 내보내기 훅 프로토타입

PyMODEXPORT_FUNC PyModExport_spam(void);

/// 모듈 내보내기 훅

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

부록: 다른 빌드 도구들

meson-python 이외의 다른 빌드 도구를 사용하면 이 튜토리얼, 단 빌드 도구 실행 섹션 자체는 제외하고 따라 할 수 있어야 합니다.

Python Packaging User Guide에는 C 언어용 도구를 선택하도록 https://packaging.python.org/en/latest/guides/tool-recommendations/#build-backends-for-extension-modules 에 대한 자세한 내용이 있습니다.

PyInit 함수가 누락되었을 때의 임시 방편

빌드 도구의 출력에 PyInit_spam 누락과 관련된 오류가 표시되면, 당분간 모듈에 다음 함수를 추가하십시오:

// 임시 방편
void *PyInit_spam(void) { return NULL; }

이것은 CPython 3.14 및 이전 버전의 확장 모듈에서 필요했던 구식 :ref:`초기화 함수 <extension-export-hook>`을 위한 임시 방편입니다. 현재 CPython에서는 필요하지 않지만, 일부 빌드 도구는 여전히 모든 확장 모듈이 이를 정의해야 한다고 가정할 수 있습니다.

이 임시 방편을 사용하면 ImportError: dynamic module does not define module export function 대신 SystemError: initialization of spam failed without raising an exception 예외가 발생합니다.

직접 컴파일하기

서드파티 빌드 도구를 사용하는 것이 강력히 권장됩니다. 왜냐하면 해당 도구가 플랫폼 및 Python 설치에 관한 다양한 세부 사항, 결과 확장 이름 지정, 그리고 나중에 작업 배포까지 처리해 주기 때문입니다.

만약 특정 시스템 을 위해, 또는 자신만을 위해 확장을 빌드하는 경우, 대신 컴파일러를 직접 실행하는 것이 좋을 수도 있습니다. 이를 수행하는 방법은 시스템별로 달라지므로, 스스로 해결해야 할 문제에 대비해야 합니다.

리눅스

리눅스에서는 Python 개발 패키지에 필요한 컴파일러 플래그를 출력하는 python3-config 명령이 포함될 수 있습니다. 이 명령을 사용한다면, 로드할 모듈에 사용하게 될 CPython 인터프리터와 일치하는지 확인하십시오. 그런 다음 다음 명령으로 시작하십시오:

gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so

이것은 sys.path`의 디렉토리에 두어야 하는 ``spam.so` 파일을 생성해야 합니다.

각주

분실물 보관소