It’s a common pattern in EDA applications to have a Qt-based GUI along with a Tcl shell for scripting and for access to the deeper functionality of the tool. Often the GUI is used for viewing results and debugging, then once a reasonable design flow is established, the GUI is disabled and the tool is run in batch mode via a script. Although as a scripting language Tcl is showing its age, its entrenched position among chip designers means it’s unlikely to go away soon and we will likely continue to see this combination. As a result it’s worthwhile to consider how Tcl and Qt interact.

In interactive applications of this type you often want users to be able to switch between the terminal (which may be the shell from which they launched the tool, or a command window embedded in your application) and the GUI, while using only a single thread. Unfortunately both Qt and Tcl normally run their own “event loops” - that is to say, a typical main program looks like this:

do_some_setup;  // user adds hooks here
run_main_loop;  // execute commands, process events, etc. for duration of process
return 0;

So it seems you have to pick one library or the other to “own” the main loop of your program. Fortunately both Tcl and Qt are cooperative, and supply functions you can call to let them process any pending events. So you can make your own event loop that calls those functions, and get the desired interactivity. I tested this using the Qt “animated tiles” demo (in main.cpp):

#include <tcl.h>
...
// supply external event loop for Tcl to use after it initializes:
Tcl_SetMainLoop([]() {
    while (true) {
        QApplication::processEvents();
        Tcl_DoOneEvent(TCL_DONT_WAIT);
    }
});

// create a Tcl interpreter and connect it to the terminal
Tcl_Main(argc, argv, [](Tcl_Interp*)->int {return 0;});

// return app.exec();

With this approach we now have smooth interactivity between the shell from which this demo was launched and the GUI. But wait, what’s that fan sound? Why is my lap getting warm? Oh right… this is a polling loop, and it never sleeps. We are constantly checking with Qt and Tcl to see if they need to process anything. Maybe what we can do is put a small sleep in between each iteration:

Tcl_SetMainLoop([]() {
    Tcl_Time wakeup_period;
    wakeup_period.sec = 0;
    wakeup_period.usec = 100000; // 10 times per second for interactivity
        while (true) {
            QApplication::processEvents();
            Tcl_WaitForEvent(&wakeup_period);
            Tcl_DoOneEvent(TCL_DONT_WAIT);
        }
    }); 

OK, that’s a bit better. “top” is no longer showing anything particularly bad and I can’t hear the fan anymore. Unfortunately the animation no longer looks smooth - probably because it’s doing all its work in 10 bursts each second, instead of spread out appropriately.

At this point I remember that underneath the hood, neither Qt nor Tcl does polling in their own event loops. Instead, they register what they’re interested in and let the operating system wake them up when something happens. In Unix this happens through the select() system call. What I really need to do is have Qt and Tcl work with each other so they can both go to sleep and wait for events of interest to either one of them, then dispatch appropriately.

After a look at the documentation I find that Tcl has had support for event loop integration going back to Motif. Tcl only requires that you supply a Notifier, which is just a struct of function pointers that provide an API for Tcl to register its interest in particular file descriptors, and to register for timer callbacks. The example I followed is here. On the implementation side, Qt provides a nice API for monitoring activity on file descriptors through the QSocketNotifier class; anytime something of interest happens Qt emits a signal that we can listen for and pass on to Tcl. The resulting code is here, and the main program from above now looks like:

QtTclNotify::QtTclNotifier::setup(); // registers my notifier with Tcl

// tell Tcl to run Qt as the main event loop once the interpreter is initialized
Tcl_SetMainLoop([](){QApplication::exec();});

// create a Tcl interpreter and connect it to the terminal
Tcl_Main(argc, argv, [](Tcl_Interp*)->int {return 0;});

Now the animation is smooth, the shell is responsive, and the CPU load is reasonable. Success!