Tcl/Tk

Tcl is a cross-platform bytecode-interpreted scripting language that was initially developed at the University of California, Berkeley, in 1988, and is popular for its graphical widget library Tk, that is also Python’s default toolkit (known as Tkinter). Tcl provides an API to embed scripts in programs written in C and other languages. At least two packages are available to call Tcl from Fortran and vice versa:

Alternatively, we may just rely on standard input/output for inter-process communication to connect Tcl with Fortran.

tkcon
Fig. 1: Enhanced Tcl/Tk console tkcon

Installation

Tcl/Tk packages are available for most Unix-like operating systems. On FreeBSD, install the Tcl shell tclsh, the Tcl/Tk interpreter wish, and the Tk graphical user interface toolkit with:

# pkg install lang/tcl86 x11-toolkits/tk86

We can then invoke the interactive Tcl shell:

$ tclsh8.6
% puts "Hello, World!"
Hello, World!

tkcon is a platform-independent Tk-based replacement for the standard console that provides an input history among other features (fig. 1).

Anonymous Pipe

The example program in Fortran will convert temperature values from °Ré to °C. The historic Réaumur scale was common in Europe until the mid-19th century, and is still used in niche markets. In comparison to °C, the freezing and boiling points of water are defined as 0 and 80 degrees respectively instead. Today, references can sometimes be found in Russian literature:

Simply because a poor student, unhinged by poverty and hypochondria, on the eve of a severe delirious illness (note that), suspicious, vain, proud, who has not seen a soul to speak to for six months, in rags and in boots without soles, has to face some wretched policemen and put up with their insolence; and the unexpected debt thrust under his nose, the I.O.U. presented by Tchebarov, the new paint, thirty degrees Reaumur and a stifling atmosphere, a crowd of people, the talk about the murder of a person where he had been just before, and all that on an empty stomach--he might well have a fainting fit!

— Fyodor Dostoevsky: Crime and Punishment, Part III, Chapter IV, p. 276

The input value in °Ré is simply read-in from stdin, converted to °C, and written to stdout:

! re2c.f90
program re2c
    implicit none
    integer :: rc
    real    :: re

    do
        read (*, *, iostat=rc) re
        if (rc /= 0) exit
        print '(f0.2)', re * 5 / 4
    end do
end program re2c

Then, simply compile the source code to the executable re2c:

$ gfortran12 -o re2c re2c.f90

The binary re2c will later be executed by our Tcl/Tk script.

Tcl/Tk application calling Fortran
Fig. 2: Tcl/Tk graphical user interface that converts temperature values (fonts, widget style, and window decorations depend on the window manager and the selected Tk theme)

The front-end script script.tcl in Tcl creates a Tk window with entry widgets, label widgets, and a single button widget. The callback routine callback will be invoked on button press events, and then opens an anonymous pipe to the Fortran program re2c.

The input value in °Re is passed through standard output to the Fortran program, and the result is read back through standard input. The converted temperature value in °C is then displayed in one of the entry widgets:

#!/usr/bin/env wish
package require Tk

# Tk window settings.
set title "Réaumur to Celsius"
set font  "Helvetica 12"
set theme "classic"

# Temperature values.
global output

set input  "0.0"    ;# Default input value.
set output ""       ;# Default output value.

# Set theme (optional), one of: clam, alt, default, classic.
ttk::style theme use $theme

# Set font to use for all widgets.
option add *font $font

# Set window title.
wm title . $title

# Create grid for our widgets.
grid [ttk::frame .frame -padding "10 10 20 20"] -column 0 -row 0 -sticky nwes
grid columnconfigure . 0 -weight 1
grid rowconfigure    . 0 -weight 1

# Add widgets.
grid [ttk::label  .frame.text   -text "Enter value in °Ré:"]        -column 1 -row 1 -columnspan 5 -sticky we
grid [ttk::entry  .frame.input  -textvariable input -width 7]       -column 1 -row 2 -sticky we
grid [ttk::label  .frame.deg_re -text "°Ré"]                        -column 2 -row 2 -sticky we
grid [ttk::button .frame.button -command [list callback] -text ">"] -column 3 -row 2 -sticky w
grid [ttk::entry  .frame.output -textvariable output -width 7]      -column 4 -row 2 -sticky we
grid [ttk::label  .frame.deg_c  -text "°C"]                         -column 5 -row 2 -sticky we

# Set padding of widgets.
foreach w [winfo children .frame] {
    grid configure $w -padx 0 -pady 5
}

focus .frame.input                  ;# Set initial focus to input widget.
bind . <Return> {[list callback]}   ;# Add key binding.

# Prevent resizing of the window.
update idletasks                    ;# Show window first to get correct size.
set size [wm geometry .]            ;# Get current window size.
regexp {(\d+)x(\d+)} $size all w h  ;# Extract width and height.
wm aspect . $w $h $w $h
wm minsize . $w $h
wm maxsize . $w $h

# The callback routine.
proc callback {} {
    global output                   ;# Set output variable as global to change widget value.
    set input [.frame.input get]    ;# Get input value.
    if {$input eq ""} return

    set f [open "| ./re2c" r+]      ;# Open anonymous pipe to program.
    puts $f $input                  ;# Write to stdout.
    flush $f                        ;# Flush buffer.
    gets $f output                  ;# Read from stdin.
    close $f                        ;# Close file handle.
}

The Tcl/Tk script script.tcl has to be executed with wish, the Tcl interpreter for graphical Tk applications:

$ wish ./script.tcl

The GUI script can be made executable:

$ chmod a+x script.tcl
$ ./script.tcl

Make sure the Fortran application re2c is located in the same directory as the Tcl/Tk script:

The graphical user interface shows the converted temperature returned from the Fortran program (fig. 2).

fortran-tcl86

The library fortran-tcl86 provides interoperability with Tcl/Tk 8.6 through Fortran 2018 interface bindings, and allows us:

On Linux, the library requires Tcl/Tk 8.6 to be installed with development headers. Fetch the source code and compile the static libraries libftcl86.a, libftclstub86.a, and libftk86.a:

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

The following program re2c shows the same graphical user interface as the last example, but this time we create a Tcl environment in Fortran, using the Tcl/Tk C API. Furthermore, we extend the interpreter by an additional Tcl command re2c written in Fortran. The UI script has to be slightly modified to execute this command.

! re2c.f90
program main
    use, intrinsic :: iso_c_binding
    use :: tcl
    use :: tcl_ext
    use :: tk
    implicit none (type, external)

    character(len=*), parameter :: SCRIPT_FILE = 'script.tcl'

    character(len=32) :: argv0
    integer           :: rc
    logical           :: file_exists
    type(c_ptr)       :: interp
    type(c_ptr)       :: ptr

    inquire (exist=file_exists, file=SCRIPT_FILE)
    if (.not. file_exists) stop 'Error: Tcl script not found'

    ! Set name of executable. Fills internal Tcl variable
    ! used by `info nameofexecutable`.
    call get_command_argument(0, argv0)
    call tcl_find_executable(trim(argv0))

    ! Create Tcl interpreter.
    interp = tcl_create_interp()
    if (.not. c_associated(interp)) stop 'Error: Tcl_CreateInterp() failed'

    ! Initialise Tcl.
    if (tcl_init(interp) /= TCL_OK) then
        call tcl_delete_interp(interp)
        stop 'Error: Tcl_Init() failed'
    end if

    ! Initialise Tk.
    if (tk_init(interp) /= TCL_OK) then
        call tcl_delete_interp(interp)
        stop 'Error: Tk_Init() failed'
    end if

    ! Expose the Fortran function as command `re2c` to Tcl.
    ptr = tcl_create_obj_command(interp, 're2c', c_funloc(re2c_cmd))

    ! Run the Tcl/Tk script.
    rc = tcl_eval_file(interp, SCRIPT_FILE)

    ! Show the Tk window.
    call tk_main_loop()
    call tcl_exit(0)
end program main

The Tcl command re2c registered with function tcl_create_obj_command() is implemented in module tcl_ext. The Fortran function re2c_cmd() reads the argument passed from Tcl, converts the given temperature, and returns the result:

! tcl_ext.f90
module tcl_ext
    use, intrinsic :: iso_c_binding
    use, intrinsic :: iso_fortran_env, only: r8 => real64
    use :: tcl
    implicit none (type, external)
    private

    public :: re2c
    public :: re2c_cmd
contains
    elemental function re2c(re) result(c)
        !! Converts temperature from degrees Reaumur to degrees Celsius.
        real(kind=r8), intent(in) :: re
        real(kind=r8)             :: c

        c = re * 5 / 4
    end function re2c

    function re2c_cmd(client_data, interp, objc, objv) bind(c)
        !! Provides the Tcl extension `re2c`.
        type(c_ptr),         intent(in), value :: client_data
        type(c_ptr),         intent(in), value :: interp
        integer(kind=c_int), intent(in), value :: objc
        type(c_ptr),         intent(in)        :: objv(*)
        integer(kind=c_int)                    :: re2c_cmd
        integer                                :: rc
        real(kind=r8)                          :: re
        type(c_ptr)                            :: return_value

        re2c_cmd = TCL_ERROR

        ! Check if argument has been passed.
        if (objc < 2) then
            call tcl_wrong_num_args(interp, 1, objv, 're number')
            return
        end if

        ! Get passed temperature value in deg Re.
        rc = tcl_get_double_from_obj(interp, objv(2), re)
        if (rc /= TCL_OK) return

        ! Convert value and return temperature in deg C.
        return_value = tcl_new_double_obj(re2c(re))

        if (c_associated(return_value)) then
            call tcl_set_obj_result(interp, return_value)
            re2c_cmd = TCL_OK
        end if
    end function re2c_cmd
end module tcl_ext

Finally, we replace the callback routine in script.tcl from the previous example to call the Tcl extension re2c provided by our interpreter instead of the binary:

proc callback {} {
    global output
    set temp [.frame.input get]
    if {$temp eq ""} return
    set output [re2c $temp]
}

Compile the extension module and build the executable re2c:

$ gfortran12 -c tcl_ext.f90
$ gfortran12 -I/usr/local/include/tcl8.6/ -I/usr/local/include/tk8.6/ \
  -L/usr/local/lib/tcl8.6/ -L/usr/local/lib/tk8.6/ \
  -o re2c re2c.f90 tcl_ext.o libftcl86.a libftk86.a -ltcl86 -ltk86

The include and library search paths for Tcl 8.6 and Tk 8.6, as well as the names of the libraries depend on the operating system and may differ (for instance, -ltk8.6 -ltcl8.6 on Linux). It is probably easier to just invoke pkg-config(1) to return the correct compiler and linker flags:

$ gfortran12 `pkg-config --cflags tk86` -o re2c re2c.f90 \
  tcl_ext.o libftcl86.a libftk86.a `pkg-config --libs tk86`

Start the graphical application (fig. 2) by running:

$ ./re2c

Fortran Libraries

GUI Builders

Further Reading

References