IPC via named pipe

Revision as of 17:11, 10 February 2022 by PureFox (talk | contribs) (→‎{{header|Wren}}: Replace an unnecessary tab.)

Named pipe, or FIFO, is a way of providing inter-process communications (IPC). The task is to demonstrate how it works, create two pipes, say, "in" and "out" (choose suitable names for your system), and write a program that works the two pipes such that:

  1. Data written to the "in" FIFO will be discarded except the byte count, which will be added to a total tally kept by the program;
  2. Whenever another process reads the "out" FIFO, it should receive the total count so far.
IPC via named pipe is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Possible issues:

  • Chances are you don't already have "in" and "out" pipes lying around. Create them within your program or without, at your discretion. You may assume they are already created for you.
  • Your program may assume it's the sole reader on "in" and the sole writer on "out".
  • Read/write operations on pipes are generally blocking. Make your program responsive to both pipes, so that it won't block trying to read the "in" pipe while leaving another process hanging on the other end of "out" pipe indefinitely -- or vice versa. You probably need to either poll the pipes or use multi-threading.
  • You may assume other processes using the pipes behave; specificially, your program may assume the process at the other end of a pipe will not unexpectedly break away before you finish reading or writing.

C

Library: pthread

<lang c>#include <stdio.h>

  1. include <sys/stat.h>
  2. include <unistd.h>
  3. include <fcntl.h>
  4. include <limits.h>
  5. include <pthread.h>

/* Flag for systems where a blocking open on a pipe will block

  entire process instead of just current thread.  Ideally this
  kind of flags should be automatically probed, but not before
  we are sure about how each OS behaves.  It can be set to 1
  even if not needed to, but that would force polling, which I'd
  rather not do.
    Linux:	won't block all threads (0)
    OpenBSD:	will block all (1)
  Other OSes: ?
  • /
  1. define WILL_BLOCK_EVERYTHING 0
  1. if WILL_BLOCK_EVERYTHING
  2. include <poll.h>
  3. endif

size_t tally = 0;

void* write_loop(void *a) { int fd; char buf[32]; while (1) {

  1. if WILL_BLOCK_EVERYTHING

/* try to open non-block. sleep and retry if no reader */ fd = open("out", O_WRONLY|O_NONBLOCK); if (fd < 0) { /* assume it's ENXIO, "no reader" */ usleep(200000); continue; }

  1. else

/* block open, until a reader comes along */ fd = open("out", O_WRONLY);

  1. endif

write(fd, buf, snprintf(buf, 32, "%d\n", tally)); close(fd);

/* Give the reader a chance to go away. We yeild, OS signals reader end of input, reader leaves. If a new reader comes along while we sleep, it will block wait. */ usleep(10000); } }

void read_loop() { int fd; size_t len; char buf[PIPE_BUF];

  1. if WILL_BLOCK_EVERYTHING

struct pollfd pfd; pfd.events = POLLIN;

  1. endif

while (1) {

  1. if WILL_BLOCK_EVERYTHING

fd = pfd.fd = open("in", O_RDONLY|O_NONBLOCK); fcntl(fd, F_SETFL, 0); /* disable O_NONBLOCK */ poll(&pfd, 1, INFTIM); /* poll to avoid reading EOF */

  1. else

fd = open("in", O_RDONLY);

  1. endif

while ((len = read(fd, buf, PIPE_BUF)) > 0) tally += len; close(fd); } }

int main() { pthread_t pid;

/* haphazardly create the fifos. It's ok if the fifos already exist, but things won't work out if the files exist but are not fifos; if we don't have write permission; if we are on NFS; etc. Just pretend it works. */ mkfifo("in", 0666); mkfifo("out", 0666);

/* because of blocking on open O_WRONLY, can't select */ pthread_create(&pid, 0, write_loop, 0); read_loop();

return 0; }</lang>

Go

<lang go>package main

import (

       "fmt"
       "io"
       "log"
       "os"
       "sync/atomic"
       "syscall"

)

const (

       inputFifo  = "/tmp/in.fifo"
       outputFifo = "/tmp/out.fifo"
       readsize   = 64 << 10

)

func openFifo(path string, oflag int) (f *os.File, err error) {

       err = syscall.Mkfifo(path, 0660)
       // We'll ignore "file exists" errors and assume the FIFO was pre-made
       if err != nil && !os.IsExist(err) {
               return
       }
       f, err = os.OpenFile(path, oflag, 0660)
       if err != nil {
               return
       }
       // In case we're using a pre-made file, check that it's actually a FIFO
       fi, err := f.Stat()
       if err != nil {
               f.Close()
               return nil, err
       }
       if fi.Mode()&os.ModeType != os.ModeNamedPipe {
               f.Close()
               return nil, os.ErrExist
       }
       return

}

func main() {

       var byteCount int64
       go func() {
               var delta int
               var err error
               buf := make([]byte, readsize)
               for {
                       input, err := openFifo(inputFifo, os.O_RDONLY)
                       if err != nil {
                               break
                       }
                       for err == nil {
                               delta, err = input.Read(buf)
                               atomic.AddInt64(&byteCount, int64(delta))
                       }
                       input.Close()
                       if err != io.EOF {
                               break
                       }
               }
               log.Fatal(err)
       }()
       for {
               output, err := openFifo(outputFifo, os.O_WRONLY)
               if err != nil {
                       log.Fatal(err)
               }
               cnt := atomic.LoadInt64(&byteCount)
               fmt.Fprintln(output, cnt)
               output.Close()
       }

}</lang>

Perl

Translation of: Raku

<lang Perl># 20200924 added Perl programming solution

use strict; use warnings;

use File::Temp qw/ :POSIX /; use POSIX qw/ mkfifo /; use Fcntl;

my ($in, $out) = map { scalar tmpnam() } 0,1 ;

for ($in, $out) { mkfifo($_,0666) or die $! };

print "In pipe : $in\nOut pipe : $out\n";

my $CharCount = 0 ;

$SIG{INT} = sub {

  for ($in, $out) { unlink $_ or die }
  print "\nTotal Character Count: $CharCount\nBye.\n" and exit

};

sysopen( IN, $in, O_NONBLOCK|O_RDONLY ) or die; sysopen( OUT, $out, O_NONBLOCK|O_RDWR ) or die;

while (1) {

  sysread(IN, my $buf, 32); # borrowed the buffer size from the c entry
  if ( length $buf > 0 ) {
     $CharCount += length $buf;
     while (sysread(OUT, $buf, 1)) {} ; # empty the write pipe
     syswrite( OUT, $CharCount ) or die;
  }
  sleep .5;

}</lang>

Output:

Terminal 1

In  pipe : /tmp/0oPaYmkOI2
Out pipe : /tmp/DdWOGVOaBI
^C
Total Character Count: 7083641
Bye.

Terminal 2

echo asdf > /tmp/0oPaYmkOI2
cat /tmp/DdWOGVOaBI
5^C
echo qwer > /tmp/0oPaYmkOI2
echo uiop > /tmp/0oPaYmkOI2
cat /tmp/DdWOGVOaBI
15^C
wc /etc/passwd
  33   63 1642 /etc/passwd
cat /etc/passwd > /tmp/0oPaYmkOI2
cat /tmp/DdWOGVOaBI
1657^C
cat /dev/urandom > /tmp/0oPaYmkOI2
^C
cat /tmp/DdWOGVOaBI
7083641^C

Phix

--
-- demo\rosetta\IPC_via_named_pipe.exw
-- ===================================
--
-- Note: uses a single bidirectional pipe, Windows only, and no attempt has been
-- made to avoid blocking, this sort of code should be put in a separate thread.
--
-- Running normally starts this as a server which automatically creates a client.
--
-- Several additional Windows-only routines have been added to builtins/pipeio.e 
-- which will not be formally documented until they are fully Linux-compatible.
--
requires("1.0.1")
requires(WINDOWS)
without js -- (obviously this will never ever run in a browser!)
include builtins/pipeio.e

constant szPipename = `\\.\pipe\mynamedpipe`,
         cl = command_line()

function rand_string()
    string res = repeat(' ',rand(100))
    for i=1 to length(res) do
        res[i] = rand_range('A','Z')
    end for
    return res
end function

if not find("-client",cl) then
    puts(1,"Server\n")
    integer dwPipeMode = or_all({PIPE_TYPE_MESSAGE,PIPE_READMODE_MESSAGE,PIPE_WAIT}),
            totalsofar = 0
    atom pBuffer = allocate(1024),
         pBytesRead = allocate(4),
         pBytesWritten = allocate(4),
         hPipe = create_named_pipe(szPipename,PIPE_ACCESS_DUPLEX,dwPipeMode)
    {} = system_exec(sprintf(`"%s" "%s" -client`,cl),8)
    while true do
        integer res = connect_named_pipe(hPipe)
        if res>1 then ?9/0 end if
        res = read_pipe(hPipe,pBuffer,1024,pBytesRead)
        if not res then
            puts(1,"\nsleep??\n")
            sleep(0.25)
        else
            integer bytesread = peek4s(pBytesRead)
            string msg = peek({pBuffer,bytesread})
            if bytesread=4 and msg="quit" then
                puts(1,"\nquit recieved, quitting\n")
                exit
            end if
            totalsofar += bytesread
            progress("bytesread:%,d  total so far:%,d ",{bytesread,totalsofar})
            string reply = sprintf("%,d",totalsofar)
            res = write_pipe(hPipe,reply,pBytesWritten)
            if not res or peek4s(pBytesWritten)!=length(reply) then ?9/0 end if
            flush_pipe(hPipe)
        end if
        disconnect_pipe(hPipe)
    end while
    free({pBytesWritten,pBuffer,pBytesRead})
    close_handle(hPipe)
else
    puts(1,"Client\n")
    puts(1,"Press Escape or Q to quit\n")
    integer running_total = 0
    while not find(get_key(),{'q','Q',#1B}) do
        string msg = rand_string(),
             reply = call_named_pipe(szPipename,msg)
        running_total += length(msg)
        bool bOK = (reply==sprintf("%,d",running_total))
        progress("set %d bytes, reply:%s, running_total:%,d  (same:%t)",
                 {length(msg),     reply,     running_total,     bOK})
        if not bOK then
            puts(1,"\nsome error, quitting...\n")
            exit
        end if
    end while
    {} = call_named_pipe(szPipename,"quit")
end if
Output:

After letting it run for about 5s before hitting escape, on separate terminal windows:

Server
bytesread:19  total so far:2,136,798
quit recieved, quitting

Client
Press Escape or Q to quit
set 19 bytes, reply:2,136,798, running_total:2,136,798  (same:true)


PicoLisp

<lang PicoLisp>(call 'mkfifo "in" "out") # Create pipes

(zero *Cnt) # Initialize byte counter

(unless (fork) # Handle "out" pipe

  (loop
     (out "out"
        (sync)
        (tell)
        (prinl *Cnt) ) ) )

(unless (fork) # Handle "in" pipe

  (let P (open "in")
     (loop
        (in P                         # Open twice, to avoid broken pipes
           (while (rd 1)                 # (works on Linux, perhaps not POSIX)
              (tell 'inc *Cnt) ) ) ) ) )

(push '*Bye '(call 'rm "in" "out")) # Remove pipes upon exit (wait) # (Terminate with Ctrl-C)</lang> Test:

$ line <out
0
$ echo abc >in
$ line <out
4
$ echo äöü >in
$ line <out
11

Racket

<lang Racket>#lang racket

(define-values (in out) (make-pipe))

Thread loops through list of strings to send
and closes port when finished

(define t1 (thread (lambda ()

                    (for ([i (list "a" "test" "sequence")])
                      (display i out)
                      (sleep 1))
                    (close-output-port out))))
Blocking call to read char, if not EOF then loop

(define t2 (thread (lambda ()

                    (define cnt 0)
                    (let loop ()
                      (when (not (eof-object? (read-char in)))
                        (set! cnt (add1 cnt))
                        (loop)))
                    (display (format "Bytes Rx: ~a\n" cnt))
                    (close-input-port in))))

(thread-wait t1) (thread-wait t2)</lang>

Raku

This example is incorrect. Please fix the code and remove this message.

Details: Blocks on the read pipe and not able to empty/update the write pipe according to the task requirement

Couldn't figure out how to get around the IO blockages as per the talk page so any help would be appreciated, thanks in advance. <lang perl6># 20200923 Added Raku programming solution

use NativeCall; use UUID; # cannot mix File::Temp with mkfifo well so use this as tmpnam

my ($in, $out) = <in out>.map: { "/tmp/$_-" ~ UUID.new } ;

sub mkfifo(Str $pathname, int32 $mode --> int32) is native('libc.so.6') is symbol('mkfifo') {*}

($in,$out).map: { die $!.message if mkfifo($_, 0o666) } ; # c style exit code

say "In pipe: ", $in; say "Out pipe: ", $out;

my atomicint $CharCount = 0;

signal(SIGINT).tap: {

  ($in,$out).map: { .IO.unlink or die } ;
  say "\nBye." and exit;

}

loop {

  given $in.IO.watch { # always true even when nothing is spinning ?
     say "Current count: ", $CharCount  ⚛+= $in.IO.open.readchars.codes;
     given $out.IO.open(:update :truncate) { # both truncate and flush aren't
        .flush or die ;                      # working on pipes so without a
        .spurt: "$CharCount\t"               # prior consumer it just appends
     }
     $out.IO.open.close or die;
  }
  sleep ½;

}</lang>

Output:

Terminal 1

named-pipe.raku
In  pipe: /tmp/in-b49e4cdf-80b6-49da-b4a3-2810f1eeeb6a
Out pipe: /tmp/out-44fcd0db-ce02-47b4-bdd3-34833f3dd621
Current count: 5
Current count: 10
Current count: 15
^C
Bye.

Terminal 2

echo asdf > /tmp/in-b49e4cdf-80b6-49da-b4a3-2810f1eeeb6a
cat /tmp/out-44fcd0db-ce02-47b4-bdd3-34833f3dd621
5       ^C
echo qwer > /tmp/in-b49e4cdf-80b6-49da-b4a3-2810f1eeeb6a
echo uiop > /tmp/in-b49e4cdf-80b6-49da-b4a3-2810f1eeeb6a
cat /tmp/out-44fcd0db-ce02-47b4-bdd3-34833f3dd621      
10      15      ^C

Ruby

This example is in need of improvement:
  • Find a way to report errors from inside open_sesame, such as Errno::ENOENT.
  • Check that open file is a FIFO: foopipe.stat.pipe?

With OpenBSD, we observe that open(2) a named pipe blocks all threads in a process. (This must be bug in thread library.) So, we fork(2) other process to call open(2), and apply UNIXSocket to send IO object.

Works with: Unix

<lang ruby>require 'socket'

  1. Ruby has no direct access to mkfifo(2). We use a shell script.

system '/bin/sh', '-c', <<EOF or abort test -p in || mkfifo in || exit test -p out || mkfifo out || exit EOF

  1. Forks a process to open _path_. Returns a _socket_ to receive the open
  2. IO object (by UNIXSocket#recv_io).

def open_sesame(path, mode)

 reader, writer = UNIXSocket.pair
 pid = fork do
   begin
     reader.close
     file = File.open(path, mode)
     writer.send_io file
   ensure
     exit!
   end
 end
 Process.detach pid
 writer.close
 return reader

end

insock = open_sesame("in", "rb") outsock = open_sesame("out", "w") inpipe, outpipe = nil count = 0 readers = [insock, outsock] writers = [] loop do

 selection = select(readers, writers)
 selection[0].each do |reader|
   case reader
   when insock
     inpipe = insock.recv_io
     puts "-- Opened 'in' pipe."
     insock.close
     readers.delete insock
     readers.push inpipe
   when outsock
     outpipe = outsock.recv_io
     puts "-- Opened 'out' pipe."
     outsock.close
     readers.delete outsock
     writers.push outpipe
   when inpipe
     count += (inpipe.read_nonblock(4096).size rescue 0)
   end
 end
 selection[1].each do |writer|
   case writer
   when outpipe
     outpipe.puts count
     puts "-- Counted #{count} bytes."
     exit
   end
 end

end</lang>

Example run:

$ ruby count.rb  
-- Opened 'in' pipe.
-- Opened 'out' pipe.
-- Counted 32 bytes.
$
$ echo 'This is line 1.' > in
$ echo 'This is line 2.' > in
$ cat out
32
$

Tcl

<lang tcl># Make the pipes by calling a subprocess... exec sh -c {test -p in || mkfifo in || exit 1;test -p out || exec mkfifo out}

  1. How many bytes have we seen so far?

set count 0

  1. Read side; uses standard fileevent mechanism (select() under the covers)

set in [open in {RDONLY NONBLOCK}] fconfigure $in -translation binary fileevent $in readable consume proc consume {} {

   global count in
   # Reads only 4kB at a time
   set data [read $in 4096]
   incr count [string length $data]

}

  1. Writer side; relies on open() throwing ENXIO on non-blocking open of write side

proc reportEveryHalfSecond {} {

   global count
   catch {

set out [open out {WRONLY NONBLOCK}] puts $out $count close $out

   }
   # Polling nastiness!
   after 500 reportEveryHalfSecond

} reportEveryHalfSecond

  1. Run the event loop until done

vwait forever</lang>

Wren

Translation of: C
Library: pthread

An embedded program using a C host as Wren has no built in support for named pipes.

As in the case of the C example, we run the 'write' loop on a separate thread and using a separate VM. The 'tally' variable is stored on the C side so it can be shared by both VM's.

This works fine on Ubuntu 20.04 and should work on other Linux distros though I can't vouch for anything else. <lang ecmascript>/* ipc_via_named_pipe.wren */

var InputFifo = "in" var OutputFifo = "out"

var O_RDONLY = 0 var O_WRONLY = 1

var PIPE_BUF = 4096

class Fifo {

   foreign static make(path, oflag)

}

class File {

   foreign static open(path, flags)
   foreign static write(fd, str, size)
   foreign static read(fd, size)
   foreign static close(fd)

}

class C {

   foreign static usleep(usec)
   foreign static tally
   foreign static tally=(newTally)

}

class Loops {

   static read() {
       while (true) {
           var fd = File.open(InputFifo, O_RDONLY)
           var len
           while ((len = File.read(fd, PIPE_BUF)) > 0) C.tally = C.tally + len
           File.close(fd)
       }
   }
   static write() {
       while (true) {
           var fd = File.open(OutputFifo, O_WRONLY)
           var ts = C.tally.toString + "\n"
           File.write(fd, ts, ts.bytes.count)
           File.close(fd)
           C.usleep(10000)
       }
   }

}

Fifo.make(InputFifo, 438) Fifo.make(OutputFifo, 438)</lang>
We now embed this script in the following C program, build and run from one terminal. <lang c>/* gcc ipc_via_named_pipe.c -o ipc_via_named_pipe -lpthread -lwren -lm */

  1. include <stdio.h>
  2. include <stdlib.h>
  3. include <sys/stat.h>
  4. include <unistd.h>
  5. include <fcntl.h>
  6. include <string.h>
  7. include <pthread.h>
  8. include "wren.h"

/* C <=> Wren interface functions */

size_t tally = 0;

void C_make(WrenVM* vm) {

   const char *pathname = wrenGetSlotString(vm, 1);
   mode_t mode = (mode_t)wrenGetSlotDouble(vm, 2);
   int res = mkfifo(pathname, mode);
   wrenSetSlotDouble(vm, 0, (double)res);

}

void C_open(WrenVM* vm) {

   const char *pathname = wrenGetSlotString(vm, 1);
   int flags = (int)wrenGetSlotDouble(vm, 2);
   int fd = open(pathname, flags);
   if (fd < 0) {
       perror(NULL);
   }
   wrenSetSlotDouble(vm, 0, (double)fd);

}

void C_write(WrenVM* vm) {

  int fd = (int)wrenGetSlotDouble(vm, 1);
  const char *str = wrenGetSlotString(vm, 2);
  size_t count = (size_t)wrenGetSlotDouble(vm, 3);
  ssize_t res = write(fd, (const void *)str, count + 1);
  wrenSetSlotDouble(vm, 0, (double)res);

}

void C_read(WrenVM* vm) {

  int fd = (int)wrenGetSlotDouble(vm, 1);
  size_t nbyte = (size_t)wrenGetSlotDouble(vm, 2);
  char buf[nbyte];
  ssize_t res = read(fd, buf, nbyte);
  wrenSetSlotDouble(vm, 0, (double)res);

}

void C_close(WrenVM* vm) {

  int fd = (int)wrenGetSlotDouble(vm, 1);
  int res = close(fd);
  wrenSetSlotDouble(vm, 0, (double)res);

}

void C_usleep(WrenVM* vm) {

  useconds_t usec = (useconds_t)wrenGetSlotDouble(vm, 1);
  int res = usleep(usec);
  wrenSetSlotDouble(vm, 0, (double)res);

}

void C_getTally(WrenVM* vm) {

   wrenSetSlotDouble(vm, 0, (double)tally);

}

void C_setTally(WrenVM* vm) {

   size_t newTally = (size_t)wrenGetSlotDouble(vm, 1);
   tally = newTally;

}

WrenForeignMethodFn bindForeignMethod(

   WrenVM* vm,
   const char* module,
   const char* className,
   bool isStatic,
   const char* signature) {
   if (strcmp(module, "main") == 0) {
       if (strcmp(className, "Fifo") == 0) {
           if (isStatic && strcmp(signature, "make(_,_)") == 0)    return C_make;    
       } else if (strcmp(className, "File") == 0) {
           if (isStatic && strcmp(signature, "open(_,_)") == 0)    return C_open;
           if (isStatic && strcmp(signature, "write(_,_,_)") == 0) return C_write;         
           if (isStatic && strcmp(signature, "read(_,_)") == 0)    return C_read;
           if (isStatic && strcmp(signature, "close(_)") == 0)     return C_close;
       } else if (strcmp(className, "C") == 0) {
           if (isStatic && strcmp(signature, "usleep(_)") == 0)    return C_usleep;
           if (isStatic && strcmp(signature, "tally") == 0)        return C_getTally;
           if (isStatic && strcmp(signature, "tally=(_)") == 0)    return C_setTally;
       } 
   }
   return NULL;

}

static void writeFn(WrenVM* vm, const char* text) {

   printf("%s", text);

}

void errorFn(WrenVM* vm, WrenErrorType errorType, const char* module, const int line, const char* msg) {

   switch (errorType) {
       case WREN_ERROR_COMPILE:
           printf("[%s line %d] [Error] %s\n", module, line, msg);
           break;
       case WREN_ERROR_STACK_TRACE:
           printf("[%s line %d] in %s\n", module, line, msg);
           break;
       case WREN_ERROR_RUNTIME:
           printf("[Runtime Error] %s\n", msg);
           break;
   }

}

char *readFile(const char *fileName) {

   FILE *f = fopen(fileName, "r");
   fseek(f, 0, SEEK_END);
   long fsize = ftell(f);
   rewind(f);
   char *script = malloc(fsize + 1);
   fread(script, 1, fsize, f);
   fclose(f);
   script[fsize] = 0;
   return script;

}

WrenVM *vm, *vm2;

void read_loop() {

   wrenEnsureSlots(vm, 1);
   wrenGetVariable(vm, "main", "Loops", 0);
   WrenHandle *method = wrenMakeCallHandle(vm, "read()");
   wrenCall(vm, method);
   wrenReleaseHandle(vm, method);

}

void* write_loop(void *a) {

   wrenEnsureSlots(vm2, 1);
   wrenGetVariable(vm2, "main", "Loops", 0);
   WrenHandle *method = wrenMakeCallHandle(vm2, "write()");
   wrenCall(vm2, method);
   wrenReleaseHandle(vm2, method);

}

int main(int argc, char **argv) {

   WrenConfiguration config;
   wrenInitConfiguration(&config);
   config.writeFn = &writeFn;
   config.errorFn = &errorFn;
   config.bindForeignMethodFn = &bindForeignMethod;
   vm = wrenNewVM(&config);
   vm2 = wrenNewVM(&config);
   const char* module = "main";
   const char* fileName = "ipc_via_named_pipe.wren";
   char *script = readFile(fileName);
   WrenInterpretResult result = wrenInterpret(vm, module, script);
   switch (result) {
       case WREN_RESULT_COMPILE_ERROR:
           printf("Compile Error!\n");
           break;
       case WREN_RESULT_RUNTIME_ERROR:
           printf("Runtime Error!\n");
           break;
       case WREN_RESULT_SUCCESS:
           break;
   }
   wrenInterpret(vm2, module, script);
   pthread_t pid;
   pthread_create(&pid, 0, write_loop, 0);
   read_loop();
   wrenFreeVM(vm);
   wrenFreeVM(vm2);
   free(script);
   return 0;

}</lang>

Output:

Finally we can test that this is working correctly by typing in the following commands from a different terminal. The string lengths returned will, of course, include the final zero byte.

$ cat out
0
$ echo test > in
$ cat out
5
$ echo "named pipe" > in
$ cat out
16

zkl

zkl doesn't open pipes but it knows about them (on Unix anyway as they are just a file). So, outside of the program, create two named pipes and read/write to them inside the program.

Translation of: C

<lang zkl>pipe:=Thread.Pipe(); // NOT a Unix pipe, for inter-thread commication fcn writeLoop(pipe){ // a thread

  out:=File("out","w");
  foreach tally in (pipe){ out.writeln(tally); out.flush(); }
  println("writeLoop done");

}.launch(pipe);

fcn readLoop(pipe){ // a thread

  tally:=0;
  in:=File("in","r").howza(3); // open for read, reading characters
  while(1){  // named pipe sets EoF after writer exits
     foreach c in (in){ pipe.write(tally+=1) } // read bytes until EoF
  }
  in.close();
  println("readLoop done");

}.launch(pipe);

while(1){ Atomic.sleep(10000); } // veg out while other talk</lang>

Output:

Terminal 1:

$ mkfifo in; mkfifo out
<wait until other two terminals get going>
$ echo "hello world" > in
$ cat ../Tests/lorem_ipsum.txt >in
Output:

In Terminal 2, start the program:

$ zkl bbb
Output:

Terminal 3: There is pretty much no delay - character enters "in", "out" sees tally.

$ cat out
1
2
3
4
5
6
7
8
9
10
11
12
...
1391
1392
1393
1394
1395
1396
1397
1398
1399