8
3

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 14

Fortranでポインタ変数のアドレスをインクリメントする

Last updated at Posted at 2018-12-14

概要

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言語のポインタ変数と同様な扱いをするには非常に難しい問題を抱えています.

  1. メモリアドレスにある値の型の情報が失われる.
  2. 実際にアドレスを持つ成分のinteger(c_intptr_t)型変数がprivateであるため,読み込めない.
  3. 何らかの方法でアドレスが読めても,同様の理由で書き込めない.

しかし,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言語の関数を作成する必要がなくなります.
まずtransfertype(c_ptr)integer(c_intptr_t)に変換し,アドレスの増分を足した後に,もう一度transferinteger(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から読めるようになります.

  1. 文字列(文字型の配列)だけに限りません.

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?