Primer on Ports

- 7 mins

Below is a writeup from the April 2017 Houston Elixir Meetup about using ports to talk to the world outside of the BEAM. Examples and slides can be found here.

Primer on Ports

Why ports?

Ports are the safest way to communicate to external programs outside of the BEAM that are local to the machine.

It can be a large time saver to re-use a solution implemented in another language instead of writing a solution from scratch in Elixir. Or possibly, Elixir isn’t always the best fit for a certain use case, but may be useful for coordination, fault-tolerance, and glue between multiple programs.

A port is owned by some process and can be communicated to via message passing, very similarly to any other process. A port will communicate to an external process on your operating system through STDIN and STDOUT (by default). Because of this, it’s best to favor passing large work loads to the external program and return information instead of a chatty implementation with smaller computations.

Ports are OTP compliant, so if you have a port open to a network socket or another application and you tear down part of your supervision structure, the port will be closed gracefully (on the Erlang side, more about that below).

Usage

Elixir has a Port module, which is merely a thin wrapper around Erlang’s port BIFs.

To open a port, simply use the command Port.open/2:

# Extremely quick demo
iex(11)> port = Port.open({:spawn, "whoami"}, [:binary])
#Port<0.1267>
iex(12)> flush()
{#Port<0.1267>, {:data, "geoff\n"}}
:ok
iex(13)> Port.info port
nil

# Open a port
iex(14)> cat_port = Port.open({:spawn, "cat"}, [:binary])
#Port<0.1268>

# Sending messages
iex(15)> send cat_port, {self(), {:command, "Hello from port!"}}
{#PID<0.80.0>, {:command, "Hello from port!"}}
iex(16)> send cat_port, {self(), {:command, "Goodbye from port!"}}
{#PID<0.80.0>, {:command, "Goodbye from port!"}}

# What's in our mailbox? Why it's messages from our port!
iex(17)> flush()
{#Port<0.1268>, {:data, "Hello from port!"}}
{#Port<0.1268>, {:data, "Goodbye from port!"}}
:ok

# Info on an open port
iex(18)> Port.info cat_port
[name: 'cat', links: [#PID<0.80.0>], id: 10144, connected: #PID<0.80.0>,
 input: 34, output: 34, os_pid: 2064]
iex(19)> send cat_port, {self(), :close}

# Close the port
{#PID<0.80.0>, :close}

# Confirm what we got in the mailbox
iex(20)> flush()
{#Port<0.1268>, :closed}
:ok

# Calling Port.info/1 on a closed port returns nil
iex(21)> Port.info cat_port
nil

Other functions

Aside on :spawn_driver and :fd:

The :spawn_driver and :fd options are for Deep Wizardry, only to be used with extreme caution and for Very Good Reasons.

Implementation

It’s generally a good practice to wrap port operations in a GenServer. When doing so, there are a few things to add to the GenServer:

def init(_) do
    port = Port.open({:spawn, "cmd args"}, [:binary])
    ## do some other stuff if necessary
    {:ok, {initial_state, port}}
  end

I like to include this in the GenServer’s init function if you’re going to have a relationship between your process and external program. This way, processes can not send your GenServer any messages until the port is up and running.

You would probably want to carry this port identifier with the GenServer’s State, mostly so you can distinguish between ports and ensure youre GenServer doesn’t act on similar looking messages from elsewhere.

  def handle_info({port, {:data, payload}}, {_old_state, port}) do
    ## do some work here
    {:noreply, {new_state, port}}
  end

You can view the the /port_demo directory for a quick implementation of a port wrapped in a GenServer. The key stuff here is included in top_server.ex. While this is a contrived example, it should give the basics on how this work on the Elixir/Erlang side.

This is a quick example of sending/receiving messages on the elixir side, and does not include full error handling.

Note:

This is a contrived example of wrapping a Unix OS program in a port. If you are simply getting information from a program and reporting back its return result, please use System.cmd/3 instead. You will save yourself a lot of time. This function also uses ports to fetch information, and it’s worthwhile to look at the implementation.

Best practices, tips, and thoughts

#!/bin/sh
"$@"
pid=$!
while read line ; do
  :
done
kill -KILL $pid

References / Further Reading

Geoff Smith

Geoff Smith

Software developer with fine arts background

rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium vimeo