この記事は誰向けか?
- バイナリの中身に興味がある人
- CとFortranの相互運用を考えている人
- C/C++/Python/RなどからFortranのクラスを操りたい人(f2008までで相互運用可能となっていないものを含む)。
はじめに
コテコテのフォートランナーがPython→C→C++を勉強してみて、なんとかFortranをPython/C/C++から利用してやろうと頑張る際の道具立てを紹介します。
iso_c_bindingsを使ったり、昔からの言い伝え(サブルーチン名の最後にアンダースコアを入れる)を使ったりすることで、CとFortranの相互運用ができるのですが、それはなぜかを自分なりに追跡した痕跡を残しておきます。
バイナリの中身を見る
まずは、Fortranのコードサンプルを列挙します。
main.f90
mod_test.f90
f77.f90
Fortranコード
同じ動作をするサブルーチンを下記の通り用意しました。
- モジュールサブルーチン
-
module_sample
:f90の機能を使った配列引数 -
module_sample_bind_c
:f77の機能を使った配列引数、binc(c)付 -
module_sample_f77_c
: f77の機能を使った配列引数、binc(c)なし
-
- 外部サブルーチン
-
sample_f77_c
:f77の機能を使った配列引数、binc(c)なし
-
fortranユーザは、これらのサブルーチンがどのような名前でバイナリに変換されているか意識したことがあるでしょうか?C言語から呼び出す場合、バイナリの中から関数名を探してリンクするので、このバイナリ化された後の名前が重要です。
program main
use mod_test
use iso_fortran_env
implicit none
integer(int32) :: n = 10
real(real64) :: a
real(real64),allocatable :: x(:)
external :: sample_f77_c
allocate(x(n))
a = 2d0
call random_number(x)
print *, "! module_sample"
call module_sample(a, x) ! Fortranのみから呼び出されることを想定
print *, "! module_sample_bind_c"
call module_sample_bind_c(n, a, x) ! bind(c)でCから呼び出し可能に
print *, "! module_sample_f77_c"
call module_sample_f77_c(n, a, x) ! F77の書き方なのでbind(c)が無くても呼び出せるが、"_"を付けても呼び出せない
print *, "! sample_f77_c"
call sample_f77_c(n, a, x) ! 昔ながらのF77. "_"を付けるとCから呼び出せる
end program main
module mod_test
use iso_c_binding
implicit none
contains
subroutine module_sample(a, x)
real(c_double) :: a
real(c_double) :: x(:) !この形式で配列を受けると、配列サイズ等も裏で渡される
integer :: i
do i = 1,size(x)
print *, i, a*x(i)
end do
end subroutine
subroutine module_sample_bind_c(n, a, x) bind(c)
integer(c_int),value :: n
real(c_double),value :: a
real(c_double) :: x(*) !先頭ポインタを受け取る(なので、サイズを別途nで渡す必要がある)
integer(c_int) :: i
do i = 1, n
print *, i, a*x(i)
end do
end subroutine
subroutine module_sample_f77_c(n, a, x) !bind(c)が無いモジュールサブルーチン
implicit none
integer :: n
double precision :: a
double precision :: x(*)
integer :: i
do i = 1, n
print *, i, a*x(i)
end do
end subroutine
end module
subroutine sample_f77_c(n, a, x) !昔懐かし外部サブルーチン
implicit none
integer :: n
double precision :: a
double precision :: x(*)
integer :: i
do i = 1, n
print *, i, a*x(i)
end do
end subroutine
nm
コマンドの出力
バイナリの中の関数名を調べるのに便利なのがnm
コマンドです。
f77.f90
をコンパイルして生成されるオブジェクト名がf77.f90.o
、
mod_test.f90
からmod_test.f90.o
が生成されるとして、その中に含まれる関数名は下記の通りです。
※gfortran
を使ってコンパイルしています。
f77.f90.o:
U _gfortran_runtime_error_at
U _gfortran_st_write
U _gfortran_st_write_done
U _gfortran_transfer_integer_write
U _gfortran_transfer_real_write
0000000000000000 T sample_f77_c_
mod_test.f90.o:
000000000000001c T __mod_test_MOD___copy___iso_c_binding_C_funptr
0000000000000000 T __mod_test_MOD___copy___iso_c_binding_C_ptr
0000000000000000 B __mod_test_MOD___def_init___iso_c_binding_C_funptr
0000000000000008 B __mod_test_MOD___def_init___iso_c_binding_C_ptr
0000000000000000 D __mod_test_MOD___vtab___iso_c_binding_C_funptr
0000000000000040 D __mod_test_MOD___vtab___iso_c_binding_C_ptr
000000000000028a T __mod_test_MOD_module_sample
0000000000000038 T __mod_test_MOD_module_sample_f77_c
U _gfortran_runtime_error_at
U _gfortran_st_write
U _gfortran_st_write_done
U _gfortran_transfer_integer_write
U _gfortran_transfer_real_write
0000000000000166 T module_sample_bind_c
シンボルの意味は下表の通りだそうです。
シンボル | 意味 |
---|---|
T | グローバル・テキスト・シンボル |
U | 未定義(実行ファイルを作るときまでに外部からリンクが必要) |
B | グローバル bss シンボル |
D | グローバル・データ・シンボル |
これらの中から、ソースコードから見覚えのある名前を抜き出すと下記のものが見つかります。
ソースコードの関数名 | オブジェクトファイル(nm コマンド出力) |
bind(c) |
---|---|---|
module_sample | __mod_test_MOD_module_sample | なし |
module_sample_f77_c | __mod_test_MOD_module_sample_f77_c | なし |
module_sample_bind_c | module_sample_bind_c | あり |
sample_f77_c | sample_f77_c_ | なし |
C言語から関数を呼ぶときは、ソースコードの関数名そのままでバイナリの中身がサーチされるようです。さて、ここでとあることに気付きますよね?
fortranではbind(c)
をした場合のみ、ソースコードの関数名がそのままオブジェクトファイルで関数名として利用されています。(bind(c)で、敢えて別名にしたり、大文字小文字を区別する名前にしたりもできます)
そして、外部サブルーチンのsample_f77_c
は、アンダースコア_
が付いた名前になっているのです。(言い伝えは正しかった!)
モジュールサブルーチンはどうでしょうか。サブルーチン名は
__モジュール名_MOD_関数名
のように変換されています。すなわち、この名前でC言語から呼び出すことができるはずです。
CからFortranを呼ぶ
呼び出すべき関数名が分かったので、C言語からFortran関数を呼ぶためのヘッダmytflib.h
を用意します。
void module_sample_bind_c(int n, double a, double* x);
void __mod_test_MOD_module_sample_f77_c(int* n, double* a, double* x);
void sample_f77_c_(int* n, double* a, double* x);
f77方式で書いたサブルーチンは、基本的にポインタ渡しになります。
bind(c)やvalue属性を活用すれば値渡しも実装可能です。
Fortranサブルーチンを呼び出すCメインプログラムは下記の通りです。
#include <stdio.h>
#include <stdlib.h>
#include "myflib.h"
void sample_c(int n, double a, double* x){
printf("!=== Call C ===: n=%d, a=%f\n",n,a);
for (int i=0;i<n;i++){
printf("%d %f\n",i+1,a*x[i]);
}
}
int main(void){
int i,n;
double a;
double* x;
n = 10;
a = 2.0;
x = (double*)malloc(n * sizeof(double));
if (x == NULL) {
printf("メモリ確保に失敗しました\n");
return 1;
}
for (i=0;i<n;i++){
x[i] = (double)i+1;
}
sample_c(n,a,x);
printf("\n!=== Call Fortran: module_sample_bind_c\n");
module_sample_bind_c(n, a, x);
printf("\n!=== Call Fortran: module_sample_f77_c ... (__mod_test_MOD_module_sample_f77_c)\n");
__mod_test_MOD_module_sample_f77_c(&n, &a, x);
printf("\n!=== Call Fortran: sample_f77_c ... (sample_f77_c_)\n");
sample_f77_c_(&n, &a, x);
return 0;
}
コンパイルコマンドはgcc
でもgfortran
でも良いのですが、gcc
を使う場合には-lgfortran
としてFortranライブラリのリンクフラグを渡す必要があります。
著者の環境では、このCプログラムのコンパイル&リンク&実行ができました。
コンパラが変わるとどうなるの?
上記で説明したサブルーチン名の命名規則(__モジュール名_MOD_関数名
など)は、コンパイラが変わると変化します。したがって、確実にCとの相互運用を可能にしたいのであれば、bind(c)
を活用する必要があるわけです。
x(:)
形式の配列引数を有するサブルーチンはCから呼べないの?
ひと昔前までは呼べませんでした。
今は、f2018規格でCFI_
で始まる種々の関数・マクロをC側で利用することにより、呼ぶことができるようになっています。
C++コードのバイナリでは関数名どうなってるの?
Fortranのモジュールと似たような感じです。テンプレート、クラスのオーバーライド、、、など、色々なモノが一意に識別できるように、コンパイラが勝手にコードの関数名に多彩な修飾語句を付与しています。
FortranのクラスをC/C++で操作するには?
void*ポインタが、そのマシン上のメモリを指す整数型であることを利用すれば、f2018までのiso_c_bindings
で取り扱うことができないデータも、わりと自在にC/C++からFortranのデータ構造を扱うことができます。概要のみを示すと下記の通りです。
-
void* fclasses;
のようなものを宣言- fortranクラスのメソッドをC側で操作したければ、そのCバインディングを(PythonのC拡張と同じような感覚で)作る
- voidポインタをfortranで受け取り、これがfortranのクラスのメモリ位置を指すようにfortran側で処理
- voidポインタをC/C++側で管理
- voidポインタを配列にしてもよく、そうすると配列の要素がポインタとなり、ポインタの指示先がFortranのクラスということになる。(pythonのlistとメモリ構成が似ている。。と勝手に思っている。)
おわりに
pythonはC言語で開発されているため、どうしてもC/C++系と相性が良いです。
とはいえ、voidポインタやFortranとCの相互運用性を活用することで、python/C/C++からFortranを自在に操ることができるということを頭の片隅に置いておくと、今後のFortran活用の幅が広がるかもしれません。