Technology and Software, Tips

Named pipes, ports and Erlang code in an Elixir project

I needed to read from a named pipe in an Elixir program, I made a mistake and I learned a few thing. Follow me.

First create the named pipe:

$ mkfifo "pipe"

In Erlang you read from it like this

$ erl
1> Fifo = open_port("pipe", [eof]).
2> receive
2>   {Fifo, {data, Data}} ->
2>     io:format("Got some data: ~p~n", [Data])
2>   end.

You can check that it works by executing echo hi > pipe from another shell.

So I expected to able to write this in Elixir

$ iex
 iex(1)> fifo = Port.open("pipe", [:eof])

However this is what I get

** (ArgumentError) argument error
:erlang.open_port({"pipe"}, [:eof])

If you can see my noob mistake in the call to Port.open don’t tell anybody yet, read on.

After trying many permutations of the arguments and read twice both http://erlang.org/doc/man/erlang.html#open_port-2 and http://elixir-lang.org/docs/stable/elixir/Port.html I gave up. I resolved to write it as an Erlang module and call it from my Elixir project. It was pretty easy:

1) Create an erlang directory in the main directory of the project. The name is not magical, you can name it as you wish.

2) Add this line into the project options of mix.exs

erlc_paths: ["erlang"],

That’s the directory you created.

3) Create an erlang/namedpipe.erl file with this code

-module(namedpipe).
-export([read/1]).

read(Pipe) ->
  Fifo = open_port(Pipe, [eof]),
  receive
    {Fifo, {data, Data}} ->
      Data
  end.

See how it can almost map 1 to 1 to Elixir. Variables are capitalized, symbols are lowercased and there are statement terminators (comma and full stop). All functions are private to the module except the explicitly exported ones.

4) Run mix and see that it compiles the Erlang file. Great!

But now

$ iex
iex(1)> :namedpipe.read("pipe")
** (ArgumentError) argument error
:erlang.open_port("pipe", [:eof])
erlang/namedpipe.erl:5: :namedpipe.read/1

Oh oh, what’s going on? I finally realized that it’s because of the wrong quote character! Single quotes are needed when passing strings to Erlang. Single quotes in Elixir are character lists, which is what Erlang needs. Double quotes are UTF-8 encoded binary data, which Erlang doesn’t understand.

So this works:

$ iex
iex(1)> :namedpipe.read('pipe')

Now go to another shell and run echo hi > pipe and confirm that it works for you too.

But wait, I did use double quotes in my first failed Elixir attempt. So did I do all of this for nothing? Embarrassingly, yes. This works:

$ iex
iex(1)> fifo = Port.open('pipe', [:eof])
iex(2)> receive do
...(2)>   {fifo, {:data, data}} ->
...(2)>     IO.puts("Got some data: #{data}")
...(2)> end

At least I learned how to embed Erlang code in an Elixir project and to care about single and double quotes.

Finally, you can embed that code in an Elixir module and call it both with a single quoted char list or a double quoted binary. Write this into lib/namedpipe.ex

defmodule NamedPipe do
  def read(pipe) when is_binary(pipe) do
    read(String.to_char_list(pipe))
  end

  def read(pipe) do
    fifo = Port.open(pipe, [:eof])
      receive do
        {fifo, {:data, data}} ->
          data
      end
  end
end

It uses guards to decide which version of the read function to call. Now

$ iex -S mix
iex(1)> NamedPipe.read("pipe")
'hi\n'
iex(2)> NamedPipe.read('pipe')
'hi\n'

Success!

Standard

Leave a comment