[GPS]: I encourage anyone that knows about extending Tcl to enhance this page. The code contained herein can be considered in the public-domain. The entire tutorial (including examples) is downloadable from: http://www.xmission.com/~georgeps/Extending_Tcl/ The goal of this document is to teach you how to extend Tcl. It covers the various methods of doing this, and the pros and cons of each method. ---- Please note that after appropriate parts of this document are finalized, they should be incoroprated into TIP 66. http://www.tcl.tk/cgi-bin/tct/tip/66.html ---- '''Reasons for extending Tcl''': * to expose an [API] function to Tcl * to increase the speed of a CPU intensive calculation '''Methods of extending Tcl''': * executable module(s) (EM) * Tcl C API 1. [SampleExtension] - the ''official'' example 1. http://members.home.net/tfwilkason/tcl/Examples/crypt.zip 1. http://www.cs.man.ac.uk/%7Efellowsd/tcl/funcproc.c 1. http://www.equi4.com/tclget/ 1. http://www.inx.de/%7Ejn/tcl-hacks/crypt-example.html 1. ftp://www.tcl.tk/pub/tcl/misc/example.tar.gz 1. http://www.iki.fi/mjk/files/test.c.txt * Tcl C API using C++ 1. ftp://ftp.forwiss.uni-passau.de/pub/Os/Unix/languages/tcl/extensions/tkmin/ 1. http://www.uni-frankfurt.de/%7Efp/Tcl/tcl-c++.txt 1. http://prdownloads.sourceforge.net/incrtcl/sampleItclExtension1.1.zip?download * Tcl C API using [Objective C] * Tcl C API using [FORTRAN] * [Critcl] * [cpptcl] * [Hush] * [Itcl++] * [Joy] * [jWrap] * [Mktclapp] * [Object Tcl] * [SWIG] * [Tcl++] * [tclbind] * [Tclblend] * [tclobj] * [TclObjectCommand] * [tcl_object] * [xWizard] * Writing a [TIP] to modify Tcl itself '''Overview of the methods''': With executable modules (EM) exposure of the Tcl C [API] is not needed. Building an executable module is generally easier than writing a C extension for Tcl. The Tcl [exec] command can be used to start a module and send it initial data. For most EM however the [open] command is more useful, because you can use it to create a two-way pipeline. Debugging is easier with an EM, because Tcl isn't involved in the function of the module. It's also reasonable to assume that an EM works with most scripting languages, and is thus not dependent on Tcl. The Tcl C [API] is powerful and for something like a new data type for a tree, threads, or other things that can not be easily done with executable modules it's a good solution. [Critcl] is a newcomer to the world of Tcl. It compiles an extension on the fly based on C code embedded in a Tcl script. It can't be used for all extension work, but for many things it's good. [[todo findout more about how well it works]] A variation on the methods employed by [Critcl] allows Fortran programmers to write extensions for Tcl - it is called Critclf (not very imaginative :), I admit) and is currently under development. Contact [AM] for more information. [Mktclapp] has an [API] for writing extensions. It can be used to generate a stand-alone application by generating C code, or a load'able shared object library. It can also be used to convert a Tcl script or scripts into C code for compilation with a C compiler, so its use is beyond just extensions. [SWIG] automates the task of generating an extension. It can generate an interface for Tcl and other languages, which can then be compiled. [[How well does it work? What is it good at doing?] [xWizard] can generate template C/C++ code for Tcl extension. It is a GUI program written by pure Tcl/Tk so it works for cross-platform. More details in http://www.neatware.com/myrmecox/professional/wizard.html ---- '''Executable Modules''' An EM is not the most difficult thing to create. You can build an EM from other Tcl scripts, Perl scripts, or some other language. Tcl supports binary data over two-way pipes, so you can fconfigure the modules channel to -translation binary, and you will not have to worry about newline conversions. The example used herein implements a small math interpreter that understands + and -. The input goes to its stdin, and it outputs a result to stdout. You can test it interactively from within a shell. /*By George Peter Staplin*/ #include #include #include #include /*A complete application would probably realloc *token if needed.*/ void getToken (char *str[], char *token[]) { char c; int outIndex = 0; while (1) { c = **str; if ('\0' == c) { break; } else if (' ' == c || '\n' == c) { if (outIndex > 0) { break; } } else { (*token)[outIndex] = c; ++outIndex; } (*str)++; } (*token)[outIndex] = '\0'; } int strIsNum (char *str) { int inIndex = 0; while (1) { char c = str[inIndex]; if ('\0' == c) { break; } if (!isdigit (c)) { return 0; } ++inIndex; } if (inIndex > 0) { return 1; } return 0; } int getDigit (char *str) { int res; if (!strIsNum (str)) { /*This could be redesigned so that it would let the caller *know the string isn't valid by returning a boolean and returning the *digit via a pointer. */ return 0; } res = atoi (str); return res; } int main (int argc, char *argv[]) { char buf[1024]; char *str; char *token = malloc (120); if (NULL == token) { perror ("unable to malloc... exiting"); exit (EXIT_FAILURE); } while (1) { str = fgets (buf, sizeof(buf), stdin); if (NULL == str) { break; } getToken (&str, &token); if (0 == (strncmp (token, "+", 1))) { int n1; int n2; getToken (&str, &token); n1 = getDigit (token); getToken (&str, &token); n2 = getDigit (token); printf ("%d\n", n1 + n2); fflush (stdout); } else if (0 == (strncmp (token, "-", 1))) { int n1; int n2; getToken (&str, &token); n1 = getDigit (token); getToken (&str, &token); n2 = getDigit (token); printf ("%d\n", n1 - n2); fflush (stdout); } else { printf ("invalid operation: %s\n", token); fflush (stdout); } } free (token); if (feof (stdin)) { return EXIT_SUCCESS; } return EXIT_FAILURE; } To use the code above as a module we will use a two-way pipe. Tcl uses the | character in the open command to create a pipe. We will open the pipe for w+ or read and write mode. The gets command is used because it understands to stop reading at the \n used in the module. The read command could also be used if you defined a certain character as the stop reading character. To read binary data a header could be read from the buffer, and within the header would be the number of bytes to read. Once you have the number of bytes to read you can use Tcl's read command and not worry about blocking. Be sure to fconfigure -translation binary beforehand if you are working with binary data. Now for the Tcl code: #!/bin/tclsh8.4 set ::mathModule [open {|./a.out} w+] proc modMath {args} { puts "Sending $args" puts $::mathModule $args flush $::mathModule return [gets $::mathModule] } proc main {} { puts [modMath + 200 300] puts [modMath - 5600 1243] puts "Done" } main ---- '''Tcl C API''' The Tcl C API is quite powerful. Tcl can be extended in this matter by using loadable compiled modules with the ''load'' command, or you can build your own wish-like shell. Our first example will be a new interactive shell that performs simple addition. This will teach you how to create a Tcl interpreter, initialize Tcl, and create a simple command for addition. #include #include #include int AddObjCmd (ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { long n1; long n2; Tcl_Obj *res; /*The command name is objv[0] so 3 arguments are expected.*/ if (3 != objc) { Tcl_WrongNumArgs (interp, 1, objv, "n1 n2"); return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[1], &n1)) { /*The error result should be set by Tcl_GetLongFromObj.*/ return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[2], &n2)) { return TCL_ERROR; } res = Tcl_NewLongObj (n1 + n2); Tcl_SetObjResult (interp, res); return TCL_OK; } int main (int argc, char *argv[]) { Tcl_Interp *interp; /*This finds Tcl's library files and performs some initialization.*/ Tcl_FindExecutable (argv[0]); interp = Tcl_CreateInterp (); if (TCL_OK != Tcl_Init (interp)) { fprintf (stderr, "Tcl_Init error: %s\n", Tcl_GetStringResult (interp)); exit (EXIT_FAILURE); } Tcl_CreateObjCommand (interp, "+", AddObjCmd, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL); while (1) { char cmd[1024]; fgets (cmd, sizeof (cmd), stdin); if (TCL_OK != Tcl_Eval (interp, cmd)) { fprintf (stderr, "error: %s\n", Tcl_GetStringResult (interp)); continue; } printf ("result is: %s\n", Tcl_GetStringResult (interp)); } return EXIT_SUCCESS; } ---- [[explain how to build a load'able extension] [[explain how to build an export list for a DLL] [[explain how to link using MSVC++] ---- '''Tcl C API using C++''' Using C++ with Tcl is similar to using it with C. There are a few tricks to using methods within a class. Within the class we create a wrapper which calls a static function within the class. #include #include /*This is specifically for C++.*/ extern "C" { #include } /*Thanks to Kevin Kenny for his help with this.*/ class Math { public: static int AddObjCmd (ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { return (reinterpret_cast(clientData))->AddMethod (interp, objc, objv); } int AddMethod (Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]); }; int Math::AddMethod (Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { long n1; long n2; Tcl_Obj *res; /*The command name is objv[0] so 3 arguments are expected.*/ if (3 != objc) { Tcl_WrongNumArgs (interp, 1, objv, "n1 n2"); return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[1], &n1)) { /*The error result should be set by Tcl_GetLongFromObj.*/ return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[2], &n2)) { return TCL_ERROR; } res = Tcl_NewLongObj (n1 + n2); Tcl_SetObjResult (interp, res); return TCL_OK; } int main (int argc, char *argv[]) { Tcl_Interp *interp; /*This finds Tcl's library files and performs some initialization.*/ Tcl_FindExecutable (argv[0]); interp = Tcl_CreateInterp (); if (TCL_OK != Tcl_Init (interp)) { fprintf (stderr, "Tcl_Init error: %s\n", Tcl_GetStringResult (interp)); exit (EXIT_FAILURE); } Math *inst = new Math; Tcl_CreateObjCommand (interp, "+", inst->AddObjCmd, (ClientData) inst, (Tcl_CmdDeleteProc *) NULL); while (1) { char cmd[1024]; fgets (cmd, sizeof (cmd), stdin); if (TCL_OK != Tcl_Eval (interp, cmd)) { fprintf (stderr, "error: %s\n", Tcl_GetStringResult (interp)); continue; } printf ("result is: %s\n", Tcl_GetStringResult (interp)); } return EXIT_SUCCESS; } [[perhaps someone more knowledgeable about C++ could improve this]] ---- '''Tcl C API using Objective-C''' To expose an Objective-C class to Tcl we can create a wrapper in much the same was as was done with C++ earlier. The following code consists of two files. First the header file (Tcl_C_API_Obj-C.h): #ifndef MTCL_H #define MTCL_H typedef struct { id obj; SEL sel; Class myClass; } MTcl_ClientData; @interface MTcl_ObjCmd : Object { ClientData cdata; Tcl_Interp *interp; int objc; Tcl_Obj *CONST*objv; MTcl_ClientData *instanceData; } @end #endif Now for the implementation (Tcl_C_API_Obj-C.m): #include #include #include #include #include "Tcl_C_API_Obj-C.h" @implementation MTcl_ObjCmd - (void) clientData: (ClientData) _cdata { cdata = _cdata; } - (void) interp: (Tcl_Interp *) _interp { interp = _interp; } - (void) argCount: (int) _argCount { objc = _argCount; } - (void) args: (Tcl_Obj *CONST []) _theArgs { objv = _theArgs; } - (void) instanceData: (MTcl_ClientData *) _idata { instanceData = _idata; } @end @interface Math : MTcl_ObjCmd - (int) AddCmd; @end @implementation Math - (int) AddCmd { long n1; long n2; Tcl_Obj *res; /*The command name is objv[0] so 3 arguments are expected.*/ if (3 != objc) { Tcl_WrongNumArgs (interp, 1, objv, "n1 n2"); return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[1], &n1)) { /*The error result should be set by Tcl_GetLongFromObj.*/ return TCL_ERROR; } if (TCL_OK != Tcl_GetLongFromObj (interp, objv[2], &n2)) { return TCL_ERROR; } res = Tcl_NewLongObj (n1 + n2); Tcl_SetObjResult (interp, res); return TCL_OK; } @end int MTcl_InstanceCaller (ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { MTcl_ClientData *instanceData = (MTcl_ClientData *) clientData; [instanceData->obj clientData: clientData]; [instanceData->obj interp: interp]; [instanceData->obj argCount: objc]; [instanceData->obj args: objv]; [instanceData->obj instanceData: instanceData]; return (int) [instanceData->obj perform: instanceData->sel]; } int main (int argc, char *argv[]) { Tcl_Interp *interp; MTcl_ClientData cdata; id obj; Tcl_FindExecutable (argv[0]); interp = Tcl_CreateInterp (); if (TCL_OK != Tcl_Init (interp)) { fprintf (stderr, "Tcl_Init error: %s\n", Tcl_GetStringResult (interp)); exit (EXIT_FAILURE); } obj = [Math new]; cdata.obj = obj; cdata.sel = @selector(AddCmd); cdata.myClass = [Math class]; Tcl_CreateObjCommand (interp, "+", MTcl_InstanceCaller, (ClientData) &cdata, (Tcl_CmdDeleteProc *) NULL); while (1) { char cmd[1024]; fgets (cmd, sizeof (cmd), stdin); if (TCL_OK != Tcl_Eval (interp, cmd)) { fprintf (stderr, "error: %s\n", Tcl_GetStringResult (interp)); continue; } printf ("result is: %s\n", Tcl_GetStringResult (interp)); } return EXIT_SUCCESS; } ---- [GPS]: I don't currently know much about [Critcl], or [SWIG], so if you would like to extend this please do so. 15jan03 [jcw] - Your "add" example above in Critcl: package require critcl critcl::cproc addup {long a long b} long { return a + b; } interp alias {} + {} addup puts "123 + 864 = [+ 123 864]" If you have tclkit and the critcl starkit, do: $ critcl add.tcl 123 + 864 = 987 $ If you want to turn this into a loadable package, use this code instead: package provide add 1.0 package require critcl critcl::cproc addup {long a long b} long { return a + b; } Then use the -pkg flag: $ critcl -pkg add.tcl Source: add.tcl Library: add.so Package: /home/jcw/lib/add $ The result is a self-contained auto-generated package: $ ls -lR lib/add/ lib/add/: total 9 drwxr-xr-x 2 jcw users 104 Jan 15 18:52 Linux-x86 -rw-r--r-- 1 jcw users 1506 Jan 15 18:52 critcl.tcl -rw-r--r-- 1 jcw users 64 Jan 15 18:52 pkgIndex.tcl lib/add/Linux-x86: total 12 -rwxr-xr-x 1 jcw users 4660 Jan 15 18:50 add.so -rw-r--r-- 1 jcw users 157 Jan 15 18:52 critcl.tcl $ ---- Somewhere, [RS] wrote some code that talks about embedding C code into your Tcl scripts... [C code generators] ---- What about extending via mechanisms like [CORBA], remote RPC, etc.? Are those kinds of ways, where you are invoking code over a network, considered ''on topic'' here? [GPS]: Any kind of [RPC] would be on topic. On a separate page linked to from here, or within this page. RPC is an interesting topic, and I look forward to hearing about it. :) ---- I added a link [http://www.iki.fi/mjk/files/test.c.txt] under the topic "Methods of extending Tcl / Tcl C API". Comments are welcome. --[mjk] ---- [How to invoke a C function from Tcl] ---- [Category Tutorial]