1515extern " C" void pythonx_handle_io_write (const char *message,
1616 const char *eval_info_bytes, bool type);
1717
18+ extern " C" void
19+ pythonx_handle_send_tagged_object (const char *pid_bytes, const char *tag,
20+ pythonx::python::PyObjectPtr *py_object,
21+ const char *eval_info_bytes);
22+
1823namespace pythonx {
1924
2025using namespace python ;
@@ -385,36 +390,46 @@ import ctypes
385390import io
386391import sys
387392import inspect
393+ import types
394+ import sys
388395
389396pythonx_handle_io_write = ctypes.CFUNCTYPE(
390397 None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool
391398)(pythonx_handle_io_write_ptr)
392399
400+ pythonx_handle_send_tagged_object = ctypes.CFUNCTYPE(
401+ None, ctypes.c_char_p, ctypes.c_char_p, ctypes.py_object, ctypes.c_char_p
402+ )(pythonx_handle_send_tagged_object_ptr)
403+
404+
405+ def get_eval_info_bytes():
406+ # The evaluation caller has __pythonx_eval_info_bytes__ set in
407+ # their globals. It is not available in globals() here, because
408+ # the globals dict in function definitions is fixed at definition
409+ # time. To find the current evaluation globals, we look at the
410+ # call stack using the inspect module and find the caller with
411+ # __pythonx_eval_info_bytes__ in globals. We look specifically
412+ # for the outermost caller, because intermediate functions could
413+ # be defined by previous evaluations, in which case they would
414+ # have __pythonx_eval_info_bytes__ in their globals, corresponding
415+ # to that previous evaluation. When called within a thread, the
416+ # evaluation caller is not in the stack, so __pythonx_eval_info_bytes__
417+ # will be found in the thread entrypoint function globals.
418+ call_stack = inspect.stack()
419+ eval_info_bytes = next(
420+ frame_info.frame.f_globals["__pythonx_eval_info_bytes__"]
421+ for frame_info in reversed(call_stack)
422+ if "__pythonx_eval_info_bytes__" in frame_info.frame.f_globals
423+ )
424+ return eval_info_bytes
425+
393426
394427class Stdout(io.TextIOBase):
395428 def __init__(self, type):
396429 self.type = type
397430
398431 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)
432+ pythonx_handle_io_write(string.encode("utf-8"), get_eval_info_bytes(), self.type)
418433 return len(string)
419434
420435
@@ -426,6 +441,24 @@ class Stdin(io.IOBase):
426441sys.stdout = Stdout(0)
427442sys.stderr = Stdout(1)
428443sys.stdin = Stdin()
444+
445+ pythonx = types.ModuleType("pythonx")
446+
447+ class PID:
448+ def __init__(self, bytes):
449+ self.bytes = bytes
450+
451+ def __repr__(self):
452+ return "<pythonx.PID>"
453+
454+ pythonx.PID = PID
455+
456+ def send_tagged_object(pid, tag, object):
457+ pythonx_handle_send_tagged_object(pid.bytes, tag.encode("utf-8"), object, get_eval_info_bytes())
458+
459+ pythonx.send_tagged_object = send_tagged_object
460+
461+ sys.modules["pythonx"] = pythonx
429462)" ;
430463
431464 auto py_code = PyUnicode_FromStringAndSize (code, sizeof (code) - 1 );
@@ -449,6 +482,16 @@ sys.stdin = Stdin()
449482 " pythonx_handle_io_write_ptr" ,
450483 py_pythonx_handle_io_write_ptr));
451484
485+ auto py_pythonx_handle_send_tagged_object_ptr = PyLong_FromUnsignedLongLong (
486+ reinterpret_cast <uintptr_t >(pythonx_handle_send_tagged_object));
487+ raise_if_failed (env, py_pythonx_handle_send_tagged_object_ptr);
488+ auto py_pythonx_handle_send_tagged_object_ptr_guard =
489+ PyDecRefGuard (py_pythonx_handle_send_tagged_object_ptr);
490+
491+ raise_if_failed (env, PyDict_SetItemString (
492+ py_globals, " pythonx_handle_send_tagged_object_ptr" ,
493+ py_pythonx_handle_send_tagged_object_ptr));
494+
452495 auto py_exec_args = PyTuple_Pack (2 , py_code, py_globals);
453496 raise_if_failed (env, py_exec_args);
454497 auto py_exec_args_guard = PyDecRefGuard (py_exec_args);
@@ -699,6 +742,37 @@ fine::Ok<> set_add(ErlNifEnv *env, ExObject ex_object, ExObject ex_key) {
699742
700743FINE_NIF (set_add, ERL_NIF_DIRTY_JOB_CPU_BOUND);
701744
745+ ExObject pid_new (ErlNifEnv *env, ErlNifPid pid) {
746+ ensure_initialized ();
747+ auto gil_guard = PyGILGuard ();
748+
749+ // ErlNifPid is self-contained struct, not bound to any env, so it's
750+ // safe to copy [1].
751+ //
752+ // [1]: https://www.erlang.org/doc/apps/erts/erl_nif.html#ErlNifPid
753+ auto py_pid_bytes = PyBytes_FromStringAndSize (
754+ reinterpret_cast <const char *>(&pid), sizeof (ErlNifPid));
755+ raise_if_failed (env, py_pid_bytes);
756+
757+ auto py_pythonx = PyImport_AddModule (" pythonx" );
758+ raise_if_failed (env, py_pythonx);
759+
760+ auto py_PID = PyObject_GetAttrString (py_pythonx, " PID" );
761+ raise_if_failed (env, py_PID);
762+ auto py_PID_guard = PyDecRefGuard (py_PID);
763+
764+ auto py_PID_args = PyTuple_Pack (1 , py_pid_bytes);
765+ raise_if_failed (env, py_PID_args);
766+ auto py_PID_args_guard = PyDecRefGuard (py_PID_args);
767+
768+ auto py_pid = PyObject_Call (py_PID, py_PID_args, NULL );
769+ raise_if_failed (env, py_pid);
770+
771+ return ExObject (fine::make_resource<ExObjectResource>(py_pid));
772+ }
773+
774+ FINE_NIF (pid_new, ERL_NIF_DIRTY_JOB_CPU_BOUND);
775+
702776ExObject object_repr (ErlNifEnv *env, ExObject ex_object) {
703777 ensure_initialized ();
704778 auto gil_guard = PyGILGuard ();
@@ -962,6 +1036,31 @@ fine::Term decode_once(ErlNifEnv *env, ExObject ex_object) {
9621036 std::make_tuple (atoms::map_set, fine::Term (items)));
9631037 }
9641038
1039+ auto py_pythonx = PyImport_AddModule (" pythonx" );
1040+ raise_if_failed (env, py_pythonx);
1041+
1042+ auto py_PID = PyObject_GetAttrString (py_pythonx, " PID" );
1043+ raise_if_failed (env, py_PID);
1044+ auto py_PID_guard = PyDecRefGuard (py_PID);
1045+
1046+ auto is_pid = PyObject_IsInstance (py_object, py_PID);
1047+ raise_if_failed (env, is_pid);
1048+ if (is_pid) {
1049+ auto py_pid_bytes = PyObject_GetAttrString (py_object, " bytes" );
1050+ raise_if_failed (env, py_pid_bytes);
1051+ auto py_pid_bytes_guard = PyDecRefGuard (py_pid_bytes);
1052+
1053+ Py_ssize_t size;
1054+ char *pid_bytes;
1055+ auto result = PyBytes_AsStringAndSize (py_pid_bytes, &pid_bytes, &size);
1056+ raise_if_failed (env, result);
1057+
1058+ auto pid = ErlNifPid{};
1059+ std::memcpy (&pid, pid_bytes, sizeof (ErlNifPid));
1060+
1061+ return fine::encode (env, pid);
1062+ }
1063+
9651064 // None of the built-ins, return %Pythonx.Object{} as is
9661065 return fine::encode (env, ex_object);
9671066}
@@ -1368,16 +1467,16 @@ FINE_INIT("Elixir.Pythonx.NIF");
13681467
13691468// Below are functions we call from Python code
13701469
1371- extern " C" void pythonx_handle_io_write (const char *message,
1372- const char *eval_info_bytes,
1373- bool type) {
1470+ pythonx::EvalInfo eval_info_from_bytes (const char *eval_info_bytes) {
13741471 // Note that we allocate EvalInfo first, so it will have the proper
13751472 // alignment and memcpy simply restores the original struct state.
13761473 auto eval_info = pythonx::EvalInfo{};
13771474 std::memcpy (&eval_info, eval_info_bytes, sizeof (pythonx::EvalInfo));
13781475
1379- auto env = enif_alloc_env ();
1476+ return eval_info;
1477+ }
13801478
1479+ ErlNifEnv *get_caller_env (pythonx::EvalInfo eval_info) {
13811480 // The enif_whereis_pid and enif_send functions require passing the
13821481 // caller env. Stdout write may be called by the evaluated code from
13831482 // the NIF call, but it may also be called by a Python thread, after
@@ -1387,6 +1486,17 @@ extern "C" void pythonx_handle_io_write(const char *message,
13871486 bool is_main_thread = std::this_thread::get_id () == eval_info.thread_id ;
13881487 auto caller_env = is_main_thread ? eval_info.env : NULL ;
13891488
1489+ return caller_env;
1490+ }
1491+
1492+ extern " C" void pythonx_handle_io_write (const char *message,
1493+ const char *eval_info_bytes,
1494+ bool type) {
1495+ auto eval_info = eval_info_from_bytes (eval_info_bytes);
1496+
1497+ auto env = enif_alloc_env ();
1498+ auto caller_env = get_caller_env (eval_info);
1499+
13901500 // Note that we send the output to Pythonx.Janitor and it then sends
13911501 // it to the device. We do this to avoid IO replies being sent to
13921502 // the calling Elixir process (which would be unexpected). Additionally,
@@ -1406,3 +1516,24 @@ extern "C" void pythonx_handle_io_write(const char *message,
14061516 << std::endl;
14071517 }
14081518}
1519+
1520+ extern " C" void
1521+ pythonx_handle_send_tagged_object (const char *pid_bytes, const char *tag,
1522+ pythonx::python::PyObjectPtr *py_object,
1523+ const char *eval_info_bytes) {
1524+ auto eval_info = eval_info_from_bytes (eval_info_bytes);
1525+
1526+ auto caller_env = get_caller_env (eval_info);
1527+ auto env = enif_alloc_env ();
1528+
1529+ auto pid = ErlNifPid{};
1530+ std::memcpy (&pid, pid_bytes, sizeof (ErlNifPid));
1531+
1532+ auto msg = fine::encode (
1533+ env, std::make_tuple (
1534+ fine::Atom (tag),
1535+ pythonx::ExObject (
1536+ fine::make_resource<pythonx::ExObjectResource>(py_object))));
1537+ enif_send (caller_env, &pid, env, msg);
1538+ enif_free_env (env);
1539+ }
0 commit comments