Erlang-like port system

I'm currently working on a Erlang-like port system. In Erlang a port is a forked os-process usually written in C which communicates with the Erlang-VM via Stdin/Stdout using a simple binary protocol and serialized datastructures in Erlang Binary Term Format.
http://erlang.org/doc/apps/erts/erl_ext_dist.html

This provides a sandbox-like environment for FFI/RPC.

By default the port program is reading from STDIN and writing to STDOUT. To avoid interference in the port communication and allow writing debug output, it is possible to use filedescriptors 3 and 4 instead of 0 and 1 for communication (option: nouse_stdio).
http://erlang.org/doc/man/erlang.html#open_port-2

I have a prototype running which communicates via Stdin/Stdout but I'd like to switch to FD 3 and 4. How can I open these filedescriptors on the rust side?

I tried something like that, but I get a SIGSEGV when using it:

        let ifd = unsafe {
            let no = libc::fileno(libc::fdopen(3, CString::new("r").unwrap().as_ptr()));
            Stdio::from_raw_fd(no)};
        let ofd = unsafe {
            let no = libc::fileno(libc::fdopen(4, CString::new("a").unwrap().as_ptr()));
            Stdio::from_raw_fd(no)};

        let cmd = Command::new("my_port")
           .stdin(ifd)
           .stdout(ofd);

From man fdopen:

The fdopen() function associates a stream with the existing file descriptor, fd.

fdopen doesn't create a file descriptor for you.
I think you want to use pipe2.

Wouldn't this do what you want ?

let cmd = Command::new("my_port")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .expect("my_port failed");

I used Stdio::piped() when using STDIN and STDOUT and it works. But I want to avoid using these because the binary data communication intereferes with e.g. printf debug output. So I need another channel for this.

Perhaps I must ask Erlang-People how they open FD 3 and 4 for communication.

On the C (actually C++) side I do it with boost::iostreams like this:

  boost::iostreams::file_descriptor_source ifd(3,  boost::iostreams::never_close_handle);
  boost::iostreams::stream<boost::iostreams::file_descriptor_source> is(ifd);

Well that's what's going to happen anyway if you use .stdin() and .stdout() on Command. These methods replace the STDIN and STDOUT in the command executed with whatever Stdio you provided. You can see it done in do_exec() in libstd/sys/unix/process.rs:

        if let Some(fd) = stdio.stdin.fd() {
            t!(cvt_r(|| libc::dup2(fd, libc::STDIN_FILENO)));
        }
        if let Some(fd) = stdio.stdout.fd() {
            t!(cvt_r(|| libc::dup2(fd, libc::STDOUT_FILENO)));
        }
        if let Some(fd) = stdio.stderr.fd() {
            t!(cvt_r(|| libc::dup2(fd, libc::STDERR_FILENO)));
        }

So really it doesn't matter wether you use Stdio::pipep() or you open an fd yourself and feed it to Stdio::from_raw_fd(). The end result is you're overwriting the STDIN and STDOUT of the forked process. (By default, if you use spawn(), the forked process would inherit the same STDIN|OUT|ERR as the current process.)

If you wish to use additional fds for communication, use pipe2 to create the fds of a unidirectional pipe. Do it twice if you want two pipes for bidirectional communication between the processes. Then you fork the process. In the parent, close the write fd of the first pipe and the read fd of the second. In the child do the reverse and then use dup2 on your two open fds to duplicate them as 3 and 4 and then close the original ones. You may now call exec. Since 3 and 4 are not marked as FD_CLOEXEC, they should still be open and usable for communication in the new program running.

To avoid calling fork and exec manually, you probably want to use the before_exec() method from CommandExt to close and dup the fds in a closure.

You'll most likely want to set the FD_CLOEXEC flag in your pipe2 calls. This way you still need to close two fds in the parent process but none in the child, they'll be cleaned up automatically (without affecting the fds 3 and 4 created with dup2).

Well that's the procedure for linux 2.6.27+ anyway. If you want to support older kernels, you'll have to use pipe and not pipe2 (see libstd/sys/unix/pipe.rs).

For windows I don't know how to do that. You could check out pipe.rs and process.rs in libstd/sys/windows to get an idea of how Command does it.

1 Like

Thank you very much for your detailed answer. I'll try my luck and perhaps someday a new crate will appear in crates.io :wink:

1 Like