Toward a Custom Script Interpreter

After yesterday's big roundup of scripting language approaches, and the decision to go with Tcl (for now), I wanted to actually try it. I am not the only person to ask "I have a … C/C++ program: how do I make it scriptable with Tcl?", which was encouraging! I almost got discouraged by the vehement advice on the Tcl wiki saying not to embed Tcl in a C/C++ program1, but was re-encouraged by another contributor's reminder (complete with citation2) that "the entire purpose of Tcl as originally envisioned, was as a common embedded language" so I forged ahead.

The goal was to write, compile and run a super simple C program, that provided a customized Tcl interpreter, and then talk to it through a REPL. The cooking show re-run starts here:

With Nix we can put our development dependencies in a shell.nix. All that is needed is a compiler and Tcl itself.

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = with pkgs; [
    bashInteractive
    gcc
    tcl
  ];
}

When that's needed, we can activate it with nix-shell. Now for the actual code, what's here is synthesized from looking at:

Basically we are making a "hello world", that can be called the same way as tclsh to provide a super-simple Tcl REPL. First, collect all of our #includes.

#include <stdlib.h>
#include <stdio.h>
#include <sys/utsname.h>
#include <sys/sysinfo.h>
#include <tcl.h>

Then define two custom commands:

// print the machine architecture
static int UnameMachineCmd(ClientData dummy, Tcl_Interp *interp,
                           int objc, Tcl_Obj *const objv[]) {
  struct utsname un;
  if (uname(&un)) {
    Tcl_SetObjResult(interp,
                     Tcl_ObjPrintf("error calling uname(): %s",
                                   Tcl_PosixError(interp)));
    return TCL_ERROR;
  } else {
    Tcl_SetObjResult(interp, Tcl_NewStringObj(un.machine, -1));
    return TCL_OK;
  }
}

// print the machine uptime
static int UptimeSecondsCmd(ClientData dummy, Tcl_Interp *interp,
                           int objc, Tcl_Obj *const objv[]) {
  struct sysinfo in;
  if (sysinfo(&in)){
    Tcl_SetObjResult(interp,
                     Tcl_ObjPrintf("error calling sysinfo(): %s",
                                   Tcl_PosixError(interp)));
    return TCL_ERROR;
  } else {
    Tcl_SetObjResult(interp, Tcl_NewLongObj(in.uptime));
    return TCL_OK;
  }
}

Now, we say how to extend Tcl with the new commands, and what to do on startup:

static int Ex_ExtendTcl (Tcl_Interp *interp) {
  Tcl_CreateObjCommand(interp, "machinetype",
                       UnameMachineCmd, NULL, NULL);
  Tcl_CreateObjCommand(interp, "uptime",
                       UptimeSecondsCmd, NULL, NULL);
  return TCL_OK;
}

int AppInit(Tcl_Interp *interp) {
  if(Tcl_Init(interp) == TCL_ERROR) return TCL_ERROR;
  if(Ex_ExtendTcl(interp) == TCL_ERROR) return TCL_ERROR;
  Tcl_SetVar(interp,"tcl_rcFileName","~/.wishrc",TCL_GLOBAL_ONLY); // ?
  return TCL_OK;
}

Finally, provide a main function that starts the interpreter when the program runs.

int main(int argc, char *argv[]) {
  Tcl_Main(argc, argv, AppInit);
  return 0;
}

We can now compile the program (within the custom environment):

nix-shell &&
g++ \
    -o main \
    -ltcl \
    main.cpp

Finally, (attempt to) use the custom REPL. I'm using Emacs so I'll set tcl-application to the compiled program main

(setq tcl-application (concat default-directory "main"))

…and then M-x inferior-tcl to open the REPL in a new buffer. Otherwise, just call main in a terminal and you'll get the same thing. If everything works, you should be able to write puts "Hello, world after [uptime]!" and get some output like Hello, world after 328337!. exit will close the REPL.

fb5b039e4cc40fd5.gif

Questions & next steps

  • how does this change if there is a render loop? does that require a separate thread?
  • what changes if there is real C++ involved?
    • in particular, how does it work to provide classes?
  • assuming you could compile this with emscripten:
    • how would you interact with the repl in a browser environment?
    • (say, if you were using codemirror)
  • can you provide some kind of autocomplete and/or docstring to the REPL user?

Footnotes:

1

The advice is, instead, to build in TCL and provide the "inner pieces in C/C++". Discussion: Why adding Tcl calls to a C/C++ application is a bad idea