@@ -694,6 +694,50 @@ static PyObject *ErlangFunction_New(PyObject *name) {
694694 return (PyObject * )self ;
695695}
696696
697+ /* ============================================================================
698+ * ErlangPid - opaque wrapper for Erlang process identifiers
699+ *
700+ * ErlangPidObject is defined in py_nif.h for use by py_convert.c
701+ * ============================================================================ */
702+
703+ static PyObject * ErlangPid_repr (ErlangPidObject * self ) {
704+ /* Show the raw term value for debugging — not a stable external format,
705+ but distinguishes different PIDs in logs and repls. */
706+ return PyUnicode_FromFormat ("<erlang.Pid 0x%lx>" ,
707+ (unsigned long )self -> pid .pid );
708+ }
709+
710+ static PyObject * ErlangPid_richcompare (PyObject * a , PyObject * b , int op ) {
711+ if (!Py_IS_TYPE (b , & ErlangPidType )) {
712+ Py_RETURN_NOTIMPLEMENTED ;
713+ }
714+ ErlangPidObject * pa = (ErlangPidObject * )a ;
715+ ErlangPidObject * pb = (ErlangPidObject * )b ;
716+ int eq = enif_is_identical (pa -> pid .pid , pb -> pid .pid );
717+ switch (op ) {
718+ case Py_EQ : return PyBool_FromLong (eq );
719+ case Py_NE : return PyBool_FromLong (!eq );
720+ default : Py_RETURN_NOTIMPLEMENTED ;
721+ }
722+ }
723+
724+ static Py_hash_t ErlangPid_hash (ErlangPidObject * self ) {
725+ Py_hash_t h = (Py_hash_t )enif_hash (ERL_NIF_PHASH2 , self -> pid .pid , 0 );
726+ if (h == -1 ) h = -2 ; /* -1 is reserved for errors in Python */
727+ return h ;
728+ }
729+
730+ PyTypeObject ErlangPidType = {
731+ PyVarObject_HEAD_INIT (NULL , 0 )
732+ .tp_name = "erlang.Pid" ,
733+ .tp_basicsize = sizeof (ErlangPidObject ),
734+ .tp_flags = Py_TPFLAGS_DEFAULT ,
735+ .tp_repr = (reprfunc )ErlangPid_repr ,
736+ .tp_richcompare = ErlangPid_richcompare ,
737+ .tp_hash = (hashfunc )ErlangPid_hash ,
738+ .tp_doc = "Opaque Erlang process identifier" ,
739+ };
740+
697741/**
698742 * Python implementation of erlang.call(name, *args)
699743 *
@@ -911,6 +955,68 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) {
911955 return NULL ;
912956}
913957
958+ /* ============================================================================
959+ * erlang.send() - Fire-and-forget message passing
960+ *
961+ * Sends a message directly to an Erlang process mailbox via enif_send().
962+ * No suspension, no blocking, no reply needed.
963+ * ============================================================================ */
964+
965+ /**
966+ * @brief Python: erlang.send(pid, term) -> None
967+ *
968+ * Fire-and-forget message send to an Erlang process.
969+ *
970+ * @param self Module reference (unused)
971+ * @param args Tuple: (pid:erlang.Pid, term:any)
972+ * @return None on success, NULL with exception on failure
973+ */
974+ static PyObject * erlang_send_impl (PyObject * self , PyObject * args ) {
975+ (void )self ;
976+
977+ if (PyTuple_Size (args ) != 2 ) {
978+ PyErr_SetString (PyExc_TypeError ,
979+ "erlang.send requires exactly 2 arguments: (pid, term)" );
980+ return NULL ;
981+ }
982+
983+ PyObject * pid_obj = PyTuple_GetItem (args , 0 );
984+ PyObject * term_obj = PyTuple_GetItem (args , 1 );
985+
986+ /* Validate PID type */
987+ if (!Py_IS_TYPE (pid_obj , & ErlangPidType )) {
988+ PyErr_SetString (PyExc_TypeError , "First argument must be an erlang.Pid" );
989+ return NULL ;
990+ }
991+
992+ ErlangPidObject * pid = (ErlangPidObject * )pid_obj ;
993+
994+ /* Allocate a message environment and convert the term */
995+ ErlNifEnv * msg_env = enif_alloc_env ();
996+ if (msg_env == NULL ) {
997+ PyErr_SetString (PyExc_MemoryError , "Failed to allocate message environment" );
998+ return NULL ;
999+ }
1000+
1001+ ERL_NIF_TERM msg = py_to_term (msg_env , term_obj );
1002+
1003+ if (PyErr_Occurred ()) {
1004+ enif_free_env (msg_env );
1005+ return NULL ;
1006+ }
1007+
1008+ /* Fire-and-forget send */
1009+ if (!enif_send (NULL , & pid -> pid , msg_env , msg )) {
1010+ enif_free_env (msg_env );
1011+ PyErr_SetString (ProcessErrorException ,
1012+ "Failed to send message: process may not exist" );
1013+ return NULL ;
1014+ }
1015+
1016+ enif_free_env (msg_env );
1017+ Py_RETURN_NONE ;
1018+ }
1019+
9141020/* ============================================================================
9151021 * Async callback support for asyncio integration
9161022 *
@@ -1288,6 +1394,10 @@ static PyMethodDef ErlangModuleMethods[] = {
12881394 "Call a registered Erlang function.\n\n"
12891395 "Usage: erlang.call('func_name', arg1, arg2, ...)\n"
12901396 "Returns: The result from the Erlang function." },
1397+ {"send" , erlang_send_impl , METH_VARARGS ,
1398+ "Send a message to an Erlang process (fire-and-forget).\n\n"
1399+ "Usage: erlang.send(pid, term)\n"
1400+ "The pid must be an erlang.Pid object." },
12911401 {"_get_async_callback_fd" , get_async_callback_fd , METH_NOARGS ,
12921402 "Get the file descriptor for async callback responses.\n"
12931403 "Used internally by async_call() to register with asyncio." },
@@ -1344,6 +1454,11 @@ static int create_erlang_module(void) {
13441454 return -1 ;
13451455 }
13461456
1457+ /* Initialize ErlangPid type */
1458+ if (PyType_Ready (& ErlangPidType ) < 0 ) {
1459+ return -1 ;
1460+ }
1461+
13471462 PyObject * module = PyModule_Create (& ErlangModuleDef );
13481463 if (module == NULL ) {
13491464 return -1 ;
@@ -1353,7 +1468,7 @@ static int create_erlang_module(void) {
13531468 * This exception is raised internally when erlang.call() needs to suspend.
13541469 * It carries callback info in args: (callback_id, func_name, args_tuple) */
13551470 SuspensionRequiredException = PyErr_NewException (
1356- "erlang.SuspensionRequired" , NULL , NULL );
1471+ "erlang.SuspensionRequired" , PyExc_BaseException , NULL );
13571472 if (SuspensionRequiredException == NULL ) {
13581473 Py_DECREF (module );
13591474 return -1 ;
@@ -1365,6 +1480,20 @@ static int create_erlang_module(void) {
13651480 return -1 ;
13661481 }
13671482
1483+ /* Create erlang.ProcessError for dead/unreachable processes */
1484+ ProcessErrorException = PyErr_NewException (
1485+ "erlang.ProcessError" , NULL , NULL );
1486+ if (ProcessErrorException == NULL ) {
1487+ Py_DECREF (module );
1488+ return -1 ;
1489+ }
1490+ Py_INCREF (ProcessErrorException );
1491+ if (PyModule_AddObject (module , "ProcessError" , ProcessErrorException ) < 0 ) {
1492+ Py_DECREF (ProcessErrorException );
1493+ Py_DECREF (module );
1494+ return -1 ;
1495+ }
1496+
13681497 /* Add ErlangFunction type to module (for introspection) */
13691498 Py_INCREF (& ErlangFunctionType );
13701499 if (PyModule_AddObject (module , "Function" , (PyObject * )& ErlangFunctionType ) < 0 ) {
@@ -1373,6 +1502,14 @@ static int create_erlang_module(void) {
13731502 return -1 ;
13741503 }
13751504
1505+ /* Add ErlangPid type to module */
1506+ Py_INCREF (& ErlangPidType );
1507+ if (PyModule_AddObject (module , "Pid" , (PyObject * )& ErlangPidType ) < 0 ) {
1508+ Py_DECREF (& ErlangPidType );
1509+ Py_DECREF (module );
1510+ return -1 ;
1511+ }
1512+
13761513 /* Add __getattr__ to enable "from erlang import name" and "erlang.name()" syntax
13771514 * Module __getattr__ (PEP 562) needs to be set as an attribute on the module dict */
13781515 PyObject * getattr_func = PyCFunction_New (& getattr_method , module );
0 commit comments