🎃

pybind11を使ってC++/FortranプログラムをPythonで動かす <その2>

に公開

はじめに

Python ➔ C++ ➔ Fortranの順にバインディングを行うことで、PythonからFortranプログラムを動かすテストの第2回です。今回は引数を介して言語間でデータの受け渡しをしてみます。入力パラメータがPython ➔ C++ ➔ Fortranの順に流れ、計算結果がFortran ➔ C++ ➔ Pythonの順に返ってくるイメージです。

プログラム例 2

2つの整数(または浮動小数点数)の和(または差)を計算するFortranプログラムを作成します。

arithmetic.f90
module add_mod
    use, intrinsic :: iso_c_binding, only: c_int
    implicit none
contains
    ! Add two integers
    function add_int(i, j) bind(C)
        integer(c_int), intent(in), value :: i, j
        integer(c_int) :: add_int
        add_int = i + j
    end function
end module

module subtract_mod
    use, intrinsic :: iso_c_binding, only: c_double
    implicit none
contains
    ! Subtract two floating-point numbers
    function subtract_double(a, b) bind(C)
        real(c_double), intent(in) :: a, b
        real(c_double) :: subtract_double
        subtract_double = a - b
    end function
end module

これを呼ぶC++プログラムです。

example.cpp
#include <pybind11/pybind11.h>

// Using Fortran code within C++
extern "C" {
    int add_int(const int i, const int j);
    double subtract_double(const double *a, const double *b);
}

// Add two integers
int c_add(const int i, const int j) {
    return add_int(i, j);
}   

// Substract two floating-point numbers
double c_subtract(const double a, const double b) {
    return subtract_double(&a, &b);
}

// Bindings with Python
PYBIND11_MODULE(example, m) {
    m.def("add_int", &c_add);
    m.def("subtract_double", &c_subtract);
}

ポイントは、

  • 整数はFortranのvalue属性で値渡し
  • 浮動小数点数はC++のポインタで参照渡し

としていることです。

Pythonから動かしてテストしてみます。

>>> import example
>>> x = 5
>>> y = 7
>>> example.add_int(x, y)
12
>>> example.subtract_double(x, y)
-2.0

プログラム例 3

次はPythonのリスト(配列)を渡してみます。C++の側はvector型で受け取ることにして、そのポインタをFortranに渡して計算を進めます。

一例として、データ列の平均と分散の計算をしてみます。まずはFortranのプログラム。

statistic.f90
module statistics_mod
    use, intrinsic :: iso_c_binding, only: c_int, c_double
    use, intrinsic :: iso_fortran_env, only: real64
    implicit none
contains
    ! Calculate mean and variance
    subroutine statistics(n, x, mean, var) bind(C)
        integer(c_int), intent(in), value :: n
        real(c_double), intent(in) :: x(n)
        real(c_double), intent(inout) :: mean, var
        real(real64) :: diff(n)

        mean = sum(x) / n
        diff(1:n) = x(1:n) - mean
        var = dot_product(diff, diff) / (n - 1)
    end subroutine
end module

これまでと同様にして、C++プログラムも作成します。計算結果はタプルにして返り値で返すようにしています。これはPythonの変数がイミュータブルで、引数を使ってデータを戻せないからです。

example.cpp
#include <vector>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

// Using Fortran code within C++
extern "C" {
    void statistics(const int n, const double *x, double *mean, double *var);
}

// Mean and variance
std::tuple<double, double> c_statistics(const std::vector<double> &v) {
    const int n = v.size();
    const double *v_ptr = v.data();
    double mean, var;

    statistics(n, v_ptr, &mean, &var);

    return std::forward_as_tuple(mean, var);  // C++11
    // return {mean, var};                    // C++17
}

// Bindings with Python
PYBIND11_MODULE(example, m) {
    m.def("statistics", &c_statistics);
}

ここでのポイントは、

  • STLコンテナを使ってPythonのリストからC++のvectorへ自動変換を行う点

にあります。そのため、pybind11/stl.h ヘッダのインクルードが必要です。

Pythonから動かしてテストしてみます。検証目的でPython単独(statisticsモジュール)の計算もしてみました。

>>> import example
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> x = rng.normal(5, 10, 1000)
>>> print('Fortran: (mean, var) =', example.statistics(x))
Fortran: (mean, var) = (4.449369335303001, 106.21722645850859)

>>> import statistics
>>> print('Python: (mean, var) =', statistics.mean(x), statistics.variance(x))
Python: (mean, var) = 4.449369335303006 106.21722645850852

メリット・デメリット

今回の例は、PythonとC++のバインディングにおいて、

  • C++側を標準仕様のstd::vectorのまま書き換えずに運用できる

という点でメリットがある一方で、

  • Pythonのリスト型データからC++のvector型データへの自動変換がコピーセマンティクスにより行われる

というデメリットも孕んでいます。そこで次回はコピーセマンティクスに拠らない手法をテストします。

参考

https://pybind11.readthedocs.io/en/stable/

Discussion