We have a Tcl script (attached below) that came with a control system,
and it’s used to get data from a Linux box connected to the system.
The script connects to a port on a Linux host, supplies a user name
and password, then requests information in a loop. When the
information is available, strings are printed on the console.
For various reasons, I need to translate this Tcl script into a C
program, and this is where I need a bit of help to get started. My
first thought was to use C sockets, and send exactly the same data to
the remote host as the Tcl script does. However, I can’t tell from the
script exactly what the Tcl sends, as the actual communication seems
to happen in system calls. Is it possible to get the contents of the
data sent to the Linux host?
There are probably options that I don’t know about, so other
approaches and suggestions are welcome. Perhaps there is some kind of
Tcl API, in which case I could make appropriate Tcl function calls
from the C program.
----------------------------------------------------------------------------
##
#
# A simple demonstration of access to Json event data.
#
set auto_path [linsert $auto_path 0 [file join [file dirname [info
script]] ../exlib]]
package require cmdline
package require json
package require http
namespace eval ::json_events {
variable n_cmdline_options [subst {
{ addr.arg {myhost:myport} "http address of server"}
{ path.arg {/poll/jev} "path to json event service"}
{ polling.arg {2} "polling period in seconds"}
{ username.arg {} "access with user name"}
{ password.arg {} "password corresponding to user
name"}
}]
#
# async reply handler for http requests
#
# Arguments
# =========
# token token generated by http package
#
proc doc_available {token} {
variable n_guard 1
}
#
# attempt to get the document at the specified url
#
# Arguments
# =========
# url fully qualified url of the required document
# query optional name-value dict to attach to the document request
as a http query
#
proc get_url {url {query {}}} {
variable n_options
variable n_guard 0
set doc {}
if {$query ne {}} {
set query [list -query {*}[::http::formatQuery {*}$query]]
}
if {[catch {set token [::http::geturl $url {*}$query -
command ::json_events::doc_available]} err]} {
puts $err
exit 0
}
set guard [after $n_options(timeout) set ::json_events:::n_guard
2]
vwait ::json_events::n_guard
switch $n_guard {
"1" {
# reply received
after cancel $guard
upvar #0 $token state
switch [::http::ncode $token] {
"200" {
set doc [::http::data $token]
}
"303" {
# a redirect from a login
set location [dict get [::http::meta $token] "Location"]
# need to get sid
set data [split [lindex [split $location ?] end] =]
if {[dict exists $data "sid"]} {
return [get_url http://$n_options(addr)/json/login
$data]
} else {
puts "gcs response to login is invalid"
exit 0
}
}
"403" {
puts "got not-authorised http error (implies session id hs
timed-out?)"
exit 0
}
default {
puts "got http error [::http::ncode $token]"
exit 0
}
}
}
"2" {
puts "server did not respond"
exit 0
}
}
::http::cleanup $token
return $doc
}
#
# Initialise all option variables, either to defaults or
# to values specified on the command line.
#
proc get_options {argv} {
variable n_cmdline_options
variable n_options
set blurb "- json events client.\n"
if {[catch {
foreach {opt val} [::cmdline::getoptions argv $n_cmdline_options
$blurb] {
set n_options($opt) $val
}
}]} {
puts [::cmdline::usage $n_cmdline_options $blurb]
exit 0
}
}
#
# The json data is an array of event objects. If there were no
# new events the json data is "[]", a zero length array.
#
# Detail the properties contained in each event object:
#
# id three part string: <node descriptor>,<can bus device
id>,<point name> (can be empty, i.e. "")
# state the "event" state (the state the point transitioned
into that generated the event)
# descriptor a text description of the event
# priority event priority (or list name)
# tags list of sub-classifications on priority
#
# Arguments
# =========
# data the json-formatted string
#
proc handle_json {data} {
# print to console
if {[catch {set json [::json::json2dict $data]}]} {
puts "data error"
exit 0
}
if {$json ne {}} {
foreach ev $json {
puts [dict get $ev id]:[dict get $ev descriptor]
}
}
}
#
# Login to get session id and pageid. The gcs web interface
# requires a valid session id for each url request. The pageid
# is the id of an event queue. This queue receives a copy of
# all event-list events. The contents of this queue form the
# reply to jev polls (and the q is emptied).
#
proc login {} {
variable n_sid
variable n_options
if {$n_options(username) eq {}} {
puts "you need to specifiy a username (-username cmd line
option)"
if {$n_options(password) eq {}} {
puts "you need to specifiy a password (-password cmd line
option)"
}
exit 0
}
if {$n_options(password) eq {}} {
puts "you need to specifiy a password (-password cmd line
option)"
exit 0
}
set q [list username $n_options(username) password
$n_options(password)]
set login_doc [get_url http://$n_options(addr)/json/login $q]
# login "document' is a json object {type: "login",sid: "<session
identifier>",pageid: <nbr>}
set n_sid [::json::json2dict $login_doc]
# don't need to keep type
dict unset n_sid type
}
#
# Poll for events. When the json data reply is available the
"handle_json"
# proc is called.
#
proc poll {} {
variable n_options
variable n_sid
handle_json [get_url http://$n_options(addr)$n_options(path)
$n_sid]
after $n_options(polling) ::json_events::poll
}
#
# execution starts here
#
# Arguments
# =========
# argv the list of command line options
#
proc run {argv} {
variable n_sid {}
variable n_options
get_options $argv
set n_options(polling) [expr {$n_options(polling) * 1000}]
set n_options(timeout) [expr {$n_options(polling) * 750}]
login
poll
vwait forever
}
}
::json_events::run $argv
http://wiki.tcl.tk/3474
http://wiki.tcl.tk/2074
though the script doesn't look as if it would lend itself
to creating a quick c replacement.
you may be better served taking your c-code and creating
a tcl-extension.
then hook your code as a tcl command into the reception event.
uwe
If this is really what you want, you can take any program that can
intercept such communication - i.e. you can use sockspy (http://
sockspy.sourceforge.net/sockspy.html), which is also in Tcl, and have
your app connect to sockspy and sockspy connect to actual server.
This way you'll get all the communication.
> There are probably options that I don’t know about, so other
> approaches and suggestions are welcome. Perhaps there is some kind of
> Tcl API, in which case I could make appropriate Tcl function calls
> from the C program.
Either make the C code a Tcl library and call it from Tcl (which is
how it's usually done in Tcl world) or use Tcl API to initialize an
interp and source the code. Tcl_CreateInterp(), Tcl_Init() and
Tcl_Eval*() shuold be a good starting points. You can just
Tcl_EvalEx("source /path/to/script.tcl") and then call Tcl_EvalObjv to
run Tcl commands passing argument list as Tcl Objects - you can just
do it with Tcl_NewStringObj() in most cases.
Truth is that nobody rewrites a Tcl TCP server in C unless they
require a performance boost. If they need such a boost, the question
of how to replace Tcl with C is moot. They either understand how to
gain the needed performance boost, or they don't. The included code is
not application specific, just a thin HTTP server grabbing a JSON
object. After that, nothing is known. It appears that the only
function is to print something to stdout. However, assuming that the
Tcl server grabbed something interesting to transmit to the real
client (some C code...hopefully?), then there is no reason to replace
the Tcl component. At some point the request must be serialized and
the response turned into something understandable by the main library.
This is what Tcl does best. Why replace this part with C code? Even
the OP has no idea why.
I assume that the OP *does* know why, but he's not telling us. He
referred to "a variety of reasons". I can think of about 3
possibilities (for why, in general, people often want to convert
something written in a "script" language into a "compiled" language):
(in no particular order)
1) Performance boost
2) Politics (e.g., the boss says it has to be in C)
3) Encryption (we don't want to release something in an easily readable
text format)
Note that there are a few other, less cynical, reasons that I would put
under category 2) - for example, as the OP noted about himself, he is a
C guy, not a Tcl guy, so it would be more in his comfort zone if the
code he works with is in C. I.e., if you've got C programmers on staff,
it makes sense for them to work in C.
Also, if you want to interface it as a library, it may be more
convenient if it is written in a "non-script" language.
--
"We should always be disposed to believe that which appears to us to be
white is really black, if the hierarchy of the church so decides."
- Saint Ignatius Loyola (1491-1556) Founder of the Jesuit Order -
If one of the reasons you want to convert is that you are more
comfortable with C, then you should explore the SIMPL toolkit (http://
www.icanprogram.com/simpl and http://icanprogram.ca/simplbook). You
may be able to "tweek" your existing Tcl/Tk script to message an
intermediary C module that you can customize. ie. use the Tcl/Tk for
creating the user interface and C for the engine. This is exactly
what the second example at the icanprogram.ca URL above is doing
albeit by a more complicated network interface (TCP/IP using
tclSurrogate protocol to a Linode) than you'd likely require.
bob
Thanks to all for the suggestions on converting the Tcl script to C. I
have some reading to do!
I should explain a bit more why I want to convert the Tcl to C. The
Linux host is part of a proprietary control system, and there is
little we can do on it. The Linux system receives events from the
hardware to which it is attached, and uses them for local purposes.
These events are also useful to us externally, so the vendor supplied
the Tcl script to view the events (these are the “strings” that are
printed on the console). However, we actually want to use the events,
in real time, on another control system where all the applications are
written in C. It would be extremely difficult to get the manager to
agree to introduce Tcl into a set of mature applications, so the only
real option is to implement the Tcl script in C.
Mike
There have already been general discussions on why switch and general
comments - but I haven't seen anyone address the particulars of what
this script is doing.
Looking below it isn't just a plain socket/cmd interface it is an Http
server that is serving up json data. the script handles hitting a logon
page (with a user password) to get a session id and then makes the
polling requests with that sesdion id to retrieve the data. This is all
done with the http package to handle much of the details. The data
returned is json data (simple ascii encoding for use across different
languages/machines/environments. The script uses the json package to
handle the details. So reimplementing this in C does not need to do JUST
what this script does, but also what those libraries do. If you need to
proceed I would look into options/libraries to help. If you try to code
everything from scratch with raw sockets you will be re0-inventing many
bugs in handling HTTP traffic. I would look into libcurl for hanlding
the http interfaces, and then see if there is a library already existing
for parsing the json. json parsing isn't that hard, especially if you
know you only need a specific subset that the system you are talking to
uses.
Bruce
If you want to generate a dll (i.e. so) then take a look at what
critcl can do for you.
If you want to embed the script in some C code then take a look at the
C interface to tcl. You can create an instance of the tcl interp in
your C application at which point you can do anything you want with
tcl from your C application.
tomk
Or, use interprocess communication. If the volume of events is not too
huge, just convert them to some serialization format (text or binary,
you choose) and output it through a pipe or socket, towards the other
system or application.
-Alex
As has been suggested already, I would just analyse the
communication with wireshark and rewrite it from scratch, does
not seem to be difficult. It is just a plain http request and answer.
Don't bother to learn Tcl just for that.
/Str.