첫 번째 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_name 및 Py_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` 파일을 생성합니다.
각주