📡

Fortranでソケットを使いたい!

に公開

はじめに

私は少し前から天邪鬼にとりつかれているので、最近流行りの C ではなく Fortran を勉強しています。
Fortran でソケットを扱うプログラムを調べると、C で書かれたコードが付属していることが殆どです。Fortran はなぜか未だにソケットを扱うための標準ライブラリがないのでこれは当然のことですが、Fortran が不完全な言語と言われているようで悔しいですよね。なので、直接 C を書かずにソケットを扱うことを試みます。

対象

  • Fortran 90 (またはそれ以降) の基礎的な文法を理解している人
  • C でソケットを使ったことがある人

使うもの

今回のミソは INTERFACE (引用仕様宣言) と iso_c_binding モジュールです。INTERFACE は、他のファイルに分割された外部手続き(サブルーチンや関数)を使うために、引数や返り値の型を記述するための構文です。iso_c_binding は C と Fortran の相互利用をするための型や手続きを含む Fortran 2003 の標準モジュールです。つまり、今回の記事は Fortran 2003 以降を対象としています。Fortran 95 以前の規格にしか対応していないコンパイラを使う場合には今回の方法は使えません。他の方法で頑張ってください。

手順

モジュールをつくる

保守性の観点から、ソケットに関する定数や手続きはモジュールにまとめるようにします。

mod_socket.f90
module mod_socket
    use, intrinsic :: iso_c_binding
    implicit none
    interface
            ! ここに C 関数の引用仕様を書いていきます。
    end interface
end module

ソケットを扱うのに必要な C 関数の仕様をひたすら書く

C 標準ライブラリのヘッダファイルを読みながら書いていきます。各関数には iso_c_binding の利用宣言が必要です。implicit none を書くのは強く推奨されます。返り値のない C 関数はサブルーチン、返り値がある C の関数は関数として宣言します。

試しに socket() 関数の仕様を追加してみましょう。ヘッダファイルでの socket() 関数の定義は以下のとおりです。

socket.h
int socket(int domain, int type, int protocol);

Fortran で C の int に対応する型は INTEGER(c_int) です。Fortran では引数は参照渡しですが、C では値渡しであるので、基本的に全ての引数には value 属性をつけます。また、BIND(C) 属性が必要です。Fortran の手続き名と C の関数名を変える場合は BIND(C, NAME=Cの関数名) のようにします。Fortran の手続きなどと名前がかぶらないようにしましょう。

mod_socket.f90
module mod_socket
    use, intrinsic :: iso_c_binding
    implicit none

    interface
+        function c_socket(domain, type, protocol) bind(c, name="socket")
+            use, intrinsic :: iso_c_binding
+            implicit none
+            integer(c_int) :: c_socket
+            integer(c_int), value :: domain, type, protocol
+        end function
    end interface
end module

どんどん書いていきます。bind() 関数を追加します。ポインタを渡す引数は全部 TYPE(c_ptr) で宣言してしまいましょう。typedef#define のことを Fortran は知りませんから、これらは定義を探して置き換えます。

mod_socket.f90 (部分)
! int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
function c_bind(sockfd, addr, addrlen) bind(c, name="bind")
    use, intrinsic :: iso_c_binding
    integer(c_int) :: c_bind
    integer(c_int), value :: sockfd
    type(c_ptr), value :: addr
    integer(c_size_t), value :: addrlen
end function

この調子で、listen(), connect(), send(), accept(), send(), recv(), accept() を追加しましょう。以下に型の対応表を置いておきます。

C Fortran
int INTEGER(c_int)
long INTEGER(c_long)
short INTEGER(c_short)
(数値として扱いたい) char INTEGER(c_signed_char)
(文字として扱いたい) char CHARACTER(c_char)
size_t INTEGER(c_size_t)
sockaddr* TYPE(c_ptr)
socklen* TYPE(c_ptr)

sockaddr構造体を定義

できましたか?ところで、sockaddr 構造体のポインタを要求する関数がいました。多くの場合 sockaddr_in 構造体で充分なはずですので、sockaddr_in 構造体を実装します。Fortran で構造体は派生型と呼ばれ、TYPE ~ END TYPE で定義します。このとき、C 構造体と互換性をもたせるために BIND(C) 属性が必要です。

mod_socket.f90
module mod_socket
    use, intrinsic :: iso_c_binding
    implicit none

+    type, bind(c) :: sockaddr_in
+        integer(c_short) :: sin_family, sin_port
+        integer(c_int) :: sin_addr
+        character(c_char) :: sin_zero(8)
+    end type sockaddr_in
+
    interface
        ! 長いので省略
    end interface
end module

足りない関数を追加

C では socket() で開いたソケットは close() を使って閉じます。しかし、Fortran の close() はファイルディスクリプタではなく装置番号を引数にとるため、別で C の close() を使えるようにしましょう。他にもNull終端文字列の長さを取得したり、比較をするための strlen()strcmp() と、バイトオーダー変換のための htons()htonl() が必要になるので、同様に引用仕様宣言を追加しましょう。後の4つの関数は頑張れば Fortran 単体でも作れるかもしれません。

SOCK_STREAM とかを定数として追加

なくてもいいですが、わかりやすくするためです。

mod_socket.f90
module mod_socket
    use, intrinsic :: iso_c_binding
    implicit none

+    ! Address Families
+    integer(c_int), parameter :: AF_INET  =  2
+    integer(c_int), parameter :: AF_INET6 = 10
+    
+    ! Type of Sockets
+    integer(c_int), parameter :: SOCK_STREAM = 1
+    integer(c_int), parameter :: SOCK_DGRAM  = 2
+
+
+    integer(c_int), parameter :: INADDR_ANY = 0
+
    type, bind(c) :: sockaddr_in
        integer(c_short) :: sin_family, sin_port
        integer(c_int) :: sin_addr
        character(c_char) :: sin_zero(8)
    end type sockaddr_in

    interface
        ! 長いので省略
    end interface
end module

完成

だいたいこんな感じになります。(いろいろ変えているので違うところもあるかも)
scrwnl/mod_socket - GitHub

動かしてみましょう

C の'\r''\n'は Fortran では iso_c_binding を利用したうえで、それぞれ c_carriage_returnc_newline という名前の定数を用い、// で連結します。また、TYPE(c_ptr) を渡す引数には、C_LOC(X)という関数にtarget属性がついた変数を渡すことでメモリ上の位置を得られるので、その値を渡します。文字列変数の末尾には、c_null_char// で追加しましょう。それ以外はだいたいCと同じです。

コンパイルするとき、mod_socket.f90を先にコンパイルし、mod_socket.modを生成する必要があります。

$ gfortran -c mod_socket.f90
$ gfortran main.f90 mod_socket.o

Hello, World!

ブラウザで接続すると、Hello, World!を表示します。簡単のために同期的に処理しています。

main.f90
program hello
    use, intrinsic :: iso_c_binding
    use mod_socket
    implicit none

    integer(c_int) :: lsock, asock, status
    type(sockaddr_in), target :: addr, client
    character(len=2), parameter :: crlf = c_carriage_return // c_new_line
    character(len=1024), target :: response = &
    "HTTP/1.1 200 OK" // crlf // &
    "Server: Fortran HTTP Server (Linux)" // crlf // &
    "Content-Type: text/html" // crlf // &
    "" // crlf // &
    "<h1>Hello, World!</h1>" // crlf // c_null_char

    integer(c_size_t) :: response_len
    lsock = c_socket(AF_INET, SOCK_STREAM, 0)

    if (lsock .lt. 0) then
        print*,"Error: Cannot make socket."
        stop 1
    end if

    status = c_memset(c_loc(addr), 0, sizeof(addr))
    addr%sin_family = AF_INET
    addr%sin_port = c_htons(8080)
    addr%sin_addr = c_htonl(INADDR_ANY)
    
    status = c_bind(lsock, c_loc(addr), sizeof(addr))
    
    if (status .lt. 0) then
        print*, "Error: Cannot bind socket"
        stop 1
    end if

    status = c_listen(lsock, 5)
    
    do
        asock = c_accept(lsock, c_null_ptr, c_null_ptr)
        response_len = c_strlen(c_loc(response))
        print*, "送信: Hello World!"
        status = c_send(asock, c_loc(response), response_len, 0)
        status = c_close(asock)
    enddo

    status = c_close(lsock)
end program

インチキ時計

ブラウザに対してインチキHTMLを送りつけます。簡単のために同期的に処理しています。

main.f90
program clock
    use, intrinsic :: iso_c_binding
    use mod_socket
    implicit none

    integer(c_int) :: lsock, asock, status
    type(sockaddr_in), target :: addr, client
    character(len=2), parameter :: crlf = c_carriage_return // c_new_line
    character(:), allocatable, target :: response

    integer(c_size_t) :: response_len
    lsock = c_socket(AF_INET, SOCK_STREAM, 0)

    if (lsock .lt. 0) then
        print*,"Error: Cannot make socket."
        stop 1
    end if

    status = c_memset(c_loc(addr), 0, sizeof(addr))
    addr%sin_family = AF_INET
    addr%sin_port = c_htons(8080)
    addr%sin_addr = c_htonl(INADDR_ANY)
    
    status = c_bind(lsock, c_loc(addr), sizeof(addr))
    
    if (status .lt. 0) then
        print*, "Error: Cannot bind socket"
        stop 1
    end if

    status = c_listen(lsock, 5)
    
    do
        asock = c_accept(lsock, c_null_ptr, c_null_ptr)
        response = &
        "HTTP/1.1 200 OK" // crlf // &
        "Server: Fortran HTTP Server" // crlf // &
        "Content-Type: text/html; charset=utf-8" // crlf // &
        "" // crlf // &
        "<!DOCTYPE html>" // crlf // &
        "<meta charset=utf-8>" // crlf // &
        "<title>The Weird Clock</title>" // crlf // &
        "<meta http-equiv=refresh content='1'>" // crlf // &
        "<h1>Date: " // ctime(time()) // "</h1>" // crlf // c_null_char

        response_len = c_strlen(c_loc(response))
        print*, "===== RESPONSE ====="
        print*, response
        status = c_send(asock, c_loc(response), response_len, 0)
        status = c_close(asock)
    enddo

    status = c_close(lsock)
end program weird_clock

小技

C の関数は返り値を利用せず捨てることができますが、Fortran ではできないようなので (変数に代入したり、式に組み込む必要がある) 、返り値をローカル変数に代入して引数として返すサブルーチンで包むといいかもしれません。その場合、返り値を代入させる変数は OPTIONALな引数で渡すとよさそうです。

おわりに

引用仕様宣言を活用してソケットを扱うことができました。必要に応じて fork() などを追加することで、マルチプロセスな HTTP サーバーなども作れるようになります。決して Fortran は負けておりません。みなさんもぜひ Fortran で実用アプリケーションを書いてみてくださいね。

参考文献

Discussion