]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-41756: Introduce PyGen_Send C API (GH-22196)
authorVladimir Matveev <vladima@fb.com>
Sat, 19 Sep 2020 01:38:38 +0000 (18:38 -0700)
committerGitHub <noreply@github.com>
Sat, 19 Sep 2020 01:38:38 +0000 (18:38 -0700)
The new API allows to efficiently send values into native generators
and coroutines avoiding use of StopIteration exceptions to signal
returns.

ceval loop now uses this method instead of the old "private"
_PyGen_Send C API. This translates to 1.6x increased performance
of 'await' calls in micro-benchmarks.

Aside from CPython core improvements, this new API will also allow
Cython to generate more efficient code, benefiting high-performance
IO libraries like uvloop.

Doc/c-api/gen.rst
Doc/data/refcounts.dat
Include/genobject.h
Misc/NEWS.d/next/Core and Builtins/2020-09-12-12-55-45.bpo-41756.1h0tbV.rst [new file with mode: 0644]
Modules/_asynciomodule.c
Objects/genobject.c
Python/ceval.c

index 74410927bfde107c8a8c1e4a5ee1f0a4a9302a0a..e098425e6364d9ef1430939d2f6dd77b68bb7f02 100644 (file)
@@ -15,6 +15,11 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
    The C structure used for generator objects.
 
 
+.. c:type:: PySendResult
+
+   The enum value used to represent different results of :c:func:`PyGen_Send`.
+
+
 .. c:var:: PyTypeObject PyGen_Type
 
    The type object corresponding to generator objects.
@@ -42,3 +47,13 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
    with ``__name__`` and ``__qualname__`` set to *name* and *qualname*.
    A reference to *frame* is stolen by this function.  The *frame* argument
    must not be ``NULL``.
+
+.. c:function:: PySendResult PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **presult)
+
+   Sends the *arg* value into the generator *gen*. Coroutine objects
+   are also allowed to be as the *gen* argument but they need to be
+   explicitly casted to PyGenObject*. Returns:
+
+   - ``PYGEN_RETURN`` if generator returns. Return value is returned via *presult*.
+   - ``PYGEN_NEXT`` if generator yields. Yielded value is returned via *presult*.
+   - ``PYGEN_ERROR`` if generator has raised and exception. *presult* is set to ``NULL``.
index 355a4d6d3fa7ba169f1c5f52bd650ab569c8f553..6b1bde37967ae9390e80784eef3d11a620b78bf9 100644 (file)
@@ -959,6 +959,11 @@ PyGen_NewWithQualName:PyFrameObject*:frame:0:
 PyGen_NewWithQualName:PyObject*:name:0:
 PyGen_NewWithQualName:PyObject*:qualname:0:
 
+PyGen_Send:int:::
+PyGen_Send:PyGenObject*:gen:0:
+PyGen_Send:PyObject*:arg:0:
+PyGen_Send:PyObject**:presult:+1:
+
 PyCoro_CheckExact:int:::
 PyCoro_CheckExact:PyObject*:ob:0:
 
index a76dc92e811c42be5e6b78ff1384919aac7acd60..7488054c68fcd8bf8e3678265c6e957e5d58129b 100644 (file)
@@ -45,6 +45,21 @@ PyAPI_FUNC(PyObject *) _PyGen_Send(PyGenObject *, PyObject *);
 PyObject *_PyGen_yf(PyGenObject *);
 PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
 
+typedef enum {
+    PYGEN_RETURN = 0,
+    PYGEN_ERROR = -1,
+    PYGEN_NEXT = 1,
+} PySendResult;
+
+/* Sends the value into the generator or the coroutine. Returns:
+   - PYGEN_RETURN (0) if generator has returned.
+     'result' parameter is filled with return value
+   - PYGEN_ERROR (-1) if exception was raised.
+     'result' parameter is NULL
+   - PYGEN_NEXT (1) if generator has yielded.
+     'result' parameter is filled with yielded value. */
+PyAPI_FUNC(PySendResult) PyGen_Send(PyGenObject *, PyObject *, PyObject **);
+
 #ifndef Py_LIMITED_API
 typedef struct {
     _PyGenObject_HEAD(cr)
diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-09-12-12-55-45.bpo-41756.1h0tbV.rst b/Misc/NEWS.d/next/Core and Builtins/2020-09-12-12-55-45.bpo-41756.1h0tbV.rst
new file mode 100644 (file)
index 0000000..b387cfd
--- /dev/null
@@ -0,0 +1,2 @@
+Add PyGen_Send function to allow sending value into generator/coroutine
+without raising StopIteration exception to signal return
index 4a1c91e9eddd67fe46600ea62a7cb907301d298c..2151f20281a31bc61abc8c2b1d39bb42bf775d97 100644 (file)
@@ -2621,6 +2621,20 @@ task_set_error_soon(TaskObj *task, PyObject *et, const char *format, ...)
     Py_RETURN_NONE;
 }
 
+static inline int
+gen_status_from_result(PyObject **result)
+{
+    if (*result != NULL) {
+        return PYGEN_NEXT;
+    }
+    if (_PyGen_FetchStopIterationValue(result) == 0) {
+        return PYGEN_RETURN;
+    }
+
+    assert(PyErr_Occurred());
+    return PYGEN_ERROR;
+}
+
 static PyObject *
 task_step_impl(TaskObj *task, PyObject *exc)
 {
@@ -2679,26 +2693,29 @@ task_step_impl(TaskObj *task, PyObject *exc)
         return NULL;
     }
 
+    int gen_status = PYGEN_ERROR;
     if (exc == NULL) {
         if (PyGen_CheckExact(coro) || PyCoro_CheckExact(coro)) {
-            result = _PyGen_Send((PyGenObject*)coro, Py_None);
+            gen_status = PyGen_Send((PyGenObject*)coro, Py_None, &result);
         }
         else {
             result = _PyObject_CallMethodIdOneArg(coro, &PyId_send, Py_None);
+            gen_status = gen_status_from_result(&result);
         }
     }
     else {
         result = _PyObject_CallMethodIdOneArg(coro, &PyId_throw, exc);
+        gen_status = gen_status_from_result(&result);
         if (clear_exc) {
             /* We created 'exc' during this call */
             Py_DECREF(exc);
         }
     }
 
-    if (result == NULL) {
+    if (gen_status == PYGEN_RETURN || gen_status == PYGEN_ERROR) {
         PyObject *et, *ev, *tb;
 
-        if (_PyGen_FetchStopIterationValue(&o) == 0) {
+        if (result != NULL) {
             /* The error is StopIteration and that means that
                the underlying coroutine has resolved */
 
@@ -2709,10 +2726,10 @@ task_step_impl(TaskObj *task, PyObject *exc)
                 res = future_cancel((FutureObj*)task, task->task_cancel_msg);
             }
             else {
-                res = future_set_result((FutureObj*)task, o);
+                res = future_set_result((FutureObj*)task, result);
             }
 
-            Py_DECREF(o);
+            Py_DECREF(result);
 
             if (res == NULL) {
                 return NULL;
index 809838a4cd2f3b50863aabc4d34f07507b55cef8..24aca988354c5ae08e6348d2c656808b486074b2 100644 (file)
@@ -137,7 +137,7 @@ gen_dealloc(PyGenObject *gen)
 }
 
 static PyObject *
-gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
+gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing, int *is_return_value)
 {
     PyThreadState *tstate = _PyThreadState_GET();
     PyFrameObject *f = gen->gi_frame;
@@ -170,6 +170,10 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
                 PyErr_SetNone(PyExc_StopAsyncIteration);
             }
             else {
+                if (is_return_value != NULL) {
+                    *is_return_value = 1;
+                    Py_RETURN_NONE;
+                }
                 PyErr_SetNone(PyExc_StopIteration);
             }
         }
@@ -230,18 +234,33 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
             /* Delay exception instantiation if we can */
             if (PyAsyncGen_CheckExact(gen)) {
                 PyErr_SetNone(PyExc_StopAsyncIteration);
+                Py_CLEAR(result);
             }
             else if (arg) {
-                /* Set exception if not called by gen_iternext() */
-                PyErr_SetNone(PyExc_StopIteration);
+                if (is_return_value != NULL) {
+                    *is_return_value = 1;
+                }
+                else {
+                    /* Set exception if not called by gen_iternext() */
+                    PyErr_SetNone(PyExc_StopIteration);
+                    Py_CLEAR(result);
+                }
+            }
+            else {
+                Py_CLEAR(result);
             }
         }
         else {
             /* Async generators cannot return anything but None */
             assert(!PyAsyncGen_CheckExact(gen));
-            _PyGen_SetStopIterationValue(result);
+            if (is_return_value != NULL) {
+                *is_return_value = 1;
+            }
+            else {
+                _PyGen_SetStopIterationValue(result);
+                Py_CLEAR(result);
+            }
         }
-        Py_CLEAR(result);
     }
     else if (!result && PyErr_ExceptionMatches(PyExc_StopIteration)) {
         const char *msg = "generator raised StopIteration";
@@ -264,7 +283,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
         _PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg);
     }
 
-    if (!result ||  _PyFrameHasCompleted(f)) {
+    if ((is_return_value && *is_return_value) || !result ||  _PyFrameHasCompleted(f)) {
         /* generator can't be rerun, so release the frame */
         /* first clean reference cycle through stored exception traceback */
         _PyErr_ClearExcState(&gen->gi_exc_state);
@@ -283,7 +302,19 @@ return next yielded value or raise StopIteration.");
 PyObject *
 _PyGen_Send(PyGenObject *gen, PyObject *arg)
 {
-    return gen_send_ex(gen, arg, 0, 0);
+    return gen_send_ex(gen, arg, 0, 0, NULL);
+}
+
+PySendResult
+PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **result)
+{
+    assert(result != NULL);
+
+    int is_return_value = 0;
+    if ((*result = gen_send_ex(gen, arg, 0, 0, &is_return_value)) == NULL) {
+        return PYGEN_ERROR;
+    }
+    return is_return_value ? PYGEN_RETURN : PYGEN_NEXT;
 }
 
 PyDoc_STRVAR(close_doc,
@@ -365,7 +396,7 @@ gen_close(PyGenObject *gen, PyObject *args)
     }
     if (err == 0)
         PyErr_SetNone(PyExc_GeneratorExit);
-    retval = gen_send_ex(gen, Py_None, 1, 1);
+    retval = gen_send_ex(gen, Py_None, 1, 1, NULL);
     if (retval) {
         const char *msg = "generator ignored GeneratorExit";
         if (PyCoro_CheckExact(gen)) {
@@ -413,7 +444,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
             gen->gi_frame->f_state = state;
             Py_DECREF(yf);
             if (err < 0)
-                return gen_send_ex(gen, Py_None, 1, 0);
+                return gen_send_ex(gen, Py_None, 1, 0, NULL);
             goto throw_here;
         }
         if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {
@@ -465,10 +496,10 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
             assert(gen->gi_frame->f_lasti >= 0);
             gen->gi_frame->f_lasti += sizeof(_Py_CODEUNIT);
             if (_PyGen_FetchStopIterationValue(&val) == 0) {
-                ret = gen_send_ex(gen, val, 0, 0);
+                ret = gen_send_ex(gen, val, 0, 0, NULL);
                 Py_DECREF(val);
             } else {
-                ret = gen_send_ex(gen, Py_None, 1, 0);
+                ret = gen_send_ex(gen, Py_None, 1, 0, NULL);
             }
         }
         return ret;
@@ -522,7 +553,7 @@ throw_here:
     }
 
     PyErr_Restore(typ, val, tb);
-    return gen_send_ex(gen, Py_None, 1, 0);
+    return gen_send_ex(gen, Py_None, 1, 0, NULL);
 
 failed_throw:
     /* Didn't use our arguments, so restore their original refcounts */
@@ -551,7 +582,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
 static PyObject *
 gen_iternext(PyGenObject *gen)
 {
-    return gen_send_ex(gen, NULL, 0, 0);
+    return gen_send_ex(gen, NULL, 0, 0, NULL);
 }
 
 /*
@@ -1051,13 +1082,13 @@ coro_wrapper_dealloc(PyCoroWrapper *cw)
 static PyObject *
 coro_wrapper_iternext(PyCoroWrapper *cw)
 {
-    return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0);
+    return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0, NULL);
 }
 
 static PyObject *
 coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg)
 {
-    return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0);
+    return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0, NULL);
 }
 
 static PyObject *
@@ -1570,7 +1601,7 @@ async_gen_asend_send(PyAsyncGenASend *o, PyObject *arg)
     }
 
     o->ags_gen->ag_running_async = 1;
-    result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0);
+    result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0, NULL);
     result = async_gen_unwrap_value(o->ags_gen, result);
 
     if (result == NULL) {
@@ -1926,7 +1957,7 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
 
     assert(o->agt_state == AWAITABLE_STATE_ITER);
 
-    retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0);
+    retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0, NULL);
     if (o->agt_args) {
         return async_gen_unwrap_value(o->agt_gen, retval);
     } else {
index f747faaebf024a3a3ba0505088355f329e90a110..3de372f45a251706a17d708254a9b6f8d882db5f 100644 (file)
@@ -2223,29 +2223,53 @@ main_loop:
         case TARGET(YIELD_FROM): {
             PyObject *v = POP();
             PyObject *receiver = TOP();
-            int err;
-            if (PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver)) {
-                retval = _PyGen_Send((PyGenObject *)receiver, v);
+            int is_gen_or_coro = PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver);
+            int gen_status;
+            if (tstate->c_tracefunc == NULL && is_gen_or_coro) {
+                gen_status = PyGen_Send((PyGenObject *)receiver, v, &retval);
             } else {
-                _Py_IDENTIFIER(send);
-                if (v == Py_None)
-                    retval = Py_TYPE(receiver)->tp_iternext(receiver);
-                else
-                    retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v);
+                if (is_gen_or_coro) {
+                    retval = _PyGen_Send((PyGenObject *)receiver, v);
+                }
+                else {
+                    _Py_IDENTIFIER(send);
+                    if (v == Py_None) {
+                        retval = Py_TYPE(receiver)->tp_iternext(receiver);
+                    }
+                    else {
+                        retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v);
+                    }
+                }
+
+                if (retval == NULL) {
+                    if (tstate->c_tracefunc != NULL
+                            && _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
+                        call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
+                    if (_PyGen_FetchStopIterationValue(&retval) == 0) {
+                        gen_status = PYGEN_RETURN;
+                    }
+                    else {
+                        gen_status = PYGEN_ERROR;
+                    }
+                }
+                else {
+                    gen_status = PYGEN_NEXT;
+                }
             }
             Py_DECREF(v);
-            if (retval == NULL) {
-                PyObject *val;
-                if (tstate->c_tracefunc != NULL
-                        && _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
-                    call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
-                err = _PyGen_FetchStopIterationValue(&val);
-                if (err < 0)
-                    goto error;
+            if (gen_status == PYGEN_ERROR) {
+                assert (retval == NULL);
+                goto error;
+            }
+            if (gen_status == PYGEN_RETURN) {
+                assert (retval != NULL);
+
                 Py_DECREF(receiver);
-                SET_TOP(val);
+                SET_TOP(retval);
+                retval = NULL;
                 DISPATCH();
             }
+            assert (gen_status == PYGEN_NEXT);
             /* receiver remains on stack, retval is value to be yielded */
             /* and repeat... */
             assert(f->f_lasti >= (int)sizeof(_Py_CODEUNIT));