Experimenting with KitCreator

Arjen Markus (15 august 2022) During this month's monthly meet-up we discussed, among other things, the possibilities of DLLs (or shared objects) that contain the Tcl and Tk libraries as well as all the configuration files. Such "KitDLLs" are part of the KitCreator build system by Roy Keene. There are more solutions, like tclkit, BAWT, with similar goals and their own pros and cons. But I decided to have a closer look at KitCreator, as I thought it might be used to build Fortran programs with Tcl/Tk as embedded resource. The fact that all configuration files are part of the KitDLL library makes distribution very easy.

So, I downloaded the source code at [L1 ] and started building under MinGW-w64/MSYS2, as that is the build environment on Windows that is close to native Windows and still support the autotools build tools. Unfortunately, this failed. The crucial error occurred in building "tclvfs". I have not found the apparent quoting error yet in the generated code:

checking for a BSD-compatible install... /usr/bin/install -c
checking whether make sets $(MAKE)... ./configure: eval: line 2963: unexpected EOF while looking for matching `"'
./configure: eval: line 2964: syntax error: unexpected end of file
+ for tryopt in "${tryopts[@]}" __fail__
+ '[' __fail__ = __fail__ ']'
+ return 1
+ die 'configure failed'
+ local msg
+ msg='configure failed'
+ echo 'configure failed'
configure failed
+ exit 1

As is usual with such code in a configure file, it is rather convoluted.

Therefore I tried instead a prebuilt KitDLL - the Tclkit for Tcl version 8.6.12 for Win64 on amd64 .

The file is called libtclkit-8.6.12-win64-amd64-kitdll-xcompile - this name does not end with .dll or .lib as a typical compiler on Windows would expect. I renamed the file to libtclkit-8.6.12-win64-amd64.dll and I used the following tiny source file for tclsh:

#include <tcl.h>

int Tcl_AppInit(Tcl_Interp *interp) {
        return(Tcl_Init(interp));
}

int main(int argc, char **argv) {
        Tcl_Main(argc, argv, Tcl_AppInit);

        return(1);
}

The relevant include file tcl.h comes from a suitable Tcl installation or source tree.

There was one small problem still: on Windows a DLL should be accompanied by a so-called import library. The GNU gcc compiler, however, circumvents this problem. Or you could use a utility to create one from the DLL.


NOTE: You better use, instead, the "software development kit", sdk, that is associated with the KitDLL. It contains the required libraries and include files in a ready form. It does not contain the above sample code for tclsh.


Anyway, I used the distribution of the GNU compilers from [L2 ] as they can be used in an ordinary DOS box (command window). The build step was easy and I got a tclsh.exe. However, the runtime name of the DLL is libtcl8612.dll, so I had to rename the DLL to that and it worked! (As noted above: the SDK contains just about all the files you need with the right names.)

Simple session:

> package require Tk
> pack [canvas .c]
> exit

I also tried to use a Fortran program to call Tcl_Main() and get an easy way to embed Tcl and Tk in such a program. That did not work immediately, most likely because of the argv argument. By using a C main program and passing the arguments argc and argv to a Fortran routine, I did get things to work - albeit in a rather primitive way:

The C main program calls a Fortran routine:

#include <tcl.h>

void ftcl_main( int argc, char **argv );

int main(int argc, char **argv) {
    ftcl_main(&argc, argv);

    return(1);
}

The Fortran code looks like this (mimicking the interfaces to the two Tcl functions):

module ftcl
    use iso_c_binding
    implicit none

    interface
        integer function Tcl_Init( interp ) bind(C, name = "Tcl_Init")
            import :: c_ptr

            type(c_ptr), value :: interp
        end function Tcl_Init
    end interface

    interface
        integer function Tcl_Main( argc, argv, inifunc ) bind(C, name = "Tcl_Main")
            import :: c_ptr, c_funptr

            integer, value        :: argc
            type(c_ptr), value    :: argv
            type(c_funptr), value :: inifunc
        end function Tcl_Main
    end interface

contains

integer function ftcl_appinit( interp ) bind(C, name = "ftcl_appinit")
    type(c_ptr), value :: interp

    write(*,*) 'In ftcl_appinit'

    ftcl_appinit = Tcl_Init( interp )
end function ftcl_appinit

integer function ftcl_main( argc, argv ) bind(C, name = 'ftcl_main')
    integer, value                                        :: argc
    type(c_ptr), value                                    :: argv

    write(*,*) 'In ftcl_main'

    ftcl_main = Tcl_Main( argc, argv, c_funloc(ftcl_appinit) )

end function ftcl_main

end module ftcl

The ideal is to use a Fortran main program, but the argv argument presented some problems. It took me a bit of experimentation and soul searching, but this Fortran program gives me the tclsh I was looking for:

module ftcl
    use iso_c_binding
    implicit none

    interface
        integer function Tcl_Init( interp ) bind(C, name = "Tcl_Init")
            import :: c_ptr

            type(c_ptr), value :: interp
        end function Tcl_Init
    end interface

    interface
        integer function Tcl_Main( argc, argv, inifunc ) bind(C, name = "Tcl_Main")
            import :: c_ptr, c_funptr

            integer, value            :: argc
            type(c_ptr), value        :: argv
            type(c_funptr), value     :: inifunc
        end function Tcl_Main
    end interface

contains

integer function ftcl_appinit( interp ) bind(C, name = "ftcl_appinit")
    type(c_ptr), value :: interp

    ftcl_appinit = Tcl_Init( interp )
end function ftcl_appinit

integer function ftcl_main( inifunc )
    interface
        integer function inifunc( interp) bind(C)
            import :: c_ptr
            type(c_ptr), value :: interp
        end function inifunc
    end interface

    integer                                               :: i, length, argc
    type(c_ptr), dimension(:), allocatable, target        :: argv
    character(len=256), dimension(:), allocatable, target :: arg

    argc = command_argument_count()

    allocate( argv(argc+1), arg(argc+1) )

    do i = 0,argc
        call get_command_argument( i, arg(i+1) )
        length = len_trim(arg(i+1))
        arg(i+1)(length+1:) = c_null_char

        argv(i+1) = c_loc(arg(i+1))
    enddo

    ftcl_main = Tcl_Main( argc+1, c_loc(argv), c_funloc(inifunc) )

end function ftcl_main

end module ftcl

program ftclsh
    use iso_c_binding
    use ftcl

    implicit none

    type(c_ptr), pointer      :: argv
    integer                   :: rc

    rc = ftcl_main( ftcl_appinit )
end program ftclsh

Most of the above code is dedicated to mimicking the proper interfaces and can be put into a library. The iso_c_binding features hide all the platform dependencies: the program works with both gfortran and Intel Fortran oneAPI.