Pythonには縁もゆかりもないC/C++をdistutilsでビルドする

Pythonには縁もゆかりもないC/C++をdistutilsでビルドする

前置き

Microsoft Visual C++ Compiler for Python 2.7はひとのためならず」に関係するといえばする。元はと言えばDistutilsは「Pythonモジュールを作るため」のものであろうから、これは「やや目的外使用」ということにはなるけれど、もちろんこれは「出来る」。

Makefile に苦痛を感じないのはきっと「ある特定の linux でしか使ったことがない」からだ。SYSV make、BSD make だけで存分に嫌になって、FSF の make に安心しつつあったのに MS の nmake でどん底に突き落とされた経験がないからだ。移植性のある makefile なんか無理だし、だいいち記述量が多過ぎるではないの、鬱陶しいたらありゃしない。Eclipse が解決? 別にそうでもないでしょーよ。

気が短い方へ

今回の「全部」は build_non_python_with_distutils_example.zip に固めてあります。気が短い方はこれだけでもいいと思うよ。

あ、Microsoft Visual C++ Compiler for Python 2.7 使う場合は、setuptools最新はまだCythonのためならずを先に読んで下さい。

SConsという手もあるが

SConsも良いのだが、「やりすぎ」が仇となっているところもあるし、(少なくとも2.3.0では)Microsoft Visual C++ Compiler for Python 2.7 に対応出来ないので、今回はパス。

一番しょうもなくて簡単なやつ

一個の C ソースファイルから一個の実行ファイルを作るだけの。

C はこんなな。(何するものかは察してください。)

ohce.c
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int main(int ac, char** av)
 5 {
 6     int i = 1;
 7     for (; i < ac; ++i) {
 8         if (i > 1) {
 9             fputs(" ", stdout);
10         }
11         /* */ {
12             const char* p = av[i] + strlen(av[i]);
13             char* rev = (char*)malloc(strlen(av[i]) + 1);
14             char* revp = rev;
15             while (p >= av[i]) {
16                 *revp++ = *--p;
17             }
18             *revp = 0;
19             fputs(rev, stdout);
20             free(rev);
21         }
22     }
23     fputs("\n", stdout);
24     return 0;
25 }

これを distutils でビルドするための setup.py はこれだけ:

setup.py
 1 # -*- coding: utf-8 -*-
 2 # -----------------------------------
 3 from distutils import log
 4 from distutils import ccompiler, sysconfig
 5 
 6 log.set_verbosity(1)  # INFO
 7 
 8 cc = ccompiler.new_compiler()
 9 sysconfig.customize_compiler(cc)
10 #cc.set_include_dirs([])
11 objects = cc.compile(['ohce.c'])
12 execname = 'ohce'
13 cc.link_executable(objects, execname)
1 me@host: ~$ python setup.py build

でビルドするのだぞ。

C++ になったくらいではへこたれない

ohce.cpp
 1 #include <cstdio>
 2 #include <cstdlib>
 3 #include <string>
 4 #include <algorithm>
 5 
 6 int main(int ac, char** av)
 7 {
 8     for (int i = 1; i < ac; ++i) {
 9         if (i > 1) {
10             fputs(" ", stdout);
11         }
12         std::string rev;
13         std::reverse_copy(
14             &av[i][0], &av[i][std::strlen(av[i])],
15             std::back_inserter(rev));
16         fputs(rev.c_str(), stdout);
17     }
18     fputs("\n", stdout);
19     return 0;
20 }
setup.py
 1 # -*- coding: utf-8 -*-
 2 # -----------------------------------
 3 from distutils import ccompiler, sysconfig
 4 
 5 cc = ccompiler.new_compiler()
 6 sysconfig.customize_compiler(cc)
 7 #cc.set_include_dirs([])
 8 if cc.compiler_type == 'msvc':
 9     extra_preargs = ['-EHsc']
10 objects = cc.compile(['ohce.cpp'], extra_preargs=extra_preargs)
11 execname = 'ohce'
12 cc.link_executable(objects, execname)

MSVC 特有の癖には毎度苦痛は感じるねぇ。

static link ライブラリのビルドとそれにリンクする実行ファイル

サンプルを書くのが面倒くさくなって、「GeographicLib抱え込み」を例にしてみることにした。

こんな構造↓…で通じる?

これの setup.py はこんな:

setup.py
 1 # -*- coding: utf-8 -*-
 2 # 何も問題なさげにみえて、そうでもない。
 3 # distutils の制限で、Geographiclib-1.40 がこの階層よりも上、
 4 # つまり「../」を含んでいると、distutils はこれを制御しようと
 5 # しない。(output_dir="_build" は意味をなくす。)
 6 # -----------------------------------
 7 import os
 8 import re
 9 from distutils import log
10 from distutils import ccompiler, sysconfig, dep_util
11 
12 log.set_verbosity(1)  # INFO
13 
14 cc = ccompiler.new_compiler()
15 sysconfig.customize_compiler(cc)
16 cc.set_include_dirs(['Geographiclib-1.40/include'])
17 if cc.compiler_type == 'msvc':
18     extra_preargs = ['-EHsc']
19 
20 # build geographiclib static link library
21 import glob
22 
23 srcs = list(glob.glob("Geographiclib-1.40/src/*.cpp"))
24 objs = ["_build/" + re.sub(r"\.cpp$", cc.obj_extension, src) for src in srcs]
25 
26 # compile if each source is newer than its corresponding target.
27 for src in dep_util.newer_pairwise(srcs, objs)[0]:
28     cc.compile([src], "_build", extra_preargs=extra_preargs)
29 cc.create_static_lib(objs, "GeographicLib", ".")
30 #
31 log.set_verbosity(0)  # 
32 
33 for fn in glob.glob("GeographicLib-1.40/examples/*.cpp"):
34     objects = cc.compile([fn], extra_preargs=extra_preargs)
35     execname = os.path.splitext(os.path.basename(fn))[0]
36     cc.link_executable(objects, execname, libraries=['GeographicLib'])

dep_util がポイントだろうな。これしないと、毎回必要もないのにコンパイルしに行っちゃうぞ。

dynamic link ライブラリのビルドとそれにリンクする実行ファイル

構造は static link のと同じで、setup.py の「下」に Geographiclib-1.40 がいる。

これの setup.py は以下:

setup.py
 1 # -*- coding: utf-8 -*-
 2 # 何も問題なさげにみえて、そうでもない。
 3 # distutils の制限で、Geographiclib-1.40 がこの階層よりも上、
 4 # つまり「../」を含んでいると、distutils はこれを制御しようと
 5 # しない。(output_dir="_build" は意味をなくす。)
 6 # -----------------------------------
 7 import os
 8 import re
 9 from distutils import log
10 from distutils import ccompiler, sysconfig, dep_util, errors
11 
12 log.set_verbosity(1)  # INFO
13 
14 cc = ccompiler.new_compiler()
15 sysconfig.customize_compiler(cc)
16 cc.set_include_dirs(['Geographiclib-1.40/include'])
17 if cc.compiler_type == 'msvc':
18     extra_preargs = ['-EHsc']
19 
20 # build geographiclib dynamic link library
21 import glob
22 
23 srcs = list(glob.glob("Geographiclib-1.40/src/*.cpp"))
24 objs = ["_build/" + re.sub(r"\.cpp$", cc.obj_extension, src) for src in srcs]
25 
26 # compile if each source is newer than its corresponding target.
27 for src in dep_util.newer_pairwise(srcs, objs)[0]:
28     # Windows 向け「__declspec(dllexport)」の対応方法はプロジェクトによりマチマチだが、
29     # GeographicLib と同じやり方が最も標準的。(MS 推奨でもある。)
30     # GeographicLib についてのそれについては、Constants.hpp 参照。
31     cc.compile([src],
32                "_build",
33                extra_preargs=extra_preargs + [
34             "-DGEOGRAPHICLIB_SHARED_LIB=1", "-DGeographicLib_EXPORTS"])
35 cc.link_shared_lib(objs, "GeographicLib", ".")
36 #
37 log.set_verbosity(0)  # 
38 
39 for fn in glob.glob("GeographicLib-1.40/examples/*.cpp"):
40     objects = cc.compile([fn], extra_preargs=extra_preargs)
41     execname = os.path.splitext(os.path.basename(fn))[0]
42     try:
43         cc.link_executable(objects, execname, libraries=['GeographicLib'])
44     except errors.LinkError as e:
45         # static link 版では起こらないリンクエラーが出るのがいる。
46         # 「distutils で 非python な C/C++ ビルド」のお題の範疇外
47         # なので気にしていないが、まぁこれは良く起こる。
48         # おそらくコンパイラオプションかリンカオプションに何か追加
49         # すれば取れると思う。(典型的には /MD とかだな。)
50         print(e)

こうなってくると、「非 Windows プログラマ」もしくは「非 C/C++ プログラマ」にはキツくなってくるわな。これぞ C/C++ の移植性地獄。

それでも GeographicLib の移植性は優秀。

distutilsだと何が嬉しいか?

結局 Python フル機能使えるので、てことな。移植性についても、まぁまぁ良い塩梅で維持出来る。

繰り返すけど、「Microsoft の nmake と SYSV make と BSD make と GNU make」に苦しんだことがないから「Makefile便利!」とか「別に大変じゃないよ」なんて言えるのだ。(CMake も入れておこうか? べつもんだけどな、これは。)

build_non_python_with_distutils_example.zipについての注意

zip の中にも同内容の注意書き入れてますが、

 1 サンプルを自作するのが面倒だったので、非常に移植性が高く linux、
 2 Windows で一度も苦労したことがない GeographicLib を例にしている、
 3 が、GeographicLib 自身もちゃんと「Windows 用ビルド環境」も、
 4 「インストーラ」も提供してくれているので、ここでやっているのは
 5 あくまでも例である。
 6 
 7 ライブラリを抱え込みたい場合にはこのようなことは良くやる。
 8 この方式を採れば、「公式バイナリが依存するランタイム」に振り回される
 9 こともないので、良い事もある。無論それは「公式のバグフィックスリリー
10 ス」を無碍にすることも意味するので、抱え込むべきかどうかはいつだって
11 悩みどころである。一般には「やらないほうがいい」とはなる。
12 (静的ライブラリよりは良い。DLL を置き換えるだけで良い、となる可能性が
13 あるから。)
14 
15 ※「GeographicLib-1.40」の中身は公式配布の全ては含まれてないです。
16   必要な場合は http://sourceforge.net/projects/geographiclib/ へ。

です。