Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Embedded Systems

Inferno Application Development with Limbo


Dec00: Programmer's Toolchest

A distributed OS for networked devices

Phillip is a graduate student in the Electrical and Computer Engineering department at Rutgers University. He is the comaintainer of the Inferno/Limbo FAQ, and author of an upcoming book on Inferno application development with Limbo. He can be contacted at [email protected].


Inferno is a distributed operating system for networked devices and Internet appliances that have constrained memory and processing resources -- set-top-boxes, personal digital assistants (PDAs), intelligent telephones, and the like. Originally developed at Lucent Technologies' Bell Labs, native versions of Inferno are distributed by Vita Nuova (http://www.vitanuova.com/) in source and binary form for the x86, Power PC, ARM (StrongARM SA1100), and MIPS platforms. In addition, Inferno is available in hosted versions for Plan 9, FreeBSD, Linux/x86, Solaris/SPARC, and Win32. Source-only versions are provided for Irix, Unixware, and HP/UX.

Inferno is available as a free distribution in binary format, along with source code for applications, device drivers, a C cross-compiler suite, and so on. However, the source code to the Inferno core itself is available only to licensed subscribers. A commercial subscription license is similar to an open-source license, and includes all the source code to Inferno, ports to various architectures, documentation, and C cross-compilers for the 680x0, Power PC, x86, and SPARC. Subscribers can distribute and sell copies of Inferno or modified versions of the OS royalty free.

Applications for Inferno are written in a programming language called "Limbo," which is syntactically similar to C/C++, Java, and Pascal. The Limbo compiler is included with the Inferno distribution. In this article, I will provide an overview of Inferno, then use Limbo to write a typical Limbo application. (For more details on the Inferno operating system, see http://www.vitanuova.com/papers/bltj.html.)

Inferno Overview

Inferno consists of three primary components: the core kernel, a virtual machine (called "Dis"), and system libraries to provide support for Limbo applications. Applications run over the virtual machine and may interact with the system libraries that provide functionality (such as file I/O). A unifying theme in the Inferno system is the representation of resources as files; all resources of the Inferno system are available to Limbo applications as files in a hierarchical file tree. A single communication protocol, Styx, is used to access all resources, be they local or remote (for more information on Styx, see http://www.vitanuova.com/papers/styx.html). An example is the access to the network protocol stacks, typically accessible from the directory /net. Example 1 shows a sample /net hierarchy.

Consider the task of connecting to, say, the DDJ web site (http://www.ddj.com/) and retrieving a file. To perform this seemingly simple task on most systems, you would either write a C program to perform operations such as opening sockets, or a Perl script that would invoke some Perl module to do the job. By comparison, carrying out this task in Inferno is surprisingly straightforward. The TCP protocol stack presents a small filesystem that is accessible from the directory /net/tcp on an Inferno system. To create a new TCP connection, you start off by reading the file /net/tcp/clone. This returns a number, say "7," which is the name of a dynamically synthesized subdirectory of /net/tcp, which you shall use for further actions. In /net/tcp/7/, you write the string "connect <ipaddr>" into the file ctl, where <ipaddr> is the IP address of the host you wish to connect to. You now have a session to http://www.ddj.com/. The files local, remote, and status in the same directory can be read for information about your local IP address and port for the connection, the remote IP and port, and the state of the connection. You may now send data to the remote end of the connection by writing it into the file /net/tcp/7/data, or receive data by reading from the same file. The retrieval of http://www.ddj.com/index.html can be accomplished with three commands from the Inferno shell; see Example 2. These files served by the system are not disk files. Read/writes to the files are intercepted by the system, and passed off to either user-level applications or to the appropriate kernel drivers as need be.

Limbo Programming

Limbo programs are made up of functions and data objects, grouped into modules. A module is usually split into two parts, the module definition and module implementation. The module definition defines the interface that the module exports to external applications, declaring functions that it provides and data objects such as constants that it makes accessible to other modules. The module implementation contains the implementations of the functions defined in the interface, along with other functions that are private to its implementation. Typically, Limbo interface declarations are placed into files with the suffix ".m" and implementations into files ending in ".b." Linking and loading is performed at run time, and a module must explicitly load any other modules it wishes to access, after including the modules' interface declaration, unloading them as need be to minimize memory usage. Type checking is performed at load time, and a loaded module must adhere to the interface of the included interface declaration, facilitating function and data hiding.

Data objects are associated with a module and occupy storage allocated for it. There are five basic data types: byte, int, big, real, and string. Table 1 lists their sizes and value ranges. The structured data types are array, list, adt, and tuple. Arrays are similar to their counterparts in C, with the addition of array slicing. Lists are lists of items of one data type. The language defines operators for performing list operations such as prepending to and removal from a list. Abstract data types (ADTs) can be used to organize data and functions. They are similar to C++ classes but do not provide inheritance and other OO features. Multiple behaviors of a module may, however, be obtained by providing different implementations for a given interface, with the decision being made at run time as to which implementation to use. Tuples are unnamed ADTs, and may be cast to ADT types. A restricted form of pointers is allowed, in the form of reference types, which may only be references to ADTs. The channel data type facilitates communication between threads. Channels provide a synchronous, typed, bidirectional communications channel between threads. A read/ write on one end of a channel must be accompanied by a write/read from the channel by the thread on the other end for the operation to complete. Any of the primitive or structured data types may be sent on a channel, provided the channel is declared to be of that type. Additionally, channels may be tied to a synthetic file in the filesystem. In such a situation, any write to the file will be seen by an application listening on the channel, and any read from the channel will be serviced by the application listening on the channel. (I will use this facility to implement a synthetic file.) Finally, there are module variables and named types. Module variables have the type of a defined module, and named types can be used to define synonyms to other data types.

Developing with Limbo

Listing One shows the module interface definition for a file server that serves a single synthetic file. Reads from this file return a dynamically generated HTML document, containing text denoting the time at which the read request was processed and writes to the file are discarded.

The interface definition for the simple file server defines a constant, PATH, and function (init). An application that wished to access functions in the module would include this module definition. When one module includes the interface definition of another, it only imports an interface -- not code. Upon compilation, no code from the modules with interfaces it included is linked in. Instead, modules explicitly load the implementations of other modules at run time, and unload them whenever they are not needed. In loading another module, an application must specify the physical location of the module implementation, be it on disk or elsewhere, and by convention most modules include a constant, PATH, to define where their implementation resides. In this case, the implementation of module SimpleFileServer is defined to reside in /usr/pip/ dis/simpleserver.dis. There may be several implementations corresponding to a given module interface declaration, and an application can pick and choose between them at run time. In such a situation, a single PATH variable will be insufficient to denote the locations of all the implementations.

Applications in Inferno are usually started by the Inferno shell, which is itself an application written in Limbo. The responsibility of the shell is to launch other applications. To do so, it must know what function in the modules to invoke. By convention, for the shell to be able to launch an application, that application must have a function init, with the signature:

init(ctxt: ref Draw->Context, args: list of string)

The first argument is a reference to the display context. (I don't use it here since the example does not perform any graphical operations on the display.) The second argument is a list of string arguments and is used by the shell (or other applications) to pass command-line arguments to the application. Naturally, applications that are not required to be executable from the shell command line need not adhere to this interface. All functions have unique signatures, derived from their name and the types and number of arguments. Module type checking is performed at run time; the shell will fail when it tries to load a module with a signature that is not as shown earlier.

The implementation includes the interface definitions of three other modules -- Draw, Sys, and Daytime -- along with its own module definition. The include statement is a part of the Limbo language itself, not a directive to a preprocessor. Draw and Sys are built-in modules provided by the Inferno system, and Daytime is an application written in Limbo. I define three global variables, draw, sys, and daytime, which are module handles and are of types Draw, Sys, and Daytime, respectively. These variables will be used to refer to a particular instance of the Draw, Sys, or Daytime modules. The Draw module provides facilities for accessing and manipulating the display, and is needed by any application that writes to the display. The Sys module provides system calls, such as those for performing I/O, and a handful of utility routines. It is needed in SimpleFileServer because it prints some diagnostics to the display, and also because it provides the system call for creating synthetic files. The included module interfaces also define various data structures and constants that are needed by the functions they implement.

The first thing you will usually do in a Limbo program is load the implementations of the modules you need, as I do for the Sys module in the first line of the init function. I don't load the Draw module because I won't be using any of its functions. I only included draw.m because of the data structures (in particular the definition of Context) that it defines. Loading a module brings an instance of that module into the virtual machine. The value of the load expression is a handle to an instance of the module, which is stored in the variable sys. Subsequent to this, you may reference member functions and data of Sys through sys, as you do in printing out the startup message. Member functions of a module are accessed with the arrow ("->") separator.

In creating a synthetic file, you first need to have the system intercept requests to the filesystem. The Srv device enables this, and you have to tell the system that all requests for entries in the current namespace should go to the Srv device first, before going to, say, the disk filesystem. The bind operation enables attachments to the namespace. The arguments to bind are the item to be bound, where to bind it, and the how to perform the bind. In this case, you want requests to be seen by the Srv device (and hence your application) first, so you bind with the sys->MBEFORE flag as the second argument to bind (MBEFORE is a constant defined in sys.m, and you use the handle you have to Sys to refer to it). Once the binding is done, you create a new entry in the file namespace with the file2chan function from the Sys module. This returns a reference to an ADT containing a pair of channels; one receives data pertaining to read requests (intercepted by Srv) to the synthetic file, and the other data pertaining to write requests. Sys->file2chan takes as parameters the name of the directory in which to serve the file, and the name of the file to be served, and returns a reference to a FileIO ADT; see Example 3.

On a write to the file created by file2chan, a tuple consisting of the file offset, written data, a file identifier, and a channel on which to respond is sent on the write channel. A server listening on the write channel might place the written data at the specified offset in a locally maintained buffer. On a read from the served file, a tuple consisting of the offset of the read request, the number of bytes to be read, a file identifier, and the channel on which the server should place the requested data, is sent on the read channel. A server listening on the channel might send data at the requested offset from its locally maintained data buffer, by writing it on the channel specified in the read request tuple. Because a server must continuously listen on requests on the channel pair, it's a good idea to hand off this task to a worker thread.

The creation of a new thread of control is trivial, and done with the "spawn" keyword. The spawn operation specifies the name of the function to use in the creation of the new thread. In this example, I create a new thread with the function worker(). The primary task of this thread is to listen on requests on the read/write channels of the FileIO structure of the served file. It listens for communications on the two channels simultaneously with the alt construct. alt selects randomly (but fairly) between channels ready to communicate. An alt statement completes when either of the channels has data ready. Thus, in worker, the alt is enclosed within a loop so that it repeatedly waits for communications on the two channels. On a write to the file, worker replies with the number of bytes written, on the channel wc, supplied in the write request. On a read from the file, worker sends the output from gendata(), which uses the time function from the Daytime module to generate a text string of the current time. This is encapsulated in HTML tags, so that the synthetic file is a valid web page.

Example 4 is a sample session of compiling and running the file server from an Inferno shell. The synthesized file behaves like an ordinary disk file. It appears in the filesystem as a "server.html" with size 0, but reading it routinely returns 147 bytes of HTML from the server. Writes to the file are discarded by the server, and never touch the disk. If you unmount the Srv device, the server.html vanishes, since the interloper or proxy is no longer there to feed the server.

Conclusion

Inferno is a beautifully designed system. Most resources are accessible through a filesystem interface, which greatly simplifies programming. The Limbo programming language is easy to learn. In Inferno, Limbo programs compile into bytecode for execution on a virtual machine. The applications interact with the system and outside world through modules built into the Inferno system, and through the file server interfaces exported by most Inferno resources. Together, Limbo and Inferno form a coherent system that is a joy to program.

DDJ

Listing One

SimpleFileServer : module
{
        PATH : con "/usr/pip/dis/simpleserver.dis";
        init : fn(ctxt : ref Draw->Context, args : list of
string);
};

Back to Article

Listing Two

implement SimpleFileServer;
include "sys.m";
include "draw.m";
include "daytime.m";
include "simpleserver.m";

sys     : Sys;
draw    : Draw;
daytime : Daytime;
chanref : ref sys->FileIO;
init(ctxt : ref Draw->Context, args : list of string)
{
        sys = load Sys Sys->PATH;
        sys->print("Server - Initialising...\n");
        sys->bind("#s", "/usr/pip", sys->MBEFORE);

        chanref = sys->file2chan("/usr/pip", "server.html");

        if (chanref == nil)
        {
           sys->print("Server - Could not create chan  file : %r\n");
        }
        spawn worker();
}
worker ()
{

        data    : array of byte;
        index   : int = 0;
        while (1)
        alt
        {
                (off, nbytes, fid, rc) := <-chanref.read =>
                {
                        if (rc == nil) break;
                        ## If this is a new read, generate a new page
                        if (index == 0)
                        {
                                data = gendata();
                        }
                        ## len returns number of bytes in an array of bytes,
                        ## number of unicode characters in a string or
                        ## number of elements in a list.
                        if (index < len data)
                        {
                                end := min(index+nbytes, len data);
                                ## Serve the reader with data tha's left
                                rc <-= (data[index:end], "");
                                index = end;
                        }
                        else
                        {
                                ## Finished serving contents of data[]
                                rc <-= (nil, "");
                                ## So next read of data will start afresh
                                index = 0;
                        }
                }
                (offset, writedata, fid, wc) := <-chanref.write =>
                {
                        if (wc == nil)
                        {
                                break;
                        }
                        wc <-= (len writedata, "");
                }
        }
}
gendata () : array of byte
{
        daytime = load Daytime Daytime->PATH;
        head := "<HTML><HEAD><TITLE>Generated HTML</TITLE></HEAD>\n";
        top  := "<BODY><CENTER><H1>Dynamically Generated</H1></CENTER>\n";
        body := daytime->time() + "\n";
        tail := "</BODY></HTML>\n";
        return array of byte (head+top+body+tail);
}
min (a, b : int) : int
{
        if (a < b)
                return a;
        return b;
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.