Unix

SWIG와 Python3... C++ 클래스를 Python 3에서 사용하기

ForceCore 2012. 4. 10. 13:41


일단 swig를 설치한다.


Python에서도 쓰고싶은 C++ 클래스를 준비한다. 이미 만들어져 있다고 치자.


필자의 경우는 mo_solver.h 란 직접 만든 헤더파일이다. 이 안엔 클래스가 여러개 들어있다.


우선은 swig를 위해 Interface 파일을 작성해야 한다.

mo_solver.h 니까 mo_solver.i 라고 하지 뭐.


// mo solver wrapper for Python 3

%module MoSolver

%{

#include "mo_solver.h"

%}


/**

 * MO solver class

 *

 * r denotes the dimension of edge weight vector.

 */

class MoSolver

{

public:

    void printGraph( std::ostream& o ) const;


public:

    MoSolver( size_t num_nodes, size_t r );

    ~MoSolver();

public:

    void init( size_t num_nodes, size_t r );

    void free();

    void addEdge( size_t par, size_t child, ... );

    void addEdge( size_t par, size_t child, const moInts& weights );

    void solve_approx( const double epsilon );

    void show_sol( std::ostream& o );

    std::tuple< std::vector<size_t>, std::vector<int> > get_sol() const;

    int verbose;

};

이런 내용으로 되어있다.

% 로 시작하는 행은 SWIG에게 특별히 내려줄 명령을 담은 것이다.

%module MoSolver 는 파이선에서 보일 모듈/클래스의 이름이다.

%{

#include "mo_solver.h"

%}

는 출력된 Wrapper Class의 해더 부분에 들어갈 코드를 적는다. mo_solver.h가 필요할 것이 뻔하니 적어줬다.


밑에 있는 클래스스러운 것은 C/C++ 로 된 부분으로서 SWIG가 보고 잘 인식할 수 있다. 여기에 적은 부분만 Python과 연동하게 해준다... private 함수까지 연동할 필요는 없으니 원본 클래스에서 public 부분만 따왔다.


귀찮으면 그냥

%include "mo_solver.h"

로 이미 있는 헤더파일을 몽땅 파이선과 연동하라고 해도 된다.


사실 인터페이스 파일을 어떻게 작성해야하는진 잘 모르는 상태다. moInts 라고 std::vector<int> 를 typedef 해놓은 것도 있는데 SWIG가 잘 인식하기나 하려나 -_-;; 이런 세세한 것 보다는 일단은 Python에 클래스가 인식이나 되게 하는 것이 1차 목표이다. 실제로 addEdge() 라든지 이런 함수들이 Python에서 잘 작동하게 되는 것은 2차 목표.


http://www.swig.org/Doc1.3/Python.html#Python_nn10

매뉴얼을 컨닝해보니까 우선은 SWIG를 이용해 wrapper를 만들고, 그 wrapper와 원본 소스는 컴파일 해서 .so (dll파일 같은거)로 합체시켜야 한다고 하네.


swig -c++ -python mo_solver.i

우선 이렇게 wrapper를 생성한다. Python 2든, Python 3이든 호환되는 코드가 나오니 -python 이라고만 하면 된다. 그 외 펄도 지원한다고 하지만 아직은 필요 없다.


mo_solver_wrap.cxx 파일이 생성되었다. 그리고 MoSolver.py 파일도 생겼다. MoSolver.py 쪽이 Python쪽에서 보게되는 인터페이스이다. 이 파일은 직접 수정하지 말고, 수정할 일이 생기면 mo_solver.i 를 수정해야 한다 (고 적혀있기도 하다... 열어보면.).


이제 .so파일을 만들자.

g++ -O2 -fPIC -c mo_solver.cpp

-> mo_solver.o 생성

g++ -O2 -fPIC -c -I/usr/include/python3.2mu -c mo_solver_wrap.cxx

-> mo_solver_wrap.o 생성

주의사항은. /usr/include/python3.2mu 라고 된 부분은 바뀔 수 있다는 것이다. 각자 시스템을 잘 뒤져서 Python.h 가 있는 디렉토리를 적어주도록 하자.


마지막으로 저 두 파일을 묶어서 .so 파일을 만들자.

g++ -shared mo_solver.o mo_solver_wrap.o -o _MoSolver.so

주의사항. MoSolver 클래스를 만들던 중이었기 때문에 _MoSolver.so 라고 이름지은 것임!


이걸 Makefile로 만들면 아래와 같다.

PYTHON_INC=-I/usr/include/python3.2mu


all: MoSolver.py _MoSolver.so

_MoSolver.so: mo_solver.o mo_solver_wrap.o
g++ -shared mo_solver.o mo_solver_wrap.o -o $@

mo_solver_wrap.cxx: MoSolver.py
MoSolver.py: mo_solver.i mo_solver.h mo_solver.cpp
swig -c++ -python mo_solver.i

mo_solver.o: mo_solver.cpp mem_pool.hpp
g++ -std=c++0x -O2 -fPIC -c mo_solver.cpp

mo_solver_wrap.o: mo_solver_wrap.cxx
g++ -std=c++0x -O2 -fPIC $(PYTHON_INC) -c mo_solver_wrap.cxx

clean:
rm -f mo_solver_wrap.c mo_solver_wrap.cxx MoSolver.py
rm -f mo_solver.o mo_solver_wrap.o _MoSolver.so


테스트 파일을 만들어보자.

mo_test.py 다.

#!/usr/bin/python3


import sys

sys.path.append('./cxx')

from MoSolver import MoSolver


solver = MoSolver( 8, 3 )

solver.verbose = True

sys.path.append('./cxx') 이건 왜 있냐면, 필자가 mo_solver.cpp 파일을 cxx 디렉토리 안에다 따로 분리했기 때문이다.

즉, mo_test.py 가 있는 곳에 cxx란 디렉토리도 있고, cxx안에 _MoSolver.so, MoSolver.py 파일이 들어있다.

저걸 적지 않으면 그 다음 줄의 from MoSolver import MoSolver 가 성공하지 못한다.


저기까진 성공했다... 화면엔 아무것도 안 나온다. 다른 작업은 아무것도 안 시켰으니까. 만약

Traceback (most recent call last):

  File "./cxx/MoSolver.py", line 16, in swig_import_helper

    fp, pathname, description = imp.find_module('_MoSolver', [dirname(__file__)])

ImportError: No module named _MoSolver


During handling of the above exception, another exception occurred:


Traceback (most recent call last):

  File "./mo_test.py", line 5, in <module>

    from MoSolver import MoSolver

  File "./cxx/MoSolver.py", line 26, in <module>

    _MoSolver = swig_import_helper()

  File "./cxx/MoSolver.py", line 18, in swig_import_helper

    import _MoSolver

ImportError: No module named _MoSolver

이런 에러 메시지가 나오면 _MoSolver.so 가 없어서 그런거니 컴파일을 다시 잘 해볼 것.



이제 테스트 파일을 바꾼다.

#!/usr/bin/python3


import sys

sys.path.append('./cxx')

from MoSolver import MoSolver


solver = MoSolver( 8, 3 )

solver.verbose = True


solver.addEdge( 0, 1, [2, 7] )

solver.addEdge( 0, 2, [1, 7] )

solver.addEdge( 1, 3, [7, 4] )


원래의 C++ 클래스에는 두개의 addEdge 함수가 있었다.

void addEdge( size_t par, size_t child, ... );

void addEdge( size_t par, size_t child, const moInts& weights );

두번째 것을 염두에 둔 것인데...


이런 에러가 난다:

Traceback (most recent call last):

  File "./mo_test.py", line 10, in <module>

    solver.addEdge( 0, 1, [2, 7] )

  File "./cxx/MoSolver.py", line 85, in addEdge

    def addEdge(self, *args): return _MoSolver.MoSolver_addEdge(self, *args)

NotImplementedError: Wrong number or type of arguments for overloaded function 'MoSolver_addEdge'.

  Possible C/C++ prototypes are:

    MoSolver::addEdge(size_t,size_t,...)

    MoSolver::addEdge(size_t,size_t,moInts const &)

헷갈릴법도 하네...
moInts에 대해 잘 모르는건가... 안다 쳐도 Python은 타입이 C++과 달라서, addEdge의 ... (variable length args) 부분에 moInts 를 준거나 구분 못할 확률 100%. 둘 중 하나를 포기하지 뭐. 포기한다면 전자의 "..." 들어간 변종을 포기하겠다. mo_solver.i 의 클래스에서 "..."들어간 버전을 삭제.

이제 에러가 바뀌었다:
Traceback (most recent call last):
  File "./mo_test.py", line 10, in <module>
    solver.addEdge( 0, 1, [2, 7] )
  File "./cxx/MoSolver.py", line 85, in addEdge
    def addEdge(self, *args): return _MoSolver.MoSolver_addEdge(self, *args)
TypeError: in method 'MoSolver_addEdge', argument 4 of type 'moInts const &'
moInts 타입을 파라메터로 정성스럽게 잘 주라는 뜻이다? 이 부분에 대해서는...
moInts는 std::vector<int> 니까, 파이선자료구조 -> C++ STL Vector로 변환하는 방법을 찾아봐야 한다.

일단 .i 파일에 두 줄을 추가했다:
// mo solver wrapper for Python 3
%module MoSolver
%include "std_vector.i"
%template(moInts) std::vector<int>;

%{
#include "mo_solver.h"
%}
에러는 아직 똑같다... 하지만 mo_test.py를 업데이트 하면...

mo_test.py:
#!/usr/bin/python3

import sys
sys.path.append('./cxx')
from MoSolver import MoSolver, moInts

solver = MoSolver( 8, 3 )
solver.verbose = True

e = moInts( [2,7] )
print( e[0] )
print( e[1] )

solver.addEdge( 0, 1, e )

출력과 에러는:
2
7
Traceback (most recent call last):
  File "./mo_test.py", line 13, in <module>
    solver.addEdge( 0, 1, e )
  File "./cxx/MoSolver.py", line 161, in addEdge
    def addEdge(self, *args): return _MoSolver.MoSolver_addEdge(self, *args)
TypeError: in method 'MoSolver_addEdge', argument 4 of type 'moInts const &'
STL 벡터도 상당히 자연스럽게 생성되고 사용 가능함을 볼 수 있다. 하지만 여전히 addEdge 만 안된다. moInts 를 못 알아보나...

mo_test.py 스크립트에서 print( e )를 해보니까 
<MoSolver.moInts; proxy of <Swig Object of type 'std::vector< int > *' at 0x7f4aa035dbd0> >
이렇게 나온다... 즉, e는 C++로 치면 포인터라서 & 로 변환하려면, C++의 "*e"에 해당하는 것을 해줘야 한단 것이다. 좋은 힌트를 얻었다. 근데 그걸 어떻게 하지...

매뉴얼을 컨닝하면... References and Pointers...
그래도 모르겠다...
그리고 31.3.9의 Pointers, references, values and arrays 를 봐도 다 똑같이 처리되는 것 처럼 나와있다.

    void addEdge( size_t par, size_t child, const std::vector<int> &weights );
이렇게 바꾸니까 잡자기 잘 되기 시작한다... 어?!?!!!
이제 감 잡았다.

mo_solver.i:
// mo solver wrapper for Python 3
%include "std_vector.i"
%template(moInts) std::vector<int>;

%module MoSolver
%{
#include "mo_solver.h"
%}

typedef std::vector<int> moInts;

class MoSolver
{
public:
    void printGraph( std::ostream& o ) const;

public:
    MoSolver( size_t num_nodes, size_t r );
    ~MoSolver();
public:
    void init( size_t num_nodes, size_t r );
    void free();
    void addEdge( size_t par, size_t child, const moInts &weights );
    void solve_approx( const double epsilon );
    void show_sol( std::ostream& o );
    std::tuple< std::vector<size_t>, std::vector<int> > get_sol() const;
    int verbose;
};
Typedef도 넣어줘서 interface파일이 제대로 Python interface를 작성할 수 있도록 했다... -_-;;;;; 아예
%include "mo_solver.h"를 해버릴까... 그래도 손으로 만들지 뭐. -_-;;

수정된 mo_test.py:
#!/usr/bin/python3

import sys
sys.path.append('./cxx')
from MoSolver import MoSolver, moInts

solver = MoSolver( 8, 3 )
solver.verbose = True

solver.addEdge( 0, 1, [2, 7] )
이렇게 바로 Python의 list를 던져줘도 잘 된다. 오옷!

이제 ostream 을 파라메터로 던져주는 함수도 써보자... cout 이나 cerr를 파이선 인터페이스쪽에선 어떻게 받아들이지?... 잘 안될거라고 한다 ㅋㅋ. 양보해서 그냥 이건 C++쪽을 수정하지 뭐. std::ostream 을 파라메터로 받는...

    void printGraph( std::ostream& o ) const;

이런 함수는... Python의 sys.stdout, sys.stderr 이런 스트림을 std::ostream 으로 wrapping해야지만 쓸 수 있는데 그게 대단히 힘든 작업이라고 한다. 포기해야지. (http://stackoverflow.com/questions/4892808/swig-pass-stream-from-python-to-c)... 에, std_iostream.i 파일이 /usr/share/swig/2.0.4/python/std_iostream.i 으로 존재하는데 되긴 할 것 갇다. 그런데 별로 중요하지도 않은데 당장 열심히 파보고 싶진 않다.

그냥 parameter 안 받는 버전을 구현했다. 함수 하나만 더 짜면 되니깐.

그리고

    std::tuple< std::vector<size_t>, std::vector<int> > get_sol() const;

C++0x 에서 생긴 std::tuple은 좀... -_-;;;;; 그리고 tuple과 관련된 .i파일이 없다(/usr/share/swig/... 에). 아마 안 될 것 갇다. 역시, 그냥 C++파일을 고치는게 좋겠다. 한꺼번에 두 개의 값을 리턴하게 하려고 std::tuple을 썼는데 좀... 따로따로 나오게 하지 뭐...

// For python...
void MoSolver::get_sol(std::vector<size_t> *path, std::vector<int> *cost) const
{
tie( *path, *cost ) = get_sol();
}
이런 함수를 추가해주고...

path = []
cost = []
solver.get_sol( path, cost )
콜러쪽에선 위와 같이 했더니...
Traceback (most recent call last):
  File "./mo_test.py", line 28, in <module>
    solver.get_sol( path, cost )
  File "./cxx/MoSolver.py", line 164, in get_sol
    def get_sol(self, *args): return _MoSolver.MoSolver_get_sol(self, *args)
TypeError: in method 'MoSolver_get_sol', argument 2 of type 'std::vector< size_t,std::allocator< size_t > > *'
이렇게 되었다... size_t 에 대한 벡터에 대해서도 알려줘야겠군.

.i파일에 아래를 새로 넣고:
%template(moSizes) std::vector<size_t>;

프로토타입은 요렇게:

void get_sol( moSizes *path, moInts *cost ) const;

인터페이스에 적힌 것 뿐 아니라 원본 헤더도 적당히 바꿔준다...
파이선에서 콜 할때는 이렇게:
path = moSizes()
cost = moInts()
solver.get_sol( path, cost )

print( "Path:" )
for p in path :
print( p )

print( "Cost:" )
for c in cost :
print( c )

그랬더니 잘 되는군.



최종 완성된 파일들을 올려보겠다. mo_solver.h, mo_solver.cpp 는 실제파일은 올리기 좀 그렇기 때문에 (딱히 뭐 특별하네 비밀인것도 아니지만 -_- 너무 복잡해서 예제 이해에 오히려 방해가 된다.) 적당히 땜빵으로 바꿔놓은 것들이다.