NAME ^

docs/pdds/pdd22_io.pod - Parrot I/O

ABSTRACT ^

Parrot's I/O subsystem.

VERSION ^

$Revision$

SYNOPSIS ^

    open P0, "data.txt", ">"
    print P0, "sample data\n"
    close P0

    open P1, "data.txt", "<"
    S0 = read P1, 12
    P2 = getstderr
    print P2, S0
    close P1

    ...

DEFINITIONS ^

A "stream" allows input or output operations on a source/destination such as a file, keyboard, or text console. Streams are also called "filehandles", though only some of them have anything to do with files.

DESCRIPTION ^

This is a draft document defining Parrot's I/O subsystem, for both streams and network I/O. Parrot has both synchronous and asynchronous I/O operations. This section describes the interface, and the IMPLEMENTATION section provides more details on general implementation questions and error handling.

The signatures for the asynchronous operations are nearly identical to the synchronous operations, but the asynchronous operations take an additional argument for a callback, and the only return value from the asynchronous operations is a status object. The callbacks take the status object as their first argument, and any return values as their remaining arguments.

The listing below says little about whether the opcodes return error information. For now assume that they can either return a status object, or return nothing. Error handling is discussed more thoroughly in the implementation section.

I/O Stream Opcodes ^

Opening and closing streams

Retrieving existing streams

These opcodes do not have asynchronous variants.

Writing to streams

Reading from streams

Retrieving and setting stream properties

Deprecated opcodes

Filesystem Opcodes ^

Network I/O Opcodes ^

Most of these opcodes conform to the standard UNIX interface, but the layer API allows alternate implementations for each.

IMPLEMENTATION ^

The Parrot I/O subsystem uses a per-interpreter stack to provide a layer-based approach to I/O. Each layer implements a subset of the ParrotIOLayerAPI vtable. To find an I/O function, the layer stack is searched downwards until a non-NULL function pointer is found for that particular slot.

Synchronous and Asynchronous Operations ^

Currently, Parrot only implements synchronous I/O operations. Asynchronous operations are essentially the same as the synchronous operations, but each asynchronous operation runs in its own thread.

Note: this is a deviation from the existing plan, which had all I/O operations run internally as asynchronous, and the synchronous operations as a compatibility layer on top of the asynchronous operations. This conceptual simplification means that all I/O operations are possible without threading support (for example, in a stripped-down version of Parrot running on a PDA). [Asynchronous operations don't have to use Parrot threads, they could use some alternate threading implementation. But it's overkill to develop two threading implementations. If Parrot threads turn out to be too heavyweight, we may want to look into a lighter weight variation for asynchronous operations.]

The asynchronous I/O implementation will use Parrot's I/O layer architecture so some platforms can take advantage of their built-in asynchronous operations instead of using Parrot threads.

Communication between the calling code and the asynchronous operation thread will be handled by a shared status object. The operation thread will update the status object whenever the status changes, and the calling code can check the status object at any time. [Twisted has an interesting variation on this, in that it replaces the status object with the returned result of the asynchronous call when the call is complete. That is probably too confusing, but we might give the status object a reference to the returned result.]

The current strategy for differentating the synchronous calls from asynchronous ones relies on the presence of a callback argument in the asynchronous calls. If we wanted asynchronous calls that don't supply callbacks (perhaps if the user wants to manually check later if the operation succeded) we would need another strategy to differentiate the two. This is probably enough of a fringe case that we don't need to provide opcodes for it, provided they can access the functionality via methods on ParrotIO objects.

Error Handling ^

Currently some of the networking opcodes (connect, recv, send, poll, bind, and listen) return an integer indicating the status of the call, -1 or a system error code if unsuccessful. Other I/O opcodes (such as getfd and accept) have various different strategies for error notification, and others have no way of marking errors at all. We want to unify all I/O opcodes so they use a consistent strategy for error notification. There are several options in how we do this.

Integer status codes

One approach is to have every I/O operation return an integer status code indicating success or failure. This approach has the advantage of being lightweight: returning a single additional integer is cheap. The disadvantage is that it's not very flexible: the only way to look for errors is to check the integer return value, possibly comparing it to a predefined set of error constants.

Exceptions

Another option is to have all I/O operations throw exceptions on errors. The advantage is that it keeps the error tracking information out-of-band, so it doesn't affect the arguments or return values of the calls (some opcodes that have a return value plus an integer status code have odd looking signatures). One disadvantage of this approach is that it forces all users to handle exceptions from I/O operations even if they aren't using exceptions otherwise.

A more significant disadvantage is that exeptions don't work well with asynchronous operations. Exception handlers are set for a particular dynamic scope, but with an asynchronous operation, by the time an exception is thrown execution has already left the dynamic scope where the exception handler was set. [Though, this partly depends on how exceptions are implemented.]

Error callbacks

A minor variation on the exceptions option is to pass an error callback into each I/O opcode. This solves the problem of asynchronous operations because the operation has its own custom error handling code rather than relying on an exception handler in its dynamic scope.

The disadvantage is that the user has to define a custom error handler routine for every call. It also doesn't cope well with cases where multiple different kinds of errors may be returned by a single opcode. (The one error handler would have to cope with all possible types of errors.) There is an easier way.

Hybrid solution

Another option is to return a status object from each I/O operation. The status object could be used to get an integer status code, string status/error message, or boolean success value. It could also provide a method to throw an exception on error conditions. There could even be a global option (or an option set on a particular I/O object) that tells Parrot to always throw exceptions on errors in synchronous I/O operations, implemented by calling this method on the status object before returning from the I/O opcode.

The advantages are that this works well with asynchronous and synchronous operations, and provides flexibility for multiple different uses. Also, something like a status object will be needed anyway to allow users to check on the status of a particular asynchronous call in progress, so this is a nice unification.

The disadvantage is that a status object involves more overhead than a simple integer status code.

IPv6 Support ^

The transition from IPv4 to IPv6 is in progress, though not likely to be complete anytime soon. Most operating systems today offer at least dual-stack IPv6 implementations, so they can use either IPv4 or IPv6, depending on what's available. Parrot also needs to support either protocol. For the most part, the network I/O opcodes should internally handle either addressing scheme, without requiring the user to specify which scheme is being used.

IETF recommends defaulting to IPv6 connections and falling back to IPv4 connections when IPv6 fails. This would give us more solid testing of Parrot's compatibility IPv6, but may be too slow. Either way, it's a good idea to make setting the default (or selecting one exclusively) an option when compiling Parrot.

The most important issues for Parrot to consider with IPv6 are:

See the relevant IETF RFCs: "Application Aspects of IPv6 Transition" (http://www.ietf.org/rfc/rfc4038.txt) and "Basic Socket Interface Extensions for IPv6" (http://www.ietf.org/rfc/rfc3493.txt).

Excerpt ^

[Below is an excerpt from "Perl 6 and Parrot Essentials", included to seed discussion.]

Parrot's base I/O system is fully asynchronous I/O with callbacks and per-request private data. Since this is massive overkill in many cases, we have a plain vanilla synchronous I/O layer that your programs can use if they don't need the extra power.

Asynchronous I/O is conceptually pretty simple. Your program makes an I/O request. The system takes that request and returns control to your program, which keeps running. Meanwhile the system works on satisfying the I/O request. When the request is satisfied, the system notifies your program in some way. Since there can be multiple requests outstanding, and you can't be sure exactly what your program will be doing when a request is satisfied, programs that make use of asynchronous I/O can be complex.

Synchronous I/O is even simpler. Your program makes a request to the system and then waits until that request is done. There can be only one request in process at a time, and you always know what you're doing (waiting) while the request is being processed. It makes your program much simpler, since you don't have to do any sort of coordination or synchronization.

The big benefit of asynchronous I/O systems is that they generally have a much higher throughput than a synchronous system. They move data around much faster--in some cases three or four times faster. This is because the system can be busy moving data to or from disk while your program is busy processing data that it got from a previous request.

For disk devices, having multiple outstanding requests--especially on a busy system--allows the system to order read and write requests to take better advantage of the underlying hardware. For example, many disk devices have built-in track buffers. No matter how small a request you make to the drive, it always reads a full track. With synchronous I/O, if your program makes two small requests to the same track, and they're separated by a request for some other data, the disk will have to read the full track twice. With asynchronous I/O, on the other hand, the disk may be able to read the track just once, and satisfy the second request from the track buffer.

Parrot's I/O system revolves around a request. A request has three parts: a buffer for data, a completion routine, and a piece of data private to the request. Your program issues the request, then goes about its business. When the request is completed, Parrot will call the completion routine, passing it the request that just finished. The completion routine extracts out the buffer and the private data, and does whatever it needs to do to handle the request. If your request doesn't have a completion routine, then your program will have to explicitly check to see if the request was satisfied.

Your program can choose to sleep and wait for the request to finish, essentially blocking. Parrot will continue to process events while your program is waiting, so it isn't completely unresponsive. This is how Parrot implements synchronous I/O--it issues the asynchronous request, then immediately waits for that request to complete.

The reason we made Parrot's I/O system asynchronous by default was sheer pragmatism. Network I/O is all asynchronous, as is GUI programming, so we knew we had to deal with asynchrony in some form. It's also far easier to make an asynchronous system pretend to be synchronous than it is the other way around. We could have decided to treat GUI events, network I/O, and file I/O all separately, but there are plenty of systems around that demonstrate what a bad idea that is.

ATTACHMENTS ^

None.

FOOTNOTES ^

None.

REFERENCES ^

  src/io/io.c
  src/ops/io.ops
  include/parrot/io.h
  runtime/parrot/library/Stream/*
  src/io/io_unix.c
  src/io/io_win32.c
  Perl 5's IO::AIO
  Perl 5's POE


parrot