@@ -1420,6 +1420,167 @@ static PyObject *py_schedule_py(PyObject *self, PyObject *args, PyObject *kwargs
14201420 return (PyObject * )marker ;
14211421}
14221422
1423+ /* ============================================================================
1424+ * InlineScheduleMarker - marker type for inline continuation without messaging
1425+ *
1426+ * When a Python handler returns an InlineScheduleMarker, the NIF detects it
1427+ * and uses enif_schedule_nif() to continue execution directly, bypassing
1428+ * the Erlang messaging layer for better performance in tight loops.
1429+ *
1430+ * Note: InlineScheduleMarkerObject is forward declared in py_nif.c
1431+ * ============================================================================ */
1432+
1433+ static void InlineScheduleMarker_dealloc (InlineScheduleMarkerObject * self ) {
1434+ Py_XDECREF (self -> module );
1435+ Py_XDECREF (self -> func );
1436+ Py_XDECREF (self -> args );
1437+ Py_XDECREF (self -> kwargs );
1438+ Py_XDECREF (self -> globals );
1439+ Py_XDECREF (self -> locals );
1440+ Py_TYPE (self )-> tp_free ((PyObject * )self );
1441+ }
1442+
1443+ static PyObject * InlineScheduleMarker_repr (InlineScheduleMarkerObject * self ) {
1444+ return PyUnicode_FromFormat ("<erlang.InlineScheduleMarker module='%U' func='%U'>" ,
1445+ self -> module , self -> func );
1446+ }
1447+
1448+ static PyTypeObject InlineScheduleMarkerType = {
1449+ PyVarObject_HEAD_INIT (NULL , 0 )
1450+ .tp_name = "erlang.InlineScheduleMarker" ,
1451+ .tp_doc = "Marker for inline continuation via enif_schedule_nif (no Erlang messaging)" ,
1452+ .tp_basicsize = sizeof (InlineScheduleMarkerObject ),
1453+ .tp_itemsize = 0 ,
1454+ .tp_flags = Py_TPFLAGS_DEFAULT ,
1455+ .tp_dealloc = (destructor )InlineScheduleMarker_dealloc ,
1456+ .tp_repr = (reprfunc )InlineScheduleMarker_repr ,
1457+ };
1458+
1459+ /**
1460+ * Check if a Python object is an InlineScheduleMarker
1461+ */
1462+ static int is_inline_schedule_marker (PyObject * obj ) {
1463+ return Py_IS_TYPE (obj , & InlineScheduleMarkerType );
1464+ }
1465+
1466+ /**
1467+ * @brief Python: erlang.schedule_inline(module, func, args=None, kwargs=None) -> InlineScheduleMarker
1468+ *
1469+ * Creates an InlineScheduleMarker that, when returned from a handler function,
1470+ * causes the NIF to use enif_schedule_nif() to continue execution directly
1471+ * without going through Erlang messaging.
1472+ *
1473+ * This is more efficient than schedule_py() for tight loops that need to yield
1474+ * to the scheduler but don't need to interact with Erlang between calls.
1475+ *
1476+ * Flow comparison:
1477+ * schedule_py: Python -> NIF -> Erlang message -> NIF -> Python
1478+ * schedule_inline: Python -> NIF -> enif_schedule_nif -> NIF -> Python
1479+ *
1480+ * Usage:
1481+ * def process_batch(data, offset=0, results=None):
1482+ * if results is None:
1483+ * results = []
1484+ * chunk_end = min(offset + 100, len(data))
1485+ * for i in range(offset, chunk_end):
1486+ * results.append(transform(data[i]))
1487+ * if chunk_end < len(data):
1488+ * if erlang.consume_time_slice(25):
1489+ * return erlang.schedule_inline('__main__', 'process_batch',
1490+ * args=[data, chunk_end, results])
1491+ * return process_batch(data, chunk_end, results)
1492+ * return results
1493+ *
1494+ * @param self Module reference (unused)
1495+ * @param args Positional args: (module, func)
1496+ * @param kwargs Keyword args: args=list/tuple, kwargs=dict
1497+ * @return InlineScheduleMarker object or NULL with exception
1498+ */
1499+ static PyObject * py_schedule_inline (PyObject * self , PyObject * args , PyObject * kwargs ) {
1500+ (void )self ;
1501+
1502+ static char * kwlist [] = {"module" , "func" , "args" , "kwargs" , NULL };
1503+ PyObject * module_name = NULL ;
1504+ PyObject * func_name = NULL ;
1505+ PyObject * call_args = Py_None ;
1506+ PyObject * call_kwargs = Py_None ;
1507+
1508+ if (!PyArg_ParseTupleAndKeywords (args , kwargs , "OO|OO" , kwlist ,
1509+ & module_name , & func_name , & call_args , & call_kwargs )) {
1510+ return NULL ;
1511+ }
1512+
1513+ /* Validate module and func are strings */
1514+ if (!PyUnicode_Check (module_name )) {
1515+ PyErr_SetString (PyExc_TypeError , "module must be a string" );
1516+ return NULL ;
1517+ }
1518+ if (!PyUnicode_Check (func_name )) {
1519+ PyErr_SetString (PyExc_TypeError , "func must be a string" );
1520+ return NULL ;
1521+ }
1522+
1523+ /* Validate args is None or a sequence */
1524+ if (call_args != Py_None && !PyTuple_Check (call_args ) && !PyList_Check (call_args )) {
1525+ PyErr_SetString (PyExc_TypeError , "args must be None, a tuple, or a list" );
1526+ return NULL ;
1527+ }
1528+
1529+ /* Validate kwargs is None or a dict */
1530+ if (call_kwargs != Py_None && !PyDict_Check (call_kwargs )) {
1531+ PyErr_SetString (PyExc_TypeError , "kwargs must be None or a dict" );
1532+ return NULL ;
1533+ }
1534+
1535+ /* Create the marker */
1536+ InlineScheduleMarkerObject * marker = PyObject_New (InlineScheduleMarkerObject , & InlineScheduleMarkerType );
1537+ if (marker == NULL ) {
1538+ return NULL ;
1539+ }
1540+
1541+ Py_INCREF (module_name );
1542+ marker -> module = module_name ;
1543+
1544+ Py_INCREF (func_name );
1545+ marker -> func = func_name ;
1546+
1547+ /* Convert args to tuple if it's a list */
1548+ if (call_args == Py_None ) {
1549+ Py_INCREF (Py_None );
1550+ marker -> args = Py_None ;
1551+ } else if (PyList_Check (call_args )) {
1552+ marker -> args = PyList_AsTuple (call_args );
1553+ if (marker -> args == NULL ) {
1554+ Py_DECREF (marker );
1555+ return NULL ;
1556+ }
1557+ } else {
1558+ Py_INCREF (call_args );
1559+ marker -> args = call_args ;
1560+ }
1561+
1562+ Py_INCREF (call_kwargs );
1563+ marker -> kwargs = call_kwargs ;
1564+
1565+ /* Capture globals and locals from caller's frame */
1566+ PyObject * frame_globals = PyEval_GetGlobals (); /* Borrowed reference */
1567+ PyObject * frame_locals = PyEval_GetLocals (); /* Borrowed reference */
1568+ if (frame_globals != NULL ) {
1569+ Py_INCREF (frame_globals );
1570+ marker -> globals = frame_globals ;
1571+ } else {
1572+ marker -> globals = NULL ;
1573+ }
1574+ if (frame_locals != NULL ) {
1575+ Py_INCREF (frame_locals );
1576+ marker -> locals = frame_locals ;
1577+ } else {
1578+ marker -> locals = NULL ;
1579+ }
1580+
1581+ return (PyObject * )marker ;
1582+ }
1583+
14231584/**
14241585 * @brief Python: erlang.consume_time_slice(percent) -> bool
14251586 *
@@ -2484,6 +2645,10 @@ static PyMethodDef ErlangModuleMethods[] = {
24842645 "Schedule Python function continuation (must be returned from handler).\n\n"
24852646 "Usage: return erlang.schedule_py('module', 'func', [args], {'kwargs'})\n"
24862647 "Releases dirty scheduler and continues via _execute_py callback." },
2648+ {"schedule_inline" , (PyCFunction )py_schedule_inline , METH_VARARGS | METH_KEYWORDS ,
2649+ "Schedule inline Python continuation via enif_schedule_nif (no Erlang messaging).\n\n"
2650+ "Usage: return erlang.schedule_inline('module', 'func', args=[...], kwargs={...})\n"
2651+ "More efficient than schedule_py for tight loops that don't need Erlang interaction." },
24872652 {"consume_time_slice" , py_consume_time_slice , METH_VARARGS ,
24882653 "Check/consume NIF time slice for cooperative scheduling.\n\n"
24892654 "Usage: if erlang.consume_time_slice(percent): return erlang.schedule_py(...)\n"
@@ -2587,6 +2752,11 @@ static int create_erlang_module(void) {
25872752 return -1 ;
25882753 }
25892754
2755+ /* Initialize InlineScheduleMarker type */
2756+ if (PyType_Ready (& InlineScheduleMarkerType ) < 0 ) {
2757+ return -1 ;
2758+ }
2759+
25902760 PyObject * module = PyModule_Create (& ErlangModuleDef );
25912761 if (module == NULL ) {
25922762 return -1 ;
@@ -2646,6 +2816,14 @@ static int create_erlang_module(void) {
26462816 return -1 ;
26472817 }
26482818
2819+ /* Add InlineScheduleMarker type to module */
2820+ Py_INCREF (& InlineScheduleMarkerType );
2821+ if (PyModule_AddObject (module , "InlineScheduleMarker" , (PyObject * )& InlineScheduleMarkerType ) < 0 ) {
2822+ Py_DECREF (& InlineScheduleMarkerType );
2823+ Py_DECREF (module );
2824+ return -1 ;
2825+ }
2826+
26492827 /* Add __getattr__ to enable "from erlang import name" and "erlang.name()" syntax
26502828 * Module __getattr__ (PEP 562) needs to be set as an attribute on the module dict */
26512829 PyObject * getattr_func = PyCFunction_New (& getattr_method , module );
0 commit comments