Skip to content

Commit c617aa1

Browse files
committed
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
1 parent 2576501 commit c617aa1

4 files changed

Lines changed: 161 additions & 4 deletions

File tree

c_src/py_callback.c

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3287,6 +3287,68 @@ static PyObject *erlang_byte_channel_cancel_wait_impl(PyObject *self, PyObject *
32873287
return erlang_channel_cancel_wait_impl(self, args);
32883288
}
32893289

3290+
/**
3291+
* @brief Look up a registered Erlang process by name.
3292+
*
3293+
* Usage: erlang.whereis(name)
3294+
* @param name: str, bytes, or erlang.Atom - the registered name
3295+
* @return erlang.Pid if found, None if not registered
3296+
*
3297+
* This is implemented by calling the '_whereis' Erlang callback which wraps
3298+
* erlang:whereis/1. This approach is used because calling enif_whereis_pid
3299+
* directly from Python threads can cause crashes in some OTP configurations.
3300+
*/
3301+
static PyObject *erlang_whereis_impl(PyObject *self, PyObject *args) {
3302+
(void)self;
3303+
PyObject *name_obj;
3304+
3305+
if (!PyArg_ParseTuple(args, "O", &name_obj)) {
3306+
return NULL;
3307+
}
3308+
3309+
/* Convert name to atom object if needed */
3310+
PyObject *atom_obj = NULL;
3311+
if (PyUnicode_Check(name_obj) || PyBytes_Check(name_obj)) {
3312+
/* Create atom from string */
3313+
PyObject *atom_args = PyTuple_Pack(1, name_obj);
3314+
if (atom_args == NULL) {
3315+
return NULL;
3316+
}
3317+
atom_obj = erlang_atom_impl(NULL, atom_args);
3318+
Py_DECREF(atom_args);
3319+
if (atom_obj == NULL) {
3320+
return NULL;
3321+
}
3322+
} else if (Py_IS_TYPE(name_obj, &ErlangAtomType)) {
3323+
atom_obj = name_obj;
3324+
Py_INCREF(atom_obj);
3325+
} else {
3326+
PyErr_SetString(PyExc_TypeError,
3327+
"whereis() argument must be str, bytes, or erlang.Atom");
3328+
return NULL;
3329+
}
3330+
3331+
/* Build args tuple for erlang.call('_whereis', atom) */
3332+
PyObject *call_name = PyUnicode_FromString("_whereis");
3333+
if (call_name == NULL) {
3334+
Py_DECREF(atom_obj);
3335+
return NULL;
3336+
}
3337+
3338+
PyObject *call_args = PyTuple_Pack(2, call_name, atom_obj);
3339+
Py_DECREF(call_name);
3340+
Py_DECREF(atom_obj);
3341+
if (call_args == NULL) {
3342+
return NULL;
3343+
}
3344+
3345+
/* Call through the existing erlang.call mechanism */
3346+
PyObject *result = erlang_call_impl(NULL, call_args);
3347+
Py_DECREF(call_args);
3348+
3349+
return result;
3350+
}
3351+
32903352
/* Python method definitions for erlang module */
32913353
static PyMethodDef ErlangModuleMethods[] = {
32923354
{"call", erlang_call_impl, METH_VARARGS,
@@ -3302,6 +3364,10 @@ static PyMethodDef ErlangModuleMethods[] = {
33023364
"Send a message to an Erlang process (fire-and-forget).\n\n"
33033365
"Usage: erlang.send(pid, term)\n"
33043366
"The pid must be an erlang.Pid object."},
3367+
{"whereis", erlang_whereis_impl, METH_VARARGS,
3368+
"Look up a registered Erlang process by name.\n\n"
3369+
"Usage: erlang.whereis(name)\n"
3370+
"Returns: erlang.Pid if registered, None otherwise."},
33053371
{"schedule", py_schedule, METH_VARARGS,
33063372
"Schedule Erlang callback continuation (must be returned from handler).\n\n"
33073373
"Usage: return erlang.schedule('callback_name', arg1, arg2, ...)\n"

src/erlang_python_sup.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ init([]) ->
5454
ok = py_state:init_tab(),
5555

5656
%% Register ALL system callbacks early, before any gen_server starts.
57-
%% This ensures callbacks like _py_sleep are available immediately.
57+
%% This ensures callbacks like _py_sleep and _whereis are available immediately.
58+
ok = py_callback:register_callbacks(),
5859
ok = py_state:register_callbacks(),
5960
ok = py_event_loop:register_callbacks(),
6061
ok = py_channel:register_callbacks(),

src/py_callback.erl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
register/2,
2929
unregister/1,
3030
lookup/1,
31-
execute/2
31+
execute/2,
32+
register_callbacks/0
3233
]).
3334

3435
%% gen_server callbacks
@@ -134,6 +135,25 @@ handle_info(_Info, State) ->
134135
terminate(_Reason, _State) ->
135136
ok.
136137

138+
%%% ============================================================================
139+
%%% System Callbacks Registration
140+
%%% ============================================================================
141+
142+
%% @doc Register system callbacks used internally by the erlang Python module.
143+
%% This includes _whereis for process lookup.
144+
-spec register_callbacks() -> ok.
145+
register_callbacks() ->
146+
?MODULE:register('_whereis', fun whereis_callback/1),
147+
ok.
148+
149+
%% @doc Callback implementation for erlang.whereis().
150+
%% Wraps erlang:whereis/1 to return the PID or undefined.
151+
-spec whereis_callback([atom()]) -> pid() | undefined.
152+
whereis_callback([Name]) when is_atom(Name) ->
153+
erlang:whereis(Name);
154+
whereis_callback(_) ->
155+
undefined.
156+
137157
%%% ============================================================================
138158
%%% Internal Functions
139159
%%% ============================================================================

test/py_pid_send_SUITE.erl

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@
3838
test_send_from_coroutine/1,
3939
test_send_multiple_from_coroutine/1,
4040
test_send_is_nonblocking/1,
41-
test_send_interleaved_with_async/1
41+
test_send_interleaved_with_async/1,
42+
%% whereis tests
43+
test_whereis_registered_process/1,
44+
test_whereis_nonexistent/1,
45+
test_whereis_with_atom/1,
46+
test_whereis_with_bytes/1,
47+
test_whereis_invalid_type/1
4248
]).
4349

4450
all() ->
@@ -63,7 +69,13 @@ all() ->
6369
test_send_from_coroutine,
6470
test_send_multiple_from_coroutine,
6571
test_send_is_nonblocking,
66-
test_send_interleaved_with_async
72+
test_send_interleaved_with_async,
73+
%% whereis tests
74+
test_whereis_registered_process,
75+
test_whereis_nonexistent,
76+
test_whereis_with_atom,
77+
test_whereis_with_bytes,
78+
test_whereis_invalid_type
6779
].
6880

6981
init_per_suite(Config) ->
@@ -276,6 +288,64 @@ test_send_interleaved_with_async(_Config) ->
276288
receive <<"interleaved_2">> -> ok after 5000 -> ct:fail(timeout_interleaved_2) end,
277289
receive <<"interleaved_3">> -> ok after 5000 -> ct:fail(timeout_interleaved_3) end.
278290

291+
%%% ============================================================================
292+
%%% erlang.whereis() Tests
293+
%%% ============================================================================
294+
295+
%% @doc Test erlang.whereis() finds a registered process.
296+
test_whereis_registered_process(_Config) ->
297+
%% Register this process
298+
Self = self(),
299+
erlang:register(py_whereis_test_proc, Self),
300+
try
301+
%% Look it up from Python
302+
{ok, Pid} = py:eval(<<"erlang.whereis('py_whereis_test_proc')">>),
303+
%% Verify we got our PID back
304+
true = is_pid(Pid),
305+
Self = Pid
306+
after
307+
erlang:unregister(py_whereis_test_proc)
308+
end,
309+
ok.
310+
311+
%% @doc Test erlang.whereis() returns None for nonexistent process.
312+
test_whereis_nonexistent(_Config) ->
313+
{ok, none} = py:eval(<<"erlang.whereis('nonexistent_process_12345')">>),
314+
ok.
315+
316+
%% @doc Test erlang.whereis() with erlang.Atom type.
317+
%% Note: Use erlang._atom() since erlang.atom() is a Python wrapper not
318+
%% available in py:eval context.
319+
test_whereis_with_atom(_Config) ->
320+
Self = self(),
321+
erlang:register(py_whereis_atom_test, Self),
322+
try
323+
{ok, Pid} = py:eval(<<"erlang.whereis(erlang._atom('py_whereis_atom_test'))">>),
324+
true = is_pid(Pid),
325+
Self = Pid
326+
after
327+
erlang:unregister(py_whereis_atom_test)
328+
end,
329+
ok.
330+
331+
%% @doc Test erlang.whereis() with bytes argument.
332+
test_whereis_with_bytes(_Config) ->
333+
Self = self(),
334+
erlang:register(py_whereis_bytes_test, Self),
335+
try
336+
{ok, Pid} = py:eval(<<"erlang.whereis(b'py_whereis_bytes_test')">>),
337+
true = is_pid(Pid),
338+
Self = Pid
339+
after
340+
erlang:unregister(py_whereis_bytes_test)
341+
end,
342+
ok.
343+
344+
%% @doc Test erlang.whereis() with invalid type raises TypeError.
345+
test_whereis_invalid_type(_Config) ->
346+
{error, {'TypeError', _}} = py:eval(<<"erlang.whereis(123)">>),
347+
ok.
348+
279349
%%% ============================================================================
280350
%%% Helper Functions
281351
%%% ============================================================================

0 commit comments

Comments
 (0)