Python

첫 번째 C API 확장 모듈

이 튜토리얼에서는 C 또는 C++로 작성된 간단한 파이썬 확장 모듈을 만드는 과정을 안내합니다.

저수준 파이썬 C API를 직접 사용합니다. 확장 모듈을 만드는 더 쉬운 방법을 원하신다면 권장되는 제3자 도구 를 참조하십시오.

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

이 튜토리얼은 기본적인 C 라이브러리를 작성할 수 있는 사람이라면 누구나 접근 가능합니다. static 함수나 링크 선언과 같이 C 초보자가 알 필요가 없는 몇몇 개념을 언급하긴 하겠지만, 성공적으로 마치는 데 이들을 이해하는 것은 필수적이지 않습니다.

이 섹션에서는 파이썬 C API가 어떤 느낌인지 익히는 데 집중합니다. 후속 장에서 다룰 오류 처리나 참조 횟수(reference counting)와 같은 중요한 개념은 가르치지 않습니다.

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

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

파이썬 패키지를 설치할 수 있어야 합니다. 이 튜토리얼은 pip (pip install)를 사용하지만, pyproject.toml 기반 프로젝트를 빌드하고 설치할 수 있는 다른 도구인 uv (uv pip install) 등으로 대체할 수 있습니다. 가급적 가상 환경 을 활성화한 상태에서 진행하십시오.

참고

이 튜토리얼은 CPython 3.15에서 추가된 API를 사용합니다. 이전 버전의 CPython과 호환되는 확장 모듈을 만들려면 이 문서의 이전 버전을 참조하십시오.

이 튜토리얼은 C11 및 C++20에서 추가된 문법을 사용합니다. 확장 모듈이 이전 표준과 호환되어야 하는 경우 파이썬 3.14 이하 버전의 문서를 참조하십시오.

진행 과정

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

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

C 표준 라이브러리의 많은 함수와 마찬가지로 이 함수도 이미 파이썬에 노출되어 있습니다. 실제 운영 환경에서는 여기서 작성할 모듈 대신 os.system() 이나 subprocess.run() 을 사용하십시오.

이 함수를 다음과 같이 파이썬에서 호출 가능하게 만들고자 합니다.

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

참고

시스템 명령인 whoami 는 사용자 이름을 출력합니다. 이 명령어는 Unix와 Windows 모두에서 동일한 이름을 사용하므로 이와 같은 튜토리얼에서 유용합니다.

헤더로 시작하기

Begin by creating a directory for this tutorial, and switching to it on the command line. Then, create a file named spammodule.c in your directory. [2]

In this file, we’ll include two headers: Python.h to pull in all declarations of the Python C API, and stdlib.h for the system() function. [3]

spammodule.c 에 다음 줄들을 추가하십시오.

#include <Python.h>
#include <stdlib.h>     // for system()

stdlib.h 및 기타 모든 표준 라이브러리 포함을 반드시 Python.h 뒤에 배치하십시오. 일부 시스템에서 파이썬은 표준 헤더에 영향을 미치는 몇 가지 전처리기 정의를 정의할 수 있습니다.

빌드 도구 실행하기

포함(include)만 완료된 상태에서는 확장 기능이 아무것도 수행하지 않습니다. 그럼에도 지금 컴파일하고 가져오기(import)를 시도해 보는 것이 좋습니다. 이를 통해 빌드 도구가 제대로 작동하는지 확인하고, 이후 내용을 따라가며 점진적인 변경 사항을 적용하고 테스트할 수 있습니다.

CPython 자체에는 확장 모듈을 구축하는 도구가 포함되어 있지 않으므로, 이를 위해 제삼자 프로젝트를 사용하는 것을 권장합니다. 이 튜토리얼에서는 meson-python 를 사용합니다. (다른 도구를 사용하려면 부록: 기타 빌드 도구 를 참조하십시오.)

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',          # 가져올 수 있는 파이썬 모듈 이름
   'spammodule.c',  # C 소스 파일
   install: true,
)

참고

See the meson-python documentation for details on configuration.

이제 pip``을 통해 *현재 디렉터리의 프로젝트*를 빌드하고 설치하십시오(.``):

python -m pip -v install .

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

pip 이 설치되어 있지 않다면, 가급적 가상 환경 에서 python -m ensurepip 를 실행하십시오. (또는 pyproject.toml 기반 프로젝트를 빌드하고 설치할 수 있는 다른 도구를 선호한다면 해당 도구를 사용하십시오.)

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

확장 기능이 컴파일되고 설치되면, 파이썬을 시작하고 이를 임포트해 보십시오. 다음과 같은 예외와 함께 실패해야 합니다.

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

모듈 내보내기 훅

모듈을 임포트하려고 할 때 발생한 예외는 파이썬이 “모듈 내보내기 함수”(또는 모듈 내보내기 훅 으로도 알려짐)를 찾고 있음을 알려주었습니다. 하나를 정의해 보겠습니다.

먼저, #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 을 반환하는 것은 내보내기 훅의 올바른 동작이 아니며, CPython은 이에 대해 문제를 제기합니다. 이는 좋은 신호로, CPython이 함수를 찾았다는 것을 의미합니다! 이제 이 함수가 유용한 일을 수행하도록 만들어 봅시다.

슬롯 테이블

내보내기 훅은 NULL 대신 모듈을 생성하는 데 필요한 정보를 반환해야 합니다. 기본 사항인 이름과 독스트링(docstring)부터 시작하겠습니다.

정보는 기본적으로 키-값 쌍인 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 슬롯은 다른 버전의 파이썬을 위해 컴파일된 확장이 인터프리터를 중단시키지 않도록 방지하는 일종의 상용구입니다.

Py_mod_namePy_mod_doc 모두의 값은 C 문자열, 즉 NUL로 끝나는 UTF-8 인코딩 바이트 배열입니다.

끝에 있는 PySlot_END 감시 항목(sentinel)에 유의하십시오. 이는 배열의 끝을 표시합니다. 이를 생략하면 정의되지 않은 동작이 발생할 수 있습니다.

배열은 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 함수를 파이썬에 직접 노출하려면, 파이썬 객체를 C 값으로 변환하고 C 반환 값을 다시 파이썬으로 변환하는 연결 코드(glue code) 계층을 작성해야 합니다.

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

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

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

현재는 인자를 무시하고, 파이썬 None 객체를 올바르게 반환하는 return 문으로 확장되는 Py_RETURN_NONE 매크로를 사용합니다.

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

메서드 정의

C 함수를 파이썬에 노출하려면 PyMethodDef 라는 구조체에 몇 가지 정보를 제공해야 합니다. [4]

  • ml_name: 파이썬 함수의 이름;

  • ml_doc: 독스트링;

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

  • ml_flags: 파이썬 인자가 C 함수에 어떻게 전달되는지 등의 상세 정보를 설명하는 플래그 세트입니다. 여기서는 당사의 spam_system 함수 시그니처와 일치하는 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}        /* Sentinel */
};

모듈 슬롯과 마찬가지로, 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 이 숫자, 즉 파이썬 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``는 사용자가 파이썬에서 전달한 객체로 설정됩니다. 값은 파이썬 문자열이어야 합니다. 여기에 포함된 정보를 사용하기 위해 이를 C 값으로 변환해야 하며, 경우 C 문자열(``const char *)로 변환합니다.

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

The function to encode a Python string into a UTF-8 buffer is named PyUnicode_AsUTF8AndSize() [6]. Call it like this:

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 포인터를 반환합니다. 모든 파이썬 C API를 호출할 때 이러한 오류 케이스를 처리해야 합니다. 이를 일반적인 방식으로 처리하는 방법은 이 문서의 이후 장에서 다룹니다. 현재로서는 PyLong_FromLong() 의 오류를 이미 올바르게 처리하고 있으니 안심하셔도 됩니다.

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

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

이제 남은 작업은 char * 버퍼로 C 라이브러리 함수인 system() 을 호출하고, 그 결과값을 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

결과

축하합니다! 파이썬 C API 확장 모듈 작성을 완료하고 이 튜토리얼을 마쳤습니다!

편의를 위해 전체 소스 파일을 제공합니다.

/// Includes

#include <Python.h>
#include <stdlib.h>     // for system()

/// Implementation of 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;
}

/// Module method table

static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="Execute a shell command.",
    },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

/// Module slot table

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
};

/// Export hook prototype

PyMODEXPORT_FUNC PyModExport_spam(void);

/// Module export hook

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

부록: 기타 빌드 도구

이 가이드는 “빌드 도구 실행” 섹션을 제외하고는 meson-python 이외의 다른 빌드 도구를 사용하여도 따라올 수 있습니다.

파이썬 패키징 사용자 지침서에 권장 도구 목록 가 포함되어 있으므로, C 언어용으로 하나를 선택하십시오.

누락된 PyInit 함수에 대한 해결책

빌드 도구 출력에서 PyInit_spam 누락에 대한 경고가 발생하면 우선 모듈에 다음 함수를 추가하십시오.

// A workaround
void *PyInit_spam(void) { return NULL; }

이는 CPython 3.14 이하 버전의 확장 모듈에서 필요했던 구식 초기화 함수 를 위한 쉴름(shim)입니다. 현재 CPython에서는 필요하지 않지만, 일부 빌드 도구는 여전히 모든 확장 모듈이 이를 정의해야 한다고 가정할 수 있습니다.

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

직접 컴파일하기

제3자 빌드 도구를 사용하는 것을 강력히 권장합니다. 이러한 도구는 플랫폼 및 파이썬 설치 환경의 세부 사항을 처리하고, 생성된 확장의 이름을 지정하며, 나중에 작업을 배포하는 과정까지 처리해 주기 때문입니다.

특정 시스템용이나 개인용으로만 확장을 빌드하는 경우 대신 컴파일러를 직접 실행할 수도 있습니다. 이 방법은 시스템마다 다릅니다. 직접 해결해야 하는 문제가 발생할 수 있음에 유의하십시오.

Linux

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

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

이 명령은 여러분이 sys.path`에 있는 디렉토리에 넣어야 하는 ``spam.so` 파일을 생성합니다.

각주

분실물 보관소