Outsourcing C helpers

Richard Suchenwirth - For combining Tcl and C code, two ways are well-known:

  • Embedding (a Tcl interpreter into a C app)
  • Extending (a Tcl interpreter statically, or with dynamic libs)

Besides, there are [exec $x] resp. [open |$x r+] to use external executables inside Tcl. In this Christmas fun project, I extended that most simple approach by defining a Tcl proc which

  • allows to define a C "proc" (in fact, the central part of a main program),
  • writes a C source file to disk ("outsourcing"),
  • compiles that to an executable in the same directory, and
  • creates a Tcl wrapper proc that in turn calls the executable "C helper".

Sounds complicated, but the implementation was quite simple, and the time consumption is not that terrible: on my P200/W95 box, compiling the sample foo below took 2.4 sec; each invocation of foo took about 160 msec.

The following model is applied:

  • Tcl arguments are passed in via argv; each is mapped to a char pointer with the same name. You may convert other representations from that (e.g. atoi in the example below)
  • The helper C code is spliced into a int main(int argc, char *argv[]) frame, stdio.h and stlib.h are included already. Other #includes can be done early in the helper code.
  • Additional C functions can be defined completely in the -with argument. Other arguments allow to override the defaults for compiler name, flags, or generation directory.
  • The helper puts its regular output to stdout. This is where exec and open read from, and you don't have to bother with allocating memory for the result.
  • In error cases just put a message to stderr, so it is seen from inside Tcl (I hate empty strings as error messages ;-) This will cause exec and the wrapper proc to error out. A FATAL macro which takes a constant string is pre-defined (see example).
  • Errors in your C code will cause an error in cproc, where the compiler's error message (at least with gcc) is reported to Tcl.

This started out as a holiday braintwister (surfing between Tcl and C in the same procedure is quite a thrill), but could be useful for tasks where C code can process a limited input (8-bit strings only) more efficiently than pure Tcl code.


For more powerful code generators, see Pipe servers in C from Tcl - Extending Tcl in C from Tcl


 proc cproc {name argl cbody args} {
    if [llength [info command $name]] {error "$name exists"}
        array set a [list -cc gcc -ccflags {-s -Wall -W -ansi -pedantic}\
                          -dir $::env(TEMP) -with {}]
        array set a $args
        set cargs ""
        set narg 0
        foreach i $argl {
            append cargs "\n\t\t char *[lindex $i 0] = argv\[[incr narg]\];"
        }        
        set nname [file nativename [file join $a(-dir) $name]]
        set   fp [open $nname.c w]
        puts $fp "/* $name.c - Generated by cproc */
           #include <stdio.h>
           #include <stdlib.h>
           #define MAXLINE 256
           #define FATAL(_s) {fprintf(stderr,\"error: %s\",_s); return -1;}
           $a(-with)
           int main(int argc, char *argv\[\]) { $cargs
            if(argc!=[incr narg]) FATAL(\"usage: $name $argl\");
             {$cbody
             }
             return 0;
           }"
        close $fp
        eval exec $a(-cc) $a(-ccflags) [list $nname.c -o $nname]
        set body "exec [list $nname]"
        foreach i $argl {append body " \$[lindex $i 0]"}
        proc $name $argl $body
 }

# That's all, now for some usage examples: 
 cproc foo {s {count 2}} {
            /* repeat a string s n times, where count holds the string rep of n */
            int n = atoi(count);
            int i;
            if(n<0) FATAL("count must be non-negative");
            for(i=0; i<n; i++) { results(s); }
 } -with {
            void results(char *x) {printf("%s", x); /* just to show a function */ }
 }

 foo grill 5
 #foo bar -1 => should raise an error

This is what was generated - file foo.c:

 /* foo.c - Generated by cproc */
           #include <stdio.h>
           #include <stdlib.h>
           #define MAXLINE 256
           #define FATAL(_s) {fprintf(stderr,"error: %s",_s); return -1;}
           
            void results(char *x) {printf("%s", x); /* just to show a function */ }
 
           int main(int argc, char *argv[]) { 
                 char *s = argv[1];
                 char *count = argv[2];
            if(argc!=3) FATAL("usage: foo s {count 2}");
             {
            /* repeat a string s n times, where count holds the string rep of n */
            int n = atoi(count);
            int i;
            if(n<0) FATAL("count must be non-negative");
            for(i=0; i<n; i++) { results(s); }
 
             }
             return 0;
           }

And on the Tcl side:

 proc foo {s {count 2}} {exec {C:\WINDOWS\TEMP\foo} $s $count}
 
 #--------------------------------------------------------- Another example: 
  cproc strrev s {
         /* Revert a string */ 
         char *cp = s+strlen(s);
         while(cp > s) putchar(*--cp);
  }
  strrev "A man, a plan, a canal: Panama" ;#=> amanaP :lanac a ,nalp a ,nam A
# The same in Tcl, for comparison - 410(Tcl) vs. 86000(cproc) microseconds:
  proc strrevert s {
         set res ""
         foreach i [split $s ""] {set res $i$res}
         set res
  }