Python 3.0 Hacks

第9回Python3にもC拡張モジュールを─Python3.0でも使える拡張モジュール開発手法の確立

目的

Python3.0に限らず、新しいバージョンのPythonがリリースされるたびに悩ましい問題があります。バイナリ製の拡張モジュールが豊富である所がPythonのひとつのウリなんですが、Pythonのメジャーバージョンをまたいでは動作しないのがツライところ。特にWindowsではコンパイラが標準でインストールされていないこともあり、他のOSほどソースからのビルドが容易ではありません。バイナリがリリースするまで待つことになると、拡張モジュールの新しいバージョンが出揃うのにどうしても時間がかかってしまうのです。これが、Pythonのニューバージョンの普及が出遅れる要因のひとつになっています。

この問題の主な要因に、ライブラリとPythonの「橋渡し部分」「C/C++の記述力」を利用して構築している事が挙げられます。ここの「橋渡し部分」のコンパイル作業によりPythonランタイムとの依存関係が発生しますので、⁠Pythonランタイムのバージョンアップ=リビルドが必要」になります。この「橋渡し部分」をPythonコードだけで作る方法を用いると、その手法で作ったモジュールはPythonのメジャーバージョンの違いに関係なく動作可能になります。最近のPythonはその橋渡しをPythonで記述できる「ctypes」というモジュールを標準で添付しています。また、ctypesベースの拡張モジュールもどんどん作られて増えてきています。

しかしこの「ctypes」は、C言語の型情報を考慮してctypes流儀の記述を自前で用意しなくてはいけないのが面倒なんです。Pythonの基本型とCの基本型の共通のものだけは自動的に変換してくれますが、その他の定数、列挙型、構造体、別名型定義などは無理です。開発中のctypeslibというツールセットを使うと、ctypes流儀のPythonコードを自動生成できます。

本記事では、C言語で作られたダイナミックリンクライブラリとヘッダから橋渡し部分のPythonモジュールを自動生成し、そのモジュールがPython3.0で動作することを示します。

仕組み

この手法では、まずC言語でダイナミックリンクライブラリを作成しておき、ctypeslibパッケージのh2xmlツールでgccxmlツールを経由して、Cヘッダを解析したXMLファイルを作成します。gccxmlの実装はgccにパッチを当てて、C/C++解析結果をXMLに出力できるようにしたものなので、そのXMLファイルには構文解析によって得られる詳細な情報が詰まっています。

次にctypeslibパッケージのxml2pyツールを利用してそのXMLファイルからPythonのコードだけでできた「橋渡し部分」つまり「ラッパーモジュール」を生成します。⁠ラッパーモジュール」はヘッダにある定義群(定数、列挙、関数、クラス、構造体、型宣言)をPythonのctypesに従った記述に置き換えたもので、普通ならctypes用のコードを書いて準備を手作業でやらねばならなかったところを、上記手法なら自動的に記述を生成できます。そのモジュールはPython2.5や2.6で動作するモジュールになっていますが、これを「Python3.0」付属の「2to3.py」ツールにてPython3.0用に変換することで「Python3.0」で動作可能なモジュールを構築できます。

利用するにはダイナミックリンクライブラリを環境変数PATHの通る場所に置いて、Pythonのモジュールをインポートするだけです。あとは、ヘッダで定義した定数や構造体を使って関数呼び出しも自在にできるようになっています。ヘッダに変更が無ければ、ダイナミックリンクライブラリのバージョン更新にも影響なく動作可能でなおかつctypesモジュールを持つPythonならどのバージョンでも動きます。

ctypeslibツールセットの準備

あらかじめ、gccxml-0.9.0をインストールしておきます。

上記サイトから gccxml-0.9.0-win32-x86.exe をダウンロード。バイナリインストーラを起動すれば簡単にインストールできます。次にctypeslibツールセットをインストールします。ソースからインストールしたい方以外は、以下のセクションはスキップしてください。

ctypeslibパッケージのインストール

以下のサイトからctypeslibパッケージを入手します。

svnか、TortoiseSVNでチェックアウトしましょう。ODEライブラリのために少しだけ修正を加えます(ODEを利用しない方はこの修正は無用です⁠⁠。

svnの場合

svn co http://svn.python.org/projects/ctypes/branches/ctypeslib-gccxml-0.9/ ctypeslib

その後以下の修正を行います。

fix(gccxmlparser.py line.112 add):
    def Converter(self, attrs): pass

fix(codegenerator.py line.58 change):
    "long double": "c_double",

その上で

ctypeslib> python setup.py install

とすると、⁠Pythonホーム/Scripts」フォルダにh2xml.pyとxml2py.pyがインストールされます。これらを利用することで、C言語ヘッダとダイナミックリンクライブラリからPythonモジュールを生成できるようになります。 しかし、このツール私の力不足でPython3ではうまく動きませんでした。

バイナリアーカイブの公開

上記セクションの処理を済ませたPython2.5バージョンのバイナリを配布します。

このアーカイブを適当なところに解凍して、そのフォルダパスを環境変数のPathに加えておいてください。

実際の手順

シンプルな利用例
h2xml windows.h -o windows.xml
xml2py windows.xml -w -s Beep -o beep.py

「-w」は実体をWindowsの標準DLLから検索するモードになります。標準でないDLLを利用する場合は「-w」の代わりに「-l hoge.dll」と名指しします。⁠-r」「-s」を省略すると、可能な限りのシンボルをコンバートします。⁠-s」は単独のシンボルを指定できます。⁠-r」は取り込むシンボルを正規表現で指定できます(⁠⁠-r」「-s」は複数指定できます⁠⁠。

beep.pyのテスト
>>> import beep
>>> beep.Beep(440, 500)
1

このようにWindowsAPIを呼び出すモジュールが簡単に作れます。Python3では文字列はUnicodeなので、末尾がAかWのAPIを利用する場合、WつきのAPIを利用することになります。

リスト1のバッチファイルを実行すると、WindowsのAPIがほとんど利用可能なモジュール「windows.py」ができます。

リスト1 build.bat
REM PYTHON3にはPython3.0をインストールしたフォルダを指定してください。
set PYTHON3=C:\Python30
set PY2TO3=%PYTHON3%\python %PYTHON3%\Tools\Scripts\2to3.py
set PYTHONPATH=

h2xml windows.h -DWIN32_LEAN_AND_MEAN -DUNICODE -c -q -o windows.xml
xml2py windows.xml -w -o windows.py
%PY2TO3% -w windows.py

windows.pyのテスト

>>> from windows import *
>>> MessageBox(0, 'メッセージ', 'タイトル', 0)
1
図1 テスト結果
図1 テスト結果

SDLのモジュールを作る!

あらかじめSDLをビルドするか、バイナリをダウンロードしましょう(とにかくヘッダ一式とダイナミックリンクライブラリさえあればいい⁠⁠。

上記に置いてあるSDL-devel-1.2.13-mingw32.tar.gzSDL_ttf-devel-2.0.9-VC8.zipのそれぞれから、includeフォルダ以下と*.dllだけを展開しておきます。

リスト2にあるコマンドでビルドできます。

リスト2 build.bat
REM PYTHON3にはPython3.0をインストールしたフォルダを指定してください。
set PYTHON3=C:\Python30
set PY2TO3=%PYTHON3%\python %PYTHON3%\Tools\Scripts\2to3.py
set PYTHONPATH=

h2xml SDL.h SDL_ttf.h -c -q -I./SDL -DSDLCALL= -o sdl.xml
xml2py sdl.xml -lSDL.dll -lSDL_ttf.dll -o sdl.py
%PY2TO3% -w sdl.py

コツはxml2pyのとき、作業中のフォルダにダイナミックリンクライブラリを置くことです。h2xmlの「-I」「-D」オプションはgccのものと同じ意味です。⁠-I」がインクルードファイル検索パスで、⁠-D」がマクロ定数定義です

sdl.pyのテスト

利用時は、ダイナミックリンクライブラリをctypesによって発見できるフォルダに置く必要があります。カレントフォルダまたは環境変数Pathに示した場所に置いておけば、ctypesのローダーが発見してくれます。

リスト3 sdl.pyのテスト用コード
 1  from sdl import *
 2 
 3  AVERAGE_NUM = 5
 4 
 5  def main_loop():
 6    frames = [0]*AVERAGE_NUM
 7    event = SDL_Event()
 8    now = SDL_GetTicks()
 9    begin = now
10    fps_suf = None
11    while 1:
12      while SDL_PollEvent(event):
13        if event.type==SDL_QUIT:
14          return
15        elif event.type==SDL_KEYDOWN:
16          if event.key.keysym.sym==SDLK_ESCAPE:
17            return
18      t = SDL_GetTicks()
19      dt = t-now
20      now = t
21      if now22        frames[-1] += 1
23      else:
24        frames.pop(0)
25        frames.append(0)
26        begin = now
27        fps_str = '{0:5.1f}fps'.format(sum(frames)/AVERAGE_NUM).encode('utf-8')
28        fps_suf = TTF_RenderUTF8_Blended(
29          DEFAULT_FONT, fps_str, SDL_Color(255,255,255,0))
30      SDL_FillRect(screen, None, 0)
31      update(dt)
32      if fps_suf:
33        SDL_BlitSurface(fps_suf, None, screen, SDL_Rect(372,424))
34      SDL_UpdateRect(screen, 0, 0, 0, 0)
35      SDL_Delay(0)
36 
37  def update(dt):
38    pass#実際の描画や計算など。
39 
40  SDL_Init(SDL_INIT_VIDEO)
41  TTF_Init()
42 
43  DEFAULT_FONT = TTF_OpenFont('C:\\windows\\fonts\\couri.ttf', 48)
44  dummy = TTF_FontHeight(DEFAULT_FONT) # font check
45 
46  options = SDL_SWSURFACE #| SDL_FULLSCREEN
47  screen = SDL_SetVideoMode(640, 480, 0, options)
48  SDL_WM_SetCaption("Sample", "Sample")
49 
50  main_loop()
51 
52  TTF_CloseFont(DEFAULT_FONT)
53  SDL_Quit()
図2 テスト結果
図2 テスト結果

ODEのモジュールを作る!

あらかじめODEをビルドするか、バイナリをダウンロードしましょう。⁠とにかくヘッダ一式とダイナミックリンクライブラリさえあればいい)

リスト4のコマンドでビルドできます。

リスト4 build.bat
REM PYTHON3にはPython3.0をインストールしたフォルダを指定してください。
set PYTHON3=C:\Python30
set PY2TO3=%PYTHON3%\python %PYTHON3%\Tools\Scripts\2to3.py
set PYTHONPATH=

h2xml ode/ode.h -Iode-0.11/include -c -q -o ode.xml
xml2py ode.xml -l libode-1.dll -o ode.py
%PY2TO3% -w ode.py

ode.pyのテスト

リスト5 自然落下現象を再現してみる。
 1  from ode import *
 2 
 3  dInitODE2(0)
 4  world = dWorldCreate()
 5  dWorldSetGravity(world,0,0,-9.80665)
 6 
 7  body = dBodyCreate(world)
 8  dBodySetPosition(body, 0.0, 0.0, 10.0)
 9  m = dMass()
10  dMassSetBox(m, 1, 0.1, 0.1, 0.1)
11  dMassAdjust(m, 1.0)
12  dBodySetMass(body, m)
13 
14  for i in range(15):
15    z = dBodyGetPosition(body)[2]
16    print('{0:4.1f}sec z={1:5.2f}'.format(i/10, z))
17    dWorldStep (world,0.1)
18 
19  dWorldDestroy(world);
20  dCloseODE();
リスト6 出力結果
0.0sec z=10.00
0.1sec z= 9.90
0.2sec z= 9.71
0.3sec z= 9.41
0.4sec z= 9.02
0.5sec z= 8.53
0.6sec z= 7.94
0.7sec z= 7.25
0.8sec z= 6.47
0.9sec z= 5.59
1.0sec z= 4.61
1.1sec z= 3.53
1.2sec z= 2.35
1.3sec z= 1.08
1.4sec z=-0.30

10m上空にあった箱を自由落下させると1.4秒後には地面に落下するという結果になりました。理論値は1.43秒なので微妙に誤差がありそうですが……。

OpenCVのモジュールを作る!

ctyes-opencvというモジュールがすでに公開されています。それはctypeslibを使っていなさそう? なんですが、結果としてはctypesによるPurePythonモジュールになっていますので「2to3.py」ツールでコンバートすれば動くようになりました。ですが、ここではあえてもう一度今回の手法で構築してみましょう。あらかじめ OpenCV1.0バイナリ版 をインストールしてください。

リスト7のコマンドでビルドできます。

リスト7 build.bat
REM PYTHON3にはPython3.0をインストールしたフォルダを指定してください。
REM OPENCV_DIRには先のOpenCVをインストールしたフォルダを指定。
set PYTHON3=C:\Python30
set PY2TO3=%PYTHON3%\python %PYTHON3%\Tools\Scripts\2to3.py
set PYTHONPATH=
set OPENCV_DIR=C:\Program Files\OpenCV

h2xml cxcore.h -I"%OPENCV_DIR%\cxcore\include" -c -q -o cxcore.xml
h2xml cv.h -I"%OPENCV_DIR%\cxcore\include" -I"%OPENCV_DIR%\cv\include" -c -q -o cv.xml
h2xml highgui.h -I"%OPENCV_DIR%\cxcore\include" -I"%OPENCV_DIR%\otherlibs\highgui" -D WIN32_LEAN_MEAN -c -q -o highgui.xml

xml2py cxcore.xml -lcxcore100.dll -o cxcore.py
xml2py cv.xml -lcv100.dll -o cv.py
xml2py highgui.xml -lhighgui100.dll -o highgui.py
%PY2TO3% -w cxcore.py
%PY2TO3% -w cv.py
%PY2TO3% -w highgui.py

cv、highguiを使ったテスト

リスト8 カメラキャプチャサンプル、ESCキーで終了します。
 1  from cv import *
 2  from highgui import *
 3 
 4  cvNamedWindow('Camera', CV_WINDOW_AUTOSIZE)
 5  capture = cvCreateCameraCapture(0)
 6  while 1:
 7    frame = cvQueryFrame(capture)
 8    cvShowImage ('Camera', frame)
 9    if cvWaitKey(5)&255 == 27:
10      break

OpenGLのモジュールを作る!

glutのヘッダとライブラリを別途入手しておきます。以下のリンクを参考に入手しました。

リスト9にあるコマンドでビルドできます。

リスト9 build.bat
REM PYTHON3にはPython3.0をインストールしたフォルダを指定してください。
set PYTHON3=C:\Python30
set PY2TO3=%PYTHON3%\python %PYTHON3%\Tools\Scripts\2to3.py
set PYTHONPATH=

h2xml GL/gl.h -c -q -o gl.xml
h2xml GL/glu.h -c -q -o glu.xml
h2xml glutdlls37beta/glut.h -c -q -I. -D_STDCALL_SUPPORTED -D_WCHAR_T_DEFINED -o glut.xml

xml2py gl.xml -lopengl32.dll -o gl.py
xml2py glu.xml -lglu32.dll -o glu.py
xml2py glut.xml -lglut32.dll -o glut.py
%PY2TO3% -w gl.py
%PY2TO3% -w glu.py
%PY2TO3% -w glut.py

OpenGLの描画テスト

リスト10 OpenGLの描画テスト
 1  from ctypes import *
 2  from gl import *
 3  from glu import *
 4  from glut import *
 5 
 6  argc = c_long(1)
 7  argv = (c_char_p*1)()
 8  argv[0] = ''
 9  glutInit(argc, argv)
10  glutInitDisplayMode(0)
11  glutInitWindowSize(200,200)
12  glutInitWindowPosition(0,0)
13  glutCreateWindow("Test")
14  glClearColor(0.0, 0.0, 1.0, 1.0)
15 
16  def display():
17    glClear(GL_COLOR_BUFFER_BIT)
18    glutSolidTeapot(0.5)
19    glFlush()
20 
21  def keyboard(ch, param1, param2):
22    if ch==0x1b:
23      quit()
24    else:
25      print(ch, param1, param2)
26 
27  keyboard_cb = CFUNCTYPE(None, c_ubyte, c_int, c_int)(keyboard)
28  glutKeyboardFunc(keyboard_cb)
29  display_cb = CFUNCTYPE(None)(display)
30  glutDisplayFunc(display_cb)
31 
32  glutMainLoop()
図3 リスト10の実行結果
図3 リスト10の実行結果

まとめ

ctypeslibを利用することで、多くのC言語による資産を簡単に取り込めることがわかりました。あと、Linuxなどでも「.dll」ファイルの代わりに「.so」ファイルを利用して同様な手順で拡張モジュールを作ることができるようです。ctypesベースの場合、ダイナミックリンクライブラリのバージョンアップでもヘッダに変更がなければそのまま動作することや、 Pythonのバージョンアップにも影響なく動作することは非常に助かります。あと試してませんがPygletやPyOpenGLもctypesベースなので、⁠2to3」ツールで移行できるかも。

欠点としてはC++のライブラリが取り込めないのと、Unicodeをshortアレイ扱いしている定義では本当にshortアレイに変換しなくてはならないところや、ネイティブコードが行っていた橋渡しをPythonコードで行う以上オーバーヘッドが増えていることなどが挙げられます。

しかし、今回の内容だけでもWindowsAPI、SDL、ODE、OpenCV、OpenGLなどがPython3で利用可能になりました。Python3への全面移行は当分先だと思っていましたが、その時期は意外ともうすぐなのかもしれませんね。

おすすめ記事

記事・ニュース一覧