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ソースコード
module hello_mod
implicit none
contains
subroutine hello() bind(C)
print *, 'Hello'
end subroutine
end module
C++ソースコード
#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も作成してみました。
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 $<
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でコンパイルができるようになりました。
参考
Discussion