wordchan

The wordchan library is an implementation of word channels. A previous version is included in ycl as word.tcl.

The library's word channels transmit words of up to 4095 bytes in length. This may be a fundamental limitation in Tcl builds that read/write 4096 bytes at a time. Working with longer words would require a way to tell apart a split word and two consecutive words. You could accomplish this with in-band signaling (for example, by only sending quoted values). The result would arguably no longer be a word channel, as it would force the user to perform an escape/unescape step instead of simply writing and reading words.

Code

#! /usr/bin/env tclsh
# Copyright (c) 2015, 2023 D. Bohdan
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

namespace eval ::wordchan {
    namespace export initialize watch read write
    namespace ensemble create

    variable maxBytes 4095
    variable storage {}
    variable version 0.1.1
}

proc ::wordchan::initialize {channelId mode} {
    variable storage
    dict set storage $channelId data {}
    dict set storage $channelId ticktock 0
    return [list initialize finalize watch read write]
}

proc ::wordchan::finalize channelId {
    variable storage
    dict unset storage $channelId
}

proc ::wordchan::watch {channelId eventspec} {}

proc ::wordchan::read {channelId count} {
    # We ignore $count.
    variable storage

    # Force one value to be read at a time.
    if {[TickTock $channelId]} {
        error EAGAIN
    } else {
        set chanData [lassign [dict get $storage $channelId data] head]
        dict set storage $channelId data $chanData
        return $head
    }
}

proc ::wordchan::write {channelId data} {
    variable maxBytes
    variable storage

    if {[string length [encoding convertto $data]] > $maxBytes} {
        error [list written value [string range $data 0 31]... exceeds \
            $maxBytes bytes]
    }

    set chanData [dict get $storage $channelId data]
    dict set storage $channelId data [list {*}$chanData $data]
    return [string length $data]
}

# Private API.

proc ::wordchan::TickTock {channelId} {
    variable storage
    set ticktock [dict get $storage $channelId ticktock]
    dict set storage $channelId ticktock [expr {!$ticktock}]
    return $ticktock
}

proc ::wordchan::Test {} {
    variable maxBytes

    set ch [chan create {read write} ::wordchan]
    chan configure $ch -buffering none

    puts -nonewline $ch [list a]
    puts -nonewline $ch [list b c]

    set readRes [::read $ch]
    if {$readRes ne {a}} {
        error [list wrong value: $readRes]
    }
    set readRes [::read $ch]
    if {$readRes ne {b c}} {
        error [list wrong value: $readRes]
    }

    for {set i 0} {$i <= $maxBytes} {incr i} {
        set id [format %04u- $i]
        set value $id[string repeat x [expr { $i - [string length $id] }]]

        puts -nonewline $ch $value
        if {[set readRes [::read $ch]] != $value} {
            error [list wrote $value but read $readRes]
        }
    }

    catch {
        puts -nonewline $ch +[string repeat x $maxBytes]
    } err
    if {![regexp exceeds $err]} {
        error [list did not get the expected error message in $err]
    }
}

# If this is the main script...
if {[info exists argv0] && ([file tail [info script]] eq [file tail $argv0])} {
    ::wordchan::Test
}

Use example

#! /usr/bin/env tclsh

source wordchan.tcl

proc main {} {
    set ch [chan create {read write} ::wordchan]
    chan configure $ch -buffering none
    puts -nonewline $ch HELLO
    puts -nonewline $ch WORLD
    puts [read $ch] ;# Prints "HELLO".
    puts [read $ch] ;# Prints "WORLD".
}

main