16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FortranAdvent Calendar 2024

Day 1

Fortranのバイナリを覗いてみよう

Last updated at Posted at 2024-11-30

この記事は誰向けか? 

  • バイナリの中身に興味がある人
  • 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言語から呼び出す場合、バイナリの中から関数名を探してリンクするので、このバイナリ化された後の名前が重要です。

main.f90
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
mod_test.f90
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
f77.f90
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を使ってコンパイルしています。

nmコマンドの出力
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を用意します。

myflib.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メインプログラムは下記の通りです。

main.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のデータ構造を扱うことができます。概要のみを示すと下記の通りです。

  1. void* fclasses;のようなものを宣言
    • fortranクラスのメソッドをC側で操作したければ、そのCバインディングを(PythonのC拡張と同じような感覚で)作る
  2. voidポインタをfortranで受け取り、これがfortranのクラスのメモリ位置を指すようにfortran側で処理
  3. voidポインタをC/C++側で管理
    • voidポインタを配列にしてもよく、そうすると配列の要素がポインタとなり、ポインタの指示先がFortranのクラスということになる。(pythonのlistとメモリ構成が似ている。。と勝手に思っている。)

おわりに

pythonはC言語で開発されているため、どうしてもC/C++系と相性が良いです。
とはいえ、voidポインタやFortranとCの相互運用性を活用することで、python/C/C++からFortranを自在に操ることができるということを頭の片隅に置いておくと、今後のFortran活用の幅が広がるかもしれません。

16
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?