概要
Fortran Advent Calendarの4日目で,派生型の基本的な使い方についてまとめました.この記事は,派生型の少し進んだ使い方として,C言語との相互利用についてまとめました.
使用環境
コンパイラ | バージョン |
---|---|
intel Parallel Studio XE Composer Edition for Fortran | 17.0.4.210 |
PGI Visual Fortran for Windows | 18.7 |
gfortran | 7.3.0 (for Ubuntu on WSL) |
更新履歴
- 2018年12月8日 誤字修正
派生型と構造体の互換性
Fortranの派生型はC言語の構造体と同じ役割をしています.
C言語とFortranを相互利用していると,派生型をC言語の関数に渡す,あるいはC言語の構造体を手続の引数として受け取る場合があります.Fortranの型は,C言語に近いところもあれば,異なるところもあります.特に配列や文字列,動的割付が関係するところはかなり違います.相互利用に際しては,Fortranの派生型がC言語の構造体と互換性を有していることを保証しなければなりません.
派生型と構造体の互換性を保証するには,bind(c)
属性を付与します.bind(c)
属性を付けた場合,いくつかの制約が生じます.明確にコンパイルエラーが出るのは,最初の2項目です.特に,2個目はFortranの配列に慣れている人からすると,かなりの痛手です.
-
sequence
は使えません. -
allocatable
あるいはpointer
属性を持つ成分は定義できません. - 整数および実数の種別を,C言語の型に対応した種別に変更します.それらの種別は
iso_c_binding
モジュールで定義されています. - 文字列を文字の配列に変更します.また,終端にNULL文字を付ける必要が生じるので,配列要素数はFortranでの文字列の長さに+1文字必要になります.
-
allocatable
あるいはpointer
属性を持つ成分は,すべてC言語のポインタ変数に相当する成分に置き換えます.
4日目の記事でも取り上げた,人の情報を管理するための派生型person
を,C言語と相互運用できるように修正していきます.
program main
use,intrinsic :: iso_fortran_env
implicit none
type,bind(c) :: person ! bind(c)属性を付与してC言語の構造体との互換性を保証
sequence ! sequenceは使用不可
character :: initial
character(:),allocatable :: name ! allocatable属性を持つ成分は持てない
integer(int32) :: age ! kind(int32)をc_intへ置き換え
integer(int32) :: birthyyyy ! kind(int32)をc_intへ置き換え
integer(int32) :: birthmm ! kind(int32)をc_intへ置き換え
integer(int32) :: birthdd ! kind(int32)をc_intへ置き換え
character(3) :: birthMonth ! 文字列の長さは1(文字の配列へ置き換え)
type(person),dimension(:),pointer :: ptrToPerson ! type(c_ptr)へ置き換え
end type person
end program main
成分name
はcharacter(4)
でしたが,allocatable
属性が使えないことを示すために,自動再割付文字列としています.これをコンパイルするとエラーがたくさん出ます.
C言語の型に対応した種別
FortranとC言語の相互運用のために,iso_c_binding
モジュールで様々なパラメータと関数が定義されています.C言語の変数に対応した種別のうち,よく使う種別を表に示します.
種別 | C言語の型 | 種別の値 | 備考 |
---|---|---|---|
c_int |
int |
41 |
integer に相当 |
c_float |
float |
4 |
real(real32) に相当 |
c_double |
double |
8 |
real(real64) に相当 |
c_long_double |
long double |
162 83 124 |
拡張倍精度 |
基本的には,整数型や単精度・倍精度実数型の種別はFortranと同じです.一方,Fortranで4倍精度といっていた型はC言語における拡張倍精度c_long_double
に相当しており,この種別はコンパイラで大きく異なります.Intelコンパイラでは16,gfortranでは12です.PGIコンパイラは4倍精度実数に対応していないので,種別は定義していますがその値は倍精度と同じです.C言語の拡張倍精度は80 bit(10バイト)なので,最も近いのはgfortranです.つまり,gfortranは実数の種別として,4, 8, 12, 16を持っていることになります.
派生型person
では整数型の成分があるので,それらの種別を置き換えます.種別はiso_c_binding
で定義されているのでそれをuseし,不要になったiso_fortran_env
を消去します.
program main
use,intrinsic :: iso_c_binding
implicit none
type,bind(c) :: person
character :: initial
character(:),allocatable :: name
integer(c_int) :: age ! kind(int32)をc_intへ置き換え
integer(c_int) :: birthyyyy ! kind(int32)をc_intへ置き換え
integer(c_int) :: birthmm ! kind(int32)をc_intへ置き換え
integer(c_int) :: birthdd ! kind(int32)をc_intへ置き換え
character(3) :: birthMonth
type(person),dimension(:),pointer :: ptrToPerson
end type person
end program main
C言語の文字列(文字の配列)
次に,Fortranの文字列をC言語の文字の配列に置き換えます.文字の配列と文字列はビット列としてみると同じなので,warningは出ますが,そのまま文字列を渡してしまうこともできます.ですが,FortranとC言語で文字列の終端の取り扱いが異なるため,文字の長さの修正が必要になります.
C言語では,文字列はNULL文字で終わります.そのため,長さ4の文字列を取り扱うためには,その4文字にNULL文字を加えた5文字とする必要があります.
char name[5];
name[0] = 'A';
name[1] = 'd';
name[2] = 'a';
name[3] = 'm';
name[4] = '\0';
上の例では4文字の名前を取り扱っています.C言語では,配列は0から始まり,配列要素番号の上限は要素数-1です.そのため,char name[4];
と宣言すると,0から3までの4文字分の要素を利用できますが,末尾1文字はNULL文字に充てる必要があります.
program main
use,intrinsic :: iso_c_binding
implicit none
type,bind(c) :: person
character :: initial
character :: name(4+1) ! 文字の配列へ置き換え,+1文字は終端のNULL文字用
integer(c_int) :: age
integer(c_int) :: birthyyyy
integer(c_int) :: birthmm
integer(c_int) :: birthdd
character :: birthMonth(3+1) ! 文字の配列へ置き換え,+1文字は終端のNULL文字用
type(person),dimension(:),pointer :: ptrToPerson
end type person
end program main
文字列の相互運用のために,iso_c_binding
モジュールでは,エスケープシーケンスが文字型のパラメータとして定義されています.NULL文字はc_null_char
という名前で定義されています.
C言語のポインタの利用
pointer
属性を持つ成分は,C言語のポインタ変数に置き換えます.C言語のポインタ変数と互換性のある型として,派生型type(c_ptr)
が定義されています.
派生型の最終的な形は以下のようになりました.
program main
use,intrinsic :: iso_c_binding
implicit none
type,bind(c) :: person
character :: initial
character :: name(4+1)
integer(c_int) :: age
integer(c_int) :: birthyyyy
integer(c_int) :: birthmm
integer(c_int) :: birthdd
character :: birthMonth(3+1)
type(c_ptr) :: ptrToPerson ! type(c_ptr)へ置き換え
end type person
end program main
type(c_ptr)
は,private
属性の整数型integer(c_intptr_t)
を成分に持つ派生型です.つまり,C言語のポインタと同じように,メモリアドレスのみを持つ整数型変数です.C言語では,char *
のようにメモリアドレスの先にあるデータが何型かを特定できるようにポインタ変数を宣言しますが,Fortranの場合はtype(c_ptr)
が持つメモリアドレスを参照することはできませんし,そのアドレスの先に何型のデータがあるかを知ることもできません.変数のメモリアドレスを関数c_loc()
を用いてtype(c_ptr)
型で取得することと,サブルーチンc_f_pointer(cptr=C言語のポインタ変数もしくはメモリアドレス, fptr=Fortranのポインタ変数[,shape=配列形状])
でC言語のポインタ変数(もしくはメモリアドレス)をFortranのポインタに変換(もしくは結合)することはできます.
変更結果
Fortranでの処理がどのように変わるかを見たあと,C言語の相互利用について見てみます.Fortranのポインタについては別の機会にまとめて本記事にリンクを張るので,ここでは何か面倒な手間が増えているなという程度に認識しておいてください.
変更前のプログラムを示します.
program main
use,intrinsic :: iso_fortran_env
implicit none
type :: person
character :: initial
character(4) :: name
integer(int32) :: age
integer(int32) :: birthyyyy
integer(int32) :: birthmm
integer(int32) :: birthdd
character(3) :: birthMonth
type(person),dimension(:),pointer :: ptrToPerson
end type person
type(person),target :: a_classroom(3)
a_classroom(1) = person("A", "Adam", 20, 1998, 12, 4, "Dec", a_classroom)
a_classroom(2) = person("N", "Nick", 21, 1997, 11, 4, "Nov", a_classroom)
a_classroom(3) = person("Z", "Zack", 22, 1996, 10, 4, "Oct", a_classroom)
print *,a_classroom(1)%name ! Adam
print *,a_classroom(2)%name ! Nick
print *,a_classroom(3)%name ! Zack
print *,a_classroom(1)%ptrToPerson(2)%name ! Nick
print *,a_classroom(3)%ptrToPerson(2)%ptrToPerson(1)%ptrToPerson(3)%name ! Zack
end program main
Fortranのみの運用
派生型にbind(c)
属性を付与し,必要な変更を加えたのが下のプログラムです.特にポインタ変数ptrToPerson
をC言語のポインタ変数に置き換えたので,プログラム中で利用するためにサブルーチンc_f_pointer()
で変換しています.
program main
use,intrinsic :: iso_c_binding
implicit none
type,bind(c) :: person
character :: initial
character :: name(4+1)
integer(c_int) :: age
integer(c_int) :: birthyyyy
integer(c_int) :: birthmm
integer(c_int) :: birthdd
character :: birthMonth(3+1)
type(c_ptr) :: ptrToPerson
end type person
type(person),target :: a_classroom(3) ! ポインタ変数から参照できるようにtarget属性を付与する
type(person),dimension(:),pointer :: ptr_person
a_classroom(1) = person("A", ["A","d","a","m",c_null_char], 20, 1998, 12, 4, ["D","e","c",c_null_char], c_loc(a_classroom))
a_classroom(2) = person("N", ["N","i","c","k",c_null_char], 21, 1997, 11, 4, ["N","o","v",c_null_char], c_loc(a_classroom))
a_classroom(3) = person("Z", ["Z","a","c","k",c_null_char], 22, 1996, 10, 4, ["O","c","t",c_null_char], c_loc(a_classroom))
print *,a_classroom(1)%name ! Adam
print *,a_classroom(2)%name ! Nick
print *,a_classroom(3)%name ! Zack
!ptrToPersonはただのメモリアドレスでしかないので,a_classroom(1)%ptrToPerson(2)%nameというアクセスはできない.
call c_f_pointer(cptr=a_classroom(1)%ptrToPerson, fptr=ptr_person, shape=[3])
print *,ptr_person(2)%name ! Nick
end program main
FortranとC言語の相互運用
C言語と相互利用できる派生型が宣言できたので,C言語の関数に派生型変数を渡してみます.Cで定義した構造体をFortranの手続に渡すことも可能ですが,現実では古いFORTRANをC/C++に置き換えることが多いので,段階的にCへ移植していくことを想定しています.
さすがにFortranで定義した派生型をC側から利用する手段はないので,C側で構造体person
とそれを初期化する関数initPerson
を定義します.FortranはCと相互利用できるようになりましたが,C++の名前修飾(name mangling)には対応していないので,名前修飾を抑制します.文字列はsprintf_s
で書き込んでいますが,C言語のお作法的によいやり方かはわかりません.よい方法があればご指摘ください.
#include<stdio.h>
#include<string.h>
struct person {
char initial;
char name[5];
int age;
int birthyyyy;
int birthmm;
int birthdd;
char birthMonth[4];
struct person *ptrToPerson;
};
extern "C" {
void initPerson(struct person *group, int num) {
group[0].initial = 'A';
group[0].age = 20;
group[0].birthyyyy = 1998;
group[0].birthmm = 12;
group[0].birthdd = 4;
group[0].ptrToPerson = group;
sprintf_s(group[0].name, "Adam");
sprintf_s(group[0].birthMonth, "Dec");
group[1].initial = 'N';
group[1].age = 21;
group[1].birthyyyy = 1997;
group[1].birthmm = 11;
group[1].birthdd = 4;
group[1].ptrToPerson = group;
sprintf_s(group[1].name, "Nick");
sprintf_s(group[1].birthMonth, "Nov");
group[2].initial = 'Z';
group[2].age = 22;
group[2].birthyyyy = 1996;
group[2].birthmm = 10;
group[2].birthdd = 4;
group[2].ptrToPerson = group;
sprintf_s(group[2].name, "Zack");
sprintf_s(group[2].birthMonth, "Oct");
}
}
これをコンパイルし,Fortranのソースをコンパイルするときにリンクします.FortranからCの関数を利用する場合,関数名の先頭にアンダースコア_
を追加することでも呼べるのですが,現代のFortranでは,Cの関数にバインドされる手続のインタフェースを宣言します.そうすることにより,関数と手続の対応が取れ,引数のチェックが可能になります.あくまで仮引数と実引数の型をチェックするので,インタフェースでの仮引数の型はCの関数と同じでなければなりません.
interface
{function, subroutine} Fortranの手続名([仮引数,仮引数,...]) bind(c, name="C言語の関数名")
[import 派生型名] ! 同じルーチン内で派生型を宣言し,仮引数の型宣言に利用する場合に必要
implicit none
[仮引数の変数宣言]
end {function, subroutine}
end interface
関数の戻り値がvoid
の場合はsubroutine
,それ以外の場合はfunction
を用います.
手続のインタフェースと同じルーチンで派生型を定義している場合,インタフェース内でその派生型を参照できません.この問題を解決するためには,import
で派生型名を読み込みます.
Fortranのみの運用のプログラムを,Cの関数を呼ぶように変更しました.Fortranでは,仮引数に何も属性を付けなければポインタ渡し(参照渡し)になりますが,value
属性を付与すると値渡しになります.ポインタ渡しか値渡しかは,C言語の関数と対応するように決めなければなりません.これはよく間違えるところなので,相互運用する場合は注意が必要です.
関数initPerson
では,person
型のポインタと配列要素数num
を渡しているので,それに対応するようにインタフェースを定義しています.そのため,実引数にはa_classroom
ではなくa_classroom(1)
を渡しています.a_classroom
とa_classroom(1)
のメモリアドレスは同じですが,型としては別物なので区別が必要です.
program main
use,intrinsic :: iso_c_binding
implicit none
type,bind(c) :: person
character :: initial
character :: name(4+1)
integer(c_int) :: age
integer(c_int) :: birthyyyy
integer(c_int) :: birthmm
integer(c_int) :: birthdd
character :: birthMonth(3+1)
type(c_ptr) :: ptrToPerson
end type person
interface
subroutine initPerson(group,num) bind(c, name = "initPerson")
use iso_c_binding
import person ! 同一ルーチン内で定義したtype(person)をinterface内で利用するために必要
implicit none
type(person),intent(inout) :: group
integer(c_int),value :: num
end subroutine initPerson
end interface
type(person) :: a_classroom(3)
type(person),dimension(:),pointer :: ptr_person
call initPerson(a_classroom(1),size(a_classroom))
print *,a_classroom(1)%name ! Adam
print *,a_classroom(2)%name ! Nick
print *,a_classroom(3)%name ! Zack
!ptrToPersonはただのメモリアドレスでしかないので,a_classroom(1)%ptrToPerson(2)%nameというアクセスはできない.
call c_f_pointer(cptr=a_classroom(1)%ptrToPerson, fptr=ptr_person, shape=[3])
print *,ptr_person(2)%name ! Nick
end program main
関数名の取得
リンクするオブジェクトにどのような関数があるかを調べるには,WindowsでVisual Studio (C)をインストールしている場合はdumpbin.exeが利用できます.dumpbin.exeはVisual Cのインストールフォルダ以下のbinフォルダに置かれています.
dumpbin.exeをsymbolsオプション付きで実行すると,オブジェクトファイルのファイルフォーマット仕様に沿ってシンボルの一覧を表示します.色々出力されますが,その中に関数名initPerson
を見つけることができます.
> dumpbin.exe /symbols オブジェクトファイル(.obj)
:
:
048 00000000 SECT10 notype () External | _initPerson
:
:
先頭にアンダースコア_
が付いています.Fortran 2003以前は,この関数のシンボル名称を利用して関数を呼んでいたということでしょう.
まとめ
iso_c_binding
,翼を授ける