Building a tcl extension with a loadable C module

F. DominicusCEO/Programmer
CERTIFIED EXPERT
Over 30 years in the programming business, and still learning and failing sometimes ;-)
Published:
I hope you'll find this tutorial useful and interesting. So let's try to extend Tcl with a new package.  For anyone more deeply interested please check out the book "Practical Programming in Tcl and Tk". It's really one of the best written books about a programming language around.

As you might know Tcl/Tk was developed to act as glue between C modules. And therefor one really can say interfacing C to Tcl/Tk is one of the easier things.

Today we built an extension with it's own package name and just one function to be called. I have used the netbeans (http://netbeans.org/features/index.html)  development environment to build this extension.

The end product of all extension usually is a shared library. One can even compile new function into tcl but I dare to say, this is not something for application development. So let's start with some prototypes:
int Tcl_and_c_Adder(ClientData clientData,
                                    Tcl_Interp *interp,
                                    int objc, Tcl_Obj *CONST objv[]);

Open in new window


ClientDate will not get used but in fact it's a untyped pointer and can be used to hand in all kind if Data into the to be called function an example of it's usage can be found in the above mentioned book page 711 following.

For us much more interesting is interp because it represents the "tcl  interpreter". We can  have one or many of them and this is somewhat unique in Tcl/TK in other examples one can not have different interpreters. This again shows the main purpose of tcl.

The objc is comparable to argc of the main function in C. It's a counter for the number of arguments. TclObj is comparable to argv. So this are just the parameters handed over to this function.

Now there is a strict naming convention. In this example  I used Tcl_and_c_ as prefix. This is e.g needed to initialize the data-structures to "know" the new functionality. ans so we come to the next code piece:

int Tcl_and_c_Init(Tcl_Interp *interp) {
                        if (Tcl_InitStubs(interp, "8.1", 0) == NULL) {
                          return TCL_ERROR;
                        }
                        Tcl_CreateObjCommand(interp, "fooadder", Tcl_and_c_Adder,
                                             NULL, NULL);
                        Tcl_PkgProvide(interp, "foo", "1.1");
                        return TCL_OK;
                      }

Open in new window




This is all what is needed to make this  extension known to tcl. At first I require that at least tcl 8.1 is used. That means this extension will work for anything starting from Version 8.1. Currently I Have 8.4 installed. Anyway this code was written some time ago and still is  running. So that means we have some security  that our extensions will work for  some time. That is a good thing ;-)

Now there's one thing one has to keep in mind. All functions return Error codes this is comparable to the COM programming model. So we can not use the return value to give back calculated results. This also means one has to check the return values on the C side rigorously. If you forget that you will probably encounter strange bugs.

Anyway let's see this:

Tcl_CreateObjCommand(interp, "fooadder", Tcl_and_c_Adder,
                                             NULL, NULL);

Open in new window

This is the connection from the C side to the Ttcl side. The new command will be named fooadder in tcl and is implemented in Tcl_and_Adder.  the next parameter can be used to hand over well any kind of data, eg a tcl  hash table and the last parameter is a delete function for cleaning up data held in ClienData, if they are e.g heap allocated. We do not need that.

That all was boiler-plate code you just have to write it once and can use if for any extension. Now we come to the implementation of the new function:
int Tcl_and_c_Adder(ClientData clientData, Tcl_Interp *interp,
                                   int objc, Tcl_Obj *CONST objv[]) {
                          int i_result = 0;
                          Tcl_Obj *result;
                          int ct = objc-1;
                          int i;
                          int i_rval;
                          while (ct > 0){
                              i_rval = Tcl_GetIntFromObj(interp, objv[ct], &i);
                              if (TCL_OK != i_rval){
                                  return TCL_ERROR;
                              }
                              i_result += i;
                              ct--;
                           }
                          result = Tcl_GetObjResult(interp);
                          Tcl_SetIntObj(result, i_result);
                          return TCL_OK;
                      
                      }

Open in new window


Now this is a handful of code. But it's not really "difficult" if you ever have done any kind of C programing with structures ;-)

I hope the names are self-explaining. Them most important are i_result (one can assume this will be some kind of number and Tcl_Obj *result). I've use the "modern" way of accessing tcl in an object oriented fashion. There's also the older "string-based" way of doing things. I for my part find my choice easier to understand and apply.

I did not even forget  to check return values of the Tcl function which--surprise!--start with Tcl.  

This:  i_rval = Tcl_GetIntFromObj(interp, objv[ct], &i); is the way to pick out certain kind of types  from the OBJC values of Tcl. Again if you know COM you can see the relationship the Tcl_Obj is a kind of variant and the names suggests it represents any kind of object.  

As you can see I check the return value and as you can see also the content of objv[ct] is put into a long. And so we can see how result values are placed. They are in the last parameter so we have "out" parameter again this  is the same as in COM. After we picked the integer we just add the value into a normal variable. So what does this function really do?  It takes parameter and add them together. Not  directly rocket-science  as you probably agree. But the question is how to we get the result into Tcl?

This is done here:
result = Tcl_GetObjResult(interp);
                          Tcl_SetIntObj(result, i_result);

Open in new window


Now you have to be a bit careful here. Obviously there is some internal Object kept for that in the Interpreter.  But we did not have allocated any space for the result because we just have a long. If you have some more complex structures you have to think of the clean-up procedures. However in our case we are really safe.

We just place an integer value in the result and this does not need any "caring" at all. But if we'd have malloced pointers we'd better have the proper clean-up routines otherwise every call of the function will leak memory. And this can be quite a lot (think e.g of some tree)....

Now that's it.  Now you just have to compile this code properly and then you can call it.

Here's how to use it on the tcl side:

At first check the auto_path variable. Here I have:

echo $auto_path
                      /usr/share/tcltk/tcl8.5 /usr/lib /usr/local/lib/tcltk /usr/local/share/tcltk /usr/lib/tcltk /usr/share/tcltk  /home/frido/programming/tcl/tcl_and_c/dist/Debug/GNU-Linux-x86

Open in new window

now it's easy to load in tclsh:

package require foo
                      1.1

Open in new window


Now, what a surprise, we got our 1.1 Version how nice ;-)

And now let's roll:

fooadder 1 2 3
                      6

Open in new window


And there we have it a loadable new package with one very important function ;-)

I hope you'd enjoyed  this small tutorial. Would be nice to hear from you how you'd find it and if you see some more of that kind. I though showing such kind of example for a handful "scripting" languages might be "interesting"


#include <tcl.h>
                      
                      int Tcl_and_c_Adder(ClientData clientData,
                                    Tcl_Interp *interp,
                                    int objc, Tcl_Obj *CONST objv[]);
                      
                      
                      
                      int Tcl_and_c_Init(Tcl_Interp *interp) {
                        if (Tcl_InitStubs(interp, "8.1", 0) == NULL) {
                          return TCL_ERROR;
                        }
                        Tcl_CreateObjCommand(interp, "fooadder", Tcl_and_c_Adder,
                                             NULL, NULL);
                        Tcl_PkgProvide(interp, "foo", "1.1");
                        return TCL_OK;
                      }
                      
                      
                      int Tcl_and_c_Adder(ClientData clientData, Tcl_Interp *interp,
                                   int objc, Tcl_Obj *CONST objv[]) {
                          int i_result = 0;
                          Tcl_Obj *result;
                          int ct = objc-1;
                          int i;
                          int i_rval;
                          while (ct > 0){
                              i_rval = Tcl_GetIntFromObj(interp, objv[ct], &i);
                              if (TCL_OK != i_rval){
                                  return TCL_ERROR;
                              }
                              i_result += i;
                              ct--;
                           }
                          result = Tcl_GetObjResult(interp);
                          Tcl_SetIntObj(result, i_result);
                          return TCL_OK;
                      
                      }

Open in new window

0
2,482 Views
F. DominicusCEO/Programmer
CERTIFIED EXPERT
Over 30 years in the programming business, and still learning and failing sometimes ;-)

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.