automatically generating socket based Tcl / C connection code, 1

started by Theo Verelst


A second page on the subject: automatically generating socket based Tcl / C connection code, 2, using bwise


To make use a pipe or as here a general socket [L1 ] [L2 ] to connect a user interface with a program or to link programs together is a long used and tried solution since at least X windows. Not that that always worked perfectly, but it's pretty ok.

Also web servers/browsers, ftp, etc. are examples of existing programs making use of socket IPC. The principle is clear enough: one makes a stream connection, and sends messages across with commands and data [L3 ].

In practice, this doesn't work out all too easy, usually. This page presents an example to do most work completely automatically to make a tcl program connect with a C program, and have the C program execute functions under control of the tcl program (alternatively: C compiled image processing on an interactive Bwise canvas).

Also, an example is easily created, where Tk is used to have a stack of buttons for testing this.

On this page I used a recent windows XP running KDE (on an X simulator), and my own compiled tcl/tk 8.4 (but like on linux) in kde windows. Also, I used the cygwin unix-like environment including gcc compiler for windows. Most or all of the materials presented here should run equally well on linux/unix, and probably other os-es, provided they have a C compiler with unix flavour sockets.

Major issues when making a socket link and programming code around it are:

  • newlines (one or two characters, newline or carriage return)
  • end of line (don't forget '\0' to terminate C strings)
  • end of message (how do you know the whole message is in)
  • flow control (no dead/live lock, buffer sizes)
  • process issues (creation, referencing, security, joint load)
  • connection control (setup, re-setup, connection with protocol, leakless cleanup, eof issues)
  • error correction/sensitivity (incomplete messages,re-syncing)

The approach taken for this test version consists of the following steps:

  1. define a list of message names, which are also used as corresponding C function names
  2. make the socket connection possible both on C and Tcl side
  3. generate automatically the C message handler and the frame of the C functions
  4. generate Tcl/Tk code to make a button for each message
  5. save and compile the generated C file
  6. link it with the socket code
  7. run the resulting C program
  8. connect the tcl program
  9. test the buttons to see if the corresponding C functions get called.

What follows are horizontal-line--separated tcl procedures and two C source files.

The tcl code should be somehow loaded in a tcl interpreter with which you'll work this session, while the C sources should be made into two files with the indicated names (preferably), probably all together working from some new-made sub-directory, where we will run the compiler on the C sources.


 # NOTE this proc is also defined in BWise

 proc open_text { {n  {}}  } {
   global textname
   if {[winfo exists .tt] == 0} {
      toplevel .tt
      set textname $n
      text .tt.t -width 40 -height 8
      frame .tt.f
      entry .tt.f.e -textvar textname -width 30
      button .tt.f.s -text Save -command {
         global textname;  set f [open $textname w];
         puts -nonewline $f [.tt.t get 0.0 end]; close $f 
      }
      button .tt.f.l -text Load -command {
         global textname; .tt.t del 0.0 end; 
         set f [open $textname r]; 
         while {[eof $f] == 0} {
            .tt.t insert end "[gets $f]\n"
         }; close $f 
      }
      bind .tt.f.e <Double-Button> {
         set textname [tk_getOpenFile]
      }
      pack .tt.t -expand y -fill both
      pack .tt.f -side bottom -expand n -fill x
      pack .tt.f.e -side left -expand y -fill x
      pack .tt.f.s -side right
      pack .tt.f.l -side right
   } {
      set textname $n
   }
   if {$textname != {}} {
      .tt.f.l invoke
   }
 }


 proc soccdecodefunc { } {
   global socfs

   .tt.t insert 0.0 {#include<string.h>}
   .tt.t insert 0.0 "\n#include<stdio.h>\n"
   # What follows is not an error, it is pre-fab C-code
   .tt.t insert end {

 int socdecode(m)
 char m[];
 }
   .tt.t insert end \{\n

   set j 0
   foreach i $socfs {
      .tt.t insert end "   if \(strcmp\(m,\"$i\"\) == 0) \{$i\(\); return\($j\);\}\n"
      incr j;
   }

   .tt.t insert end "\}\n\n"
 } 

 # you may want to pick a higher default port number on linux 
 proc socconnect { {port {4100}} } {
   global socsockid

   if ![info exists socsockid] { set socsockid stdout}
   if {$socsockid == "stdout"} {
      catch {set socsockid [socket localhost $port]}
      if {$socsockid == "stdout"} {puts "connect failed\n"; set socsockid stdout; return} ;
      fileevent $socsockid readable {
          if [eof $socsockid] {
             close $socsockid;
             set socsockid stdout
          } {
             # print incoming lines on stdout
             puts "[gets $socsockid]"
          }
      }
   } { puts "soc: connect tried while allready connected, ignored\n" }
 } 


 proc socfuncframe { {n} } {
 # make a simple C function body for message n in the tt.t text widget
   global socfs
   .tt.t insert end "$n\(\)\n{\n   printf(\"called:$n\\n\");\n}\n\n"

   lappend socfs $n
 } 

 proc socgencframe { {messagenames {message1 message2 message3}} } {

 # generate C functions frame in .tt.t text widget
 # and create window with buttons for each message

   global socfs
   .tt.t del 0.0 end ; 
   set socfs $messagenames
   soccdecodefunc

   catch {unset socfs};
   foreach m $messagenames {socfuncframe $m} ;
   socgenui
 } 

 proc socgenui { } {
   global socfs

   catch {toplevel .socbuts}
   foreach i [winfo children .socbuts] {destroy $i}
   foreach i $socfs {
      pack [button .socbuts.$i -text $i -command "socsend $i"] -side top -fill x
   }
 } 

 proc socsend { {m} } {
   global socsockid

   if [eof $socsockid] { close $socsockid; set socsockid stdout}
   puts $socsockid $m
   flush $socsockid
 } 

 /* serv2.c */
 /* server exa */
 /* make sure you have either defined. my linux is redhat as to this writing */

 #ifdef CYGWIN 
 #include "cygwin/socket.h"
 #include "cygwin/in.h"
 #endif

 #ifdef LINUX 
 #include "linux/socket.h"
 #include "linux/in.h"
 #include "linux/time.h"
 #endif

                       /* this is non-checked for now */
 #define INMAX 8*1024
 #define SERV_TCP_PORT 4100
 /*you may want to pick a higher default port number on linux */

 int sockfd,newsockfd,clilen;
 struct sockaddr_in cli_addr, serv_addr;
 char inlin[INMAX];
 struct timeval timeout;
 fd_set fdvar;

 int do_read()
 {
   int rr, n;
   n = INMAX;

   FD_ZERO(&fdvar);
   FD_SET(newsockfd, &fdvar);

   timeout.tv_sec = 0;
   timeout.tv_usec = 0;

   if (select(newsockfd+1,&fdvar,(fd_set *) 0, 
      (fd_set *) 0, (struct timeval *) 0) <= 0) return(0);
   if (FD_ISSET(newsockfd,&fdvar)  == 0) 
      { return(0); }
   rr = read(newsockfd,inlin,n);
   if (rr <= 0) {
      rr = 0;
   }
   inlin[rr] = '\0';
   return(rr);
 }

 int do_write(b,n)
 char b[];
 int n;
 {
   int rr;
   rr = write(newsockfd,b,n);
   return(rr);
 }

 serv_main()
 {
   if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) <0 ) {
      printf("Error: can't open socket.\n");
      exit(-1);
   }

   memset( &serv_addr, 0, sizeof(serv_addr) );
   serv_addr.sin_family = AF_INET;
   serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
   serv_addr.sin_port = htons(SERV_TCP_PORT);

   if (bind(sockfd, (struct sockaddr *) &serv_addr, 
    sizeof(serv_addr)) < 0) {
      printf("Error: cannot bind socket.\n");
      exit(-1);
   }

   printf("waiting for connection ...\n");
   listen(sockfd,5);

      clilen = sizeof(cli_addr);
      newsockfd = accept(sockfd, 
         (struct sockaddr *) &cli_addr, &clilen);
      if (newsockfd < 0) {
         printf("Error opening new socket.\n");
         exit(-1);
      }
   FD_ZERO(&fdvar);
   FD_SET(newsockfd, &fdvar);
   timeout.tv_sec = 0;
   timeout.tv_usec = 0;

   inlin[0] = '\0';
 }

 /* stub.c */ 

 #include<stdio.h>

 extern int serv_main();
 extern int do_read();
 extern int do_write();

 extern char inlin[];

 extern socdecode(char *m);

 main()
 {
   int l;
   serv_main();

   while (1) {
      if ((l = do_read()) >0) {
         printf("C received string:%s",inlin);
         fflush(stdout);
         inlin[l-2] = '\0';
         socdecode(inlin);
         fflush(stdout);
         do_write(inlin,l-2);
         do_write("\n",1);
      }
   }
 }

We will now be working both from the Tcl prompt, I usually prefer a console window, and alternatively from the shell prompt, such as bash or csh or what you have (cygwin has bash as default, as does linux).

First, in the tcl console type

 open_text test.c
 set l {}; for {set i 0} {$i < 10} {incr i} {lappend l "message$i"}
 socgencframe $l

Probably you get an error after the first command, which should be ignored (clicked away). The last tcl/tk proc has made a list of ten buttons in a separate window, which we'll use to test our C program with.

http://www.theover.org/Wiki/cframebut.jpg

Press the 'save' button in the text window, next to the entry with 'test.c', to save the automatically generated C message handlers.

http://www.theover.org/Wiki/cframett.jpg

If you want, the functions could first be edited into what you want, instead of the test printf.

Edit serv2.c, and make sure you before the includes type

 #define CYGWIN

or LINUX according to what you use. Alternatively, figure out where the include files are relative to probably /usr/include on your system.

Now we can compile the C sources with a standard C compiler (like gcc), possibly needing machine/OS specific net-libraries (not needed on cygwin or linux)

 gcc -c serv2.c
 gcc -c stub.c
 gcc -c test.c

 gcc -o stub.exe stub.o serv2.o test.o

We could have compiled (but not linked) the first two beforehand, they don't change during our test. Note that in the stub code, there is non-nice handling of newline and generally not much error checking going on, but it worked for me (I didn't want to include string.h and such, I will later...). Alternatively, we could have typed 'gcc -o stub stub.c serv2.c test.c'.

Now we have our C program, run it in a shell (so that its output becomes visible)

 ./stub

The ./ may not be necessary depending on you PATH variable. It prints that it is waiting for a socket connection to be initiated from some other party, so let us give it what it wants from out tcl console:

 socconnect

makes a stream in tcl and connects to the C program. When a button is pressed, the corresponding (text) message will be sent over the stream to the C program, which in turn prints that that data arrived:

http://www.theover.org/Wiki/cframesh.jpg

and the C message handler when all is well should find the corresponding handler function, and call it, which makes it print a short acknowledgement, too.

Finally, the C 'stub' function returns the message to the stream, so after all is done, the message is copied back to the sender, our Tcl/Tk program, which is happy to puts on the console what it received.

http://www.theover.org/Wiki/cframecon.jpg

The whole process should be pretty fast, even printing included, more than up to UI speed, and all programs together should use fairly small amount of memory.

Finally, when the select functions and tcl eventhandler work as intended, the load on the machine should be minimal, too.


Motivations and subsequent work include: