13
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?

More than 5 years have passed since last update.

FortranAdvent Calendar 2018

Day 7

Fortranの派生型とC言語の構造体の相互運用

Last updated at Posted at 2018-12-07

概要

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

成分namecharacter(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_classrooma_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,翼を授ける

  1. 調査した環境での値です.

  2. Intelコンパイラ

  3. PGIコンパイラ

  4. gfortran

13
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
13
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?