概要
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
と名付け,その内容を記述します.今回はコマンドライン引数を読むことが目的であるため,引数としてargc
とargv
を設けます.
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
には下図のように文字列が納められます.
この文字列から個別にオプションを取り出すために,次のような手順を考えます.
- 1番目のオプション(文字列)のメモリアドレスを,1文字目からNull文字
\0
が現れるまでインクリメントして文字数を数える. - オプションの長さがわかったら,その長さの文字型配列(
char []
)をFortranの文字列に変換する. -
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 ソースファイル
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;
}
}
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