EurekaMoments

ロボットや自動車の自律移動に関する知識や技術、プログラミング、ソフトウェア開発について勉強したことをメモするブログ

PythonからC言語の関数を呼び出してユニットテストする環境を作る: Cython編

目次

はじめに

自分は普段の仕事でC言語のプログラムを実装するのがメインです。しかしながら、今の職場にて自分が実装したC言語のソフトをデバッグするためには、実際のコントローラに書き込んでからベンチシミュレータで動かす、くらいしか方法がありません。
いちいちコントローラに書き込んだりするのが面倒だし、コントローラ内部でステップ実行なんて事もできないので、開発用PCの中である程度のデバッグを行う方法はないか、と考えていたところで思いついたのが、PythonからC言語のプログラムを呼び出してデバッグするというものでした。
今回は、数あるやり方の一つであるCythonを使ったやり方を試してみたので、その流れや注意点について書こうと思います。

Cythonとは

C言語で書かれたコードをPythonから呼び出すための仲介役になってくれるもの、というのが自分の認識です。これまで自分はCythonという言語の存在を知らなかったのですが、下記の記事のおかげで基本的な部分は理解できたと思います。

qiita.com

qiita.com

PythonとC言語を組み合わせるメリット

デバッグしたいC言語の関数をPythonのスクリプトから呼び出し、その実行結果をユニットテストで評価したり、結果をmatplotlibで可視化したりできるようにするのが今回の狙いです。これによってデバッグの質をあげることができます。また、テストコード側をPythonにすることで、テストケースを変更したり追加したりするたびにビルドする手間が省けるのもメリットの一つだと思います。

参考記事

今回の取り組みにあたりいろいろハマりました。そういう時には下記の記事が大変参考になりました。

darden.hatenablog.com

ステップ1: C言語のコードを書く

まずはデバッグしたいC言語のコードを準備します。今回は例として、下記のようなシンプルな足し算を行う関数を実装しました。

こちらがヘッダーファイルのClangSample.hです。二つの変数をメンバとして保持する構造体を定義してそのオブジェクトを返す関数、そしてその2変数を足し算して計算結果を返す関数の二つを宣言しています。

#ifndef __CLANG_SAMPLE_H__
#define __CLANG_SAMPLE_H__

#include <stdio.h>

typedef struct{
    int a;
    int b;
}Pair;

extern Pair make_pair(int a, int b);

extern int add(int c, int d);

#endif // __CLANG_SAMPLE_H__

次にこちらが、各関数の詳細を定義したClangSample.cファイルです。

#include "ClangSample.h"

int main(void)
{
    printf("%d + %d = %d\n", 3, 4, add(3, 4));
    return 0;
}

int add(int c, int d)
{
    return c + d;
}

Pair make_pair(int a, int b)
{
    Pair p;

    p.a = a;
    p.b = b;

    return p;
}

ステップ2: C言語のコードをラッピングするCythonコードを書く

ここで書くCythonコードは、PythonコードとC言語との仲介役になります。Cythonのコードを書いたファイルは.pyxという拡張子になります。ファイル名はUnitTestSample.pyxとします。

# -*- coding: utf-8 -*-
"""
Cython code for using function in Clang.
These functions can be called by Python.
Last update: 2018/11/18
"""

# define function from Clang header file
cdef extern from "ClangSample.h":
    ctypedef struct Pair:
        int a
        int b
    
    Pair make_pair(int a, int b)
    
    int add(int c, int d)

# define function executed by Python
def py_add(c, d):
    return add(c, d)

def py_make_pair(a, b):
   return make_pair(a, b)

最初のcdef extern from "ClangSample.h"で、先程のC言語コードのヘッダーファイルをインポートしています。このブロック内で、Python側で利用したいC言語側の構造体や関数をCython内でプロトタイプ宣言します。
それ以降で書いているdef py_add(c, d)とdef py_make_pair(a, b)は実際にPython内で実行する関数であり、その内部では先程プロトタイプ宣言したC言語側の関数を実行するようにしておきます。

ステップ3: Cythonコードをビルドするsetup.pyを書く

C言語で書いたClangSample.cをラッピングしてCythonコードUnitTestSample.pyxをビルドするスクリプト setup.pyを下記のように書きます。

# -*- coding: utf-8 -*-
"""
Setup python file for Cython.
Last update: 2018/11/18
"""

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
from Cython.Build import cythonize

src_fls = ['UnitTestSample.pyx', 'ClangSample.c']

ext = Extension('UnitTestSample', sources=src_fls)

setup(
    name='UnitTestSample',
    cmdclass={'build_ext': build_ext},
    ext_modules=cythonize([ext])
)

最初のsrc_flsでは、ビルドするCythonとC言語のソースファイル名を書き並べておきます。次のExtensionは拡張モジュール(Pythonから呼びたいC言語のコード達)であり、任意のモジュール名とそれに含まれるソースファイル一式をリストアップします。このリストはext_modulesという引数に渡しておきます。
もう一つの引数であるcmdclassには、setup実行時に一緒に実行するコマンドを指定します。ここでは、C/C++による拡張モジュールをビルドするためのコマンドであるbuild_extというものを指定します。
他にもいろいろな書き方がありますが、それについては下記の記事に詳しく書かれています。
Python モジュールの配布 (レガシーバージョン) — Python 3.9.4 ドキュメント

ステップ4: コンパイル・ビルドする

テスト対象コードであるClangSample.h/c、CythonコードUnitTestSample.pyx、ステップ3で書いたsetup.pyを同じディレクトリに入れて、下記のコマンドでコンパイル・ビルドします。

python setup.py build_ext --inplace

コマンドのオプションで--inplaceを付ければ、同じディレクトリにビルドされたバイナリが作成されます。バイナリは.pyd(Windowsの場合)という拡張子のファイルになります。

ビルドする際のハマりポイント

自分のようなWindows環境下でビルドした場合、下記のようなエラーが出てビルドが通らない事があります。

unable to find vcvarsall.bat

これはC/C++で書かれた拡張モジュールをコンパイルするために必要なVisual Stdioのコンパイラの一部であり、上記のエラーが出た時は自分のPythonのバージョンに応じてVisual C++のBuild Toolsをインストールする必要があります。このエラーと対処方法の詳細は下記の記事が参考になります。

How to deal with the pain of "unable to find vcvarsall.bat" | Python

blog.sky-net.pw

ステップ5: ユニットテストのPythonコードを書く

Pythonに標準で付属しているunittestモジュールを利用したテストコードを書いて、ここまでに書いたC言語のコードをテストしてみましょう。既にビルドしたUnitTestSampleモジュールと、Pythonのunittestモジュールをimportして下記のようなテストコードを書きます。

# -*- coding: utf-8 -*-
"""
Unit Test code for C language code.

Test of function add

Last update: 2018/11/18
"""

import UnitTestSample
import unittest

class UnitTestAdd(unittest.TestCase):
    def test_a(self):
        input_a = UnitTestSample.py_make_pair(10, 5)
        ans_a   = UnitTestSample.py_add(input_a['a'], input_a['b'])
        self.assertEqual(ans_a, 15, 'Test A')
    
    def test_b(self):
        input_b = UnitTestSample.py_make_pair(23, 40)
        ans_b   = UnitTestSample.py_add(input_b['a'], input_b['b'])
        self.assertEqual(ans_b, 63, 'Test B')
    
    def test_c(self):
        input_c = UnitTestSample.py_make_pair(-120, 500)
        ans_c   = UnitTestSample.py_add(input_c['a'], input_c['b'])
        self.assertEqual(ans_c, 380, 'Test C')

if __name__ == '__main__':
    unittest.main()

テストA(10+5=15), B(23+40=63), C(-120+500=380)という3パターンの足し算をテストします。実行すると下記のような結果となり、3パターン全てのテストが走り想定通りの結果となったことが分かります。
f:id:sy4310:20181123130534p:plain
ちなみに、試しにテストBが期待する計算結果とならないようにしてみましょう。テストBの計算を2+40に変更して63とならないようにしました。そうすると、
f:id:sy4310:20181123130905p:plain
となり、テストBが失敗したことが分かります。
ここまでに載せたコードはGitHubでも公開していますので参考にどうぞ。

github.com

今後の課題

とりあえずPythonからC言語のコードを呼び出せるようにはなりましたが、正直Cythonのコードを書かなきゃいけないのは面倒だと感じました。今後はもう少し簡単にできる方法がないかを探してみようと思います。