cURL

cURL is a command-line tool and C library for client access to HTTP, FTP, SMTP, IMAP, and other network protocols. We can invoke cURL either by executing the command-line tool from Fortran, or by calling libcurl routines through the fortran-curl interface bindings:

Installation

On FreeBSD, just install the package curl:

# pkg install ftp/curl

The package contains the cURL command-line tool as well as the development headers. On Linux, the required development header may have to be installed separately.

Command-Line

In Fortran, we can simply execute cURL from command-line in order to retrieve arbitrary data from a web server. In this particular example, a RESTful web service is accessed that returns the current time in London. The response is temporarily stored in a text file that will be read from Fortran afterwards:

! curl.f90
program main
    use, intrinsic :: iso_fortran_env, only: stderr => error_unit
    implicit none
    character(len=*), parameter   :: URL    = 'http://worldtimeapi.org/api/timezone/Europe/London.txt'
    character(len=*), parameter   :: PARENT = '/tmp/'
    character(len=:), allocatable :: cmd, tmp_file
    character(len=512)            :: buf
    integer                       :: fu, rc

    call random_seed()

    ! Set output of cURL to random file, for example `/tmp/aUqCmPev.tmp`.
    call random_file(file_path=tmp_file, parent=PARENT, n=12, ext='.tmp')
    cmd = 'curl -s -o "' // tmp_file // '" "' // URL // '"'

    ! Run cURL from command-line.
    call execute_command_line(cmd, exitstat=rc)

    if (rc /= 0) then
        write (stderr, '(a, i0)') 'HTTP request failed: ', rc
        stop
    end if

    ! Open the temporary file.
    open (action='readwrite', file=tmp_file, iostat=rc, newunit=fu, status='old')

    if (rc /= 0) then
        write (stderr, '(3a, i0)') 'Reading file "', tmp_file, '" failed: ', rc
        stop
    end if

    ! Read and output contents of file.
    do
        read (fu, '(a)', iostat=rc) buf
        if (rc /= 0) exit
        print '(a)', trim(buf)
    end do

    ! Close and delete file.
    close (fu, status='delete')
contains
    subroutine random_file(file_path, parent, n, ext)
        !! Returns a random file path in string `file_path`, with parent
        !! directory 'parent`, for instance: `/tmp/aUqCmPev.tmp`.
        !!
        !! The returned file name contains `n` random characters in range
        !! [A-Za-z], plus the given extension.
        character(len=:), allocatable, intent(inout) :: file_path
        character(len=*),              intent(in)    :: parent
        integer,                       intent(in)    :: n
        character(len=*),              intent(in)    :: ext
        character(len=n)                             :: tmp
        integer                                      :: i, l
        real                                         :: r(n)

        file_path = parent
        l = len(parent)
        if (parent(l:l) /= '/') file_path = file_path // '/'

        call random_number(r)

        do i = 1, n
            if (r(i) < 0.5) then
                tmp(i:i) = achar(65 + int(25 * r(i)))
            else
                tmp(i:i) = achar(97 + int(25 * r(i)))
            end if
        end do

        file_path = file_path // tmp // ext
    end subroutine random_file
end program main

The example outputs the timezone information of London:

$ gfortran10 -o curl curl.f90
$ ./curl
abbreviation: BST
client_ip: XXX.XXX.XXX.XXX
datetime: 2020-05-24T00:09:30.924031+01:00
day_of_week: 0
day_of_year: 144
dst: true
dst_from: 2020-03-29T01:00:00+00:00
dst_offset: 3600
dst_until: 2020-10-25T01:00:00+00:00
raw_offset: 0
timezone: Europe/London
unixtime: 1590275370
utc_datetime: 2020-05-23T23:09:30.924031+00:00
utc_offset: +01:00
week_number: 21

libcurl

Instead of running the cURL command-line tool, we can call the libcurl library routines through the Fortran 2008 interface bindings fortran-curl. First, we need to build the interface library libfortran-curl.a:

$ git clone https://github.com/interkosmos/fortran-curl
$ cd fortran-curl/
$ make

In Fortran, we then import the module curl. A cURL instance is created with curl_easy_init(). We can then set options through the routine curl_easy_setopt(), and start the request with curl_easy_perform(). Finally, the instance must be deleted by calling curl_easy_cleanup(). The option CURLOPT_WRITEFUNCTION is used to set a callback function. We may pass arbitrary data as a C pointer through option CURLOPT_WRITEDATA to the dummy argument data.

! http.f90
module callback
    use, intrinsic :: iso_c_binding
    use :: curl, only: c_f_str_ptr
    implicit none
    private
    public :: write_callback
contains
    ! static size_t callback(void *ptr, size_t size, size_t nmemb, void *data)
    function write_callback(ptr, sze, nmemb, data) bind(c)
        !! Callback function for `CURLOPT_WRITEFUNCTION` that simply outputs
        !! the IMAP messages.
        type(c_ptr),            intent(in), value :: ptr            ! C pointer to response chunk.
        integer(kind=c_size_t), intent(in), value :: sze            ! Always 1.
        integer(kind=c_size_t), intent(in), value :: nmemb          ! Size of response chunk.
        type(c_ptr),            intent(in), value :: data           ! C pointer to argument passed by caller.
        integer(kind=c_size_t)                    :: write_callback ! Bytes written.
        character(len=:), allocatable             :: str

        write_callback = int(0, kind=c_size_t)
        if (.not. c_associated(ptr)) return

        allocate (character(len=nmemb) :: str)  ! Allocate string, size equals chunk size.
        call c_f_str_ptr(ptr, str)              ! Convert C char pointer to Fortran character.
        write (*, '(a)', advance='no') str      ! Write response chunk to standard output.
        deallocate (str)

        write_callback = nmemb                  ! Return number of bytes written.
    end function write_callback
end module callback

program main
    use, intrinsic :: iso_c_binding
    use :: curl
    use :: callback
    implicit none

    character(len=*), parameter :: PROTOCOL = 'http'
    character(len=*), parameter :: URL      = 'http://worldtimeapi.org/api/timezone/Europe/London.txt'
    type(c_ptr)                 :: curl_ptr
    integer                     :: rc

    curl_ptr = curl_easy_init()

    if (.not. c_associated(curl_ptr)) then
        stop 'Error: curl_easy_init() failed'
    end if

    ! Set curl options.
    rc = curl_easy_setopt(curl_ptr, CURLOPT_DEFAULT_PROTOCOL, PROTOCOL // c_null_char)
    rc = curl_easy_setopt(curl_ptr, CURLOPT_URL,              URL // c_null_char)
    rc = curl_easy_setopt(curl_ptr, CURLOPT_WRITEFUNCTION,    c_funloc(write_callback))

    ! Send request.
    if (curl_easy_perform(curl_ptr) /= CURLE_OK) then
        print '(a)', 'Error: curl_easy_perform() failed'
    end if

    call curl_easy_cleanup(curl_ptr)
end program main

The callback routine must have the bind(c) attribute. By default, arguments are passed by reference unless the particular dummy argument has the value attribute.

The Fortran program has to be linked against the static interface library libfortran-curl.a and -lcurl:

$ gfortran10 -I/usr/local/include/ -L/usr/local/lib/ -o http http.f90 libfortran-curl.a -lcurl
$ ./http
abbreviation: BST
client_ip: XXX.XXX.XXX.XXX
datetime: 2020-06-09T22:28:16.386066+01:00
day_of_week: 2
day_of_year: 161
dst: true
dst_from: 2020-03-29T01:00:00+00:00
dst_offset: 3600
dst_until: 2020-10-25T01:00:00+00:00
raw_offset: 0
timezone: Europe/London
unixtime: 1591738096
utc_datetime: 2020-06-09T21:28:16.386066+00:00
utc_offset: +01:00
week_number: 24

The response of the server is directly printed to screen. In order to further process the response , we may pass a pointer to the callback routine via the cURL option CURLOPT_WRITEDATA. See the fortran-curl repository for more examples.

References