Contents

Introduction

This page documents an IPC facility for object icon, based on three Unix IPC facilities, namely shared memory, semaphores and message queues. This facility enables communication between different processes, either within the same or a different programs.

The shared library

The RTL/C portion of the library is contained in a shared library objecticonipclib.so, which is built in the lib/native subdirectory of the distribution.

Creating a sub-process

Creating a sub-process is easy, and can be done with the System.fork() function, or alternatively using the util.Process class, which is just a wrapper around System.fork() and some other common commands. Here is a simple use of the Process class.

Download ipc0.icn

import
   io(write),
   util(Process)

procedure run()
   write("In child")
end

procedure main()
   local c

   write("In parent")

   c := Process{run()}
   c.start()
   c.close()

   write("Finished")
end

This program when compiled prints

In parent
In child
Finished

The child process body is given (as a co-expression) in the Process constructor. In the parent process in the main procedure, invoking the start() method of the child begins the process, whilst the close() method waits for the child to finish.

This is very straightforward, but for things become more complicated when the child and parent process wish to communicate with one another. Consider the following expanded version of the above program.

Download ipc1.icn

import
   io(write),
   util(Process)

global var

procedure run()
   write("In child, setting var")
   var := 2
end

procedure main()
   local c

   var := 1

   write("In parent var=", var)

   c := Process{run()}
   c.start()
   c.close()

   write("Finished, var=", var)
end

The output of this program is :-

In parent var=1
In child, setting var
Finished, var=1

This is not what you would expect at first glance; the child's attempt to set the variable has had no effect on the parent. This is because at the point the child process is created, it is given a complete copy of the memory space of the parent process, and so all assignments affect the copy, not the original.

Shared Memory

To solve this problem, shared memory can be used. Here is the same program, but this time with the variable var replaced by an instance of the Shm shared memory class.

Download ipc2.icn

import
   io(write),
   ipc(Shm),
   util(Process)

global var

procedure run()
   write("In child, setting var")
   var.set_value(2)
end

procedure main()
   local c

   # Create a Shm instance with initial value 1
   var := Shm.create_private(1)

   write("In parent var=", var.get_value())

   c := Process{run()}
   c.start()
   c.close()

   write("Finished, var=", var.get_value())

   var.close()
end

Now the output is :-

In parent var=1
In child, setting var
Finished, var=2

Here, var is an instance of the Shm class, and is created with the "factory" method create_private(). The parameter 1 is just the initial value. Then get_value() and set_value() are used to get/set the value of the object, and finally close() cleans up the operating system resources used by the instance (if this is not done, it will be done automatically when the program ends, and a warning message output). These various calls delegate their work to the C functions in the shared library.

More or less anything can be used as the parameter to set_value(), but note that the value is copied into and out of the shared variable, ie reference semantics do not apply. So, for example :-

   var := Shm.create_private([1,2,3])

   put(var.get_value(), 4)  # No effect; just adds 4 to the copy returned by get_value
   every write(!var.get_value())  # 1,2,3

   var.set_value(put(var.get_value(), 4))  # OK, sets the value again with the 4 added 
   every write(!var.get_value())   # 1,2,3,4

As another example, here we set an instance of a class into the shared memory :-

Download ipc3.icn

import
   io(write),
   ipc(Shm)

class MyClass()
   private x

   public thing()
      write("In thing, x=", x)
   end

   public new(x)
      self.x := x
      return
   end
end

procedure main()
   local var, c, d
   var := Shm.create_private()

   c := MyClass(10)
   var.set_value(c)

   d := var.get_value()
   d.thing()

   write("c=", image(c), " d=", image(d))

   var.close()
end

The output is :-

In thing, x=10
c=object MyClass#1(1) d=object MyClass#2(1)

Note how c and d are different instances.

There is one restriction regarding the value of the shared variable, and that is that it must be encodable by the lang.encode() procedure, which converts an arbitrary object into a string. Some things are by their nature not encodeable, such as a FileStream instance.

Semaphores

The Sem class provides the classic semaphore mechanism, based on the underlying Unix implementation, which can be used for inter-process co-ordination, critical regions, and so on.

Here is an example in which a child process creates some information and puts it into a shared memory variable, whilst the parent retrieves that information. The processes are co-ordinated by two semaphores to make sure they remain in step.

Download ipc4.icn

import
   io(write),
   ipc(Sem, Shm),
   util(Process)

global var, sem1, sem2

procedure run()
   local i
   every i := 1 to 10 do {
      # Wait till okay to set value
      sem1.wait()
      var.set_value(i)
      # Signal value set
      sem2.signal()

      # Sleep for 1/2 second
      delay(500)
   }
end

procedure main()
   local i, c
   var := Shm.create_private()

   # Create two private semaphores with initial value 0.
   sem1 := Sem.create_private(0)
   sem2 := Sem.create_private(0)

   c := Process{run()}
   c.start()

   repeat {
      # Signal okay to set value
      sem1.signal()
      # Wait till value set
      sem2.wait()
      i := var.get_value()
      write(i)
      if i = 10 then 
         break
   }
   c.close()
   var.close()
   sem1.close()
   sem2.close()

   write("Finished okay")
end

Note that the semaphores are created and destroyed in a similar way to shared memory.

Other than the standard wait and signal methods, the Sem class also has methods to get and set the semaphore value, and to poll it for a value without suspending.

Message queues

The third IPC facility is message queues. One process adds messages to the queue and another one reads the messages. A message can be any Icon value, subject to the same rules as for shared memory, described above. Here is the same example as just shown above, but using a message queue instead.

Download ipc5.icn

import
   io(write),
   ipc(Msg),
   util(Process)

global mq

procedure run()
   local i
   every i := 1 to 10 do {
      # Send the message
      mq.send(i)

      # Sleep for 1/2 second
      delay(500)
   }
end

procedure main()
   local i, c

   mq := Msg.create_private()

   c := Process{run()}
   c.start()

   repeat {
      i := mq.receive()
      write(i)
      if i = 10 then 
         break
   }
   c.close()
   mq.close()

   write("Finished")
end

As you can see, this particular task is much simpler using message queues.

Public IPC resources

All of the examples above used "private" resources; for example the message queue was created with

 mq := Msg.create_private() | stop("Couldn't create:", &why)

But it is possible to provide a pre-arranged key in order to create a "public" resource instead. For example

 mq := Msg.create_public(999) | stop("Couldn't create:", &why)

After this succeeds a message queue will exist with key 999. If the call fails, then this usually means a queue with id 999 already exists.

This message queue may then be opened by another process, possibly in another program.

 mq := Msg.open_public(999) | stop("Couldn't open resource:", &why)

Similar factory procedures exist for semaphores and shared memory.

There is one difference between "private" and "public" resources: the "public" variety aren't destroyed automatically at the end of the creating process; they have to be destroyed explicitly either by calling the close() method, or ultimately using the Unix commands ipcs and ipcrm.

Contents