概要
Fortranには,ポインタ変数に対する算術演算は存在しません.この記事では,Fortranでもポインタ変数が持つアドレスをインクリメントする方法について述べます.
結論から言うと,C言語の関数とポインタを利用するので,Fortranでというタイトルは少し過大かもしれません.
使用環境
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) |
更新履歴
- 2018年12月15日
- 章題目の誤字を修正.
- 使用したコンパイラとその出力の対応がずれていたので修正.
- 使用したコンパイラとバージョンの情報を追記.
C言語におけるポインタ
C言語のポインタ変数とは,ざっくりいうとメモリアドレスを格納する変数です.メモリアドレスは整数なので,ポインタ変数が保持する値は整数型ですが,宣言の際にそのメモリアドレスに何型の値があるかの情報を明記します.メモリアドレス+そこにある値の型の特定の二つが,ポインタを利用する上で欠かすことができません.
#include<stdio.h>
int main()
{
int a = 10;
int *ptr_a;
ptr_a = &a;
printf("%d, %d\n", a, *ptr_a); ! 10, 10
printf("%p, %p\n", &a, ptr_a); !0x7fffcb7b131c, 0x7fffcb7b131c
return 0;
}
C言語のポインタ変数は,演算をすることで当該ポインタ変数が指すアドレスを変化させることができます.このとき,単純に1を足すという演算を行っただけでも,当該メモリアドレスに存在する値の型のサイズ(バイト数)分アドレスが変化します.
#include<stdio.h>
int main()
{
int a[] = {2,1};
int *ptr_a;
ptr_a = a;
printf("%d, %p\n", *ptr_a, ptr_a); ! 2, 0x7fffe2062a90
ptr_a++;
printf("%d, %p\n", *ptr_a, ptr_a); ! 1, 0x7fffe2062a94
return 0;
}
この挙動を利用して,文字型配列に対するポインタ変数をインクリメント/デクリメントすることで,当該ポインタ変数が指すアドレスを連続的に変化させて文字列を読み出すことができます.
#include<stdio.h>
int main()
{
char *str = "hello world\n";
while(*str != '\n'){
printf("%c, %p\n", *str, str);
str++;
}
return 0;
}
Fortranにおけるポインタ
Fortranのポインタも,メモリアドレスとそのアドレスにある型が何型かの情報を管理しています.Fortranはメモリアドレスを意識しなくてもよい言語なので,C言語にのようにメモリアドレスを操作することは想定されていません.想定されているFortranの使用用途ではポインタの取り扱いは全く苦ではありません.
しかしながら,C言語との相互利用では,メモリアドレスの操作が必要になる場面があります.著者が遭遇したのは,C言語の文字列を読む場面です.
C言語では,文字列1を関数に渡す時に,その先頭アドレスを渡します.受け取った関数側では,Null文字を検出するまでアドレスを単項演算子でインクリメントして文字列を読みます.
C言語との相互利用では,C言語のポインタ変数をtype(c_ptr)
に置き換えることになります.この置き換えは,C言語のポインタ変数と同様な扱いをするには非常に難しい問題を抱えています.
- メモリアドレスにある値の型の情報が失われる.
- 実際にアドレスを持つ成分の
integer(c_intptr_t)
型変数がprivate
であるため,読み込めない. - 何らかの方法でアドレスが読めても,同様の理由で書き込めない.
しかし,type(c_ptr)
はinteger(c_intptr_t)
型変数のみを成分にもつ派生型なので,種別が同じ整数をtype(c_ptr)
であると騙すことができれば,type(c_ptr)
のアドレスを変更できます.
- コンストラクタ
c_ptr()
は,成分がprivate
属性のため利用できません. - Fortranの関数は,型のチェックに引っかかるので利用できません.
そこで,アドレスをC言語の関数でポインタ変数として受取り,関数内で編集して,ポインタ変数を返すという手法を採用します.
アドレスをインクリメントする関数
C言語では,ポインタ変数の宣言において,アドレスの先にある値が何型かという情報を明記することは上で述べました.そして,type(c_ptr)
ではメモリアドレスにある値の型の情報が失われることも指摘しました.
そのため,C言語側でアドレスを1バイトずつインクリメントするようにして,インクリメントするバイト数を引数で渡すことにしました.1バイト単位でアドレスを取り扱うためにchar
型に対するポインタchar *
を利用します.
関数内では,引数として受け取ったポインタ変数address
と,バイト数の増分increment_bytes
を足しています.
仮引数の型を定めたのと同様の理由で,戻り値もchar型のポインタchar *
としています.
extern "C" {
char* incrementAddress(char *address, int increment_bytes) {
return address + increment_bytes;
}
}
インタフェースは次のように書けます.
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
引数はどちらも値渡しとしています.Fortranはポインタ渡しが標準であるため,値渡しとしない場合には,C言語側の関数の引数char *address, int increment_bytes
の型を変更する必要があります.
byte
の実引数はユーザが管理しなければなりません.sizeof()
を利用するのが無難です.種別がバイト数に等しいという決まりが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
integer(c_intptr_t) :: inc
character(:),allocatable,target :: str
integer,target :: a(10) = [1,2,3,4,5,6,7,8,9,10]
character,pointer :: ptr_s
integer ,pointer :: ptr_i
integer :: i
str = "abcdefghijklmnopqrstuvwxyz"//c_null_char
print *,len(str),str ! 27 abcdefghijklmnopqrstuvwxyz
inc = sizeof(c_null_char) ! character型のバイト数
addr = c_loc(str) ! 文字列strの先頭アドレス
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
inc = sizeof(0) ! integer型(標準の種別)のバイト数
addr = c_loc(a) ! 配列aの先頭アドレス
do i=1, size(a)
call c_f_pointer(addr,ptr_i)
print *,ptr_i
addr = incrementAddress(addr, byte=inc)
end do
end program main
これを実行すると,一つずつ文字と数字が表示されます.
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
1
2
3
4
5
6
7
8
9
10
黒魔術
transfer
関数は,private
属性など意にも介さずビット列を別の型に変換します.
program main
use, intrinsic :: iso_c_binding
implicit none
type(c_ptr) :: addr
integer(c_intptr_t) :: add,inc
inc = sizeof(c_null_char) ! アドレスの増分
add = transfer(addr,add) ! 整数型をポインタに変換
print *, add ! I 21646640 g 140737119488032
print *, add+inc ! 21646641 140737119488033
addr = transfer(add+inc,addr) ! 増分を足した整数をポインタに変換
end program main
これを利用すると,わざわざC言語の関数を作成する必要がなくなります.
まずtransfer
でtype(c_ptr)
をinteger(c_intptr_t)
に変換し,アドレスの増分を足した後に,もう一度transfer
でinteger(c_intptr_t)
からtype(c_ptr)
に変換します.
下のプログラムでは,見本となる変数を用意する手間を省くために,長さ0のinteger(c_intptr_t)
型配列[integer(c_intptr_t)::]
を見本とし,配列サイズを1としています.
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
integer(c_intptr_t) :: inc
character(:),allocatable,target :: str
character,pointer :: ptr_s
str = "abcdefghijklmnopqrstuvwxyz"//c_null_char
print *,len(str),str ! 27 abcdefghijklmnopqrstuvwxyz
inc = sizeof(c_null_char) ! character型のバイト数
addr = c_loc(str) ! 文字列strの先頭アドレス
call c_f_pointer(addr,ptr_s)
do while( ptr_s(1:1) /= c_null_char )
print *,ptr_s(1:1)
addr = transfer( transfer(addr,[integer(c_intptr_t)::],1)+inc, addr )
call c_f_pointer(addr,ptr_s)
end do
end program main
まとめ
これを利用することで,C言語のコマンドライン引数char **argv
をFortranから読めるようになります.
-
文字列(文字型の配列)だけに限りません. ↩