Michał Kalbarczyk

  • elixir
  • erlang
  • ruby
  • erlix

Elixir, Ruby, don’t fight. Talk… with Erlix

This article describes another way, how Elixir and Ruby can talk to each other. We will use Erlix this time. This method makes Ruby process act like the Erlang node, which is connected to Erlang VM over the network.

We will make some kind of chat between Ruby and Elixir. There will be two separate parts. Elixir and Ruby project.

Elixir Project

We need to create new Elixir project.

$ mix new chat_ex
$ cd chat_ex

And add mod to application:

defmodule ChatEx.Mixfile do
  use Mix.Project

  def project do
    [app: :chat_ex,
     version: "0.1.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  def application do
    [
      applications: [:logger],
      mod: {ChatEx, []}
    ]
  end

  defp deps do
    []
  end
end

We need a little bit configuration

@process_name :ex_rb
@ruby_process_name :ruby

@processname is_ a name of process that will receive messages from Ruby @rubyprocessname is the name of the process, that will store PID of the process that ruby send just after connecting to the node.

Ok, write a function that will be executed when out application start.

def start(_type, _args) do
  pid = spawn(&receive_messages/0)
  Process.register(pid, @process_name)
  read_line
end

When our application starts we need to spawn a process that will receive messages from ruby and register it with a name so ruby can locate it. Also, we need to start a loop that will read messages from the console and send it to Ruby.

We need two types of messages. One message will send us Ruby process PID, we will need to save it for future usage. And second one that will receive our messages.

def receive_messages do
  receive do
    # message with process pid
    {:register, pid} ->
      IO.puts "Ruby connected!"
      Agent.start_link(fn -> pid end, name: @ruby_process_name)
    # message with message
    {:message, message} ->
      IO.puts "Message from ruby: #{message}"
  end
 receive_messages
end

Nice, while register message will come, we start a new Agent that will store Ruby process PID. When we receive a message with text, we will just display it on the console.

What about reading from the console ? Right! The read_line function will do that.

def read_line do
  case IO.read(:stdio, :line) do
    :eof -> :ok
    {:error, reason} -> IO.puts "Error: #{reason}"
    data ->
      if Process.registered |> Enum.member?(@ruby_process_name) do
        ruby = Agent.get(@ruby_process_name, &(&1))
        send ruby, data
      end
  end
  read_line
end

When we will receive any line from STDIO, we will send it to our Ruby process. We can get pid of this process from Agent, and send our data.

We’re finished! Our module looks like this:

defmodule ChatEx do
  use Application

  @process_name :ex_rb
  @ruby_process_name :ruby

  def receive_messages do
    receive do
      {:register, pid} ->
        IO.puts "Ruby connected!"
        Agent.start_link(fn -> pid end, name: @ruby_process_name)
      {:message, message} ->
        IO.puts "Message from ruby: #{message}"
    end
    receive_messages
  end

  def read_line do
    case IO.read(:stdio, :line) do
      :eof -> :ok
      {:error, reason} -> IO.puts "Error: #{reason}"
      data ->
        if Process.registered |> Enum.member?(@ruby_process_name) do
          ruby = Agent.get(@ruby_process_name, &(&1))
          send ruby, data
        end
    end
    read_line
  end

  def start(_type, _args) do
    pid = spawn(&receive_messages/0)
    Process.register(pid, @process_name)
    read_line
  end
end

We’ve got all we need, let’s move to Ruby Project!

Ruby Project

Also in Ruby, we need to create a Ruby project.

$ mkdir chat_rb
$ cd chat_rb
$ bundle init

Add erlix gem to Gemfile:

# frozen_string_literal: true
source 'https://rubygems.org'
gem 'erlix'

and bundle it.

$ bundle

Now we’re creating the main file of our application. First, we need to require bundler to include all dependencies. So let’s create file named main.rb

#!/usr/bin/ruby
require 'bundler'
Bundler.require

Next, we will need a few constants:

COOKIE = 'cookie'
HOST = `hostname -s`.strip
NODE_NAME = 'ruby'
DST_NODE_NAME = 'elixir'
DST_NODE = "#{DST_NODE_NAME}@#{HOST}"
DST_PROC = 'ex_rb'

So, let’s describe them:

  • COOKIE is a name of Erlang cookie. Nodes to communicate to each other have to have the same name.
  • HOST host that we will connect to, in this case, is our computer
  • NODENAME_ name of node written in ruby
  • DSTNODENAME name of node that we will connect to
  • DSTNODE_ full name of node that we connect to
  • DSTPROC_ name of process that will receive our messages

We’ve got all information needed to connect.

Erlix::Node.init(NODE_NAME, COOKIE)
connection = Erlix::Connection.new(DST_NODE)

Fist line will initialize our node, next one will connect to Erlang node. Just after connection we need to send registration message, so Elixir will know that we’re connected, and save out PID.

connection.esend(
 DST_PROC,
 Erlix::Tuple.new([
   Erlix::Atom.new('register'),
   Erlix::Pid.new(connection)
 ])
)

We’re registered, next, we need a thread that will receive messages from Elixir, and print them on the console.

Thread.new do
 while true do
   message = connection.erecv
   puts 'Message from elixir: #{message.message.data}'
 end
end

OK, the missing part is a loop that reads data from the console and sends them to Elixir.

while true
  input = STDIN.gets
  connection.esend(
    DST_PROC,
    Erlix::Tuple.new([
      Erlix::Atom.new("message"),
      Erlix::Atom.new(input)
    ])
  )
end

And we’re done! Our final script will look like this:

#!/usr/bin/ruby

require 'bundler'
Bundler.require

COOKIE = 'cookie'
HOST = `hostname -s`.strip
NODE_NAME = 'ruby'
DST_NODE_NAME = 'elixir'
DST_NODE = "#{DST_NODE_NAME}@#{HOST}"
DST_PROC = 'ex_rb'

Erlix::Node.init(NODE_NAME, COOKIE)

connection = Erlix::Connection.new(DST_NODE)

puts "Connected to #{DST_NODE}"

connection.esend(
  DST_PROC,
  Erlix::Tuple.new([
    Erlix::Atom.new("register"),
    Erlix::Pid.new(connection)
  ])
)

Thread.new do
  while true do
    message = connection.erecv
    puts "Message from elixir: #{message.message.data}"
  end
end

while true
  input = STDIN.gets
  connection.esend(
    DST_PROC,
    Erlix::Tuple.new([
      Erlix::Atom.new("message"),
      Erlix::Atom.new(input)
    ])
  )
end

Connecting…

We need to start our Elixir application with parameters that we used in Ruby project.

$ cd chat_ex
$ elixir --sname elixir --cookie cookie  -S mix run

Compiling… Done! Elixir node run, now Ruby.

$ cd chat_rb
$ ruby main.rb

Great! Connected to …@… nice, take a look at Elixir console… Ruby connected Wow it works. Let’s send some messages. On Elixir console just type, for example, hello from elixir end hit enter! What is on Ruby console ? Our message! Message from elixir: hello from elixir! Now from Ruby. Type on ruby console hello from ruby again hit enter. What is on elixir console ? Right: Message from ruby: hello from ruby! We’re connected. Another great success!

Unstable!

After some benchmarks, I figure out that erlix is very unstable. Erlix crashes after about 1500 messages. Unfortunately, memory management is broken, there is a lot of TODOin the source code.