顯示具有 Tcl 標籤的文章。 顯示所有文章
顯示具有 Tcl 標籤的文章。 顯示所有文章

2009-06-18

Get a list of all Tcl commands by Tcl_InfoObjCmd

I am combining Tcl library and GNU readline to provide a Tcl line editing environment. One of the problems I have is the command completion of GNU readline library. In order to do command completion, you have to provide a list of currently available Tcl commands to readline. I know it is already there in Tcl language, i.e, info commands. But, it also dumps the list to the console, :(. Imaging that when you press TAB key, the program prints out all commands first and then the commands matched your request. This is not acceptable. This also means calling Tcl_Eval("info commands") to get result back to process is not an option to me. With hours of searching over Tcl source code, I can't find a public C functionhat does the job. But, fortunately, an internal function Tcl_InfoObjCmd seems to be the cure (Well, its signature may be changed in the future, I know!!) Just use following code to get result,
   Tcl_Obj* objPtr[2];
   objPtr[0] = Tcl_NewStringObj( "info", -1 );
   objPtr[1] = Tcl_NewStringObj( "commands", -1 );

   // I know this function is internal to Tcl, but it seems no other C
   // functions to get all command names out of Tcl.
   Tcl_InfoObjCmd( 0, d_interp, 2, objPtr );

   return getResult().split( " ", QString::SkipEmptyParts );

2009-06-11

Auto-execute shell built-ins or executables in Tcl interactive mode

When I was comparing my customized Tcl line editing environment with the one provided by Tcl_Main(), I figured that one thing Tcl_Main has and my implementation hasn't is the auto-execute shell built-ins or executables. We both are calling Tcl_EvalObjEx() to evaluate commands and same commands are passed into this function on both sides. Why does my environment behave differently? After minutes of scratching heads and searching over Tcl source code, I noticed the Tcl command unknown defined in init.tcl that gets called after the command being executed is identified as non-existed in Tcl interpreter. It tries to run non-existed commands in normal shell if Tcl is at interactive mode. Tcl knows itself is in interactive mode by checking the value of a boolean variable tcl_interactive and the value of this variable is linked to the result of C call isatty(0). If you are customizing your own version of Tcl editing environment and want to execute shell built-ins automatically just like me, you probably need to tell Tcl about this interactive stuff by isatty(0).

2009-05-06

Implement your own shell mode based on Tcl_Main()

In case your clients told you that they will be very happy to see a shell mode of your application implemented because they see this mode as a productivity add (why? who knows? customers always rule!)

Your application already has Tcl interpreter embedded. Why not just take advantage of the code written by Tcl developers? Using it will save us a lot of efforts. After all, you're hired to solve problems, not to reinvent the wheels. But, how?

Tcl has defined a function Tcl_Main() which is the entry function of tclsh. Now, take a look at the generic/tclMain.c in Tcl source. It is called as,

    Tcl_Main(argc, argv, AppInit /* init function */);

and this ~AppInit~ must be declared as

    int AppInit(Tcl_Interp* interp);

In my implementation, this AppInit function is responsible for

  1. Set Tcl library paths pointed by tcl variable tcl_library.
  2. Hack tcl command /history/ so that every time a Tcl command is executed at the shell, it is logged at log file (See definition of Tcl_RecordAndEvalObj() in generic/tclHistory.c). The hack should look something like,
    if {[auto_load ::history]} {#this is why library_path must be specified
        rename ::tcl::HistAdd ::Init::OldHistAdd
    }
    proc ::tcl::HistAdd {command {exec {}}} {
       #
       ...
    
       # my log command
       if {[catch {interp invokehidden {} _log_command_ --cmd [string trim $command]}] != 0} {
           puts "# ERROR: Command '[string trim $command]' is not logged"
           return {}
       }
       #
    }
    
    You should then provide a hidden command _log_command_ for logging.
  3. Register exit handler by Tcl_CreateExitHandler() for cleanup.
  4. Source my initialization scripts.
  5. Register my commands in Tcl interpreter.
  6. Change prompt string.
    set tcl_prompt1 {puts -nonewline stdout {my_shell> }};
    

2009-05-01

Wrap Tcl_ObjCommand in an object-oriented way

People know that Tcl is widely used in the world. It uses C-like syntax and provides Tcl/C binding. For people who know C and want their applications to support script, Tcl is a good solution.

Tcl is written in C. If a Tcl interpreter is required to be embedded in the application and you want to write Tcl command in C/C++, we usually need to do something that makes Tcl a bit object-oriented. How to create an object-oriented Tcl language is not my intention. What I am trying to do is to wrap Tcl in a way that writing of Tcl commands can be more object-oriented (maybe you can see it as a pattern). And if you know any better way, please let me know.

If we are required to create a command called run_me in Tcl interpreter and it is kinda expensive to write this command in tcl script, we can write it in C/C++. I am sure we all agree to this. The internal work is that an associated C function gets called when run_me is executed in tclsh.

First, a Tcl_CreateObjCommand can be used to create this Tcl to C binding and it needs parameters as follows:

Tcl_CreateObjCommand(interp, /* Tcl_Interp* */
                    "run_me",    /* const char*: Tcl command name */
                    &my_run_me_c_func,    /* C function to be called */
                    0, 0 );

In the simplest form, you can just add my_run_me_c_func at global namespace. Of course, it's not a good enough solution for C++ developer because you may add too many functions in global namespace. It shouldn't surprise you that I use a class hierarchy to manage these C bindings. And it shouldn't surprise you again that I apply Command Pattern in this scenario. The base class of the hierarchy may look like,

class TclCommand {
public:
    virtual ~TclCommand() {}
    int execute( ClientData clientData,
                 Tcl_Interp* interp,
                 int objc,
                 Tcl_Obj* const objv[] ) {
        // print help if having -h option

        // syntax checking by is_valid()
 
        // everything is fine, call do_execute()

        return TCL_OK;
    }
protected:
    TclCommand();
    virtual int  do_execute( /* necessary arguments */ ) = 0;
    virtual bool is_valid( /* arguments from command line */ ) = 0;
    virtual void help( /* necessary arguments */ ) = 0;
};

class RunMeTclCommand : public TclCommand {
private:
    // Override pure virtual functions in base class.
};

For different Tcl commands, different TclCommand class should be added in this class hierarchy. The problem is how to let Tcl know run_me and RunMeTclCommand are connected?

First, every Tcl command is linked to a local static function TclCommandLinker and it dispatches call to appropriate TclCommand object. Its content may look like

static int
TclCommandLinker( ClientData clientData,
               Tcl_Interp* interp,
               int objc,
               Tcl_Obj* const objv[] )
{
    // ...
    int length = 0;
    TclCommand* cmd = TclCommandMap::instance()->getMapping(
           Tcl_GetStringFromObj( objv[0], &length ) );
    if ( cmd ) {
       return cmd->execute( clientData, interp, objc, objv );
    }
    return TCL_ERROR;
}

You can see from above that a singleton object TclCommandMap used here to get proper TclCommand mapping. This object can be implemented by std::map as,

class TclCommandMap {
public:
    static TclCommandMap* instance();
    void addMapping(const std::string& name, TclCommand* cmd);
    Command* findMapping(const std::string& name);
    // ...
};

Second, Tcl knows this TclCommandLinker through,

Tcl_CreateObjCommand( interp,
                    "run_me",
                    TclCommandLinker,
                    0, 0 );

Finally, when a Tcl command is registered into Tcl interpreter,

// ...
TclCommandMap::instance()->addMapping("run_me",
                                    new RunMeTclCommand( /* parameters */ );
// ...

That's it. We're done.