Embedding Tcl in C

Category: Tutorial

I was looking for a nice way to embed an interpretter for a program I am currently writing. I first considered embedding LuaJIT, but felt that running commands from the REPL wasn't very friendly.

For example, I imagined a user needing to repeatively call a step function:

step()
step()
step()
...

This gets old very fast, needing to supply empty parentheses for each call didn't seem like a good user experience.

Tcl on the other hand doesn't use parentheses for function calls, so the above might look like:

step
step
step
...

Tcl I don't think is the easiest language to use, but for basic interactions with my program I find it has a friendlier user interface. I wasn't able to find too much online at the time and decided to create this tutorial.

For this project I'm using Visual Studio 2022 Build Tools (with clang support). To build Tcl from source, it's as simple as launching the x64 Native Tools Commands Prompt and running the buildall.vc.bat file.

We'll first start off with the basic interpreter loop. Tcl's C API has a convient function for us, Tcl_Main which will set up the intrepreter allowing us to supply our own Tcl_AppInitProc to extend it.

typedef int Tcl_AppInitProc(
        Tcl_Interp *interp);
#include <stdlib.h>
#include <tcl.h>

int tcl_app_init_proc(Tcl_Interp *interp) {
    // TODO: add commands
    return TCL_OK;
}

int main(int argc, char **argv) {
    Tcl_Main(argc, argv, tcl_app_init_proc);
    return EXIT_SUCCESS;
}

Upon compiling, we will be able to execute our program and interact with the interpreter.

C:\Users\jacob\tcl_example>main.exe
% puts [expr 1 + 3]
4

Now we want to actually extend the intrepreter by adding commands that the user can call from the interpreter. We do this by adding a call to Tcl_CreateObjCommand inside our tcl_app_init_proc.

Let's create a simple command that calls MessageBoxA.

First, we modify our tcl_app_init_proc to register our command:

int tcl_app_init_proc(Tcl_Interp *interp) {
    Tcl_CreateObjCommand(interp, "message_box", message_box_cmd, NULL, NULL);

    return TCL_OK;
}

The string inside Tcl_CreateObjCommand is the name of the command that the user will call. Now we create the message_box_cmd function.

static int message_box_cmd(
    ClientData dummy, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    if (objc != 3) {
        Tcl_AppendResult(interp, "wrong # args: should be \"",
            Tcl_GetString(objv[0]), " message title\"", NULL
        );
        return TCL_ERROR;
    }

    MessageBoxA(NULL,
      Tcl_GetString(objv[1]),
      Tcl_GetString(objv[2]),
      MB_OK
    );

    return TCL_OK;
}

We ignore ClientData, and notice that the count of the arguments will always be one higher than that which we expect our function to take. That is because our command is actually a Tcl list, where the first argument is a Tcl string object containing the name of our command.

Now putting this all together we have,

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

#include <windows.h>

static int message_box_cmd(
    ClientData dummy, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    if (objc != 3) {
        Tcl_AppendResult(interp, "wrong # args: should be \"",
            Tcl_GetString(objv[0]), " message title\"", NULL
        );
        return TCL_ERROR;
    }

    MessageBoxA(NULL,
      Tcl_GetString(objv[1]),
      Tcl_GetString(objv[2]),
      MB_OK
    );

    return TCL_OK;
}

int tcl_app_init_proc(Tcl_Interp *interp) {
    Tcl_CreateObjCommand(interp, "message_box", message_box_cmd, NULL, NULL);

    return TCL_OK;
}

int main(int argc, char **argv) {
    Tcl_Main(argc, argv, tcl_app_init_proc);
    return EXIT_SUCCESS;
}

Example

Here's a slightly more involved example that retrieves a list of process names and IDs utilizing EnumProcesses, OpenProcess, and GetModuleBaseNameA from the Windows API (psapi.h).

static int get_process_list_cmd(
    ClientData dummy, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    DWORD pids[1024], nbytes, npids;

    if (objc > 1) {
        Tcl_AppendResult(interp, "wrong # args: should be \"",
            Tcl_GetString(objv[0]), "\"", NULL
        );
        return TCL_ERROR;
    }

    if (!EnumProcesses(pids, sizeof(pids), &nbytes)) {
        Tcl_AppendResult(interp, "EnumProcesses failed", NULL);
        return TCL_ERROR;
    }

    npids = nbytes / sizeof(DWORD);

    Tcl_Obj *processes_list = Tcl_NewListObj(npids, NULL);
    for(int i = 0; i < npids; i++) {
        Tcl_Obj *process_info = Tcl_NewListObj(2, NULL);
        char buff[256] = {0};

        HANDLE handle = OpenProcess(
            PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
            FALSE,
            pids[i]
        );

        if(handle) {
            if(GetModuleBaseNameA(handle, NULL, buff, sizeof(buff) / sizeof(buff[0])) == 0) {
                Tcl_AppendResult(interp, "GetModuleBaseNameA failed", NULL);
                return TCL_ERROR;
            }
        }

        Tcl_Obj *tcl_pid = Tcl_NewLongObj(pids[i]);
        Tcl_ListObjAppendElement(interp, process_info, tcl_pid);

        Tcl_Obj *tcl_name = Tcl_NewStringObj(buff, strlen(buff));
        Tcl_ListObjAppendElement(interp, process_info, tcl_name);

        Tcl_ListObjAppendElement(interp, processes_list, process_info);
    }

    Tcl_SetObjResult(interp, processes_list);

    return TCL_OK;
}
% foreach e [get_process_list] {
    puts "name: [lindex $e 1], pid: [lindex $e 0]"
}

name: sihost.exe, pid: 3572
name: svchost.exe, pid: 2688
name: svchost.exe, pid: 3996
name: nvcontainer.exe, pid: 19556
name: svchost.exe, pid: 20712
name: nvcontainer.exe, pid: 5020
name: taskhostw.exe, pid: 10424
name: Explorer.EXE, pid: 20692
...