MOST is the Miniature Object System for Tequila
provides OOP that can be easily distributed across a network using jcw's Tequila.
MOST creates an object system that duplicates the one provided by LOST. However, rather than keeping object data in procs, this one keeps it in arrays. Since Tequila (https://wiki.tcl-lang.org/1243 ) provides a way to distribute and share Tcl arrays, this means you can create distributed network objects by adding the Tequila "attach" command - one line standing between you and a powerful distributed network object system. No other object system for Tcl includes this functionality.
[not to be confused with Maynard Operation Specific Technique, for example] - it implements objects as arrays - not a huge improvement over LOST except for one thing: it was written for the first version of Tequila, which implements distributed arrays and persistancy. You can do some absurdly powerful things with these tools with very little effort.
Because Tequila has moved on to something that is more complex, MOST doesn't provide as much bang for the buck. For people looking for something quicker and easier to deal with, try MOST. I have included the source code for the original version of Tequila here as well, to make it a one-stop solution for people wanting distributed objects.
The T1 source here was extracted from [L1 ] It was written by Jean-Claude Wippler. MOST is by Larry Smith and its creation was financed by Eolas as part of another project which has moved on and no longer requires MOST. Thanks Michael Doyle.
# MOST - Mini Object System for Tequila - most.tcl rename unknown _unknown proc unknown { name args } { upvar $name self if {![ catch {set type $self(class)} ]} { } else { eval _unknown $name $args return } proc $name args "uplevel @ $name \$args" uplevel trace variable $name u \[ list rename $name "" \] uplevel $name $args } proc class { name instvars methods } { upvar $name type set type(class) $name set type(instvars) $instvars foreach var $instvars { regsub -all $var $methods self(&) methods } regsub -all class $methods self(&) methods foreach { methodname arglist body } $methods { set type($methodname-body) $body set type($methodname-args) $arglist } } proc new { class name args } { upvar $name self upvar $class type set self(class) $class foreach instvar $type(instvars) { set self($instvar) "" } if { [ llength $args ] > 0 } { uplevel @ $name $args } } proc @ { this { msg $ } args } { upvar $this self upvar $self(class) class foreach arg $class($msg-args) { set $arg [ lindex $args 0 ] set args [ lrange $args 1 end ] } return [ eval $class($msg-body) ] } proc this { args } { upvar this this uplevel 2 $this $args }
#!/bin/sh # Copyright (c) 1999-2000 Jean-Claude Wippler <[email protected]> # # Tequilas - the "Tequila Server" implements shared persistent arrays #\ exec tclkit "$0" ${1+"$@"} # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Imlementation notes: # # Commands starting with "Tqs" can be called from the remote client # The rest uses lowercase "tqs" to prevent this (and for uniqueness) # # There is one global array which is used for all information which # this server needs to carry around and track, called "tqs_info": # # tqs_info(pending) - id of pending "after" request, unset if none # tqs_info(timeout) - milliSecs before timed commit, unset if never # tqs_info(verbose) - log level: 0=off, 1=req's, 2=notify, 3=reply # # External views (type "X") are stored as files in directory, one item # per text file. This can be used to store large amounts of text in # regular files, outside Metakit (though commit doesn't apply to them): # # tqs_external(view) - directory name, set for each external view # # Valid while processing an incoming request: # tqs_info(port) - socket name of current client request # # The following will be defined for individual views: # tqs_notify($view) - socket name of client to notify on changes # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # conditonal logging output proc tqsPuts {level msg} { global tqs_info if {$level <= $tqs_info(verbose)} { puts $msg } } # return a displayable string of limited length proc tqsDisplay {str len} { if {[string length $str] > $len} { set str "[string range $str 0 $len]..." } regsub -all {[^ -~]} $str {?} str return $str } # remote execution of any Metakit command, added 20-02-2000 proc TqsRemote {cmd args} { eval mk::$cmd $args } # return the names of all views currently on file proc TqsInfo {} { mk::file views tqs } # get or set log level (see above for meaning of values 0..3) proc TqsVerbose {{level ""}} { global tqs_info if {$level != ""} { set tqs_info(verbose) $level } return $tqs_info(verbose) } # define a view (Metakit's equivalent concept for a Tcl array) # if the second argument is true, all existing data is removed # the third arg is used to specify a binary (B) of memo format (M) # if the third arg is "X", use a directory with files for storage proc TqsDefine {view {clear 0} {type S}} { if {$type == "X"} { global tqs_external set tqs_external($view) "" if {$clear} { catch {file delete -force $view.data} tqsTrace $view "" u } file mkdir $view.data #catch {mk::view delete tqs.$view} } else { mk::view layout tqs.$view "name text:$type date:I" if {$clear && [mk::view size tqs.$view] > 0} { mk::view size tqs.$view 0 tqsTrace $view "" u } #file delete -force $view.data } return } # get rid of a view proc TqsUndefine {view} { global tqs_external if {[info exists tqs_external($view)]} { file delete -force $view.data unset tqs_external($view) } else { mk::view delete tqs.$view } tqsTrace $view "" u return } # return the list of all keys, like "array names view" proc TqsNames {view} { set result {} global tqs_external if {[info exists tqs_external($view)]} { foreach x [glob -nocomplain $view.data/*] { regsub {.*/} $x {} x lappend result $x } } else { mk::loop c tqs.$view { lappend result [mk::get $c name] } } return $result } # return the number of keys, like "array size view" proc TqsSize {view} { set result {} global tqs_external if {[info exists tqs_external($view)]} { set result [llength [glob -nocomplain $view.data/*]] } else { set result [mk::view size tqs.$view] } return $result } # return an existing value, lookup by key, like "set view(key)" proc TqsGet {view key} { global tqs_external if {[info exists tqs_external($view)]} { set fd [open $view.data/$key] fconfigure $fd -translation binary set v [read $fd] close $fd return $v } else { set n [mk::select tqs.$view name $key] mk::get tqs.$view!$n text ;# throws error if absent } } # store a value, create if necessary, like "set view(key) data" # the optional last arg can be used to force a specific timestamp proc TqsSet {view key data {timestamp ""}} { global tqs_external if {[info exists tqs_external($view)]} { set fd [open $view.data/$key w] fconfigure $fd -translation binary puts -nonewline $fd $data close $fd # timestamp is ignored } else { set n [mk::select tqs.$view name $key] if {[llength $n] == 0} { set n [mk::view size tqs.$view] } elseif {[mk::get tqs.$view!$n text] == $data} { return ;# no change, ignore } if {$timestamp == ""} { set timestamp [clock seconds] } mk::set tqs.$view!$n name $key text $data date $timestamp } tqsTrace $view $key w return } # Append a value, create if entry did not exist proc TqsAppend {view key data} { global tqs_external if {[info exists tqs_external($view)]} { set fd [open $view.data/$key a] fconfigure $fd -translation binary puts -nonewline $fd $data close $fd } else { set n [mk::select tqs.$view name $key] if {[llength $n] > 0} { if {[string length $data] == 0} then return ;# no change set data "[mk::get tqs.$view!$n text]$data" } mk::set tqs.$view!$n name $key text $data date [clock seconds] } tqsTrace $view $key w return } # delete an existing entry by key, similar to "unset view(key)" proc TqsUnset {view key} { global tqs_external if {[info exists tqs_external($view)]} { file delete $view.data/$key } else { set n [mk::select tqs.$view name $key] if {[llength $n] == 0} { return ;# no change, ignore } mk::row delete tqs.$view!$n } tqsTrace $view $key u return } # return all key/value pairs, like "array get view" # if set, the optional arg sets up change notification proc TqsGetAll {view {tracking 0}} { set result {} global tqs_external if {[info exists tqs_external($view)]} { foreach x [TqsNames $view] { lappend result $x [TqsGet $view $x] } } else { mk::loop c tqs.$view { eval lappend result [mk::get $c name text] } } if {$tracking} { tqsSubscribe $view } return $result } # like TqsGetAll, returns modification dates instead of contents # this can be used by the client to synchronize and track dates # if set, the optional arg sets up change notification proc TqsListing {view {tracking 0}} { set result {} global tqs_external if {[info exists tqs_external($view)]} { foreach x [TqsNames $view] { lappend result $x [file mtime $view.data/$x] } } else { mk::loop c tqs.$view { eval lappend result [mk::get $c name date] } } if {$tracking} { tqsSubscribe $view } return $result } # called to set up notification for a client proc tqsSubscribe {view} { global tqs_info tqs_notify # remember the client IP and listening number for this view tqsPuts 1 "Notification set up for '$view': $tqs_info(port)" lappend tqs_notify($view) $tqs_info(port) } # called to unset all notifications for a client proc tqsUnsubscribe {port} { global tqs_notify foreach {k v} [array get tqs_notify] { set n [lsearch -exact $v $port] if {$n >= 0} { tqsPuts 1 " Forget notify for $k" if {[llength $v] > 1} { set tqs_notify($k) [lreplace $v $n $n] } else { unset tqs_notify($k) tqsPuts 1 " No more notifications for $k" } } } } # set a number of key/value pairs, like "array set view pairs" proc TqsSetAll {view pairs} { foreach {key value} $pairs { TqsSet $view $key $value } } # save changes to file now proc TqsCommit {} { global tqs_info set n [clock clicks] mk::file commit tqs tqsPuts 1 "Commit done ([expr {[clock clicks] - $n}])" after cancel TqsCommit catch {unset tqs_info(pending)} return } # change commit timer, default is to commit with explicit calls proc TqsTimer {{timer ""}} { global tqs_info after cancel TqsCommit if {$timer == ""} { catch {unset tqs_info(timeout)} } else { if {[info exists tqs_info(pending)]} { set tqs_info(pending) [after $timer TqsCommit] } set tqs_info(timeout) $timer } } # handles tracing of all view changes (there's no read tracing) # this is also the place where delayed commits are scheduled proc tqsTrace {view key operation} { global tqs_info tqs_notify if [info exists tqs_notify($view)] { switch $operation { write { set req [list Set $view $key [TqsGet $view $key]] } unset { set req [list Unset $view $key] } } # this is the data that gets sent out set msg "[string length $req]\n$req" foreach p $tqs_notify($view) { if {$p == $tqs_info(port)} continue ;# skip originator if [catch { tqsPuts 2 [tqsDisplay "Notify $p - $req" 65] puts -nonewline $p $msg #flush $p } error] { tqsPuts 1 "Notify to $p failed for $view $key" tqsPuts 1 " Reason: $error" catch {close $p} tqsUnsubscribe $p } } } if {![info exists tqs_info(pending)] && [info exists tqs_info(timeout)]} { set tqs_info(pending) [after $tqs_info(timeout) TqsCommit] } } # called whenever a request comes in proc tqsRequest {sock} { global tqs_info if {[gets $sock bytes] > 0} { set request [read $sock [lindex $bytes 0]] if ![eof $sock] { # debugging: incoming request tqsPuts 1 [tqsDisplay " $request" 65] set tqs_info(port) $sock set err [catch {uplevel #0 Tqs$request} reply] set msg [list Reply $err $reply] puts -nonewline $sock "[string length $msg]\n$msg" # debugging: returned results if {[string length $reply] > 0} { tqsPuts 3 " result: [tqsDisplay $reply 54]" } #flush $sock return } } tqsPuts 1 "Closing $sock" close $sock tqsUnsubscribe $sock } # called whenever a connection is opened proc tqsAccept {sock addr port} { global tqs_info fconfigure $sock -translation binary -buffering none fileevent $sock readable [list tqsRequest $sock] } # this can be called to start a background server proc tqsStart {port} { global tqs_notify tqs_external array set tqs_notify {} foreach x [glob -nocomplain *.data] { regsub {\.data$} $x {} x set tqs_external($x) "" } socket -server tqsAccept $port } # this wraps the server into a standalone, it runs until shutdown proc tqsRun {port} { global tqs_info set tqs_info(shutdown) [clock seconds] # these status messages are not disabled if verbose is off puts "Tequila server on port $port started." tqsStart $port vwait tqs_info(shutdown) puts "Tequila server on port $port has been shut down." } # client-callable: terminate a server started with "tqsRun" proc TqsShutdown {} { global tqs_info # returns number of seconds since the server was started # main effect is setting tqs_info(shutdown), which ends vwait set tqs_info(shutdown) [expr {[clock seconds]-$tqs_info(shutdown)}] } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # This script can be used standalone, in which case the code below will # be run, or as part of a scripted document, which expects a "package # ifneeded tequilas ..." to have been set up. In that case, the code # below will not be executed, allowing the caller so set up different # parameter values before calling tqsRun or tqsStart (background use). # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if {[lsearch -exact [package names] tequilas] < 0} { package require Mk4tcl mk::file open tqs tequilas.dat -nocommit set tqs_info(verbose) 0 ;# default logging is off TqsTimer 30000 ;# default commit timer is 30 seconds tqsRun 20458 ;# default port is 20458 }
# Copyright (C) 1999-2000 Jean-Claude Wippler <[email protected]> # # Tequila - client interface to the Tequila server package provide tequila 1.5 namespace eval tequila { namespace export open close do attach variable _socket variable _reply # setup communication with the tequila server proc open {addr port} { variable _socket set _socket [socket $addr $port] fconfigure $_socket -translation binary -buffering none fileevent $_socket readable tequila::privRequest } # setup callback for when link to server fails proc failure {cmd} { variable _socket trace add variable _socket unset $cmd } # terminate communication (this is usually not needed) proc close {} { variable _socket ::close $_socket } # set up to pass almost all MK requests through to the server # note that mk::loop is not implemented, is only works locally # added 20-02-2000 proc proxy {} { namespace eval ::mk { foreach i {file view row cursor get set select channel} { proc $i {args} "eval ::tequila::do Remote $i \$args" } } } # send a request to the server and wait for a response proc do {args} { variable _socket variable _reply "" catch { puts -nonewline $_socket "[string length $args]\n$args" while {[string length $_reply] == 0} { vwait tequila::_reply } } set error 0 set results "" foreach {error results} $_reply break if {[string compare $error 0] == 0} { return $results } if {[string length $results] > 0} { error $results } error "Failed network request to the server." } # prepare for automatic change propagation proc attach {array args} { array set opts {-fetch 1 -tracking 1 -type S} array set opts $args global $array do Define $array 0 $opts(-type) if {$opts(-fetch)} { set command GetAll } else { set command Listing } array set $array [do $command $array $opts(-tracking)] trace add variable $array {write unset} tequila::privTracer } # called whenever a request comes in (private) proc privRequest {} { variable _socket variable _reply if {[gets $_socket bytes] > 0} { set request [read $_socket $bytes] if ![eof $_socket] { uplevel #0 tequila::privCallBack_$request return } } # trouble, make sure we stop a pending request set _reply [list 1 "Lost connection with the tequila server."] ::close $_socket unset _socket } # handles traces to propagate changes to the server (private) proc privTracer {a e op} { if {$e != ""} { switch $op { write { do Set $a $e [set ::${a}($e)] } unset { do Unset $a $e } } } } # called by the server to return a result proc privCallBack_Reply {args} { variable _reply set _reply $args } # called by the server to propagate an element write proc privCallBack_Set {a e v} { global $a if {![info exists ${a}($e)] || [set ${a}($e)] != $v} { trace remove variable $a {write unset} tequila::privTracer set ${a}($e) $v trace add variable $a {write unset} tequila::privTracer } } # called by the server to propagate an element delete proc privCallBack_Unset {a e} { global $a if {[info exists ${a}($e)]} { trace remove variable $a {write unset} tequila::privTracer unset ${a}($e) trace add variable $a {write unset} tequila::privTracer } } }
source most.tcl class int {value} { = {} { set value [expr int(round($args))]} ++ {} { incr value } $ {} { return $value } test {} { uplevel $this ++ } test2 {} { this ++ } }
new int i = 3 while { [i] < 10 } { puts "[i]" i ++ } i test2 puts "after \[i test\] value is [i]" exit
And if you wanted to make "i" a shared object, you would add these lines at the top (with something appropriate instead of "get-host-name"
source tequilac.tcl # set up a connection to the central Tequila server eval tequila::open [get-host-name] 20458
And the following after the "new":
# setup for a global object "i" to be shared tequila::attach i
Is pretty simple for sharable object system, no? I'm sure it could use a bit of polish, but I don't have time to do it - and it does seem to work for me.