cURL

cURL is a command-line tool and C library that provides access to HTTP, FTP, SMTP, IMAP, and other protocols.

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 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 in from Fortran:

! 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   :: PATH = '/tmp/'
    character(len=:), allocatable :: cmd, tmp_file
    character(len=512)            :: buf
    integer                       :: fu, rc

    ! Set output of cURL to random file in `/tmp/`.
    call random_file(file_path=tmp_file, parent=PATH, 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 temporary file.
    open (action='read', file=tmp_file, iostat=rc, newunit=fu)

    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)
    call execute_command_line('/bin/rm ' // tmp_file, exitstat=rc)
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_seed()
        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 libcurl directly by linking the Fortran 2008 interface bindings fortran-curl. The libcurl development headers must be installed to allow linking.

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 an arbitrary callback function function callback(ptr, sze, nmemb, data) bind(c). We can 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
    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
        integer(kind=c_size_t), intent(in), value :: sze
        integer(kind=c_size_t), intent(in), value :: nmemb
        type(c_ptr),            intent(in), value :: data
        integer(kind=c_size_t)                    :: write_callback
        character(len=:), allocatable             :: str

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

        allocate (character(len=nmemb) :: str)
        call c_f_str_ptr(ptr, str)
        write (*, '(a)', advance='no') str
        deallocate (str)

        write_callback = nmemb
    end function write_callback

    subroutine c_f_str_ptr(c_str, f_str)
        !! Utility routine that copies a C string, passed as a C pointer, to a
        !! Fortran string.
        type(c_ptr),      intent(in)           :: c_str
        character(len=*), intent(out)          :: f_str
        character(kind=c_char, len=1), pointer :: char_ptrs(:)
        integer                                :: i

        if (.not. c_associated(c_str)) then
            f_str = ' '
            return
        end if

        call c_f_pointer(c_str, char_ptrs, [ huge(0) ])
        i = 1

        do while (char_ptrs(i) /= c_null_char .and. i <= len(f_str))
            f_str(i:i) = char_ptrs(i)
            i = i + 1
        end do

        if (i < len(f_str)) f_str(i:) = ' '
    end subroutine c_f_str_ptr
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

Before compiling the example, we first have to build the cURL bindings for Fortran (curl.o and curlv.o). The application must be linked against libcurl with -lcurl:

$ gfortran10 -I/usr/local/include/ -L/usr/local/lib/ -o http http.f90 curl.o curlv.o -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

In order to store the response of the server in a variable, we can pass a pointer to our callback routine via the cURL option CURLOPT_WRITEDATA.

References