본문 바로가기

진리는어디에/Python

[Python] C/C++ 연동 'extension' 모듈

들어가며

지난 포스트에서는 C로 만들어진 공용 라이브러리를 파이썬의 ctypes 모듈을 통해 호출하는 방법에 대해 알아 보았다. 이번 포스트에서는 C로 만들어진 공용 라이브러리를 일반 파이썬 모듈 처럼 사용 할 수 있는 방법에 대해 살펴 보겠다.

C언어로 파이썬 확장 모듈을 만드는 과정을 요약하면 아래와 같다.

  1. 파이썬으로 부터 인자를 넘겨 받아 처리 할 수 있는 C 함수를 작성한다.
  2. 모듈의 메소드 정보를 저장하는 PyMethodDef 배열을 생성한다.
  3. 모듈 자체의 정보를 저장하는 PyModuleDef 구조체를 생성한다.
  4. PyInit_<모듈이름>을 가지는 초기화 함수를 정의한다.
  5. setup.py 파일을 생성하여 모듈을 빌드한다.

위 과정을 완료하게 되면 C로 작성된 파이썬 모듈이 생성되고, 아래와 같이 일반 파이썬 모듈을 호출하는 것과 동일한 방법으로 호출 할 수 있다.

import c_module

print(c_module.add(1, 2))
print(c_module.sub(1, 2))

관련 전체 코드는 [여기]에서 확인 할 수 있다. 최대한 자세한 주석을 추가해 놓았으니 주석만 보더라도 전체적인 사용 방법을 익히기에는 무리가 없을거라 생각한다.

C 함수 작성

C로 파이썬 모듈을 만들기 위해 가장 먼저 할 일은 파이썬으로 부터 인자를 넘겨 받아 계산 후 다시 파이썬으로 결과를 넘겨 줄 수 있는 함수를 작성하는 것이다.

헤더 인클루드

#include <python.h>

C에서 파이썬과 연동하기 위해 필요한 자료 구조들은 "python.h"에 정의 되어 있다. 필자의 경우는 아래 위치에 파이썬이 설치되어 있지만 각각의 환경에 따라 다른 위치에 파이썬이 설치 되어 있을 수도 있다.

모듈 메소드 작성

// import c_module
// print(c_module.add(1, 2))

PyObject* py_add(PyObject* self, PyObject* args)
{
    PyTupleObject* tuple = (PyTupleObject*)args;
    PyLongObject* a = (PyLongObject*)(tuple->ob_item[0]);
    PyLongObject* b = (PyLongObject*)(tuple->ob_item[1]);

    std::cout << "add" << "(" << a->ob_digit[0] << ", " << b->ob_digit[0] << ")" << std::endl;
    int result = a->ob_digit[0] + b->ob_digit[0];

    PyObject* pyResult = Py_BuildValue("i", result);
    return pyResult;
}

앞에서 우리는 파이썬에서 C로 작성된 모듈에 정의된 함수를 호출하는 코드 'c_module.add(1, 2)'를 살펴 보았다. C에서는 정수는 단지 4바이트 메모리 영역이지만 파이썬에서는 별도의 객체로써 관리된다. 이렇게 파이썬과 C는 데이터를 저장하고 처리하는 방법이 다르므로 파이썬에서 넘겨지는 값을 그대로 C/C++에서 가져다 사용 할 수는 없다.

파이썬과 C 사이의 데이터 교환을 위해서는 '파이썬 C API'에서 제공하는 각 데이터 타입에 매칭되는 자료 구조를 사용해야 한다. 파이썬에서 'c_module.add(1, 2)'와 같이 메소드를 호출하게 되면 호출에 사용된 인자들은 튜플에 담겨져 C에게 전달 된다.

예를 들어 인자 1, 2는 각각 정수를 나타내는 PyLongObject에 담겨 PyTupleObject에 묶여져 넘겨진다. 위 예제의 6라인은 PyObject* 타입으로 넘어온 인자를 PyTupleObject*로 캐스팅 후 튜플의 순서 대로 인자를 PyLongObject*에 할당하고 있다.

12라인에서는 PyLongObject 객체의 ob_digit 멤버를 이용해 인자로 넘어온 정수를 얻어와 더한 결과를 Py_BuildValue 함수를 이용해 파이썬이 이해 할 수 있는 PyObject 객체로 만들어 리턴하고 있다.

※ ob_digit를 단순 long 타입 멤버가 아닌 배열 형태로 되어 있는 이유는 [여기]에 따로 정리 해두었으니 궁금하신 분들은 살펴 보도록 하자.

위의 예제 처럼 복잡한 캐스팅을 통해 인자를 파싱하는 대신 아래처럼 간단히 PyArg_ParseTuple 함수를 이용해 해결 할 수도 있다.

PyObject* py_sub(PyObject* self, PyObject* args)
{
    int a = 0;
    int b = 0;

    if (false == PyArg_ParseTuple(args, "ii", &a, &b))
    {
        return nullptr;
    }

    std::cout << "sub" << "(" << a << ", " << b << ")" << std::endl;
    int result = a - b;
    
    PyObject* pyResult = Py_BuildValue("i", result);
    return pyResult;
}

PyMethodDef

C/C++ 함수의 작성이 완료되었으면 이제 부터 파이썬 모듈에서 C/C++함수를 호출 할 수 있도록 메소드 정보를 등록 하도록 한다. 파이썬 모듈에서 사용되는 메소드 정보는 PyMethodDef 구조체의 배열로 정의 된다.

struct PyMethodDef 
{
    const char  *ml_name;   /* 파이썬에서 호출할 메소드 이름 */
    PyCFunction ml_meth;    /* 메소드가 구현되어 있는 C함수 포인터 */
    int         ml_flags;   /* 호출 구성 방법을 나타내는 METH_XXX 비트 플래그 조합 */
    const char  *ml_doc;    /* __doc__ 어트리뷰트 또는 NULL */
};

앞에서 우리는 py_add와 py_sub C 함수를 작성했다. 이 함수들을 파이썬에서 각각 add와 sub라는 이름으로 호출 되도록 하기 위해서는 아래와 같이 PyMethodDef 배열에 등록 하도록 한다.

PyMethodDef method_defs[] = {
    {"add", py_add, METH_VARARGS /* 가변 인자를 의미*/, "Integer add" /*설명*/},
    {"sub", py_sub, METH_VARARGS, "Integer sub"},
    {nullptr, nullptr, 0, nullptr}
};

배열의 끝을 나타내기 위해 마지막 요소는 모든 필드를 NULL과 0으로 세팅한다.

※ METH_VARARGS와 같은 비트 플래그의 보다 자세한 정보는 [여기]에서 찾을 수 있다.

PyModuleDef

메소드의 등록이 완료 되었다면 모듈 자체의 정보를 정의해주어야 한다. 모듈에 대한 정보는 PyModuleDef 구조체를 이용한다. PyModuleDef는 파이썬에서 모듈 객체를 만드는데 필요한 모든 정보를 담고 있는 모듈 정의 구조체로써 일반적으로 각 모듈에 대해 PyModuleDef는 정적으로 초기화된 하나의 변수만 있다.

PyModuleDef module_def = {
    PyModuleDef_HEAD_INIT,
    "c_module",                 // 모듈 이름
    "document string",          // 모듈 설명
    -1,                         // Size of per-interpreter state 또는 그냥 -1
    method_defs                 // 메소드 정보를 담은 배열
};

모듈 초기화 함수

모듈에서 사용할 메소드의 등록과 모듈 정보의 등록이 완료 되었다면 파이썬에서 모듈을 초기화 하는데 사용되는 초기화 함수를 작성해주어야 한다. 초기화 함수는 "PyInit_XXX"와 같은 이름을 가지고 있고 'XXX' 부분에 사용 되는 함수의 이름은 다음 챕터에 나올 모듈 빌드시 setup.py에 지정되는 이름과 같아야 한다.

PyMODINIT_FUNC PyInit_c_module()
{
    return PyModule_Create(&module_def);
}

여기까지 C/C++ 코드에서 할 일은 끝났다.

확장 모듈 빌드

파이썬 모듈은 다른 C프로그램 처럼 비주얼 스튜디오나 gcc를 이용해 직접 빌드하지 않는다. 파이썬 확장 모듈은 파이썬에 포함된 distutils를 사용하여 빌드할 수 있다. distutils는 드라이버 스크립트인 setup.py라는 평범한 파이썬 파일을 이용한다. setup.py에는 빌드와 모듈 인스톨에 관한 정보가 기술 되며 대부분 간단한 경우 아래와 비슷한 형식을 가진다.

# file : setup.py

# -*- coding: utf-8 -*-

from distutils.core import setup, Extension

setup(
    name="PackageName",                 # 패키지 이름
    ext_modules=[
        Extension(
            "c_module",                 # 모듈 이름. 
                                        # 초기화 함수 PyInit_<modulename>에서 <modulename>과 같아야 한다
            ["c_module_main.cpp"],      # 빌드에 사용 될 C/C++ 파일
            include_dirs = ["."],       # include 디렉토리 패스
        )
    ]
)

setup.py를 이용한 빌드 명령은 아래와 같다.

  • 빌드 + 설치
    python setup.py install​
  • 빌드와 설치 분리
    python setup.py build
    python setup.py install
  • 컴파일러 변경
    python setup.py build --compiler=mingw32
    python setup.py install

파이썬 모듈을 빌드하기 위해서는 윈도우라면 비주얼 스튜디오의 cl 컴파일러나 리눅스의 경우 gcc 컴파일러가 설치 되어 있어야 한다.

PS D:\blog\375> python setup.py install
running install
running build
running build_ext
building 'c_module' extension
....
c_module_main.obj : error LNK2001: 확인할 수 없는 외부 기호 __imp__Py_BuildValue
c_module_main.obj : error LNK2001: 확인할 수 없는 외부 기호 __imp__PyModule_Create2
c_module_main.obj : error LNK2001: 확인할 수 없는 외부 기호 __imp__PyArg_ParseTuple
build\lib.win32-3.9\c_module.cp39-win_amd64.pyd : fatal error LNK1120: 3개의 확인할 수 없는 외부 참조입니다.

만일 빌드시 위와 같은 에러가 발생한다면 대부분의 경우 파이썬은 64비트인데 컴파일러는 32비트이거나 아니면 그 반대의 경우다. 컴파일러와 파이썬의 플랫폼을 맞춰 주면 된다. 필자의 경우는 64비트 파이썬에 윈도우의 비주얼 스튜디오 환경이므로 윈도우 실행 창에서 "x64 Native Tools Command Prompt for VS 2019" 를 검색하여 64비트용 터미널을 띄워서 빌드 했다.

D:\blog\375>python setup.py install
running install
running build
running build_ext
building 'c_module' extension
...
running install_lib
copying build\lib.win-amd64-3.9\c_module.cp39-win_amd64.pyd -> C:\Program Files\Python39\Lib\site-packages
running install_egg_info

이상 C로 파이썬 확장 모듈 만드는 것이 성공했다.

모듈 import

앞에서 C를 이용한 파이썬 확장 모듈을 만드는 것까지 완료 했다. 이번에는 확장 모듈을 파이썬에서 호출하는 방법에 대해 살펴 보자. 사실 이 부분은 아주 쉽다. 그냥 일반 파이썬 모듈을 import하는 것과 동일한 방법을 이용하면 된다.

import c_module

print(c_module.add(1, 2))
print(c_module.sub(1, 2))

# OUTPUT
# add(1, 2)
# 3
# sub(1, 2)
# -1

마치며

이상 C를 이용하여 파이썬 확장 모듈을 만들고 사용하는 방법에 대해 살펴 보았다. 뭔가 스크립트 언어와 컴파일 언어를 하나로 묶어서 사용한다고 하니 괜시리 어려워 보이지만 복잡한 개념나 스킬이 요구되는 기술이 아니기 때문에 한번 따라 해보면 쉽게 이해가 갈 것이다. 파이썬에서 성능이 필요하거나 C/C++의 기능이 필요한 경우 파이썬 extension을 이용하면 좋을 것이다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!