1515extern " C" void pythonx_handle_io_write (const char *message,
1616 const char *eval_info_bytes, bool type);
1717
18+ extern " C" void pythonx_handle_send (const char *pid_bytes, const char *tag,
19+ pythonx::python::PyObjectPtr *py_object,
20+ const char *eval_info_bytes);
21+
1822namespace pythonx {
1923
2024using namespace python ;
@@ -385,36 +389,46 @@ import ctypes
385389import io
386390import sys
387391import inspect
392+ import types
393+ import sys
388394
389395pythonx_handle_io_write = ctypes.CFUNCTYPE(
390396 None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool
391397)(pythonx_handle_io_write_ptr)
392398
399+ pythonx_handle_send = ctypes.CFUNCTYPE(
400+ None, ctypes.c_char_p, ctypes.c_char_p, ctypes.py_object, ctypes.c_char_p
401+ )(pythonx_handle_send_ptr)
402+
403+
404+ def get_eval_info_bytes():
405+ # The evaluation caller has __pythonx_eval_info_bytes__ set in
406+ # their globals. It is not available in globals() here, because
407+ # the globals dict in function definitions is fixed at definition
408+ # time. To find the current evaluation globals, we look at the
409+ # call stack using the inspect module and find the caller with
410+ # __pythonx_eval_info_bytes__ in globals. We look specifically
411+ # for the outermost caller, because intermediate functions could
412+ # be defined by previous evaluations, in which case they would
413+ # have __pythonx_eval_info_bytes__ in their globals, corresponding
414+ # to that previous evaluation. When called within a thread, the
415+ # evaluation caller is not in the stack, so __pythonx_eval_info_bytes__
416+ # will be found in the thread entrypoint function globals.
417+ call_stack = inspect.stack()
418+ eval_info_bytes = next(
419+ frame_info.frame.f_globals["__pythonx_eval_info_bytes__"]
420+ for frame_info in reversed(call_stack)
421+ if "__pythonx_eval_info_bytes__" in frame_info.frame.f_globals
422+ )
423+ return eval_info_bytes
424+
393425
394426class Stdout(io.TextIOBase):
395427 def __init__(self, type):
396428 self.type = type
397429
398430 def write(self, string):
399- # The evaluation caller has __pythonx_eval_info_bytes__ set in
400- # their globals. It is not available in globals() here, because
401- # the globals dict in function definitions is fixed at definition
402- # time. To find the current evaluation globals, we look at the
403- # call stack using the inspect module and find the caller with
404- # __pythonx_eval_info_bytes__ in globals. We look specifically
405- # for the outermost caller, because intermediate functions could
406- # be defined by previous evaluations, in which case they would
407- # have __pythonx_eval_info_bytes__ in their globals, corresponding
408- # to that previous evaluation. When called within a thread, the
409- # evaluation caller is not in the stack, so __pythonx_eval_info_bytes__
410- # will be found in the thread entrypoint function globals.
411- call_stack = inspect.stack()
412- eval_info_bytes = next(
413- frame_info.frame.f_globals["__pythonx_eval_info_bytes__"]
414- for frame_info in reversed(call_stack)
415- if "__pythonx_eval_info_bytes__" in frame_info.frame.f_globals
416- )
417- pythonx_handle_io_write(string.encode("utf-8"), eval_info_bytes, self.type)
431+ pythonx_handle_io_write(string.encode("utf-8"), get_eval_info_bytes(), self.type)
418432 return len(string)
419433
420434
@@ -426,6 +440,24 @@ class Stdin(io.IOBase):
426440sys.stdout = Stdout(0)
427441sys.stderr = Stdout(1)
428442sys.stdin = Stdin()
443+
444+ pythonx = types.ModuleType("pythonx")
445+
446+ class PID:
447+ def __init__(self, bytes):
448+ self.bytes = bytes
449+
450+ def __repr__(self):
451+ return "<pythonx.PID>"
452+
453+ pythonx.PID = PID
454+
455+ def send(pid, tag, object):
456+ pythonx_handle_send(pid.bytes, tag.encode("utf-8"), object, get_eval_info_bytes())
457+
458+ pythonx.send = send
459+
460+ sys.modules["pythonx"] = pythonx
429461)" ;
430462
431463 auto py_code = PyUnicode_FromStringAndSize (code, sizeof (code) - 1 );
@@ -449,6 +481,16 @@ sys.stdin = Stdin()
449481 " pythonx_handle_io_write_ptr" ,
450482 py_pythonx_handle_io_write_ptr));
451483
484+ auto py_pythonx_handle_send_ptr = PyLong_FromUnsignedLongLong (
485+ reinterpret_cast <uintptr_t >(pythonx_handle_send));
486+ raise_if_failed (env, py_pythonx_handle_send_ptr);
487+ auto py_pythonx_handle_send_ptr_guard =
488+ PyDecRefGuard (py_pythonx_handle_send_ptr);
489+
490+ raise_if_failed (env,
491+ PyDict_SetItemString (py_globals, " pythonx_handle_send_ptr" ,
492+ py_pythonx_handle_send_ptr));
493+
452494 auto py_exec_args = PyTuple_Pack (2 , py_code, py_globals);
453495 raise_if_failed (env, py_exec_args);
454496 auto py_exec_args_guard = PyDecRefGuard (py_exec_args);
@@ -699,6 +741,37 @@ fine::Ok<> set_add(ErlNifEnv *env, ExObject ex_object, ExObject ex_key) {
699741
700742FINE_NIF (set_add, ERL_NIF_DIRTY_JOB_CPU_BOUND );
701743
744+ ExObject pid_new (ErlNifEnv *env, ErlNifPid pid) {
745+ ensure_initialized ();
746+ auto gil_guard = PyGILGuard ();
747+
748+ // ErlNifPid is self-contained struct, not bound to any env, so it's
749+ // safe to copy [1].
750+ //
751+ // [1]: https://www.erlang.org/doc/apps/erts/erl_nif.html#ErlNifPid
752+ auto py_pid_bytes = PyBytes_FromStringAndSize (
753+ reinterpret_cast <const char *>(&pid), sizeof (ErlNifPid));
754+ raise_if_failed (env, py_pid_bytes);
755+
756+ auto py_pythonx = PyImport_AddModule (" pythonx" );
757+ raise_if_failed (env, py_pythonx);
758+
759+ auto py_PID = PyObject_GetAttrString (py_pythonx, " PID" );
760+ raise_if_failed (env, py_PID);
761+ auto py_PID_guard = PyDecRefGuard (py_PID);
762+
763+ auto py_PID_args = PyTuple_Pack (1 , py_pid_bytes);
764+ raise_if_failed (env, py_PID_args);
765+ auto py_PID_args_guard = PyDecRefGuard (py_PID_args);
766+
767+ auto py_pid = PyObject_Call (py_PID, py_PID_args, NULL );
768+ raise_if_failed (env, py_pid);
769+
770+ return ExObject (fine::make_resource<ExObjectResource>(py_pid));
771+ }
772+
773+ FINE_NIF (pid_new, ERL_NIF_DIRTY_JOB_CPU_BOUND );
774+
702775ExObject object_repr (ErlNifEnv *env, ExObject ex_object) {
703776 ensure_initialized ();
704777 auto gil_guard = PyGILGuard ();
@@ -1368,16 +1441,16 @@ FINE_INIT("Elixir.Pythonx.NIF");
13681441
13691442// Below are functions we call from Python code
13701443
1371- extern " C" void pythonx_handle_io_write (const char *message,
1372- const char *eval_info_bytes,
1373- bool type) {
1444+ pythonx::EvalInfo eval_info_from_bytes (const char *eval_info_bytes) {
13741445 // Note that we allocate EvalInfo first, so it will have the proper
13751446 // alignment and memcpy simply restores the original struct state.
13761447 auto eval_info = pythonx::EvalInfo{};
13771448 std::memcpy (&eval_info, eval_info_bytes, sizeof (pythonx::EvalInfo));
13781449
1379- auto env = enif_alloc_env ();
1450+ return eval_info;
1451+ }
13801452
1453+ ErlNifEnv *get_caller_env (pythonx::EvalInfo eval_info) {
13811454 // The enif_whereis_pid and enif_send functions require passing the
13821455 // caller env. Stdout write may be called by the evaluated code from
13831456 // the NIF call, but it may also be called by a Python thread, after
@@ -1387,6 +1460,17 @@ extern "C" void pythonx_handle_io_write(const char *message,
13871460 bool is_main_thread = std::this_thread::get_id () == eval_info.thread_id ;
13881461 auto caller_env = is_main_thread ? eval_info.env : NULL ;
13891462
1463+ return caller_env;
1464+ }
1465+
1466+ extern " C" void pythonx_handle_io_write (const char *message,
1467+ const char *eval_info_bytes,
1468+ bool type) {
1469+ auto eval_info = eval_info_from_bytes (eval_info_bytes);
1470+
1471+ auto env = enif_alloc_env ();
1472+ auto caller_env = get_caller_env (eval_info);
1473+
13901474 // Note that we send the output to Pythonx.Janitor and it then sends
13911475 // it to the device. We do this to avoid IO replies being sent to
13921476 // the calling Elixir process (which would be unexpected). Additionally,
@@ -1406,3 +1490,23 @@ extern "C" void pythonx_handle_io_write(const char *message,
14061490 << std::endl;
14071491 }
14081492}
1493+
1494+ extern " C" void pythonx_handle_send (const char *pid_bytes, const char *tag,
1495+ pythonx::python::PyObjectPtr *py_object,
1496+ const char *eval_info_bytes) {
1497+ auto eval_info = eval_info_from_bytes (eval_info_bytes);
1498+
1499+ auto caller_env = get_caller_env (eval_info);
1500+ auto env = enif_alloc_env ();
1501+
1502+ auto pid = ErlNifPid{};
1503+ std::memcpy (&pid, pid_bytes, sizeof (ErlNifPid));
1504+
1505+ auto msg = fine::encode (
1506+ env, std::make_tuple (
1507+ fine::Atom (tag),
1508+ pythonx::ExObject (
1509+ fine::make_resource<pythonx::ExObjectResource>(py_object))));
1510+ enif_send (caller_env, &pid, env, msg);
1511+ enif_free_env (env);
1512+ }
0 commit comments