목차

Embedded Python

개괄

파이썬이 다른 언어와 어떻게 잘 어울릴 수 있는지 증명하는 과정? 파이썬으로도 좋은 GUI를 만들 수 있겠지만, 시스템쪽으로 Qt library를 사용하기로 결정하였고, 이것으로 사용하면 더욱 좋은 그래픽 인터페이스를 만들 수 있겠다는 생각을 하였다. 그러나 네트워크 및 인증 모듈 등을 도입하는 과정에서 기존의 C/C++도 나쁘지는 않지만, 파이썬의 기능이 워낙 막강하고 편리하기 때문에 일단 파이썬을 핵심 모듈로 사용하고, 그 이외의 모듈은 Qt를 사용하는 방법을 채택하였다.

이 방법이 아름다운 이유는, 핵심 라이브러리는 여러 시스템, MacOS, Windows, Linux 등에 포팅된다. C/C++로 GUI를 일단 생성하기로 마음먹었지만, 차후 C# 내지는 다른 언어로도 작성될 수 있다고 생각한다. 그런데 이 핵심 모듈을 여러 언어로 작성하기보다, 파이썬으로만 고수한다면 어떨까? 핵심 모듈과 다른 모듈과의 인터페이스만 잘 맞추어주면 되는 것 아닐까.

파이썬 자체의 생산성이 워낙 높기 때문에 핵심 모듈이 점차 다른 언어로 뒷받침된다 하더라도 일단은 파이썬으로도 충분히 커버 가능할 것이다. 이런 이유로 embedded python을 계속 생각하게 되었다.

파이썬을 타 언어와 접목하는 방법에는 두 가지 요령이 있다. 다른 언어가 파이썬의 모듈을 실행하는 것, 반대로 파이썬에서 타 언어의 모듈을 실행하는 방법이다. 전자가 현재 문서에서 말하는 '내장된 파이썬(embedded python)'이고, 후자는 파이썬이 다른 언어로 '확장'되므로 '확장된 파이썬(extended python)'이라 칭한다.

이 문서에서는 내장된 파이썬에 주목하고 이것을 사용하기 위해 어떻게 해야 하는지에 대해 실질적으로 접근해 보도록 한다. 전반적인 사항에 대해서는 생략하고, 주로 파이썬에서 제공하는 C 라이브러리를 직접 사용하는 법과, boost library를 이용하여 보다 편리하게 사용하는 법에 대해 논하도록 하자.

물론 파이썬은 C/C++ 뿐 아니라 C#, Java 등과도 어울릴 수 있으나 문서를 참고하면 일단 C API에 대해서만 서술하고 있다. 일단 C API를 직접 활용하는 방법과, 그리고 boost::python 사용하여 보다 쉽게 코드를 작성하는 방법에 대해 기록하고자 한다.

설명하려는 사람이 가진 마법의 단어가 있다. “매뉴얼을 보세요.” 물론 나도 모든 것을 설명할 수 없기에 결국에는 이 마법의 단어를 사용할 수 밖에는 없겠지만, 최소한 매뉴얼을 보고 언제 어디서든 방법을 따라할 만큼의 디테일한 설명, 다시 말해 매뉴얼이 다 하지 못한 이야기 정도는 할 것이다.

이 문서가 설명하고자 하는 embedded python의 목표는 다음과 같다.

  1. 파이썬으로 작성한 모듈의 변수, 함수, 클래스를 외부 프로그램이 임의로 생성할 수 있도록 한다.
  2. 생성된 객체에 값을 입력하고, 접근하며, 출력을 받아올 수 있도록 한다.

C 라이브러리를 직접 활용하기

C 프로그램에서 파이썬 부리기: Hello, World

다음과 같은 코드를 작성합니다. 무지하게 간단하지만, 천천히 시작하도록 하지요.

hello_py.c
#include <Python.h>
 
int main(int argc, char** argv)
{
  Py_SetProgramName(argv[0]);
  Py_Initialize();
  PyRun_SimpleString("print \'Hello, World!\'\n");
  Py_Finalize();
  return 0;
}

유닉스 상에서 컴파일하기

문서를 참고하면 이렇게 작성한 코드는 컴파일을 해야 합니다. 하지만 라이브러리 링크 정보와 헤더 정보를 주지 않으면 에러가 나게 되겠죠. 문서는 이 라이브러리 링크 정보와 헤더 정보를 쉽게 알아내는 법에 대해 설명합니다.

파이썬 버전이 2.7인 경우,

그러므로 이 정보를 이용하면 다음과 같이 위 코드를 컴파일 할 수 있습니다.

gcc `python2.7-config --cflags` `python2.7-config --ldflags` -o hello_py hello_py.c

그런데 이렇게 매번 컴파일하기 힘드니, Makefile을 따로 만들어 두겠습니다.

Makefile
CC = /usr/bin/gcc
CONFIG = /opt/local/bin/python2.7-config
CFLAGS = `$(CONFIG) --cflags`
LDFLAGS = `$(CONFIG) --ldflags`
 
TAR = hello_py.out # 여기에 .out 파일을 추가하세요
OBJ = $(TAR:.out=.o)
SRC = $(TAR:.out=.c)
 
all: $(TAR)
.o.out: $(OBJ)
	$(CC) $(LDFLAGS) -o $@ $<
 
.c.o: %.c
	$(CC) $(CFLAGS) -c $<
 
clean:
	rm -fr $(TAR) $(OBJ)

TAR = hello_py.out 이 적힌 줄에 공백으로 계속 실행 파일의 이름을 입력하면 그에 맞게 소스가 컴파일됩니다.

이렇게 만들어진 바이너리 파일을 실행하면 다음과 같은 결과가 나올 것입니다.

Hello, World!

첫번째 실행 결과에 대해

결과는 성공적이지만, 왠지 석연치 않습니다. PyRun_SimpleString() 함수는 이름 그대로 너무 간단합니다. 우리의 목표에는 미치지 못하는 결과입니다.

두번째 코드: 함수 호출

두번째는 파이썬의 함수를 호출해 볼 것입니다. 치사하게 인자가 없는 함수를 부르진 않을게요. 이것도 문서에 있는 예제로, 다음과 같은 역할을 하도록 프로그램되어 있습니다.

multiply.py
def multiply(a,b):
    print "Will compute", a, "times", b
    c = 0
    for i in range(0, a):
        c = c + b
    return c

우리는 이 파이썬 함수를 C 코드에서 부를 것입니다. 그러나 이전처럼 뭔가 소스 코드가 하드코딩 되어 있다든지 하는 법 없이 동작하게 할 것입니다. 문서에서는 긴 소스를 죽 나열해두고 나중에 설명을 한 반면, 여기서는 어떻게 프로그램이 흘러가는지에 대해 중점을 맞추고 서술하겟습니다. 그 편이 더 이해하기 좋습니다.

Py_SetProgramName()

Py_Initialize() 함수가 호출되기 전에 호출해야 하는 함수입니다. 파이썬이 실행 중 가지게 되는 자신의 이름입니다. 그러니까 이 함수로 전달된 값은 파이썬의 sys.argv[0]와 동일한 것입니다. 이 함수는 선택적이어서 굳이 호출하지 않아도 됩니다. 이 경우 이름은 'python'이 된다고 합니다.

Py_Initialize(), Py_Finalize()

파이썬 API 사용 전후에 반드시 불려야 하는 함수입니다.

파이썬 코드로부터 어떤 일들이 일어나는지 먼저 유추해보기

우선 multiply.py를 부르는 파이썬 스크립트와, 그것을 실행했을 때 일어나는 과정에 대해 간단히 생각해보죠. 이걸 우선 유념해두고 코드를 읽는 편이 낫다고 생각합니다. 그럼 간단하게 multiply.py를 사용하는 파이썬 스크립트를 생각해 보죠.

import multiply
if __name__ == '__main__':
    print multiply.multiply(4, 5)

이 코드는 다음과 같이 동작합니다.

  1. multiply 모듈(multply.py 파일)을 로드합니다.
    1. 그러면 파이썬은 자신이 알고 있는 경로에서 multiply.py라는 파일을 찾아서 import를 시도합니다.
    2. 파일을 찾으면 성공적으로 모듈을 로딩할 것이고, 아니면 에러를 낼 것입니다.
  2. if 블록을 수행합니다. 파이썬은 명시적인 main() 함수가 없으므로 보통 이렇게 그 스크립트를 실행합니다. multiply 모듈의 함수 multiply를 호출합니다. 두 개의 정수 인자 4와 5도 같이 함수에 넘깁니다.
  3. 함수는 정의된 동작을 수행하고 정수를 하나 넘깁니다. 그리고 그것을 print문을 이용하여 출력합니다.
  4. 에러가 없다면 정상적으로 종료될 것입니다.

C 코드에서 파이썬 함수 호출

상당히 간단한 함수 호출이지만, 상당히 번거로운 작업들이 많습니다. 이 번거로운 작업들의 패턴은 크게 이렇게 생각할 수 있습니다.

C의 데이터를 파이썬의 객체형으로 표현

우선 C의 데이터를 파이썬의 객체형으로 표현하는 법에 대해 설명합니다. C에서 파이썬의 문자열을 생성하는 함수는 PyString_FromString()입니다. 리턴 형은 PyObject*인데 모든 파이썬의 데이터는 이 PyObject입니다. 파이썬의 어떤 자료형이라도 PyObject로 표현되므로, 항상 주의하여야 합니다.

  char *str = NULL;
  PyObject* pString;
  str = ...;
  pString = PyString_FromString(str);

위 코드에서 PyString_FromString()의 반환을 참조하는 pString과 다른 API를 활용하여 마치 파이썬에서 문자열 함수를 사용하는 것과 유사하게 사용할 수 있습니다. 그러나 모든 기능이 제공되는 것은 아닙니다. 문서의 Concrete Objects Layer를 참고하면 더 많은 자료형에 대해 객체 생성을 할 수 있습니다.

파이썬 모듈을 찾아 import 하기

Importing Modules의 문서를 참고합니다.

한 예는 PyImport_Import()입니다. 모듈 이름을 넘기면 모튤을 로딩합니다. 반환값은 그 모듈을 참조합니다. 이 때 모듈 이름은 C의 문자열이 아니라, 파이썬의 문자열입니다.

PyObject *pModuleName;
PyObject *pSysModule;
 
pModuleName = PyString_FromString("sys");
pSysModule = PyImport_Import(pModuleName);

파이썬의 변수/함수/클래스를 참조하고 호출하기

파이썬의 변수, 함수, 클래스는 모두 모듈의 '속성(attribute)'입니다. 그러므로 해당 모듈에서 변수, 함수, 클래스 등을 참조하려면, 'Object Protocol' 섹션의 PyObject_GetAttrString()함수를 이용합니다. 이 외에도 여러 함수가 있으므로 문서를 한 번 꼼꼼하게 읽어 둘 필요가 있습니다.

PyObject *pVersion;
PyObject *pFunction;
PyObject *pReturn;
 
pVersion = PyObject_GetAttrString(pSysModule, "version");
pFunction = PyObject_GetAttrString(pSysModule, "getfilesystemencoding");
pReturn = PyObject_CallObject(pFunction, NULL);
class sampleClass:
    def __init__(self, arg1, arg2, arg3):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3
        ...

위와 같은 파이썬 클래스가 정의되어 있습니다. 이것을 인스턴스화 하는 법은 아래와 같습니다.

PyObject *pSampleClass;
PyObject *pInstance;
PyObject *pArgList;
 
pSampleClass = PyObject_GetAttrString(pSampleClass, "SampleClass");
pArgList = Py_BuildValue("sid", arg1, arg2, arg3); /* string, integer, double */
pInstance = PyObject_CallObject(pSampleClass, pArgList);

PyObject_CallObject()는 호출할 수 있는 (callable) 오브젝트를 호출하고, 값을 반환받습니다. getfilesystemencoding는 호출 가능하므로 pReturn에 값이 반환될 것입니다. 반환된 값을 C에서 어떻게 끄집어 내야 할지는 좀 더 후에 서술하겠습니다.

반환된 값을 C로 끄집어내기

PyObject의 데이터를 C로 가져오는 함수는 Py[XXX]_As[YYY] 스타일로 정의되어 있습니다. 그래서 오브젝트가 실수형인 경우 실수에서 double 타입으로 가져올 때는 PyFloat_AsDouble()을 사용하고, 정수형인 경우 PyInt_AsLong()을 사용하는 형태를 취하고 있습니다.

int intReturn;
float floatReturn;
Py_complex complexReturn;
 
pIntReturn = PyInt_AsLong(pyIntObject);
pFloatReturn = (float)PyFloat_AsDouble(pyFloatObject);
pComplexReturn = PyComplex_AsCComplex(pyComplexObject);

리스트나 딕셔너리에서 값을 가져올 때는 PyList_Size(), PyList_GetItem(), PyList_SetItem(), PyDict_Size(), PyDict_GetItem(), PyDict_SetItem()을 사용합니다. 사용은 매우 직관적입니다.

레퍼런스 카운팅

파이썬에서는 메모리 관리를 할 필요가 없습니다. 내부적으로 메모리 관리를 해 주지요. 그러나 이 편리한 기능은 C API에서까지 사용할 수는 없습니다. 그러므로 직접 우리가 생성된 PyObject 마다 레퍼런스 카운팅을 해 주어야 합니다. Py_INCREF(), Py_DECREF(), 함수들은 다 그런 레퍼런트 카운팅을 위해 존재합니다!

한편 PyList_SetItem(), PyTuple_SetItem()이 두 함수는 레퍼런스 카운팅을 자기가 차지합니다.

두번째 예제 코드

이제 소스 코드를 볼께요. 간단한 함수 호출 하나를 하는데 좀 많이 난리를 쳐야 합니다. 원래의 소스 코드와는 달리 메모리 NULL 체크들은 생략했습니다. 물론 실전에서는 해서는 안 될 방법이지만, 핵심적은 흐름을 보다 명확히 보기 위해서입니다. 위 Makefile에 TAR 변수에 call.out을 추가해주세요.

call.out
#include <Python.h>
 
int main(int argc, char** argv)
{
  int i;
  PyObject *pModule, *pFunc;
  PyObject *pArgs, *pValue;
 
  if (argc < 3) {
    fprintf(stderr, "Usage: call.out pythonfile function [args]\n");
    return 1;
  }
 
  Py_SetProgramName(argv[0]);
  Py_Initialize();
 
  /* modified part */
  PyRun_SimpleString(
         "import sys\n"
         "sys.path.append('.')\n");
  pModule = PyImport_ImportModule(argv[1]);
 
  pFunc = PyObject_GetAttrString(pModule, argv[2]);
  pArgs = PyTuple_New(argc - 3); 
 
  for(i = 0; i < argc - 3; ++i) {
    pValue = PyInt_FromLong(atoi(argv[i+3]));
    PyTuple_SetItem(pArgs, i, pValue);
  }
 
  pValue = PyObject_CallObject(pFunc, pArgs);
  printf("Result of call: %ld\n", PyInt_AsLong(pValue));
  Py_DECREF(pArgs);
  Py_DECREF(pValue);
  Py_DECREF(pFunc);
  Py_DECREF(pModule);
 
  Py_Finalize();
  return 0;
}

if 블록을 다 제거하여 훨씬 보기는 편합니다. 원래 예제 코드와 다른 점은 바로 이 부분입니다.

PyRun_SimpleString("import sys\n"
		   "sys.path.append('.')\n");
pModule = PyImport_ImportModule(argv[1]);

처음 PyRun_SimpleString()은 현재 디렉토리도 모듈 검색 범위에 넣도록 하기 위해 추가된 것입니다. 아마 multiply.py 파일은 대개 이 소스 코드와 같은 경로에 두고 있을 것입니다. 그런데 이 코드를 넣지 않으면 아마 에러가 날 수도 있습니다. 그리고 원래 소스는 PyObject *pName을 선언하여 argv[1]을 파이썬의 문자열 형태로 만듭니다. 그리고 Py_DECREF()를 호출하지요. 그렇지만 char* 형태를 입력으로 받는 PyImport_ImportModule()이 있으므로 이것으로 갈음할 수 있습니다.

make
./call.out multiply multiply 3 5
Will compute 3 times 5
Result of call: 15

세번째 예제

세번째 예제는 조금 더 다양하게 가 보도록 하죠. urllib을 이용해 웹페이지의 소스 코드를 가져와, 외부 링크 즉, http…로 시작되는 URL을 가져오도록 합니다.

count_href.py
#!/usr/bin/python
import urllib
import re
 
href_expr = re.compile(r'<a href=\"(http://.+?)\"', re.DOTALL|re.MULTILINE)
 
class count_href(object):
	def __init__(self):
		self.results = []
 
	def count(self, url):
		html = self.get_html(url)
		found = href_expr.findall(html)
		d = {"url": url, "links": []}
		for item in found:
			d["links"].append(item)
		self.results.append(d)
 
	def get_html(self, url):
		opened = urllib.urlopen(url)
		html = opened.read()
		opened.close()
		return html
 
	def get_result(self):
		return self.results

이 모듈은 이렇게 사용하겠죠.

import count_href
 
if __name__ == '__main__':
	counter = count_href.count_href()
	counter.count('http://www.google.com/en')
	counter.count('http://www.daum.net/')
	counter.count('http://www.naver.com/')
 
	result = counter.get_result()
	for item in result:
		print "URL: " + item["url"]
		print len(item["links"])
		#for link in item["links"]:
		#	print link

이 모듈의 클래스를 C 코드에서 가져오도록 합니다.

count_href.c
#include <assert.h>
#include <Python.h>
 
void extract(PyObject* instance)
{
	PyObject *returned_object;
	Py_ssize_t i, list_size;
 
	returned_object = PyObject_CallMethod(instance, "get_result", NULL);
 
	/* returned_object: list of dicts.
		 every dict has keys: url, links
		 links is also list */
	list_size = PyList_Size(returned_object);
 
	for(i = 0; i < list_size; ++i) {
 
		PyObject *dict_item, *url, *links;
		Py_ssize_t links_size, j;
 
		dict_item = PyList_GetItem(returned_object, i);
		assert(dict_item != NULL);
 
		url = PyDict_GetItemString(dict_item, "url");
		assert(url != NULL);
 
		links = PyDict_GetItemString(dict_item, "links");
		assert(links != NULL);
 
		links_size = PyList_Size(links);
		printf("url: %s\t", PyString_AsString(url));
		printf("%d entries\n", (int)links_size);
 
		for(j = 0; j < links_size; ++j) {
 
			PyObject *entry_item = PyList_GetItem(links, j);
 
			assert(entry_item != NULL);
			printf("%s\n", PyString_AsString(entry_item));
		}
 
		Py_DECREF(returned_object);
	}
}
 
int main(int argc, char** argv)
{
	int i;
	PyObject *module, *class, *instance;
	int size;
 
	if(argc < 2) {
		fprintf(stderr, "Usage: count_href.out [URLS...]\n");
		return 1;
	}
 
	Py_Initialize();
	PyRun_SimpleString(
			"import sys\n"
			"sys.path.append('.')\n");
 
	module = PyImport_ImportModule("count_href");
	assert(module != NULL);
 
	class = PyObject_GetAttrString(module, "count_href");
	assert(class != NULL);
 
	instance = PyObject_CallObject(class, NULL);
	assert(instance != NULL);
 
	for(i = 1; i < argc; ++i) {
		printf("count_href: %s\n", argv[i]);
		PyObject_CallMethod(instance, "count", "s",  argv[i]);
	}
 
	extract(instance);
	Py_Finalize();
 
	return 0;
}

정리

boost.python 사용하기

C API를 활용하여 원하는 기능을 구현할 수도 있지만, 간단한 데이터를 가져오는데도 상당히 코드가 많이 필요합니다. API를 간단히 파악하는 정도로 두고, 보다 간결하고 편리하게 쓸 수 있는 라이브러리를 찾아야 할 것 같습니다.

파이썬 위키에 다른 언어와 파이썬을 연동하는 방법이 잘 나와 있습니다. 여기서는 C++의 대표적인 라이브러리인 Boost C++ Libraries를 사용하도록 하겠습니다.

Makefile
CC = /usr/bin/g++
PYTHON_CONFIG = /opt/local/bin/python2.7-config
 
BOOST_INC_PATH = /opt/local/include
BOOST_LIB_PATH = /opt/local/lib
CFLAGS = `$(PYTHON_CONFIG) --cflags` -I $(BOOST_INC_PATH)
LDFLAGS = `$(PYTHON_CONFIG) --ldflags` -L $(BOOST_LIB_PATH) -lboost_python-mt
 
TAR = test.out hello_py.out # 여기에 .out 파일을 추가하세요
OBJ = $(TAR:.out=.o)
SRC = $(TAR:.out=.cpp)
 
all: $(TAR)
.o.out: $(OBJ)
	$(CC) $(LDFLAGS)  -o $@ $<
 
.cpp.o: %.cpp
	$(CC) $(CFLAGS)  -c $<
 
clean:
	rm -rf $(TAR) $(OBJ)

첫번째 boost.python 예제

hello_py.cpp
#include <boost/python/exec.hpp>
 
int main(int argc, char** argv)
{
	Py_Initialize();
	boost::python::exec("print \'Hello, World!\'\n");
	Py_Finalize();
	return 0;
}

현재는 별다른 것이 없어 보이네요

두번째 boost.python 예제

call.cpp
#include <iostream>
#include <boost/python.hpp>
 
namespace bp = boost::python;
 
void workaround_for_local_modules()
{
	PyRun_SimpleString(
		"import sys\n"
		"sys.path.append('.')\n");
}
 
int main(int argc, char** argv)
{
    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }
 
	Py_Initialize();
	workaround_for_local_modules();
 
	try {
		bp::object module_name;
		bp::object module;
		bp::object func;
 
		module_name = bp::object(bp::handle<>(PyString_FromString(argv[1])));
		module = bp::object(bp::handle<>(PyImport_Import(module_name.ptr())));
		func = module.attr(argv[2]);
 
		if(func && PyCallable_Check(func.ptr())) {
			bp::object tuple;
 
			tuple = bp::object(bp::handle<>(PyTuple_New(argc - 3)));
			for(int i = 0; i < argc - 3; ++i) {
				PyObject* arg;
				arg = PyInt_FromLong(atoi(argv[i+3]));
				PyTuple_SetItem(tuple.ptr(), i, arg);
			}
 
			bp::object valueObj;
			int value;
 
			valueObj = bp::object(bp::handle<>(PyObject_CallObject(func.ptr(), tuple.ptr())));
			value = bp::extract<int>(valueObj);
 
			std::cout << value << std::endl;
		}
 
	} catch(bp::error_already_set const &) {
		PyErr_Print();
	}
 
	Py_Finalize();
	return EXIT_SUCCESS;
}

PyObjectboost::python::object로 래핑되어 있습니다. 또한 boost::python::handle<>을 이용하면 PyObject의 레퍼런스를 관리해 줍니다. boost::python의 대부분의 기능은 extending에 초점이 맞춰져 있고 embedding에는 그다지 많은 기능을 제공하지 않는 편이므로 상당히 많은 부분에서 C API를 그대로 가져와 사용해야 합니다.

세번째 boost.python 예제

count_href.cpp
 

참고 사이트