Python3でCのAPI

# なぜイマサラAPI？

Pythonを高速化させる方法は色々あると思います。  

* Pythonのリストの処理をNumpyの配列にする
* Numbaを使う
* Cythonを使う
* Boost.Pythonを使う
* SWIGを使う

他にもあるかもしれませんが、CをAPI化する理由としては。  

* 既に存在しているCのコードを利用したい
* 部分的な処理がボトルネックでCで実行すると改善することが見込まれる
* 再配布したい

特に最後の項目は、最近のsetuptoolsが進化して、buildとwheel化が容易になってきたため、API化も再考する余地がありそうです。  

筆者のようにCを全く書けない人間が、有識者にもらった小規模のCのプログラムを有効活用したい場合等、ニッチなところで需要があるような気がします。

# 題材

サンプルとして、[エラトステネスの篩](https://ja.wikipedia.org/wiki/エラトステネスの篩)というアルゴリズムを使ってみます。  
詳しくは寡聞にして知らないのでリンク先を参考にしていただくとして、素数判定法のアルゴリズムの一つで、指定された整数以下の素数を探索をします。  
例えば、与えられた整数が10であるならば、[2, 3, 5, 7]が探索されます。

Cのコードは[こちら](http://d.hatena.ne.jp/mizchi/20100802/1280705734#20100802f1)を参考にしました。 

```c
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

#define MAX 1000000

double gettimeofday_sec()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + (double)tv.tv_usec*1e-6;
}


double prim(int n)
{
  double start,end;
  start = gettimeofday_sec();

  int i, j;
  int p[n];

  for(i=0 ; i<n ; i++) p[i] = 0;
  p[0] = 1;
  
  for(i=2 ; i<=n/2 ; i++){
    for(j=2 ; i*j<=n ; j++){
      if(p[i*j-1] == 0)
        p[i*j-1] = 1;
    }
  }
  
  end = gettimeofday_sec();
  return end - start;
}
```

探索結果を全て出力すると多すぎるため、処理時間を返す関数としました。  

# 準備

## virtualenv
予め仮想環境の作成を強く推奨します。  
今回は`virtualenvwrapper`がインストールされていることを前提に進めていきます。  
仮想環境名は`api_test`とします。  
`mkvirtualenv`のキーワード引数`--python=`にビルドしたいpythonのバージョンを指定します。  
これは後述するパッケージのビルド(wheelの作成)に大きく影響するため、パッケージを配布する対象のPythonを指定します。  
筆者の場合、Pathは`/usr/bin/python3.5`となりましたが、ご自身の環境に合わせてください。

```bash
mkvirtualenv api_test --python=/usr/bin/python3.5
```

## ディレクトリ構成
任意のディレクトリを作成し、最終的には下記の構成になります。  

```bash
.
├── api_sample
│   ├── __init__.py
│   └── py
│       └── __init__.py
├── prim.c
└── setup.py
```

ファイルやディレクトリの作成がメンドウなかたは、筆者のgithubのリポジトリからcloneする方法もあります。  

```bash
git clone https://github.com/drillan/python3_c_api_sample.git
```

# CのコードをPython API化
ディレクトリ直下の`prim.c`を作成して編集します。

## ヘッダ
Python API を取り込みます。

```c
#include <Python.h>
```

`Python.h`で定義されているユーザから可視のシンボルは、全て接頭辞`Py`または`PY`が付いているようです。
システムによっては標準ヘッダの定義に影響するようなプリプロセッサ定義を行っているので、 `Python.h`をいずれの標準ヘッダよりも前にインクルードしないといけないようです。  

## Cの関数をPythonのオブジェクト化
今回は上記Cのサンプルコードの`prim()`関数をPythonのモジュールとして扱うことにします。  

```c
static PyObject *
prim(PyObject *self, PyObject *args)
{
  int n;
  if(!PyArg_ParseTuple(args, "i", &n))
    return NULL;
  
  double start,end;
  start = gettimeofday_sec();
  
  int i, j;
  int p[n];

  for(i=0 ; i<n ; i++) p[i] = 0;
  p[0] = 1;
  
  for(i=2 ; i<=n/2 ; i++){
    for(j=2 ; i*j<=n ; j++){
      if(p[i*j-1] == 0)
        p[i*j-1] = 1;
    }
  }
  
  end = gettimeofday_sec();
  return Py_BuildValue("d", end - start);
}
```

`self`引数には、モジュールレベルの関数であればモジュールが、メソッドにはオブジェクトインスタンスが渡されます。  
`args`引数は、引数の入った Pythonタプルオブジェクトへのポインタになります。タプル内の各要素は、呼び出しの際の引数リストにおける各引数に対応します。  
引数がPythonから与えられるため、このようなハンドリングが必要となります。  
また、与えられた引数はCの型に変換する必要があるため、`PyArg_ParseTuple()`を使用しています。  
第二引数の`"i"`はint型を指し、`"d"`ならdouble型、`"s"`ならchar型といった感じになります。
最後に`Py_BuildValue()`によって再度、Pythonで受け取れる型に返します。

このように、Cの関数をPythonのAPI化するには、Python -> C -> Pythonという流れを意識する必要があってメンドウですね。  

## メソッドテーブルの登録
上記で作成した`prim()`関数をPythonのメソッドとして呼び出すことができるよう、メソッドテーブルに登録します。  

```c
static PyMethodDef methods[] = {
	{"prim", prim, METH_VARARGS},
	{NULL, NULL}
};
```

3つのエントリは順番にメソッド名、C実装へのポインタ、呼び出しをどのように行うかを示すフラグビットになります。  
`METH_VARARGS`はPyCFunction型のメソッドで典型的に使われる呼び出し規約で、関数への引数がタプル形式で与えられます。  
キーワード引数を与えたい場合には`METH_KEYWORDS`を指定します。　

## docstring
通常は`PyDoc_STRVAR()`を使って生成するようです。  
後述のモジュールの登録時にdocstringとして登録できます。  

```c
PyDoc_STRVAR(api_doc, "Python3 API sample.\n");
```

## モジュールの登録
Pythonからimportできるよう、モジュール名等を登録します。  
モジュール名、docstring、モジュールのメモリ領域、メソッドを指定します。  

```c
static struct PyModuleDef cmodule = {
   PyModuleDef_HEAD_INIT,
   "c",   /* name of module */
   api_doc, /* module documentation, may be NULL */
   -1,       /* size of per-interpreter state of the module,
                or -1 if the module keeps state in global variables. */
   methods
};
```

今回、モジュール名は`c`としました。  
3つめの`-1`を指定すると、そのモジュールはグローバルな状態を持つためにサブ・インタープリターをサポートしていないということになるようです。筆者自身もわからないので、詳しく知りたいかたは[PEP 3121](https://www.python.org/dev/peps/pep-3121/)を参照してください。  

## 初期化関数
`c`モジュールがimportされた際に`PyInit_c()`関数が呼ばれます。  
`PyModule_Create()`関数にて上記で定義したモジュールを生成し、初期化関数に返します。(`__init__.py`みたいなもの？)  
`Pyinit_name()`関数はモジュール名によって名前が異なるので注意してください。  

## 最終的なprim.cの内容

```c:prim.c
#include <Python.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

#define MAX 1000000

double gettimeofday_sec()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + (double)tv.tv_usec*1e-6;
}

static PyObject *
prim(PyObject *self, PyObject *args)
{
  int n;
  if(!PyArg_ParseTuple(args, "i", &n))
    return NULL;
  
  double start,end;
  start = gettimeofday_sec();
  
  int i, j;
  int p[n];

  for(i=0 ; i<n ; i++) p[i] = 0;
  p[0] = 1;
  
  for(i=2 ; i<=n/2 ; i++){
    for(j=2 ; i*j<=n ; j++){
      if(p[i*j-1] == 0)
        p[i*j-1] = 1;
    }
  }
  
  end = gettimeofday_sec();
  return Py_BuildValue("d", end - start);
}

static PyMethodDef methods[] = {
	{"prim", prim, METH_VARARGS},
	{NULL, NULL}
};

PyDoc_STRVAR(api_doc, "Python3 API sample.\n");

static struct PyModuleDef cmodule = {
   PyModuleDef_HEAD_INIT,
   "c",   /* name of module */
   api_doc, /* module documentation, may be NULL */
   -1,       /* size of per-interpreter state of the module,
                or -1 if the module keeps state in global variables. */
   methods
};

PyInit_c(void)
{
	return PyModule_Create(&cmodule);
}
```

# `__init__.py`に登録
そのままビルドしてimportすることもできますが、パッケージを意識して`api_sample/__init__.py`に登録しておきます。

```python3:api_sample/__init__.py
import c
```

# おまけ、Pythonコードの作成
せっかくなのでPythonで同じコードを書いて速度を比較してみます。  
`api_sample/py/__init__.py`に`prim.c`と同等のコードを書いて保存します。  
上記とファイル名は同じですが階層が異なるので注意してください。  

```python3:api_sample/py/__init__.py
import time

MaxNum = 1000000


def prim(n):
    start = time.time()
    prime_box = [0 for i in range(n)]
    prime_box[0], prime_box[1] = 1, 1
    for i in range(n)[2:]:
        j = 1
        while i * (j + 1) < n:
            prime_box[i * (j + 1)] = 1
            j += 1
    end = time.time()
    return end - start

if __name__ == '__main__':
    print(prim(MaxNum))
```

# ビルド
いよいよビルドですが、なんと`setup.py`に書いておくだけで`setuptools`がビルドしてくれるようです。便利な時代になりました。  
ということで`setup.py`を作成します。

```python3:setup.py
from setuptools import setup, Extension

c = Extension('c', sources=['prim.c'])
setup(name="api_sample", version="0.0.0",
      description="Python3 API Sample",
      packages=['api_sample'], ext_modules=[c])
```
`setuptools.Extension()`でCのソースコードの場所を指定することで、Pythonの拡張としてビルドされることがポイントでしょうか。  
`setuptools.setup()`のキーワード引数`ext_modules`に上記で指定したモジュール名を指定することで、Pythonのモジュールとして呼び出すことが可能となります。  

```bash
python setup.py build
```

上記を実行することで必要なモジュールがビルドされ、`build`ディレクトリに保存されます。

# インストール
早速インストールして使ってみましょう。

```bash
python setup.py install
pip freeze
```
問題なくインストールできれば、下記のように出力されます。

```
api-sample==0.0.0
```

# 実行

まずはPythonのコードから実行してみます。

```bash
python -c "from api_sample import py; print(py.prim(1000000))"
```
筆者の環境ではこれくらいでした

```
5.6855926513671875
```

続いてCのコード。

```bash
python -c "from api_sample import c; print(c.prim(1000000))"
```

```
0.0776968002319336
```

大分速くなりました！

# 配布

wheelを作成します。

```bash
python setup.py bdist_wheel
```

筆者の環境では`api_sample-0.0.0-cp35-cp35m-linux_x86_64.whl`というファイルができました。

上記を実行すると`dist`ディレクトリに実行環境に合わせたwheelファイルが作成されます。  
virtualenvを推奨した最大の理由がこれで、仮想環境を切り替えてwheelを再作成することにより、環境に合わせたパッケージが作成されるため、配布が大変に便利です。  

# Windows環境
ビルドが必要なパッケージでよくネックとなるのがWindows環境です。  
通常はVisual Studioが必要なのですが、[Visual C++ Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools)をインストールすることでビルドが可能になります。  
これをインストールした状態で、`python setup.py bdist_wheel`を実行することで、Windows用のwheelがビルドされ、ビルド環境がないユーザに対しても配布することが可能となります。  
また、32bit、64bitそれぞれのPythonをインストールし、それぞれのvirtualenv環境でビルドすることにより、32bit/64bit両方に対応したwheelを作成することができます。  

[PyPI](https://pypi.python.org/)に各プラットフォームに対応したwheelを登録しておけば、ユーザは`pip install`で簡単にインストールすることができます。

# 参考リンク

* [C や C++ による Python の拡張](http://docs.python.jp/3/extending/extending.html)
* [C および C++ 拡張のビルド](http://docs.python.jp/3/extending/building.html)
* [Windows での Python 2.7, 3.4, 3.5 の拡張モジュールビルド環境](http://qiita.com/methane/items/2210712763b91e75fdf0)