cURL

cURL is a command-line tool and C library for client access to several network protocols, depending on the selected build-time options. Among them:

On FreeBSD, install the package ftp/curl with:

# pkg install ftp/curl

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

We can invoke cURL either by executing the command-line tool from Fortran, or by calling libcurl routines through Fortran interface bindings:

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
    implicit none
    character(len=*), parameter :: URL    = 'http://worldtimeapi.org/api/timezone/Europe/London.txt'
    character(len=*), parameter :: PARENT = '/tmp/'

    character(len=512)            :: buf
    character(len=:), allocatable :: cmd, tmp_file
    integer                       :: file_unit, stat

    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=stat)

    if (stat /= 0) then
        print '("Error: HTTP request failed: ", i0)', stat
        stop
    end if

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

    if (stat /= 0) then
        print '("Error: reading file ", a, " failed: ", i0)', tmp_file, stat
        stop
    end if

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

    ! Close and delete file.
    close (file_unit, 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:

$ gfortran13 -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 just have to import the module curl. A cURL instance is created with curl_easy_init(), and options set through routine curl_easy_setopt(). Then, run 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 client_data.

The example program opens a scratch file to temporarily store the HTTP response. To retrieve the response, we simply rewind the file, and read all lines sequentially.

! 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 *client_data)
    function write_callback(ptr, sze, nmemb, client_data) bind(c)
        !! Callback function for `CURLOPT_WRITEFUNCTION` that simply outputs
        !! the HTTP response chunk-wise.
        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 :: client_data !! C pointer to argument passed by caller.

        integer(kind=c_size_t)        :: write_callback ! Bytes written.
        character(len=:), allocatable :: chunk          ! Data to be written.
        integer, pointer              :: file_unit      ! Passed file unit.

        write_callback = int(0, kind=c_size_t)

        if (.not. c_associated(ptr)) return
        if (.not. c_associated(client_data)) return

        call c_f_pointer(client_data, file_unit)        ! Get unit of scratch file.
        call c_f_str_ptr(ptr, chunk, nmemb)             ! Convert C char pointer to Fortran character.
        write (file_unit, '(a)', advance='no') chunk    ! Write response chunk to scratch file.

        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'

    character(len=4096) :: line
    type(c_ptr)         :: curl_ptr
    integer, target     :: file_unit
    integer             :: rc, stat

    ! Create new scratch file.
    open (action='readwrite', iostat=stat, status='scratch', newunit=file_unit)
    if (stat /= 0) stop 'Error: failed to open scratch file'

    ! Initialise curl.
    rc = curl_global_init(CURL_GLOBAL_DEFAULT)
    curl_ptr = curl_easy_init()
    if (.not. c_associated(curl_ptr)) stop 'Error: curl_easy_init() failed'

    ! Set curl options.
    rc = curl_easy_setopt(curl_ptr, CURLOPT_DEFAULT_PROTOCOL, PROTOCOL)
    rc = curl_easy_setopt(curl_ptr, CURLOPT_URL,              URL)
    rc = curl_easy_setopt(curl_ptr, CURLOPT_WRITEFUNCTION,    c_funloc(write_callback))
    rc = curl_easy_setopt(curl_ptr, CURLOPT_WRITEDATA,        c_loc(file_unit))

    ! Send request and clean-up.
    rc = curl_easy_perform(curl_ptr)
    call curl_easy_cleanup(curl_ptr)
    call curl_global_cleanup()

    if (rc /= CURLE_OK) stop 'Error: curl_easy_perform() failed'

    ! Output contents of scratch file.
    rewind (file_unit)

    do
        read (file_unit, '(a)', iostat=stat) line
        if (stat /= 0) exit
        print '(a)', trim(line)
    end do

    close (file_unit)
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:

$ gfortran13 -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

See the fortran-curl repository for more examples.

Fortran Libraries

References