1
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

FortranでC言語のダブルポインタを受け取ってコマンドライン引数を取得する

概要

Fortran Advent Calendar 14日目の記事において,ポインタ変数がもつアドレスをインクリメントする方法について説明しました.そして,まとめ

C言語のコマンドライン引数char **argvをFortranから読めるようになります.

と書きました.この記事では,その具体的なやり方について説明します.黒魔術は使わず,FortranとC言語の相互運用,C言語のダブルポインタに対する理解を深めることも目的とします.

本記事では,実行したコマンド全体(オプション付き)をコマンドライン引数あるいは引数全体とよび,個別の引数(オプション)および実行ファイル名を統一的にオプションとよぶことにします.

なお,身も蓋もない話ですが,Fortranにはコマンドライン引数を取得するget_command()サブルーチンが存在します.これ以外にも,オプションの個数を取得するcommand_argument_count()関数と各オプションを取得するサブルーチンget_command_argument()もあります.C言語ではmain関数の引数としてコマンドライン引数が渡されますが,Fortranは任意のタイミングでコマンドライン引数を参照できるので,こちらの方が柔軟性があると思います1

使用環境

Fortranコンパイラ(バージョン) Cコンパイラ(バージョン)
intel Parallel Studio XE Composer Edition for Fortran (17.0.4.210) Microsoft(R) C/C++ Optimizing Compiler (19.00.24234.1 for x86)
gfortran (7.3.0) gcc (7.3.0)

アドレスをインクリメントする関数(再掲)

以前の記事で作った関数を示します.関数内では,引数として受け取ったポインタ変数addressと,バイト数の増分increment_bytesを足しています.
char型に対するポインタchar *を用いることで1バイトずつインクリメントできるようにし,インクリメントするバイト数を引数で渡します.
戻り値もchar型のポインタchar *としていますが,もしかしたらvoid *でもよいかもしれません.

extern "C" {
    char* incrementAddress(char *address, int increment_bytes) {
        return address + increment_bytes;
    }
}

Fortran側のインタフェースは次のように書きました.引数はどちらも値渡しとしています.FortranとC言語では,標準がポインタ渡しか値渡しかで異なるためです.アドレスをインクリメントする関数を無駄に複雑にしないために,C言語の都合にあわせています.byteの実引数はsizeof()を利用して決定します.

    interface
        type(c_ptr) function incrementAddress(address, byte) bind(c,name="incrementAddress")
            use,intrinsic :: iso_c_binding
            implicit none
            type(c_ptr)        ,intent(in),value :: address
            integer(c_intptr_t),intent(in),value :: byte
        end function incrementAddress
    end interface

下のプログラムでは,C言語のポインタ変数がもつアドレスを変化させ,そのポインタ変数をFortranのポインタ変数に変換して参照しています.

program main
    use, intrinsic :: iso_c_binding
    implicit none

    interface
        type(c_ptr) function incrementAddress(address, byte) bind(c,name="incrementAddress")
            use,intrinsic :: iso_c_binding
            implicit none
            type(c_ptr)        ,intent(in),value :: address
            integer(c_intptr_t),intent(in),value :: byte
        end function incrementAddress
    end interface

    type(c_ptr) :: addr        ! C言語のポインタ変数
    integer(c_intptr_t) :: inc ! アドレスの増分

    character(:),allocatable,target :: str ! 走査する文字列

    character,pointer :: ptr_s ! C言語のポインタ変数を変換して1文字を参照するためのポインタ変数

    str = "abcd"//c_null_char

    inc = sizeof(c_null_char) ! character型のバイト数
    addr = c_loc(str)         ! 文字列strの先頭アドレス

    ! Null文字が出てくるまで,先頭から1文字ずつアドレスをずらしていく
    call c_f_pointer(addr,ptr_s)
    do while( ptr_s(1:1) /= c_null_char )
        print *,ptr_s(1:1)
        addr = incrementAddress(addr, byte=inc)
        call c_f_pointer(addr,ptr_s)
    end do
end program main

これを実行すると,一つずつ文字と数字が表示されます.

 a
 b
 c
 d

C言語からのFortranの手続呼出し

前節の例では,C言語のポインタ変数を介してFortranの文字列のアドレスを参照していました.今回は,C言語が主で,C言語のプログラム中からFortranの手続(サブルーチン/関数)を呼出し,呼び出された手続の中でコマンドライン引数を読むようにします.

C言語側から呼ばれるサブルーチンをf_mainと名付け,その内容を記述します.今回はコマンドライン引数を読むことが目的であるため,引数としてargcargvを設けます.

    subroutine f_main(argc, argv) bind(c,name="f_main")
        use,intrinsic :: iso_c_binding
        implicit none

        integer(c_int),value      :: argc           ! オプションの個数
        type(c_ptr)   ,intent(in) :: argv(0:argc-1) ! オプション(文字列)を指すポインタの配列

    end subroutine f_main

配列を0開始にしているのは,command_argument_count()およびget_command_argument(),あるいはC言語と動作を一致させるためです.

C言語側から見える名前の設定

サブルーチン名の後ろにあるbind(c, name = "f_main")は,インタフェースとは逆で,C言語側に見える名前を設定します.かつては,C言語からFortranのサブルーチンを呼ぶには,サブルーチン名を全てを大文字として取り扱う必要がありました2が,それはもう昔のお話です.

仮引数の型

C言語との相互利用において,仮引数の型宣言は少し厄介です.C言語のint argcに対応するargcはコマンドを含むオプションの個数を表しており,C言語でもその値を参照します.Fortranに渡すときも,値渡しが自然です.argcを値渡しとするために,value属性を付けています.

argvはC言語のchar **argvあるいはchar *argv[]に対応しているので,それに見合った型を用いる必要があります.char **argv各オプションを表す文字列へのポインタの配列です.Fortranで引数の文字列を扱えるようにするために,このポインタの配列を渡すことになります.

FortranとC言語の引数の対応は理解するのが厄介なのですが,「C言語からFortranにメモリアドレスを渡すと,そのメモリアドレスを1段階辿って値が渡されている」と見なすことができます.ものすごくざっくりと考えて,C言語から&付きでFortranに渡した変数は,Fortran側ではC言語における通常の(&無しの)変数として扱われる,と見なすことができます.

下のプログラムを例にすると,int型変数aメモリアドレスを渡すと,Fortranのサブルーチン内ではアドレスが辿られてaが参照できるようになると見なせます3

char a='a';
f_addr(&a); //aのメモリアドレス&aを渡すと,Fortran側で値aが参照できるようになる
subroutine f_addr(a) bind(c,name=f_addr)
    use,intrinsic :: iso_c_binding
    implicit none
    character :: a
end subroutine f_addr

配列の場合は,C言語ではその変数名で先頭要素のアドレスを参照できるので,&を付けずに関数に渡すとポインタ渡しになります.

char  a[] = { 'a', 'b', 'c' };
char *b   = "abc";
f_array(a, 3); //配列の場合はaがa[0]のメモリアドレス(&a[0])を表す
f_array(b, 4); //文字列でも理屈は同じ
subroutine f_array(a,n) bind(c,name="f_array")
    implicit none

    character            :: a(n)
    integer(c_int),value :: n
end subroutine f_array

さて,文字列の配列に近づいてきました.C言語はFortranと大きく異なり,多次元配列は存在しません.形式的に2次元配列を宣言できますが,宣言した変数はポインタの配列となり,配列の各要素が1次元配列の先頭要素を指すメモリアドレスを持ちます.この事実に合うようにFortran側の仮引数の型を決めるとすると,次のようなポインタの配列を利用することになります4

subroutine f_dp(a,n) bind(c,name="f_dp")
    use,intrinsic :: iso_c_binding
    implicit none

    type(c_ptr)          :: a(n)
    integer(c_int),value :: n
end subroutine f_dp

ポインタの配列であっても,変数名で先頭要素のアドレスを参照できることは変わりません.ポインタの配列char **argvの変数名argvは,先頭要素を指すメモリドレス&argv[0]を参照するので,Fortranのサブルーチンにはargvを渡します.

char *argv[] = { "./a.out", "-h", "-D" };
printf("%p\n", &argv[0]); // 008FFED4
f_dp(&argv[0],3);         // 1文字目argv[0]のメモリアドレス&argv[0]を渡すと,Fortran側でargv[0]が参照できるようになる

printf("%p\n", argv);     // 008FFED4
f_dp(argv,3);             // &argv[0]と同じ

printf("%p\n",  argv   ); // 008FFED4
printf("%p\n", &argv[0]); // 008FFED4
printf("%p\n", &argv[1]); // 008FFED8
printf("%p\n", &argv[2]); // 008FFEDC

このようにしてポインタの配列の先頭アドレスを渡して派生型type(c_ptr)の配列で受け取り,Fortranのポインタ変数へ変換することで参照します.

C言語のmain関数

C言語側では,外部関数を呼び出すためにプロトタイプ宣言を行います.Fortranのサブルーチンは値を返さないので,C言語では関数の戻り値をvoidとします.実引数はargc, argvと単純に渡せるようにし,実引数の値が渡されるかメモリアドレスが渡されるかは,Fortranの仮引数の型宣言で決定します.

extern "C" void f_main(int, char **); //プロトタイプ宣言

int main(int argc, char **argv) {

    f_main(argc, argv);

    return 0;
}

extern "C" {
    char* incrementAddress(char *address, int increment_bytes) {
        return address + increment_bytes;
    }
}

引数全体からのオプションの取り出し

コマンドライン引数全体の文字列から,個別のオプションを取り出します.仮に

$ ./a.out -h -D

として実行すると,コマンドライン引数argvには下図のように文字列が納められます.
memorymap

この文字列から個別にオプションを取り出すために,次のような手順を考えます.

  1. 1番目のオプション(文字列)のメモリアドレスを,1文字目からNull文字\0が現れるまでインクリメントして文字数を数える.
  2. オプションの長さがわかったら,その長さの文字型配列(char [])をFortranの文字列に変換する.
  3. argc個のオプションを取り出すまで手順1.,2.を繰り返す.
    type(c_ptr) :: addr
    character,dimension(:),pointer :: c
    integer(c_intptr_t),parameter :: inc = sizeof(c_null_char) ! アドレスの増分

    integer :: lenOpt, opt
    type(c_arg),allocatable :: arg(:) ! 個別のオプションを保持するための派生型

    allocate(arg(0:argc-1))

    do opt = 0,argc-1
        addr   = argv(opt) ! opt番目のオプションの先頭アドレス

        ! Null文字を検出するまでメモリを走査して文字数をカウント
        lenOpt = 0
        call c_f_pointer(addr, c, [1])
        do while(c(1) /= c_null_char)
            lenOpt = lenOpt+1
            addr = incrementAddress(addr, byte=inc)
            call c_f_pointer(addr, c, [1])
        end do

        ! 文字の配列を文字列に変換
        allocate(character(lenOpt) :: arg(opt)%v) ! 長さlenOptの文字列
        call c_f_pointer(argv(opt), c, [lenOpt])  ! lenOpt要素の文字の配列
        arg(opt)%v = transfer(c, arg(opt)%v)

        print *,arg(opt)%v
    end do

ここで,type(c_arg)は,自動再割付け文字列vを成分に持つ派生型です.

    type,public :: c_arg
        character(:),allocatable :: v
    end type c_arg

コンパイルと実行

C言語のソースをmain.cpp,Fortranのソースをf_main.f90として,個別にコンパイル,リンクします.完全なソースファイルは付録に示しておきます.

$ gcc -c main.cpp
$ gfortnra -c f_main.f90
$ gcc -lgfortran *.o

WindowsでVisual Studioを利用する場合は,Visual C++のコンソールアプリケーションのプロジェクトを作成し,そこにFortranのスタティックライブラリのプロジェクトを追加します.

前節の例と同じ2個のオプションをつけて実行します.

$ ./a.out -h -D
 ./a.out
 -h
 -D
D:\>a.exe -h -D
 a.exe
 -h
 -D

コマンドライン引数全体が個別の3個のオプションに分解されていることが確認できます.

まとめ

C言語のポインタは闇.

付録1 Fortran標準の手続によるコマンドライン引数の取得

Fortranにはコマンドライン引数全体を取得するサブルーチンget_command(),オプションの個数を取得するcommand_argument_count()関数,各オプションを取得するサブルーチンget_command_argument()もあります.

関数 機能
get_command([command = コマンドライン引数, length = コマンドライン引数の長さ, status=取得状態]) コマンドライン引数全体を取得する
command引数が指定された場合,実行コマンドも含む全てのコマンドライン引数を文字列として取得する
length引数が指定された場合,コマンドライン引数の文字列の長さを取得する
statusは,コマンドライン引数を正常に取得できれば0,文字列の変数がコマンドライン引数よりも短ければ-1,それ以外で正常に取得できなかった場合は正の値
command_argument_count() 実行コマンドを含まないオプションの個数を標準種別の整数型で返す
get_command_argumet(number=オプション番号, [value = オプション, length = オプションの長さ, status=取得状態]) 指定された番号(0$\le$number$\le$command_argument_count())のオプションを取得する(0は実行コマンド)
value引数が指定された場合,指定されたオプションを文字列として取得する
length引数が指定された場合,指定されたオプションの文字列の長さを取得する
statusは,オプションを正常に取得できれば0,文字列の変数がオプションよりも短ければ-1,それ以外で正常に取得できなかった場合は正の値

引数command, length, valueは省略可能ですが,現実的な使い方としてすべてを省略することはないでしょう.

個別のオプションを取得するには,まずcommand_argumet_count()でオプションの個数を取得し,doループの中でget_command_argument()によって各オプションの長さを取得した後,その長さの文字列を動的に割り付けてget_command_argument()でオプションを取得するのが定石です.

subroutine f_main() bind(c,name="f_main")
    use,intrinsic :: iso_c_binding
    implicit none

    integer :: argc
    integer :: opt,lenOpt
    character(:),allocatable :: argv
    type(c_arg),allocatable :: arg(:) ! 個別のオプションを保持するための派生型

    ! 全コマンドライン引数を取得する場合
    call get_command(length = lenOpt)
    allocate( character(lenOpt) :: argv)
    call get_command(command = argv)
    print *,argv

    ! 個別のオプションを取得する場合
    argc = command_argument_count() ! 実行コマンドを含まないオプションの個数
    allocate(arg(0:argc))           ! 0は実行コマンド
    do opt = 0,argc
        call get_command_argument(number=opt,length=lenOpt)

        allocate(character(lenOpt) :: arg(opt)%v)
        call get_command_argument(number=opt,value=arg(opt)%v)

        print *,arg(opt)%v
    end do

end subroutine f_main

付録2に示したf_main.f90内のサブルーチンf_mainをで置き換え,Cから引数なしで呼び出すと,以下の結果が得られました.

D:\>a.exe -h -D
 a.exe  -h -D
 a.exe
 -h
 -D

gfortranの場合

gfortranでは,コンパイルには成功しますが,実行ファイルを作成する段階でエラーが出でます.f_main内でget_command()を呼び出すだけでも以下のエラーが表示されます.

f_main.o: 関数 `f_main' 内:
f_main.f90:(.text+0x46d): `_gfortran_get_command_i4' に対する定義されていない参照です
collect2: error: ld returned 1 exit status

オブジェクトファイルから実行ファイルを作成するコマンドをgcc -lgfortran *.oからgfortran *.oに変更すると,実行ファイルは無事作成できますが,コマンドライン引数は取得できません.引数の長さlengthは0ですが,取得状態statusも0になっているので,何かエラーが生じているわけではありません.

f_mainをFortranのメインルーチンから呼んだ時は正しく引数が取得できているので,メインルーチン以外でもget_command等は実行できるようです.

gfortranでは,少なくともメインルーチンをFortranで書いたプログラムでないと,get_command等でコマンドライン引数を取得できないのでしょうか?よくわかりません.

付録2 ソースファイル

main.cpp
extern "C" void f_main(int, char **);

int main(int argc, char **argv) {

    f_main(argc, argv);

    return 0;
}

extern "C" {
    char* incrementAddress(char *address, int increment_bytes) {
        return address + increment_bytes;
    }
}
f_main.f90
module f_routine
    use,intrinsic ::  iso_c_binding
    implicit none
    private
    public f_main

    ! 個別のオプションを保持するための派生型
    type,public :: c_arg
        character(:),allocatable :: v
    end type c_arg

    interface
        type(c_ptr) function incrementAddress(address, byte) bind(c,name="incrementAddress")
            use,intrinsic :: iso_c_binding
            implicit none
            type(c_ptr)        ,intent(in),value :: address
            integer(c_intptr_t),intent(in),value :: byte
        end function incrementAddress
    end interface

    contains

    subroutine f_main(argc, argv) bind(c,name="f_main")
        use,intrinsic :: iso_c_binding
        implicit none

        integer(c_int),value      :: argc
        type(c_ptr)   ,intent(in) :: argv(0:argc-1)

        type(c_ptr) :: addr
        character,dimension(:),pointer :: c
        integer(c_intptr_t),parameter :: inc = sizeof(c_null_char) ! アドレスの増分

        integer :: lenOpt, opt
        type(c_arg),allocatable :: arg(:) ! 個別のオプションを保持するための派生型

        allocate(arg(0:argc-1))

        do opt = 0,argc-1
            addr   = argv(opt) ! opt番目のオプションの先頭アドレス

            ! Null文字を検出するまでメモリを走査して文字数をカウント
            lenOpt = 0
            call c_f_pointer(addr, c, [1])
            do while(c(1) /= c_null_char)
                lenOpt = lenOpt+1
                addr = incrementAddress(addr, byte=inc)
                call c_f_pointer(addr, c, [1])
            end do

            ! 文字の配列を文字列に変換
            allocate(character(lenOpt) :: arg(opt)%v) ! 長さlenOptの文字列
            call c_f_pointer(argv(opt), c, [lenOpt])  ! lenOpt要素の文字の配列
            arg(opt)%v = transfer(c, arg(opt)%v)

            print *,arg(opt)%v
        end do

    end subroutine f_main
end module f_routine

  1. Fortran 2003まで標準規格に入っていなかったことを考えると,もっと使いやすくてもよいと思いますが. 

  2. Fortranで小文字を使ってサブルーチン名を定義しても,C言語からは全て大文字で見えるので,C言語から呼ぶときは全て大文字にしなければならない,ということです. 

  3. あくまで理解のためにそう見なしているだけで,正確な説明ではありません. 

  4. Fortranにポインタの配列はありませんが,派生型type(c_ptr)の配列はC言語のポインタ変数の配列となります. 

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?