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
= spawn(&receive_messages/0)
pid Process.register(pid, @process_name)
read_lineend
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_messagesend
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
= Agent.get(@ruby_process_name, &(&1))
ruby
send ruby, dataend
end
read_lineend
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_messagesend
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
= Agent.get(@ruby_process_name, &(&1))
ruby
send ruby, dataend
end
read_lineend
def start(_type, _args) do
= spawn(&receive_messages/0)
pid Process.register(pid, @process_name)
read_lineend
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
'https://rubygems.org'
source 'erlix' gem
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
'bundler'
require 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)
Erlix::Connection.new(DST_NODE) connection =
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
true do
while
message = connection.erecv'Message from elixir: #{message.message.data}'
puts end
end
OK, the missing part is a loop that reads data from the console and sends them to Elixir.
while true
STDIN.gets
input =
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
'bundler'
require 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)
Erlix::Connection.new(DST_NODE)
connection =
"Connected to #{DST_NODE}"
puts
connection.esend(DST_PROC,
Erlix::Tuple.new([
Erlix::Atom.new("register"),
Erlix::Pid.new(connection)
])
)
Thread.new do
true do
while
message = connection.erecv"Message from elixir: #{message.message.data}"
puts end
end
while true
STDIN.gets
input =
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.