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.
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
@process_name is a name of process that will receive messages from Ruby @ruby_process_name 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!
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 computerNODE_NAME
name of node written in rubyDST_NODE_NAME
name of node that we will connect toDST_NODE
full name of node that we connect toDST_PROC
name of process that will receive our messagesWe’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
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!
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.