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:
The approach taken for this test version consists of the following steps:
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.
Press the 'save' button in the text window, next to the entry with 'test.c', to save the automatically generated C message handlers.
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:
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.
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: