HOWTO make a Statically Linked Tcl extension

This document attempts to describe the steps you need to follow to add a Static extension into a Tcl interpreter for versions of Tcl before Tcl8.0 (it hasn't changed much for Tcl8.0 and later - see later docs).

By Static extension we mean that we are creating a custom version of a tcl interpreter that supports some additional functionality beyond that of the interpreter base. For versions of Tcl greater than 7.5 it is possible to create extensions that are dynamically loaded into a standard tcl interpreter (both explicitly and on demand) we will address that in a later document.

We do not cover directly many of the issues to do with how to implement an extension or how to interact with the Tcl interpreter.

For additional info see the Tcl man page for "load" and the dll example code from ftp://ftp.smli.sun.com/pub/tcl/example.tar.{Z,gz,zip}
i.e example.tar.gz

For approaches to automating these steps where libraries and headers already exist see SWIG, ..

A New Command

Extensions to Tcl are done by adding one or many commands supplying the required new capability- any desired control or modification of state information is usually hidden behind these commands. Usually the hardest part of this is choosing the interface to present to a script and the commands (and options) to be supported.

Within a Tcl interpreter, a command is enabled by two operations:

Implementing a command

The function implementing a Tcl command has the signature:

        typedef int Tcl_CmdProc(
            ClientData clientData,
            Tcl_Interp *interp,
            int argc,
            char *argv[]);

The value returned from the command must be one of the codes TCL_OK TCL_ERROR, TCL_RETURN, TCL_BREAK or TCL_CONTINUE describing the completion status of the command. Most commands return one or other of TCL_OK or TCL_ERROR

In addition the function must set the interpreter result to point to a string value indicating the result of a command (for TCL_OK) or an error message (for TCL_ERROR).

Registering a command

The interpreter doesn't actually know about a command until it is registered with it. The command that does this is

    Tcl_CreateCommand(Tcl_Interp *interp, 
                      char * cmdName, 
                      Tcl_CmdProc *proc, 
                      ClientData clientData, 
                      Tcl_DeleteProc *deleteProc)
which returns a Token for the registered command Simple commands tend to have no need for either clientData and or deleteProc and set them both to NULL.

Versioning and Version requirements

From Tcl7.5 (for the benefit of auto loaded code) Tcl added the concept of an extension package and package versions. Basically this allows an extension to declare what its package name and version is (Tcl_PkgProvide() ) and declare what versions of other extensions (or Base Tcl) it requires (Tcl_PkgRequire).

Normally before you register the command(s) your package needs to call Tcl_PkgRequire for each dependant package your package has, Failures return a NULL ptr and set the interpreter result string.

After you're registered your commands you call Tcl_PkgProvide() which describes the name and version of the Package you've just defined.

Defining your Package (or commands)

The cleanest way of specifying your package is to create a single function (conventionally named <yourpkg>_Init() ) with signature

        int <yourpkg>_Init( Tcl_interp *interp)

here you do the dependency checking (Tcl_PkgRequire()) for your package, define your commands (Tcl_CreateCommand) and setup any other initial state your package needs and declares what your package is (Tcl_PkgProvide()). The return value is TCL_OK or TCL_ERROR describing the success or otherwise of initialising the package.

This provides a single point for the definition of your extension

Tcl Initialisation - TclAppInit.c

Tcl is provided as a library (libtcl) which provides the Tcl interpreter and core Tcl commands. It also provides the capability for a basic shell application structure with a function called Tcl_Main() which is designed to be called from your C main() procedure. This function does three things :

  • creates an interpreter and initialises some global variables in it (env and argv),
  • calls Tcl_AppInit(), which is a function you supply to initialise the interpreter
  • and reads a script file or goes into an interactive input execute loop.

    Most distributions provide a file tclAppInit.c (or tcl<extn>AppInit.c) implementing a shell interpreter. This has a callto Tcl_Main() from the usual main() definition and provides a default implementation of Tcl_AppInit() that initialises the Tcl core functionality plus whatever extension they support.

    To add your extension (its commands) statically (so that it is initially and permanently available) to a tcl interpreter, you modify the Tcl_AppInit() function in tclAppInit.c to call your <yourPkg>_Init function above and/or call Tcl_CreateCommand for application-specific commands, if they weren't already created by the init procedures.

    Useful fns for implementing a command (wrt Interpreter access)

    Tcl_SetResult()
    set the string to be returned to the interprete from this cmdProc
    Tcl_Eval
    evaluate some text as a piece of tcl script
    Tcl_SetVar
    Set the value of a Tcl Variable
    Tcl_GetVar
    Get the value of a Tcl Variable

    Example

    This is a slight modification of the example cited above

    Suppose we decide we need a new command whose sole purpose is to return a string (that sez the command is not implemented yet). It takes a single arg (-upper) specifying that the returned string is to be upper cased ( yes its contrived but if you want non trivial commands look at some of the existing extensions)

    First we need to define the CmdProc implementing the command (in a file like nyiCmd.c )

    /* File: nyiCmd.c */
    #include "tcl.h"
    ...
    static int
    NyiCmd(clientData, interp, argc, argv)
        ClientData clientData;
        Tcl_Interp *interp;
        int argc;
        char **argv;
    {
        int upcase =0;
    
        /* error - too many options */
        if (argc > 2 )
        {
            Tcl_SetResult(interp, "nyi cmd has 0 or a single option", TCL_STATIC);
            return TCL_ERROR;
        }
    
        /* must be option */
        if (argc ==1 )
        {
            /* match option we recognise */
            if (strcmp(argv[1], "-upper" ) == 0 )
                upcase=1;
            else            /* error */
            {
                Tcl_SetResult(interp, 
                    "Unrecognised option: expected nyi [-upper] ", TCL_STATIC);
                return TCL_ERROR;
            }
        }
    
        /* just to show a possible use of clientdata 
         * non null indicates want permanent uppercase */
        if (clientData != NULL)
            upcase=1;
        
        /* return cmd name with appended string */
        Tcl_SetResult(interp, argv[0], TCL_STATIC);
        if (upcase)
            Tcl_AppendResult(interp, " NOT YET IMPLEMENTED", NULL);
         else
            Tcl_AppendResult(interp, " not yet implemented", NULL);
    
        return TCL_OK;
    }   
    

    Now we want to make a function that initialises our pkg (not really necessary in this case since our pkg defines only a single cmd but it makes things easier for a dynamically loaded extension) We can do this in the same file as above - nyiCmd.c

    
    ...
    
    Nyi_Init (interp)
        Tcl_Interp *interp;
    {  
    
        /* dependencies - 7.* only  */
        if (Tcl_PkgRequire(interp, "tcl", "7.5, 0) == NULL)
            return TCL_ERROR;
    
        /* define  a script cmd nyiCommand  that does what NyiCmd specifies */
        Tcl_CreateCommand (interp, "nyiCommand ", NyiCmd,
            (ClientData) NULL, (Tcl_CmdDeleteProc*) NULL);   
    
        /* this is the same except it uses client data as a flag */
        Tcl_CreateCommand (interp, "nyiCommandUpper ", NyiCmd,
            (ClientData) NULL, (Tcl_CmdDeleteProc*)1);   
    
    
        if (Tcl_PkgProvide(interp, "Nyi", "1.0") == TCL_ERROR)
            return TCL_ERROR;
    
        return TCL_OK;
    }
    ...
    

    now we need to modify the file tclAppInit.c function Tcl_AppInit() to call our pkg initialisation function.
    (This is from the tcl7.6 distribution, I've removed comments and ifdefs to make it a bit clearer what we're adding)

    
    ....
    
    int
    Tcl_AppInit(interp)
        Tcl_Interp *interp;         /* Interpreter for application. */
    {
        if (Tcl_Init(interp) == TCL_ERROR) {
            return TCL_ERROR;
        }
    
        /*
         * Call the init procedures for included packages.  Each call should
         * look like this:
         *
         * if (Mod_Init(interp) == TCL_ERROR) {
         *     return TCL_ERROR;
         * }
         *
         * where "Mod" is the name of the module.
         */
        
        /*++++++++++  HERES OUR ADDITION ++++++++++++++++ */
        if (Nyi_Init(interp) == TCL_ERROR) {
             return TCL_ERROR;
        }
        /*+++++++++++++++  THATS IT  ++++++++++++++++++++ */
    
        /*
         * Call Tcl_CreateCommand for application-specific commands, if
         * they weren't already created by the init procedures called above.
         */
    
        /*
         * Specify a user-specific startup file to invoke if the application
         * is run interactively.  Typically the startup file is "~/.apprc"
         * where "app" is the name of the application.  If this line is deleted
         * then no user-specific startup file will be run under any conditions.
         */
    
        Tcl_SetVar(interp, "tcl_rcFileName", "~/.tclshrc", TCL_GLOBAL_ONLY);
        return TCL_OK;
    }
    

    Recompilation of the above changed file (tclAppInit.c) and the additional file(s) that implement your extension (nyiCmd.c) and linking with libtcl.{a,so} should give you a static interpreter (nyitclsh) with the additional tcl cmd(s) "nyiCommand" and "nyiCommandUpper".

    e.g.
    
     cc -belf -c -I/usr/local/include nyiCmd.c 
     cc -belf -Wl,-Bexport tclAppInit.o  nyiCmd.o
        -L/usr/local/lib -ltcl7.6 -lsocket -lm -lc  -o nyitclsh 
    

    Hops (hops@sco.com) $ Last Modified: $Date: 1995/11/14 17:34:15 $: