🎃

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

に公開

はじめに

C++をPythonにバインディングするライブラリに pybind11 というものがあります。一方、FortranにはC++との相互運用を可能にする組み込みモジュールの仕組みがありますので、これらを使えばPython ➔ C++ ➔ Fortranの順にバインディングを行うことで、PythonからFortranプログラムを動かせるようになります。いくつかの例を紹介したいと思います。

pybind11のインストール

私はPythonのパッケージ管理(仮想環境)に uv というツールを使用しています。今回はそのuvの仮想環境内にpybind11をインストールすることにします。インストールにはpipを使用しました。

# uvで管理するプロジェクトを作成
uv init my_project
cd my_project

# 仮想環境を起動
uv venv
source .venv/bin/activate

# pipでインストール
uv pip install pybind11

プログラム例 1

画面に Hello を出力するFortranプログラムと、それを呼び出すC++プログラムを用意します。

Fortranソースコード

hello.f90
module hello_mod
    implicit none
contains
    subroutine hello() bind(C)
        print *, 'Hello'
    end subroutine
end module

C++ソースコード

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

// Using Fortran code within C++
extern "C" {
    void hello();
}

void c_hello() {
    hello();
}

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

ポイントは、

  • Fortranプログラムのサブルーチンにある bind(C) 属性(C++ / Fortran バインディング用)
  • C++プログラムの extern "C" による外部リンケージ(C++ / Fortran バインディング用)
  • C++プログラムの pybind11.h ヘッダインクルード(Python / C++ バインディング用)
  • C++プログラムの PYBIND11_MODULE マクロ(Python / C++ バインディング用)

の4つです。

コンパイルします。

gfortran -fPIC -c hello.f90
g++ -fPIC $(python3 -m pybind11 --includes) -c example.cpp
g++ -shared -o example$(python3 -m pybind11 --extension-suffix) hello.o example.o -lgfortran

example.cpython-310-x86_64-linux-gnu.so のような名前の共有ライブラリができます。ここにC++プログラムの関数がPythonのモジュールのようにして埋め込まれますので、Pythonではこれをインポートして呼び出すことができます。

>>> import example
>>> example.hello()
 Hello

Makefile / CMakeLists.txt

コンパイル手順を簡略化するために、MakefileやCMakeLists.txtも作成してみました。

Makefile
CXX=g++
F90=gfortran
CXXFLAGS=-fPIC
F90FLAGS=-fPIC
INCLUDES=$(shell python3 -m pybind11 --includes)
LIBS=-lgfortran

TARGET=example$(shell python3 -m pybind11 --extension-suffix)

CXXSRCS=example.cpp
F90SRCS=hello.f90
CXXOBJS=$(subst .cpp,.o,$(CXXSRCS))
F90OBJS=$(subst .f90,.o,$(F90SRCS))

all:    $(TARGET)

$(TARGET):      $(F90OBJS) $(CXXOBJS)
        $(CXX) -shared -o $@ $^ $(LIBS)

.SUFFIXES: .cpp .f90 .o

.cpp.o:
        $(CXX) $(CXXFLAGS) $(INCLUDES) -c $<

.f90.o:
        $(F90) $(F90FLAGS) -c $<
CmakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(example LANGUAGES CXX Fortran)

# Find the pybind11 package
set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)

# Build Fortran programs
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_library(fortran_libs hello.f90)

# Build C++ programs
pybind11_add_module(${PROJECT_NAME} example.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE fortran_libs)

CMakeLists.txtではfind_packageコマンドを使用していますが、前提条件としてpybind11Config.cmakeへのファイルアクセスが保証されている必要があります。見つからない場合はpybind11-configコマンドの応用で、
echo $(pybind11-config --pkgconfigdir)/../cmake/pybind11
とすればファイルへのパスが得られますので、これを環境変数CMAKE_PREFIX_PATHに登録しておきましょう。私の場合は、仮想環境ごとにランタイムにおいて登録が行われるようにしたかったので、CMakeLists.txt内に下記を追加しました。

execute_process(
    COMMAND pybind11-config --pkgconfigdir
    OUTPUT_VARIABLE pybind11_pkgconfigdir
)
string(STRIP ${pybind11_pkgconfigdir} pybind11_pkgconfigdir)
list(APPEND CMAKE_PREFIX_PATH "${pybind11_pkgconfigdir}/../cmake/pybind11")

これでmakeやcmakeでコンパイルができるようになりました。

参考

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

Discussion