본문 바로가기

진리는어디에/Python

Python embedding 2 - Calling Python function from C/C++ Appilcation

/**

에구..날씨는 추워지고 옆구리는 허전해 지고, 덩달아 지갑까지 말썽이군요. 얼마전에 다녀가신 파산 신의 영험하심으로 아직도 가난에 허덕이고 있습니다. 인생 살아 오면서 여지껏 만원 이상의 돈을 빌려 본적이 없는 것 같은데, 태어나서 처음으로 20만원이라는 거금을 빌려 보았습니다. 하지만 중요한 것은 이 돈이 제 생활비로 들어 갈 것이 아니라, 여러 경조사금으로 모두 날아갈 돈이라는 것..ㅠㅠ

차마 더 어려우신 분들이 많기에, 여러분의 불우 이웃이 바로 접니다~..라는 말은 절대 하지 못하겠네요. 날씨가 추워지고 있습니다. 주변에 어려우신 분들 한번 둘러보시고, 살짝쿵 도움의 손길 한번 건네 보세요. 평소에는 아무렇지 않게 살다가 연말에만 도움의 손길 내밀려니 왠지 쑥스러워서 그러지 못하시는 분들~ 연말에 한번만 하시는 것도 상당히 고마운 겁니다. 부끄러워 하시지 않으셔도 됩니다.

올연말에는 우리 아그들 한번 보러 고향 가야 되는데..흑..ㅠㅠ

*/

Overview
지난번의 포스팅(http://kukuta.tistory.com/69)에서는 파이썬 스크립트를 C/C++에 임베딩(embedding) 하기위해서 필요한 기본 함수, 파이썬 인터프리터의 초기화와 해제 방법, 멀티 쓰레드 어플리케이션에서의 파이썬 인터프리터의 상태 유지에 대해서 집중적으로 알아 보았습니다.

하지만 정작 파이썬 스크립트를 실행 하는 것에 있어서는 PyRun_SimpleString이라는 단순한 예제만을 들었기 때문에 실전에 직접 적용하기에는 다소 부족한 면이 없지 않아 있었습니다.

오늘 포스팅에서는 파이썬 스크립트를 실행하는 부분에 초점을 맞추어 알아 보도록 하겠습니다. 하지만 파이썬 API의 자세한 해석 보다는 전체적인 사용 방법을 위주로 설명 할 것입니다. 함수의 파라메터나 리턴값, 함수의 역할 등에 대해서 궁금하시다면 파이썬 API 문서를 참고 하시면 됩니다.

앞으로 쓰일 예들을 위해서 간단한 파이썬 스크립트를 하나 작성 해 보도록 하겠습니다 :

# file : PythonFuncs.py
def PythonFunc_No_Parameter() :
    print 'This is PythonFunc no parameter'

def PythonFunc_With_Parameter(param) :
    print 'This is PythonFunc with parmeter()'+str(param)

파라메터를 받지 않는 파이썬 함수와 파라메터가 필요한 파이썬 함수, 이렇게 두 개의 함수를 준비 했습니다. 이제 이 파일을 로드하고 실행하는 C/C++코드를 살펴 보도록 하겠습니다. 성격 급하신 분들을 위해 전체 코드를 먼저 준비 했습니다.

Calling Python function from C/C++ Appilcation

코드가 다소 길고 복잡하군요. 한 번에 다 보기 힘드니 따로 떼서 차근차근 분석 해 보도록 합시다.

5     PyObject *pName, *pModule, *pDict, *pFunc, *pValue;

PyObject라는 것은 C/C++ 형태의 데이터들을 파이썬에서 인식할 수 있도록 해주는 객체 입니다. 아무래도 데이터 타입의 구분이 없는 파이썬과, 확실하게 구분을 해줘어야 하는 C/C++이 사이에서 서로를 이해시키기 위해서는 특별한 자료 구조가 필요 하겠지요? 그 역할을 하기 위해 제공되는 것이 PyObject 입니다. 이 PyObject라는 자료 구조를 통해서 C/C++에서 파이썬 인터프리터로 파라메터를 넘겨 줄 수 있고, 파이썬 스크립트에서 리턴하는 데이터를 C/C++에서 읽어 올 수 잇는 것입니다. 파이썬 API에서 제공하는 함수들을 통해 C/C++ 형태의 데이터들을 PyObject로 변환 할 수 있습니다.

13     // Initialize the Python Interpreter
14     Py_Initialize();

파이썬 인터프리터를 초기화 합니다. PyInitialize 함수에 대한 설명은 이전 포스트(http://kukuta.tistory.com/69)에 소개 되어 있습니다. 이 함수에 대해서 잘 모르시는 분은 이전 포스트를 먼저 살펴 보세요.

16     // Build the name object
17     pName = PyString_FromString(argv[1]);
18
19     // Load the module object
20     pModule = PyImport_Import(pName);

PyString_FromString 함수를 이용하여 C/C++의 문자열을 pName이라는 이름을 가진 PyObject라는 파이썬 객체로 변환 합니다. 파이썬 API 문서를 보면 이 함수의 리턴 값이 'New reference'라고 되어 있습니다. 이것에 의미는 뒤에 설명 됩니다만, 지금은 이 함수의 리턴 값이 'New reference'라는 것에만 주목 해 주세요.

그리고 PyString_FromString 함수를 통해 만들어진 pName을 이용하여 파이썬 모듈을 로드합니다. 정확히는 로드라기 보다는 파이썬 모듈을 __builtins__ 라는 글로벌 변수에 넣는 것입니다만, 여기서는 단순히 파이썬 스크립트 파일을 로드 한다라는 정도로 알고 넘어 가도록 하겠습니다. 이 때 모듈파일을 로드하기 위해서 PyImport_Import 라는 함수가 사용됩니다. 파이썬 API문서를 따르자면 이 함수도 'New reference'를 리턴하는 군요. 이렇게 스크립트 파일의 로드가 끝났습니다.

22     // pDict is a borrowed reference
23     pDict = PyModule_GetDict(pModule);
24
25     // pFunc is also a borrowed reference
26     pFunc = PyDict_GetItemString(pDict, argv[2]);

파일에 대한 로드를 마치고, 파이썬 함수를 로드하는 부분입니다. PyModule_GetDict 함수는 모듈 내의 함수들을 담고 있는 딕셔너리(dictionary) 객체를 리턴 합니다.

PyDict_GetItemString 함수를 이용하여 지정된 딕셔너리 객체에서 함수를 얻어 올 수 있습니다. 이 함수들의 리턴 값은 이전에 나왔던 두 함수와는 달리 "Borrowed reference"라는 것에 주목 해 주세요. 예제에서는 함수를 저정하기 위해서 메인 함수의 두번째 파라메터를 이용했습니다.

28     if (PyCallable_Check(pFunc))

함수의 호출 가능여부를 위해 PyCallable_Check 함수를 이용합니다. 이 함수는 인자로 넘어오는 객체가 호출 가능한 함수일 경우 1을, 그렇지 않은 경우 0을 반환 합니다.

33             pArgs = PyTuple_New(argc - 3);
34
35             for (i = 0; i < argc - 3; i++)
36             {
37                 pValue = PyInt_FromLong(atoi(argv[i + 3]));
38                 if (!pValue)
39                 {
40                     PyErr_Print();
41                     return 1;
42                 }
43                 PyTuple_SetItem(pArgs, i, pValue);
44             }
45
46             pValue = PyObject_CallObject(pFunc, pArgs);
47
48             if (pArgs != NULL)
49             {
50                 Py_DECREF(pArgs);
51             }

PyObject_CallObject 함수를 이용해 파이썬 스크립트의 함수를 호출 합니다. 이 코드에서 두 가지를 유의 깊게 보셔야 합니다. 첫째는 C/C++에서 파이썬 함수로 파라메터를 넘겨주는 방법입니다. C/C++에서 파이썬 스크립트의 함수를 호출 하면서 파라메터를 넘겨 주기 위해서는 파라메터들을 하나의 튜플에 넣어야 합니다. 그 튜플을 만들어 주는 함수가 PyTuple_New 이구요, 인자로 튜플의 사이즈를 받습니다.  이렇게 생성된 튜플을 PyObject_CallObject의 파라메터로 넘겨 주므로써 파이썬 스크립트의 함수는 C/C++이 넘겨주는 데이터들을 인식 할 수 있는 것입니다. 만일 아무런 인자도 넘겨주지 않기를 원한다면 튜플 객체의 포인터 대신 'NULL'을 넘기면 됩니다.

둘 째는 Py_DECREF라는 함수입니다. 파이썬 API 문서를 살펴 보면 PyTuple_New 함수도 위에서 언급된 PyString_FromString, PyImport_Import 함수들 처럼 새로운 레퍼런스를 생성 한다고 합니다. 파이썬에서는 C/C++에서 처럼 사용자가 명시적으로 메모리를 할당 하고 해제 하는 불편함을 제거하는 대신 각 변수마다 레퍼런스 카운터를 두어, 이 레퍼런스 카운터가 0이 되면 변수에 할당된 메모릴를 해제하는 메커니즘을 가지고 있습니다. 하지만 그 메커니즘은 어디까지나 파이썬 인터프리터의 관리 하에 있을 때에 적용되는 것이고, C/C++ 영역에서는, 인터프리터가 관리 할 수 없으므로 사용자가 직접 레퍼런스 카운터를 더하고 빼는 작업을 해야만 합니다. 그래서 새로운 레퍼런스를 생성하는 PyObject객체를 만들게 되면 Py_DECREF 함수를 이용해 레퍼런스 카운터를 감소 시켜야만 합니다. 하지만 PyModule_GetDict, PyDict_GetItemString 같이 PyObject 객체를 만들기는 하지만 새로운 레퍼런스를 생성하는 것이 아닌 레퍼런스를 빌려 오는(레퍼런스 카운터가 증가 하지 않는)경우에는 Py_DECREF 함수를 적용해서는 안됩니다.

62     // Clean up
63     Py_DECREF(pModule);
64     Py_DECREF(pName);
65
66     // Finish the Python Interpreter
67     Py_Finalize();

파이썬 스크립트의 사용이 끝나면 할당된 자원들을 해제 해 주어야 합니다. pModule과 pName 객체는 새로운 레퍼런스를 리턴하는 함수에 의해 생성 되었기 때문에 Py_DECREF 함수를 통해 레퍼런스 카운터를 감소시켜 줘야만 합니다. 그리고 모든 파이썬 스크립트의 사용이 끝나면 Py_Finalize를 호출하여 파이썬 인터프리터의 전역 자원들을 해제합니다.

요약
임베딩 된 파이썬 함수를 호출하는 순서는

  1. 파이썬 인터프리터 초기화
  2. 파이썬 모듈(파일) 로드
  3. 모듈내의 함수를 딕셔너리 형태로 얻어오기
  4. 딕셔너리 내에서 호출 대상 함수 지정
  5. 함수의 호출 가능 여부 테스트
  6. 파라메터 변환 또는 파라메터가 불필요 한 경우는 NULL 셋팅
  7. 호출
  8. 할당된 자원 해제

정도로 요약 할 수 있겠습니다.

Ref.
 http://www.codeproject.com/KB/cpp/embedpython_1.aspx

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