callbackoverridingtclinterpreter

implementing a Tcl "atreturn" feature?


I'm using a Tcl-based framework for building packages (MacPorts) and would like to implement an atexit-like feature where I can print some information in case a build fails and the framework aborts.

A simple atexit mechanism would work, with a callback that prints a message that I'd set before a build and unset in a so-called post-build block (only executed in case of success).

Except that my code runs in a safe interpreter that hides the exit command so I cannot rename/override it.

I thought I would just rename/override the return function, adapting "MS"'s code from https://wiki.tcl-lang.org/page/AtExit+handlers :

 namespace eval AtReturn {
    variable atReturnScripts [list]

    proc atReturn script {
        variable atReturnScripts
        lappend atReturnScripts \
                [uplevel 1 [list namespace code $script]]
    }

    namespace export atReturn
 }

 rename return AtReturn::ReturnOrig
 proc return {{arg {}} {code 0} {args {}}} {
     variable AtReturn::atReturnScripts
     set n [llength $atReturnScripts]
     while {$n} {
        catch [lindex $atReturnScripts [incr n -1]]
     }
#      rename return {}
#      rename AtReturn::ReturnOrig return
#      namespace delete AtReturn
     if {${arg} eq "-code"} {
         AtReturn::ReturnOrig -code ${code} {*}${args}
     } elseif {${arg} ne {}} {
         AtReturn::ReturnOrig ${arg}
     } else {
         AtReturn::ReturnOrig ${code}
     }
 }

but for some reason that lands me in an infinite recursion. (Idem when I use the original code that restores the stock return function and then calls that).

Am I overlooking something or trying the impossible?

What about something like trace add execution return enter YourCleanupProc?


Solution

  • There are several ways to run some code on exit from a procedure, all implemented with trace. It's not normal to have any ability to modify the result of the procedure.

    1. trace add execution procName leave registers a trace that can see what the result is, but runs after the scope exits. It has the consequence of running on every exit of that procedure (until explicitly unregistered, of course).
    2. trace add variable someUnusedLocalVarName unset registers a trace that runs when the scope is cleaned up. It allows you to observe the exit of a particular scope, but needs to be applied during the running of that scope; it can't be set up before the procedure starts running.

    If you want to modify the result, you should run the code inside some wrapping command, probably built with try or catch, which is at least somewhat intrusive. (However, uplevel can be used to make the amount of intrusion seem less.)

    Tcl doesn't expose anything to do that even partially reliably at interpreter exit other than replacing the exit command (which only really applies to the main interpreter of a process). The reason is that if an interpreter has started being deleted, it isn't safe to run commands within it any more. (Exit handlers in C typically shouldn't call into the interpreter.) If a safe interpreter wants to provide something like that, it needs its trusted parent to handle the callback for it, probably by doing the call into the safe interp prior to commencing the formal deletion process.

    Overwriting return is not guaranteed to work at all. Neither successful nor erroring exit paths from a procedure necessarily use it.