Skip to content

Commit 4b44e53

Browse files
authored
Merge pull request #39 from benoitc/feature/whereis-lookup
Add erlang.whereis() for Python to lookup registered Erlang PIDs
2 parents 2576501 + c617aa1 commit 4b44e53

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)