From c617aa1d93595ff04e052985ed3ed2787c468ba9 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Wed, 18 Mar 2026 23:37:18 +0100 Subject: [PATCH] Add erlang.whereis() for Python to lookup registered Erlang PIDs Implement whereis lookup that allows Python code to find Erlang processes registered with erlang:register/2. Implementation: - Add erlang.whereis() Python function that accepts str, bytes, or Atom - Use Erlang callback mechanism (_whereis) instead of direct enif_whereis_pid to avoid crashes with certain OTP configurations - Register _whereis callback in py_callback:register_callbacks/0 at startup Usage: pid = erlang.whereis("my_server") # Returns Pid or None pid = erlang.whereis(erlang.atom("my_server")) # Also works with atoms Tests: - test_whereis_registered_process: lookup existing registered process - test_whereis_nonexistent: lookup non-registered name returns None - test_whereis_with_atom: lookup using erlang.Atom type - test_whereis_with_bytes: lookup using bytes - test_whereis_invalid_type: invalid argument raises TypeError --- c_src/py_callback.c | 66 ++++++++++++++++++++++++++++++++++ src/erlang_python_sup.erl | 3 +- src/py_callback.erl | 22 +++++++++++- test/py_pid_send_SUITE.erl | 74 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 161 insertions(+), 4 deletions(-) diff --git a/c_src/py_callback.c b/c_src/py_callback.c index 783bf38..b818348 100644 --- a/c_src/py_callback.c +++ b/c_src/py_callback.c @@ -3287,6 +3287,68 @@ static PyObject *erlang_byte_channel_cancel_wait_impl(PyObject *self, PyObject * return erlang_channel_cancel_wait_impl(self, args); } +/** + * @brief Look up a registered Erlang process by name. + * + * Usage: erlang.whereis(name) + * @param name: str, bytes, or erlang.Atom - the registered name + * @return erlang.Pid if found, None if not registered + * + * This is implemented by calling the '_whereis' Erlang callback which wraps + * erlang:whereis/1. This approach is used because calling enif_whereis_pid + * directly from Python threads can cause crashes in some OTP configurations. + */ +static PyObject *erlang_whereis_impl(PyObject *self, PyObject *args) { + (void)self; + PyObject *name_obj; + + if (!PyArg_ParseTuple(args, "O", &name_obj)) { + return NULL; + } + + /* Convert name to atom object if needed */ + PyObject *atom_obj = NULL; + if (PyUnicode_Check(name_obj) || PyBytes_Check(name_obj)) { + /* Create atom from string */ + PyObject *atom_args = PyTuple_Pack(1, name_obj); + if (atom_args == NULL) { + return NULL; + } + atom_obj = erlang_atom_impl(NULL, atom_args); + Py_DECREF(atom_args); + if (atom_obj == NULL) { + return NULL; + } + } else if (Py_IS_TYPE(name_obj, &ErlangAtomType)) { + atom_obj = name_obj; + Py_INCREF(atom_obj); + } else { + PyErr_SetString(PyExc_TypeError, + "whereis() argument must be str, bytes, or erlang.Atom"); + return NULL; + } + + /* Build args tuple for erlang.call('_whereis', atom) */ + PyObject *call_name = PyUnicode_FromString("_whereis"); + if (call_name == NULL) { + Py_DECREF(atom_obj); + return NULL; + } + + PyObject *call_args = PyTuple_Pack(2, call_name, atom_obj); + Py_DECREF(call_name); + Py_DECREF(atom_obj); + if (call_args == NULL) { + return NULL; + } + + /* Call through the existing erlang.call mechanism */ + PyObject *result = erlang_call_impl(NULL, call_args); + Py_DECREF(call_args); + + return result; +} + /* Python method definitions for erlang module */ static PyMethodDef ErlangModuleMethods[] = { {"call", erlang_call_impl, METH_VARARGS, @@ -3302,6 +3364,10 @@ static PyMethodDef ErlangModuleMethods[] = { "Send a message to an Erlang process (fire-and-forget).\n\n" "Usage: erlang.send(pid, term)\n" "The pid must be an erlang.Pid object."}, + {"whereis", erlang_whereis_impl, METH_VARARGS, + "Look up a registered Erlang process by name.\n\n" + "Usage: erlang.whereis(name)\n" + "Returns: erlang.Pid if registered, None otherwise."}, {"schedule", py_schedule, METH_VARARGS, "Schedule Erlang callback continuation (must be returned from handler).\n\n" "Usage: return erlang.schedule('callback_name', arg1, arg2, ...)\n" diff --git a/src/erlang_python_sup.erl b/src/erlang_python_sup.erl index 6912e37..c0da692 100644 --- a/src/erlang_python_sup.erl +++ b/src/erlang_python_sup.erl @@ -54,7 +54,8 @@ init([]) -> ok = py_state:init_tab(), %% Register ALL system callbacks early, before any gen_server starts. - %% This ensures callbacks like _py_sleep are available immediately. + %% This ensures callbacks like _py_sleep and _whereis are available immediately. + ok = py_callback:register_callbacks(), ok = py_state:register_callbacks(), ok = py_event_loop:register_callbacks(), ok = py_channel:register_callbacks(), diff --git a/src/py_callback.erl b/src/py_callback.erl index 7474eeb..0eed9ad 100644 --- a/src/py_callback.erl +++ b/src/py_callback.erl @@ -28,7 +28,8 @@ register/2, unregister/1, lookup/1, - execute/2 + execute/2, + register_callbacks/0 ]). %% gen_server callbacks @@ -134,6 +135,25 @@ handle_info(_Info, State) -> terminate(_Reason, _State) -> ok. +%%% ============================================================================ +%%% System Callbacks Registration +%%% ============================================================================ + +%% @doc Register system callbacks used internally by the erlang Python module. +%% This includes _whereis for process lookup. +-spec register_callbacks() -> ok. +register_callbacks() -> + ?MODULE:register('_whereis', fun whereis_callback/1), + ok. + +%% @doc Callback implementation for erlang.whereis(). +%% Wraps erlang:whereis/1 to return the PID or undefined. +-spec whereis_callback([atom()]) -> pid() | undefined. +whereis_callback([Name]) when is_atom(Name) -> + erlang:whereis(Name); +whereis_callback(_) -> + undefined. + %%% ============================================================================ %%% Internal Functions %%% ============================================================================ diff --git a/test/py_pid_send_SUITE.erl b/test/py_pid_send_SUITE.erl index 99f7abc..69955e4 100644 --- a/test/py_pid_send_SUITE.erl +++ b/test/py_pid_send_SUITE.erl @@ -38,7 +38,13 @@ test_send_from_coroutine/1, test_send_multiple_from_coroutine/1, test_send_is_nonblocking/1, - test_send_interleaved_with_async/1 + test_send_interleaved_with_async/1, + %% whereis tests + test_whereis_registered_process/1, + test_whereis_nonexistent/1, + test_whereis_with_atom/1, + test_whereis_with_bytes/1, + test_whereis_invalid_type/1 ]). all() -> @@ -63,7 +69,13 @@ all() -> test_send_from_coroutine, test_send_multiple_from_coroutine, test_send_is_nonblocking, - test_send_interleaved_with_async + test_send_interleaved_with_async, + %% whereis tests + test_whereis_registered_process, + test_whereis_nonexistent, + test_whereis_with_atom, + test_whereis_with_bytes, + test_whereis_invalid_type ]. init_per_suite(Config) -> @@ -276,6 +288,64 @@ test_send_interleaved_with_async(_Config) -> receive <<"interleaved_2">> -> ok after 5000 -> ct:fail(timeout_interleaved_2) end, receive <<"interleaved_3">> -> ok after 5000 -> ct:fail(timeout_interleaved_3) end. +%%% ============================================================================ +%%% erlang.whereis() Tests +%%% ============================================================================ + +%% @doc Test erlang.whereis() finds a registered process. +test_whereis_registered_process(_Config) -> + %% Register this process + Self = self(), + erlang:register(py_whereis_test_proc, Self), + try + %% Look it up from Python + {ok, Pid} = py:eval(<<"erlang.whereis('py_whereis_test_proc')">>), + %% Verify we got our PID back + true = is_pid(Pid), + Self = Pid + after + erlang:unregister(py_whereis_test_proc) + end, + ok. + +%% @doc Test erlang.whereis() returns None for nonexistent process. +test_whereis_nonexistent(_Config) -> + {ok, none} = py:eval(<<"erlang.whereis('nonexistent_process_12345')">>), + ok. + +%% @doc Test erlang.whereis() with erlang.Atom type. +%% Note: Use erlang._atom() since erlang.atom() is a Python wrapper not +%% available in py:eval context. +test_whereis_with_atom(_Config) -> + Self = self(), + erlang:register(py_whereis_atom_test, Self), + try + {ok, Pid} = py:eval(<<"erlang.whereis(erlang._atom('py_whereis_atom_test'))">>), + true = is_pid(Pid), + Self = Pid + after + erlang:unregister(py_whereis_atom_test) + end, + ok. + +%% @doc Test erlang.whereis() with bytes argument. +test_whereis_with_bytes(_Config) -> + Self = self(), + erlang:register(py_whereis_bytes_test, Self), + try + {ok, Pid} = py:eval(<<"erlang.whereis(b'py_whereis_bytes_test')">>), + true = is_pid(Pid), + Self = Pid + after + erlang:unregister(py_whereis_bytes_test) + end, + ok. + +%% @doc Test erlang.whereis() with invalid type raises TypeError. +test_whereis_invalid_type(_Config) -> + {error, {'TypeError', _}} = py:eval(<<"erlang.whereis(123)">>), + ok. + %%% ============================================================================ %%% Helper Functions %%% ============================================================================