I/O Objects

Magma provides the IO type to represent objects that can have I/O performed on them, such as files, pipes, and sockets. A uniform set of I/O operations are provided for all I/O objects, as well as some special operations for specific types. We will often use the term channel to refer to an I/O object.

Contents

IOType(I) : IO -> MonStgElt
Given an I/O object I, returns the subtype of I as a string; currently this will be one of "file", "pipe", or "socket", as appropriate.

Files

Magma provides a file I/O object for interacting with external files. The string returned by IOType for such an object is "file". Note that there are also several intrinsics that interact with files using the file name alone (for examples, see the sections on printing and input).

Open(name, mode) : MonStgElt, MonStgElt -> IO
Returns a Magma file object associated with the file named name for access of type mode. Tilde expansion is performed on name. The possible values for mode are those of the standard C library function fopen() on the current operating system, and have the same interpretation. Thus one should give the value "r" for mode to open the file for reading and give the value "w" for mode to open the file for writing, etc. (Note that in the Windows version of Magma, the character "b" should be appended to mode if the file is desired to be opened in binary mode.)

Once a file object is created the various I/O operations described later can be performed on it. A file is closed by deleting it (i.e., by use of the delete statement or by reassigning the variable associated with the file); there is no Fclose function. This ensures that the file is not closed while there are still multiple references to it. (The function is called Open instead of Fopen to follow Perl-style conventions.)

OpenTest(name, mode) : MonStgElt, MonStgElt -> BoolElt, IO
Attempts to create a file object as described in Open above. If it is successful then true will be returned together with the new file object. If the file cannot be opened then false is returned.

Pipes

Magma provides a pipe I/O object for interacting with external programs. The string returned by IOType for such an object is "pipe". Note that there are also some intrinsics that interact with programs using the program name alone (for examples, see the sections on input and system calls).

Pipes are not currently available on Windows platforms. This will be addressed in a future release.

POpen(cmd, mode) : MonStgElt, MonStgElt -> IO
Runs the shell command cmd in a new process and returns a Magma pipe object associated with this command with access of type mode. The possible values for mode are those of the standard C library function popen() on the current operating system, and have the same interpretation. Thus one should give the value "r" for mode when wanting to read from the pipe and give the value "w" for mode when wanting to write to the pipe.

Bidirectional pipes are not currently supported, but see the Pipe intrinsic for a method for sending input to and receiving output from a single command.

Example IO_GetTime (H3E12)

We write a function which returns the current time as 3 values: hour, minutes, seconds. The function opens a pipe to the UNIX date command and applies regular expression matching to the output to extract the relevant fields.
> function GetTime()
>     I := POpen("date", "r");
>     date := Read(I);
>     _, _, f := Regexp("([0-9][0-9]):([0-9][0-9]):([0-9][0-9])", date);
>     h, m, s := Explode(f);
>     return h, m, s;
> end function;
> h, m, s := GetTime();
> h, m, s;
14 30 01
> h, m, s := GetTime();
> h, m, s;
14 30 04

Sockets

Magma provides a socket I/O type for communicating over the network. The string returned by IOType for such an object is "socket".

Sockets may be used to establish communication channels between machines on the same network. Once established, they can be read from or written to in much the same ways as more familiar I/O constructs like files. One major difference is that the data is not instantly available, so the I/O operations take much longer than with files.

Strictly speaking, a socket is a communication endpoint whose defining information consists of a network address and a port number. (Even more strictly speaking, the communication protocol is also part of the socket. Magma only uses TCP sockets, however, so we ignore this point from now on.)

The network address selects on which of the available network interfaces communication will take place; it is a string identifying the machine on that network, in either domain name or dotted-decimal format. For example, both "localhost" and "127.0.0.1" identify the machine on the loopback interface (which is only accessible from the machine itself), whereas "foo.bar.com" or "10.0.0.3" might identify the machine in a local network, accessible from other machines on that network. Currently only IPv4 addresses are supported.

The port number is just an integer that identifies the socket on a particular network interface. It must be less than 65 536. A value of 0 will indicate that the port number should be chosen by the operating system.

There are two types of sockets, which we will call client sockets and server sockets. The purpose of a client socket is to initiate a connection to a server socket, and the purpose of a server socket is to wait for clients to initiate connections to it. (Thus the server socket needs to be created before the client can connect to it.) Once a server socket accepts a connection from a client socket, a communication channel is established and the distinction between the two becomes irrelevant, as they are merely each side of a communication channel.

In the following descriptions, the network address will often be referred to as the host. So a socket is identified by a (host, port) pair, and an established communication channel consists of two of these pairs: (local-host, local-port), (remote-host, remote-port).

Socket(H, P : parameters) : MonStgElt, RngIntElt -> IOSocket
    LocalHost: MonStgElt                Default: none
    LocalPort: RngIntElt                Default: 0
Attempts to create a (client) socket connected to port P of host H. Note: these are the remote values; usually it does not matter which local values are used for client sockets, but for those rare occasions where it does they may be specified using the parameters LocalHost and LocalPort. If these parameters are not set then suitable values will be chosen by the operating system. Also note that port numbers below 1 024 are usually reserved for system use, and may require special privileges to be used as the local port number.
Socket( : parameters) : -> IOSocket
    LocalHost: MonStgElt                Default: none
    LocalPort: RngIntElt                Default: 0
Attempts to create a server socket on the current machine, that can be used to accept connections. The parameters LocalHost and LocalPort may be used to specify which network interface and port the socket will accept connections on; if either of these are not set then their values will be determined by the operating system. Note that port numbers below 1 024 are usually reserved for system use, and may require special privileges to be used as the local port number.
WaitForConnection(S) : IO -> IO
This may only be used on server sockets. It waits for a connection attempt to be made, and then creates a new socket to handle the resulting communication channel. Thus S may continue to be used to accept connection attempts, while the new socket is used for communication with whatever entity just connected. Note: this new socket is not a server socket.

Other Socket Operations

SocketInformation(S) : IO -> Tup, Tup
This routine returns the identifying information for the socket as a pair of tuples. Each tuple is a {<host, port>} pair --- the first tuple gives the local information and the second gives the remote information. Note that this second tuple will be undefined for server sockets.
IsServerSocket(S) : IO -> BoolElt
Returns whether S is a server socket or not.

Example IO_Sockets (H3E13)

Here is a trivial use of sockets to send a message from one Magma process to another running on the same machine. The first Magma process sets up a server socket and waits for another Magma to contact it.
> // First Magma process
> server := Socket(: LocalHost := "localhost");
> SocketInformation(server);
<localhost, 32794>
> S1 := WaitForConnection(server);
The second Magma process establishes a client socket connection to the first, writes a greeting message to it, and closes the socket.
> // Second Magma process
> S2 := Socket("localhost", 32794);
> SocketInformation(S2);
<localhost, 32795> <localhost, 32794>
> Write(S2, "Hello, other world!");
> delete S2;
The first Magma process is now able to continue; it reads and displays all data sent to it until the socket is closed.
> // First Magma process
> SocketInformation(S1);
<localhost, 32794> <localhost, 32795>
> repeat
>     msg := Read(S1);
>     msg;
> until IsEof(msg);
Hello, other world!
EOF

A Note on Socket Security

A server socket cannot tell which user is connecting to it. (Indeed, the very concept is not well-defined if the connection is made from another machine.) Thus it is possible for someone on another machine on the same network to connect to "your" socket, whether by accident or otherwise, and this cannot be directly distinguished from an intended connection from a process under your control. Problems may then arise if the data from this unexpected connection does not meet whatever requirements your code has.

If you are reasonably sure that no-one else with access to your machine or its network will act in such a way, then you can simply use the socket functions as described above. Otherwise, some kind of authentication process would be desirable. A simple method is to require that client connections first send some data known only to the server, and have the server reject connections which do not do so.

A more robust and convenient version of authentication is being worked upon, and will appear in a later patch release.

V2.28, 13 July 2023