TclOO Channels

DKF: This page contains some experimental code to create channels (TIP#219) and transforms (TIP#230) using TclOO to provide the support machinery. It's not really finished yet.

APN tcllib has modules with TclOO-based virtual channel support classes as well as several channel implementations (possibly based on the code below).


package require TclOO 0.6
package provide chanobj 0.1

namespace eval ::chanobj {
    namespace path ::oo

    namespace export \
        readableChannel writableChannel readwriteChannel \
        readTransform writeTransform readwriteTransform

    ######################################################################
    # Support for scripted channels a la TIP 219
    #

    class create Channel {
        variable options channel blocking readableEvents writableEvents
        constructor {initialOptions args} {
            set options $initialOptions
            dict set options -objectCommand [self]
        }
        method initialize {id mode} {
            set channel $id
            set methods [info object methods [self] -all]
            set result {}
            foreach m {
                initialize finalize watch read write seek configure cget
                cgetall blocking
            } {
                if {$m in $methods} {
                    lappend result $m
                }
            }
            return $result
        }
        destructor {
            if {$channel ne ""} {
                close $channel
            }
        }
        method finalize {id} {
            set channel {}
            my destroy
        }
        method watch {id events} {
            set readableEvents [expr {"read" in $events}]
            set writableEvents [expr {"write" in $events}]
        }
        method Post events {
            chan postevent $channel $events
        }
        method configure {id option value} {
            dict set options $option $value
        }
        method cget {id option} {
            return [dict get $options $option]
        }
        method cgetall {id} {
            return $options
        }
        method blocking {id mode} {
            set blocking $mode
        }
    }

    class create readableChannel {
        superclass chanobj::Channel
        variable location
        constructor {initialOptions {seekable 0}} {
            set location 0
            if {$seekable} {
                oo::objdefine [self] mixin ::chanobj::Seekable
            }
            next $initialOptions
        }
        method read {id count} {
            set bytes [my ReadAt $location $count]
            incr location [string length $bytes]
            return $bytes
        }
        method ReadAt {position count} {
            return -code error "method not implemented"
        }
    }

    class create writableChannel {
        superclass chanobj::Channel
        variable location blocking
        constructor {initialOptions {seekable 0}} {
            set location 0
            if {$seekable} {
                oo::objdefine [self] mixin ::chanobj::Seekable
            }
            next $initialOptions
        }
        method write {id bytes} {
            set length [my WriteAt $location $bytes]
            incr location $length
            return $length
        }
        method WriteAt {position bytes} {
            return -code error "method not implemented"
        }
    }

    class create readwriteChannel {
        superclass chanobj::readableChannel chanobj::writableChannel
        constructor {initialOptions {seekable 0}} {
            next $initialOptions $seekable
        }
    }

    class create Seekable {
        variable location
        method seek {id offset base} {
            switch $base {
                start {
                    if {$offset < 0} {
                        set location 0
                    } else {
                        set location $offset
                    }
                }
                current {
                    if {$offset + $location < 0} {
                        set location 0
                    } else {
                        incr location $offset
                    }
                }
                end {
                    set end [my GetEnd]
                    if {$offset + $end < 0} {
                        set location 0
                    } else {
                        set location [expr {$offset + $end}]
                    }
                }
            }
            return $location
        }
        method GetPosition {} {
            return $location
        }
        method GetEnd {} {
            return -code error "method not implemented"
        }
    }

    ######################################################################
    # Support for scripted channel transformss a la TIP 230
    #

    class create Transform {
        variable chan readFilter writeFilter
        constructor {channelId} {
            set chan $channelId
            chan push $chan [self]
        }
        destructor {
            if {$chan ne ""} {
                chan pop $chan
            }
        }
        method initialize {handle mode} {
            set readFilter [expr {"read" in $mode}]
            set writeFilter [expr {"write" in $mode}]
            set result {}
            set methods [info object methods [self] -all]
            foreach m {
                clear finalize initialize
                drain limit? read
                flush write
            } {
               if {$m in $methods} {
                   lappend result $m
               }
            }
            return $result
        }
        method finalize {handle} {
            set chan {}
        }
        method clear {handle} {
        }
    }

    class create readTransform {
        variable defaultLimit
        superclass chanobj::Transform
        constructor channelId {
            next $channelId
            set defaultLimit -1
        }
        method drain {handle} {
            my read $handle {}
        }
        method limit? {handle} {
            return $defaultLimit
        }
        method read {handle buffer} {
            my TransformIn $buffer
        }
        method TransformIn {binData} {
            return $binData
        }
    }

    class create writeTransform {
        superclass chanobj::Transform
        method flush {handle} {
            my write $handle {}
        }
        method write {handle buffer} {
            my TransformOut $buffer
        }
        method TransformOut {binData} {
            return $binData
        }
    }

    class create readwriteTransform {
        superclass chanobj::readTransform chanobj::writeTransform
    }
}

if 0 {

Now for the demonstration of the above machinery.

}

if {$::argv0 eq [info script]} {
    namespace import chanobj::*
    oo::class create uppercaser {
        superclass readTransform
        constructor ch {
            next $ch
            # Since we're going to unstack and we're simple, we need to make
            # the limit small to curb buffering problems.
            my variable defaultLimit
            set defaultLimit 1
        }
        method TransformIn data {
            # A very simple upper-casing example
            return [string toupper $data]
        }
    }
    oo::class create mangler {
        superclass writeTransform
        method TransformOut data {
            binary scan $data c* v
            set i -1
            foreach c $v {
                incr i
                if {$c >= 32} {
                    lset v $i [expr {$c | 1}]
                }
            }
            binary format c* $v
        }
    }

    oo::class create stringChan {
        superclass readableChannel
        variable s
        constructor {string encoding} {
            next [dict create -original $string] 0
            set s [encoding convertto $encoding $string]
        }
        method ReadAt {p c} {
            return [string range $s $p [expr {$p+$c-1}]]
        }
        self method new {s {enc utf-8}} {
            set c [chan create read [next $s $enc]]
            chan configure $c -encoding $enc
            return $c
        }
    }

    oo::class create hopper {
        superclass writableChannel
        variable buf
        constructor {} {
            next {} 0
            set buf {}
        }
        method WriteAt {pos bytes} {
            # Not seekable, so can ignore $pos
            append buf $bytes
            return [string length $bytes]
        }
        method cget {id opt} {
            if {$opt eq "-data"} {
                return $buf
            }
            next $id $opt
        }
        method cgetall {id} {
            dict replace [next $id] -data $buf
        }
        self method new {} {
            chan create write [next]
        }
    }

    set f [open [info script]]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts ---------
    set xform [uppercaser new $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts ---------
    $xform destroy
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts ---------
    set xform [mangler new stdout]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts ---------
    $xform destroy
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    close $f

    puts ---------
    set f [stringChan new "1 potato\n2 potato\n3 potato\n4...\n"]
    puts [gets $f]
    puts [gets $f]
    puts [gets $f]
    # Next line used to test untidy closing behaviour.
    #[fconfigure $f -objectCommand] destroy
    chan copy $f stdout
    close $f    

    puts ---------
    set f1 [hopper new]
    puts $f1 "The quick brown fox"
    puts $f1 "jumps over the lazy dog."
    flush $f1
    puts -nonewline [fconfigure $f1 -data]
    close $f1
    puts ---------
}