Question

I have many vendor supplied M-Code routines as part of a much larger product that use READ and WRITE directly to interact with the current device. I can't change that code. I want to wrap some of those routines in a system where I can supply input and capture output interactively.

Currently, this is implemented by opening a TCP connection to a remote host and making that the current device. READ and WRITE are indeed connected to the socket. This is fairly inconvenient though as it requires a separate service that listens on a TCP socket be set up and coordinate with the local job to make the whole process work. I also have to turn off nagle and skip buffering or the connection becomes latency driven or stalls. (e.g. TCP OPEN option /SEN=1 aka +Q). Unfortunately, that results in many 1 byte TCP segments and is also very inefficient.

I would much rather drive the whole interaction through a single process. Ideally, I could have calls to READ, WRITE and the other functions that operate on the current device trigger some M-Code or callbacks in the Caché Callin C interface or a user extension module to provide the required functions on the back end. That way, I can manage IO on my own terms without needing interprocess coordination. I haven't been able to find an entry point to set that up though.

Is there such a thing as a user-defined device in Caché?

For UNIX hosts, there is a way to use an existing file descriptor as a device, which could be useful, but that doesn't seem to be implemented on Windows.

One thing I have considered is to create a new process, have Windows redirect STDIN and STDOUT with SetStdHandle to pipes I control from within the same process, use Callin to connect to Caché and let it use the default device which is supposed to be STDIN and STDOUT. Anyone know if that would actually work?

Was it helpful?

Solution

Caché actually supports arbitrary IO redirection. This is possible with an undocumented feature. Not recommended, but unlikely to change.

Annotated code is below. In this example, I choose to redirect the IO to a %Stream.GlobalBinary - you can whatever you like with it. The key is the specific names of the labels - you can specify a different routine with the use $io:: call, and specify those labels elsewhere.

//The ProcedureBlock = 0 is important
//  it allows for calling of labels within the ClassMethod
ClassMethod testIORedirection() [ ProcedureBlock = 0 ]
{
    //Create a stream that we will redirect to
    set myStream = ##class(%Stream.GlobalBinary).%New()

    //Redirect IO to the current routine - makes use of the labels defined below
    use $io::("^"_$ZNAME)

    //Enable redirection
    do ##class(%Device).ReDirectIO(1)

    //Any write statements here will be redirected to the labels defined below
    write "Here is a string", !, !
    write "Here is something else", !

    //Disable redirection
    do ##class(%Device).ReDirectIO(0)

    //Print out the stream, to prove that it worked
    write "Now printing the string:", !
    write myStream.Read()


    //Labels that allow for IO redirection
    //Read Character - we don't care about reading
rchr(c)      quit
    //Read a string - we don't care about reading
rstr(sz,to)  quit
    //Write a character - call the output label
wchr(s)      do output($char(s))  quit
    //Write a form feed - call the output label
wff()        do output($char(12))  quit
    //Write a newline - call the output label
wnl()        do output($char(13,10))  quit
    //Write a string - call the output label
wstr(s)      do output(s)  quit
    //Write a tab - call the output label
wtab(s)      do output($char(9))  quit
    //Output label - this is where you would handle what you actually want to do.
    //  in our case, we want to write to myStream
output(s)    do myStream.Write(s)  quit
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top