I am trying to mix Elixir with Python using ErlPort, so I decided to read some tutorials about it and the documentation related about everything involved. I understand how works the logic and what does each function. However, I am having problems casting a message and receiving the Python response.
Based on what I read and what I have done I understand that when I cast a message with cast_count/1
, this is handle by handle_cast/2
and then is handle by the Python function handle_message()
and then this one cast the message with the function cast_message()
and the imported one cast()
from erlport.erlang
. Finally, Elixir should handle the message received from Python with handle_info/2
. I think this function is not being executed but I don't know the reason, although I have investigated a lot this stuff in different sources and in the documentation of GenServer and ErlPort.
In my case I have the next structure: lib/python_helper.ex
to make ErlPort works and lib/server.ex
to call and cast the Python functions.
lib/python_helper.ex
defmodule WikiElixirTest.PythonHelper do
def start_instance do
path =
[:code.priv_dir(:wiki_elixir_test), "python"]
|> Path.join()
|> to_charlist()
{:ok, pid} = :python.start([{:python_path, path}])
pid
end
def call(pid, module, function, arguments \\ []) do
pid
|> :python.call(module, function, arguments)
end
def cast(pid, message) do
pid
|> :python.cast(message)
end
def stop_instance(pid) do
pid
|> :python.stop()
end
end
lib/server.ex
defmodule WikiElixirTest.Server do
use GenServer
alias WikiElixirTest.PythonHelper
def start_link() do
GenServer.start_link(__MODULE__, [])
end
def init(_args) do
session = PythonHelper.start_instance()
PythonHelper.call(session, :counter, :register_handler, [self()])
{:ok, session}
end
def cast_count(count) do
{:ok, pid} = start_link()
GenServer.cast(pid, {:count, count})
end
def call_count(count) do
{:ok, pid} = start_link()
GenServer.call(pid, {:count, count}, :infinity)
end
def handle_call({:count, count}, _from, session) do
result = PythonHelper.call(session, :counter, :counter, [count])
{:reply, result, session}
end
def handle_cast({:count, count}, session) do
PythonHelper.cast(session, count)
{:noreply, session}
end
def handle_info({:python, message}, session) do
IO.puts("Received message from Python: #{inspect(message)}")
{:stop, :normal, session}
end
def terminate(_reason, session) do
PythonHelper.stop_instance(session)
:ok
end
end
priv/python/counter.py
import time
import sys
from erlport.erlang import set_message_handler, cast
from erlport.erlterms import Atom
message_handler = None
def cast_message(pid, message):
cast(pid, (Atom('python', message)))
def register_handler(pid):
global message_handler
message_handler = pid
def handle_message(count):
try:
print('Received message from Elixir')
print(f'Count: {count}')
result = counter(count)
if message_handler:
cast_message(message_handler, result)
except Exception as e:
print(e)
pass
def counter(count=100):
i = 0
data = []
while i < count:
time.sleep(1)
data.append(i+1)
i = i + 1
return data
set_message_handler(handle_message)
Note: I removed @doc
to light the code snippets. And yes, I know sys
isn't being used at this moment and that catch Exception
in Python try
block is not the best approach, it is just temporal.
If I test it in iex
(iex -S mix
), I get the next:
iex(1)> WikiElixirTest.Server.cast_count(19)
Received message from Elixir
Count: 19
:ok
I want to note that call_count/1
and handle_call/1
works fine:
iex(3)> WikiElixirTest.Server.call_count(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
What I am doing wrong that the communication of Elixir with Python is successful but not the communication of Python with Elixir when I cast a message?
Well, @Everett reported in the question that I closed wrong the atom in counter.py
.
def cast_message(pid, message):
cast(pid, (Atom('python', message)))
This must be:
def cast_message(pid, message):
cast(pid, (Atom('python'), message))
Although this doesn't solve the problem, it helps me to get with the solution. After fixed the first part ((Atom('python'), message)
), when I execute cast_count/1
I get a message from Python:
iex(1)> WikiElixirTest.Server.cast_count(2)
Received message from Elixir
Count: 2
:ok
bytes object expected
Although it confuses me a bit at the first time because I awaited something like "Received message from Python: bytes object expected", due handle_info/2
. However, I decided to inspect the code of ErlPort and I found the error in the line 66 of erlterms.py
, part of the Atom
class. Thus the error was in the first part of the message, the atom, so I specified it as binary:
def cast_message(pid, message):
cast(pid, (Atom(b'python', message)))
Then I checked it:
iex(2)> WikiElixirTest.Server.cast_count(2)
:ok
Received message from Elixir
Count: 2
Received message from Python: [[1, 2]]
And it works nicely! So the problem there, even taking into account my mistyping of the message tuple, was that Atom()
needs a binary.
Probably this misunderstanding may be due the first ErlPort tutorial I followed uses Python 2, version in which str
is a string of bytes and in Python 3 it would be necessary to convert the string to bytes, due str
is a string of text.