How to embed Tcl in C applications

How to embed Tcl in C Applications provides details on embedding Tcl in both C and C++ programs.

See Also

Why adding Tcl calls to a C/C++ application is a bad idea
An argument in favour of modifying a program to be an extension of Tcl instead of embedding Tcl in the program!
Adding Tcl/Tk to a C application
Includes a spirited description of the deep difficulties.
BOOK Practical Programming in Tcl and Tk, Third edition
acknowledged best treatment in a book of subject of embedding interpreters. Chapter 44, C Programming and Tcl , is particularly relevant.
Cameron Laird's personal notes on how to use C with Tcl
Points to several related tutorials.
Combining Fortran and Tcl in one program
Fossil's TH1 / Tcl source file
How Fossil dynamically loads and integrates with Tcl on multiple platforms.
tclsh and wish
These programs are themselves examples of embedding Tcl in a C program.
mktclapp, by D. Richard Hipp
Enormously simplifies for mere mortals the task of embedding Tk in programs written in C
Tcl_Init
The function that initializes an embedded Tcl interpreter.
TES, by David Gravereaux
Serves as a model for several of the more esoteric aspects of embedding an interpreter.
Writing Tcl-Based Applications in C
Yet another variation on this same theme. It's the closest this Wiki apparently has, as of early July 2003, to a minimal C application which invokes Tcl scripts.
Invoking Tcl commands from Cplusplus
Quickly jumps past the basics to what CL regards as rather esoteric performance considerations.

Documentation

TIP 66 , Stand-alone and Embedded Tcl/Tk Applications
Strongly recommended.

Books by JO, Brent Welch, and Clif Flynt have minimal examples.

Description

Although having Tcl be the primary program rather than the embedded program is usually a better choice, Tcl was designed to be easily embedded, so embedding Tcl in a program is a matter of just a few lines of C code:

#include <stdlib.h>
#include <tcl.h>
Tcl_Interp *interp;
int ExtendTcl (Tcl_Interp *interp) {
    /*
    Create Tcl Commands, etc.
    */
    return TCL_OK;
}

int main (int argc ,char *argv[]) {
    Tcl_FindExecutable(argv[0]);
    interp = Tcl_CreateInterp();
    if (Tcl_Init(interp) != TCL_OK) {
        return EXIT_FAILURE;
    }
    if (ExtendTcl(interp) != TCL_OK) {
        fprintf(stderr ,"Tcl_Init error: %s\n" ,Tcl_GetStringResult(interp));
        exit(EXIT_FAILURE);
    }
    /*
    do stuff
    */
    Tcl_Finalize();
    return EXIT_SUCCESS;
}

That's all there is to it.

Then, to extend an embedded Tcl, create a function that has the appropriate signature to be registered as a command, such as the StringCatCmd from TclCmdMZ.c in the Tcl sources, and modify the ExtendTcl function slightly:

static int
StringCatCmd(
    ClientData dummy,                /* Not used. */
    Tcl_Interp *interp,                /* Current interpreter. */
    int objc,                        /* Number of arguments. */
    Tcl_Obj *const objv[])        /* Argument objects. */
{
    int i;
    Tcl_Obj *objResultPtr;

    if (objc < 2) {
        /*
         * If there are no args, the result is an empty object.
         * Just leave the preset empty interp result.
         */
        return TCL_OK;
    }
    if (objc == 2) {
        /*
         * Other trivial case, single arg, just return it.
         */
        Tcl_SetObjResult(interp, objv[1]);
        return TCL_OK;
    }
    objResultPtr = objv[1];
    if (Tcl_IsShared(objResultPtr)) {
        objResultPtr = Tcl_DuplicateObj(objResultPtr);
    }
    for(i = 2;i < objc;i++) {
        Tcl_AppendObjToObj(objResultPtr, objv[i]);
    }
    Tcl_SetObjResult(interp, objResultPtr);

    return TCL_OK;
}

int ExtendTcl (Tcl_Interp *interp) {
    if (Tcl_CreateObjCommand(
        interp, "stringcat", StringCatCmd, NULL, NULL) != TCL_OK) {
        return TCL_ERROR
    }
    return TCL_OK
}

Dynamically Loading Tcl

Instead of linking a program against the Tcl shared library, a program that embeds Tcl can link against the Tcl stubs static library archive, and then use dlsym or GetProcAddress to load the Tcl shared library and initialize the interpreter. The advantage of this is that the embedding program isn't linked against the Tcl shared library, and can use the stubs mechanism to interface with any version of the Tcl shared library that meets its minimum requirements. Another advantage is that because USE_TCL_STUBS is defined, other stubs libraries that are distributed with Tcl, such TclOO, are readily available.

How to dynamically link against the tcltk .dll/.so libs at runtime in an embedded interpreter:

  1. first pull in the required tcl library-version+path-name from the environment or some such.
  2. dlopen/loadlibrary the required tcl library
  3. pull Tcl_CreateInterp from the dll dlsym/GetProcAddress
  4. create the interp
  5. run Tcl_InitStubs(interp,"8.2",0)(In the hardlinked stubs library)
  6. Tcl_FindExecutable(NULL); (set tcl paths etc?)
  7. dlopen the matching tk library
  8. dlsym(tkhandle,"Tk_Init") / GetProcAddress(tklibhandle,"Tk_Init")
  9. (*tk_init)(interp); activate Tk_Init
  10. run Tk_InitStubs(interp,"8.2",0); (in the hardlinked libtkstub8.x)
  11. carry on as per usual.

Compile your "tcl-embedded" software with the -DUSE_TCL_STUBS and -DUSE_TK_STUBS and link against the static libtclstub8.2.a and libtkstub8.2.a libraries.

David Gravereaux suggests that the steps (7),(8) and (9) and (10) could be simply replaced with the calls

  1. Tcl_PkgRequireEx(interp,"Tk","8.2"....)
  2. Tk_InitStubs
  3. Tk_Init

That would probably be better because there is the off chance that you may be trying to pull in a tk library incompatible with the tcl library. Using PackageRequire should pick up the correct one automagically I would guess.

PT 2003-07-07: This is very interesting - linking applications rather than just extensions using the Tcl stubs mechanism. Interesting enough that I gave a try - some example Windows source is attached below.

/* tclembed.c - Copyright (C) 2003 Pat Thoyts <[email protected]>
 *
 * Sample of an embedded Tcl application linked using the Tcl stubs mechanism
 * Method taken from https://wiki.tcl-lang.org/2074
 *
 * ----------------------------------------------------------------------
 * This source code is public domain.
 * ----------------------------------------------------------------------
 *
 * $Id: 2074,v 1.34 2006-08-15 18:00:05 jcw Exp $
 */
 
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tchar.h>
 
#define USE_TCL_STUBS
#include <tcl.h>
 
typedef Tcl_Interp *(*LPFNTCLCREATEINTERP)();
 
static Tcl_Interp *InitializeTcl(int argc, char *args[]);
 
int
main(int argc, char *argv[])
{
     Tcl_Interp *interp;
     int r = TCL_OK;
 
     interp = InitializeTcl(argc, argv);
     if (interp == NULL) {
         fprintf(stderr, "error: failed to initialize Tcl runtime\n");
     } else {
         if (argc > 1) {
             r = Tcl_EvalFile(interp, argv[1]);
             printf(Tcl_GetStringResult(interp));
         }
         Tcl_DeleteInterp(interp);
     }
 
     return r;
}
 
static Tcl_Interp *
InitializeTcl(int argc, char *argv[])
{
     Tcl_Interp *interp = NULL;
     Tcl_DString dString;
     TCHAR szLibrary[16];
     char *args;
     int nMinor;
     LPFNTCLCREATEINTERP lpfnTcl_CreateInterp;
     HINSTANCE hTcl = NULL;
     
     for (nMinor = 5; hTcl == NULL && nMinor > 2; nMinor--) {
         wsprintf(szLibrary, _T("tcl8%d.dll"), nMinor);
         hTcl = LoadLibrary(szLibrary);
     }
 
     if (hTcl != NULL) {
         lpfnTcl_CreateInterp = (LPFNTCLCREATEINTERP)
             GetProcAddress(hTcl, "Tcl_CreateInterp");
         if (lpfnTcl_CreateInterp != NULL) {
             interp = lpfnTcl_CreateInterp();
             if (interp != NULL) {
                 Tcl_InitStubs(interp, "8.2", 0);
                 Tcl_FindExecutable(argv[0]);
                 Tcl_InitMemory(interp);
                 Tcl_Init(interp);
             }
         }
     }
     return interp;
}

PYK 2018-07-12: Here is an example for posix systems:

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

#include <tcl.h>

Tcl_Interp *loadtcl (char *name) {
    void *handle;
    void (*findExecutable)();
    Tcl_Interp *(*createInterp)();
    int (*init)();
    Tcl_Interp *interp;

    if ((handle = dlopen("libtcl.so" ,RTLD_LAZY)) == 0) {
        fprintf(stderr ,"error loading libtcl\n");
        goto error;
    }
    if ((*(void**)(&findExecutable) = dlsym(handle ,"Tcl_FindExecutable")) == 0) {
        fprintf(stderr ,"could not find Tcl_FindExecutable");
        goto error;
    }

    if ((*(void**)(&createInterp) = dlsym(handle ,"Tcl_CreateInterp")) == 0) {
        fprintf(stderr ,"could not find Tcl_CreateInterp");
        goto error;
    }

    findExecutable(name);
    interp = createInterp();
    if (Tcl_InitStubs(interp ,"8.6" ,0) == NULL) {
        fprintf(stderr ,"could not initialize Tcl");
        goto error;
    }

    if (Tcl_Init(interp) != TCL_OK) {
        goto error;
    }

    return interp;

error:
    return NULL;
}

int main (int argc ,char *argv[]) {
    Tcl_Interp *interp = loadtcl(argv[0]);
    if (interp == NULL) {
        goto error;
    }
    Tcl_GlobalEval(interp ,"puts {hello, world}");
error:
    return 1;
}

Macro Approach Vs Event Approach

Davy aptly observes that there are "essentially two models:

1) Macro style. Tcl_CreateInterp/Tcl_EvalFile/TclDeleteInterp. Simple to implement, but doesn't maintain a persistency and is unfriendly to the parent application by blocking it until the macro finishes. WinCVS is an example of this.

2) Event style. Source the scripts at the beginning to setup their procs and entry points and maintain the interp(s) for the application lifetime. Entry is made to Tcl through its event loop by 'tossing' jobs. It is possible to run Tcl's execution in a separate thread or 'meld' the applications event with Tcl's event loop resulting in a friendlier GUI to the user. This allows the use of Tk, too."

Accessing TclOO In an Embedded Tcl

PYK 2015-12-19:

The TclOO C functions are not exported from the Tcl shared library. A program that embeds Tcl can access them through the stubs mechanism if the program links both to the Tcl shared library and to the Tcl stubs static archive. The steps are:

  1. The compilation unit that includes tcl.h and calls Tcl_CreateInterp() should not define USE_TCL_STUBS or USE_TCLOO_STUBS, and should not include tcloo.h.
  2. Another compilation unit should define USE_TCL_STUBS, USE_TCLOO_STUBS, include tcl.h and tclOO.h, and also call both Tcl_InitStubs() and Tcl_OOInitStubs().
  3. Other compilation units that call tcloo.h should define USE_TCLOO_STUBS, and include tcloo.h.

Below is an example composed of several files:

tclext.h

int example_main(Tcl_Interp *interp);

main.c

#include <stdlib.h>
#include <tcl.h>
#include "tclext.h"


Tcl_Interp *interp;
/* This function embeds Tcl */
int main (int argc ,char * argv[]) {
    Tcl_FindExecutable(argv[0]);
    interp = Tcl_CreateInterp();
    if (Tcl_Init(interp) != TCL_OK) {
        fprintf (
            stderr ,"Tcl_Init error: %s\n" ,Tcl_GetStringResult (interp));
        exit(EXIT_FAILURE);
    }
    if (example_main(interp) != TCL_OK) {
        fprintf (
            stderr ,"example_main error: %s\n" ,Tcl_GetStringResult (interp));
        exit(EXIT_FAILURE);
    }
    Tcl_Finalize();
    exit(EXIT_SUCCESS);
}

tclext.c

#define USE_TCL_STUBS
#include <tcl.h>
#define USE_TCLOO_STUBS
#include <tclOO.h>

#include "tclext.h"

#define METHOD_ARGS ClientData clientData ,Tcl_Interp *inter ,Tcl_ObjectContext objectContext ,int objc ,Tcl_Obj *const *objv

int fly (METHOD_ARGS) {
    Tcl_Eval(interp ,"puts {I'm Flying!}");
}

const static Tcl_MethodType FlyMethodType = {
    TCL_OO_METHOD_VERSION_CURRENT
    ,"fly"
    ,fly
    ,NULL
    ,NULL
};

/* This function extends Tcl */
int example_main(Tcl_Interp *interp) {
    if (Tcl_InitStubs(interp ,"8.6" ,0) == NULL) {
        return TCL_ERROR;
    }
    if (Tcl_OOInitStubs(interp) == NULL) {
        return TCL_ERROR;
    }
    Tcl_Channel chan = Tcl_GetStdChannel(TCL_STDOUT);
    Tcl_Obj *name = Tcl_NewStringObj("::oo::class" ,-1);
    Tcl_Object classobj = Tcl_GetObjectFromObj(interp ,name);
    Tcl_Class class = Tcl_GetObjectAsClass(classobj);
    Tcl_Obj *objectname = Tcl_NewStringObj("fly" ,-1);
    Tcl_Object o = Tcl_NewObjectInstance(
        interp ,class ,"bigbird" ,NULL ,0 ,0 ,0);
    Tcl_NewInstanceMethod(interp ,o ,objectname ,1 ,&FlyMethodType ,NULL);
    Tcl_Eval(interp ,"bigbird fly");
    return TCL_OK;
}

Makefile:

PROGRAM_NAME = myprogram

CPPFLAGS = -I. -I/path/to/tcl/include
LDFLAGS = -L/path/to/tcl/lib
CC=/usr/bin/gcc

#CCDEBUGFLAG = -g


all:
   $(CC) -c $(CCDEBUGFLAG) -o tclext.o $(CPPFLAGS) $(CFLAGS) tclext.c
   $(CC) $(CCDEBUGFLAG) -o $(PROGRAM_NAME) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS)     main.c tclext.o -ltclstub8.6 -ltcl

Misc

TV: Maybe the title could be seen the other way around, where one embeds a computing core, written in C for instance, in a tcl/tk program which controls it and gives it UI elements.

One way to do this which at least doesn't require compiles of the Tcl/Tk version you need is to go where X servers have gone before and use sockets to pass information between the C program and the Tcl/Tk program part, which is a good incentive to do a decent design of the interactions, and for many user interfaces, a not so hard, and automatable (scriptable..) protocol can be used, assuming the socket communication is mastered appropriately. The latter is a matter of making good use of separable blocks and flush commands, and using the same idea as in the event loop (which is the same in the (underlyingm too) C 'select' function. And it requires the socket interfaces to work properly, for instance guaranteeing delivery of all data at least after some time (which is fine for a UI), which, unfortunately, is a problem under windows (MS), at least when I used the cygwin compiler, which can compile quite hefty unix stuff and make it work fine, I got errors related to stream data getting temporarily stuck in buffers, until new data was pushed in. Recently I found that that could be because I was using openGL simulataneously, so I hope it can be solved, and lets not hope winsock is the real reason. I checked some packages which 'test' sockets, and on an XP system they were absolutely not reliable, but depending on the kind. Also, I don't know if under windows, and on the Mac, there is such efficiency saving thing as a pipe (UNIX sockets) when all for starters takes place on the same machine, which lets you not take the whole tcp/ip stack and deamons along in your communication time and memory and process switching time.

An example, from years ago, actually, and the first things I did like this were even older and done on HP UX before tcl even had sockets of its own (but open |probe & worked fine) can be seen here:

http://members.tripod.com/~theover/mesa.html

When well done, one could use any scripting language (tcl alone, tcl/tk, tcl/tk with a web server, perl, another C program, too, visual basic, even smalltalk, and what else), or special program to control a certain piece of program. When the protocol design is done even better, one can even write generally useable mergers and switchers and protocol analysers, and while doing that get away from the nitty gritty of a lot of OO hassle (I did objective-C for years, but still haven't found all I'm looking for, lets say)

TV 2003-10-08: I just tried compiling it on a recent cygwin on XP, and it seems the AUX library from the older mesa is dropped, at least the include file isn't there. I'll look into that, and try linux. A rewrite based on recent example code I may do, expecially I want to see the socket / opengl select() combination, which used to make a tcl control program over a socket link have to limit its output bandwidth/lines per second.

jcw: "aux" is a black hole in Windows (so is "nul", "lpt", "com1", and a few more - all are device names, it's best to avoid these, especially in tars from Unix)

AM: The "aux" library Theo is talking about is one distributed with OpenGL, it has naught to do with pseudo-files like "AUX" under the former MSDOS ...

TV: I had to squeeze in a new harddisk in a quite reasonable machine I can luckily use, and when transfering from the old one, I was stupid enough to let XP do its own installation stuff after I did a raw partition copy, with the old disk as second disk. Neither XP nor cygwin have completely come around to my views on how everything from the partition numbering, registry info, OS dirs and of course PATH like environment vars should perfectly come together (putting it mildly) ...

It works allright (even unattended for weeks running a tclhttpd webserver), and the cygnus gnu gcc compiler isn't unuseable, but maybe it should be simply reinstalled (as sort of a matter of principle I don't like to be forced to that) to get flawless paths and everything. Anyhow, I'm working on getting the thing back together. On the abovementioned page, ALL mesa libraries are available, I think also my (adapted) sources from that time, but recompiling that was fine 5 years ago, currently I think it is preferable to use the cygwin supplied libs and stubs to OS level openGL support.

I can use a fairly fast most recent RedHat linux capable machine, which I intend to use also for video graphics things, where the compiler, Xwindows with NVidea accelerator, and openGL are spinning now, which should also run the above example, I'll try it out, and maybe make a new version.