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:
- Embed Tcl Calls in C/C++ code (Tcl wiki)
- hpaluch-pil/tcl-cpp-example (GitHub)
- Building a custom tclsh (Tcl wiki)
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.
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:
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