Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions c_src/py_callback.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion src/erlang_python_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
22 changes: 21 additions & 1 deletion src/py_callback.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
register/2,
unregister/1,
lookup/1,
execute/2
execute/2,
register_callbacks/0
]).

%% gen_server callbacks
Expand Down Expand Up @@ -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
%%% ============================================================================
Expand Down
74 changes: 72 additions & 2 deletions test/py_pid_send_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() ->
Expand 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) ->
Expand Down Expand Up @@ -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
%%% ============================================================================
Expand Down
Loading