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.

沒有留言:

張貼留言