From 1205024e2b9e69ee10ab27c0168ff987a8245c88 Mon Sep 17 00:00:00 2001
From: Andreas Kloeckner <inform@tiker.net>
Date: Tue, 10 May 2022 16:02:00 -0500
Subject: [PATCH] Warn/error if queue used after context manager exit

---
 doc/runtime_queue.rst  |  9 +++++-
 pyopencl/__init__.py   |  5 ++++
 src/wrap_cl.hpp        | 65 +++++++++++++++++++++++++++++++++---------
 src/wrap_cl_part_1.cpp |  1 +
 test/test_wrapper.py   |  9 ++++++
 5 files changed, 75 insertions(+), 14 deletions(-)

diff --git a/doc/runtime_queue.rst b/doc/runtime_queue.rst
index f120c61e..fbe0b664 100644
--- a/doc/runtime_queue.rst
+++ b/doc/runtime_queue.rst
@@ -32,7 +32,7 @@ Command Queue
             enqueue_stuff(queue, ...)
 
     :meth:`finish` is automatically called at the end of the ``with``-delimited
-    context.
+    context, and further operations on the queue are considered an error.
 
     .. versionadded:: 2013.1
 
@@ -42,6 +42,13 @@ Command Queue
 
         Added the sequence-of-properties interface for OpenCL 2.
 
+    .. versionchanged:: 2022.1.4
+
+        Use of a command queue after its context manager completes
+        is now considered an error. :mod:`pyopencl` will warn about this
+        for a transitionary period and will start raising an exception
+        in 2023.
+
     .. attribute:: info
 
         Lower case versions of the :class:`command_queue_info` constants
diff --git a/pyopencl/__init__.py b/pyopencl/__init__.py
index a72f66b9..3f8dc83d 100644
--- a/pyopencl/__init__.py
+++ b/pyopencl/__init__.py
@@ -261,6 +261,10 @@ class CompilerWarning(UserWarning):
     pass
 
 
+class CommandQueueUsedAfterExit(UserWarning):
+    pass
+
+
 def compiler_output(text):
     import os
     from warnings import warn
@@ -706,6 +710,7 @@ def _add_functionality():
 
     def command_queue_exit(self, exc_type, exc_val, exc_tb):
         self.finish()
+        self._finalize()
 
     def command_queue_get_cl_version(self):
         return self.device._get_cl_version()
diff --git a/src/wrap_cl.hpp b/src/wrap_cl.hpp
index 4c40e373..63500c84 100644
--- a/src/wrap_cl.hpp
+++ b/src/wrap_cl.hpp
@@ -1378,17 +1378,26 @@ namespace pyopencl
   {
     private:
       cl_command_queue m_queue;
+      // m_finalized==True indicates that this command queue should no longer
+      // be used. An example of this is if a command queue is used as a context
+      // manager, after the 'with' block exits.
+      //
+      // This mechanism is not foolproof, as it is perfectly possible to create
+      // other Python proxy objects referring to the same underlying
+      // cl_command_queue. Even so, this ought to flag a class of potentially
+      // very damaging synchronization bugs.
+      bool m_finalized;
 
     public:
       command_queue(cl_command_queue q, bool retain)
-        : m_queue(q)
+        : m_queue(q), m_finalized(false)
       {
         if (retain)
           PYOPENCL_CALL_GUARDED(clRetainCommandQueue, (q));
       }
 
       command_queue(command_queue const &src)
-        : m_queue(src.m_queue)
+        : m_queue(src.m_queue), m_finalized(false)
       {
         PYOPENCL_CALL_GUARDED(clRetainCommandQueue, (m_queue));
       }
@@ -1397,6 +1406,7 @@ namespace pyopencl
           const context &ctx,
           const device *py_dev=nullptr,
           py::object py_props=py::none())
+      : m_finalized(false)
       {
         cl_device_id dev;
         if (py_dev)
@@ -1513,7 +1523,24 @@ namespace pyopencl
       }
 
       const cl_command_queue data() const
-      { return m_queue; }
+      {
+        if (m_finalized)
+        {
+          auto mod_warnings(py::module_::import("warnings"));
+          auto mod_cl(py::module_::import("pyopencl"));
+          mod_warnings.attr("warn")(
+              "Command queue used after exit of context manager. "
+              "This is deprecated and will stop working in 2023.",
+              mod_cl.attr("CommandQueueUsedAfterExit")
+              );
+        }
+        return m_queue;
+      }
+
+      void finalize()
+      {
+        m_finalized = true;
+      }
 
       PYOPENCL_EQUALITY_TESTS(command_queue);
 
@@ -1547,7 +1574,7 @@ namespace pyopencl
           case CL_QUEUE_PROPERTIES_ARRAY:
             {
               std::vector<cl_queue_properties> result;
-              PYOPENCL_GET_VEC_INFO(CommandQueue, m_queue, param_name, result);
+              PYOPENCL_GET_VEC_INFO(CommandQueue, data(), param_name, result);
               PYOPENCL_RETURN_VECTOR(cl_queue_properties, result);
             }
 #endif
@@ -1561,7 +1588,7 @@ namespace pyopencl
       {
         cl_context param_value;
         PYOPENCL_CALL_GUARDED(clGetCommandQueueInfo,
-            (m_queue, CL_QUEUE_CONTEXT, sizeof(param_value), &param_value, 0));
+            (data(), CL_QUEUE_CONTEXT, sizeof(param_value), &param_value, 0));
         return std::unique_ptr<context>(
             new context(param_value, /*retain*/ true));
       }
@@ -1573,15 +1600,19 @@ namespace pyopencl
       {
         cl_command_queue_properties old_prop;
         PYOPENCL_CALL_GUARDED(clSetCommandQueueProperty,
-            (m_queue, prop, PYOPENCL_CAST_BOOL(enable), &old_prop));
+            (data(), prop, PYOPENCL_CAST_BOOL(enable), &old_prop));
         return old_prop;
       }
 #endif
 
       void flush()
-      { PYOPENCL_CALL_GUARDED(clFlush, (m_queue)); }
+      { PYOPENCL_CALL_GUARDED(clFlush, (data())); }
       void finish()
-      { PYOPENCL_CALL_GUARDED_THREADED(clFinish, (m_queue)); }
+      {
+        cl_command_queue queue = data();
+
+        PYOPENCL_CALL_GUARDED_THREADED(clFinish, (queue));
+      }
 
       // not exposed to python
       int get_hex_device_version() const
@@ -1589,7 +1620,7 @@ namespace pyopencl
         cl_device_id dev;
 
         PYOPENCL_CALL_GUARDED(clGetCommandQueueInfo,
-            (m_queue, CL_QUEUE_DEVICE, sizeof(dev), &dev, nullptr));
+            (data(), CL_QUEUE_DEVICE, sizeof(dev), &dev, nullptr));
 
         std::string dev_version;
         {
@@ -2318,10 +2349,12 @@ namespace pyopencl
     buf = ward->m_buf.buf;
     len = ward->m_buf.len;
 
+    cl_command_queue queue = cq.data();
+
     cl_event evt;
     PYOPENCL_RETRY_IF_MEM_ERROR(
       PYOPENCL_CALL_GUARDED_THREADED(clEnqueueReadBuffer, (
-            cq.data(),
+            queue,
             mem.data(),
             PYOPENCL_CAST_BOOL(is_blocking),
             device_offset, len, buf,
@@ -2355,10 +2388,12 @@ namespace pyopencl
     buf = ward->m_buf.buf;
     len = ward->m_buf.len;
 
+    cl_command_queue queue = cq.data();
+
     cl_event evt;
     PYOPENCL_RETRY_IF_MEM_ERROR(
       PYOPENCL_CALL_GUARDED_THREADED(clEnqueueWriteBuffer, (
-            cq.data(),
+            queue,
             mem.data(),
             PYOPENCL_CAST_BOOL(is_blocking),
             device_offset, len, buf,
@@ -2442,10 +2477,12 @@ namespace pyopencl
 
     buf = ward->m_buf.buf;
 
+    cl_command_queue queue = cq.data();
+
     cl_event evt;
     PYOPENCL_RETRY_IF_MEM_ERROR(
       PYOPENCL_CALL_GUARDED_THREADED(clEnqueueReadBufferRect, (
-            cq.data(),
+            queue,
             mem.data(),
             PYOPENCL_CAST_BOOL(is_blocking),
             buffer_origin, host_origin, region,
@@ -2490,10 +2527,12 @@ namespace pyopencl
 
     buf = ward->m_buf.buf;
 
+    cl_command_queue queue = cq.data();
+
     cl_event evt;
     PYOPENCL_RETRY_IF_MEM_ERROR(
       PYOPENCL_CALL_GUARDED_THREADED(clEnqueueWriteBufferRect, (
-            cq.data(),
+            queue,
             mem.data(),
             PYOPENCL_CAST_BOOL(is_blocking),
             buffer_origin, host_origin, region,
diff --git a/src/wrap_cl_part_1.cpp b/src/wrap_cl_part_1.cpp
index 4b0ec771..8d62ef5d 100644
--- a/src/wrap_cl_part_1.cpp
+++ b/src/wrap_cl_part_1.cpp
@@ -120,6 +120,7 @@ void pyopencl_expose_part_1(py::module &m)
         py::arg("context"),
         py::arg("device").none(true)=py::none(),
         py::arg("properties")=py::cast(0))
+      .def("_finalize", &cls::finalize)
       .DEF_SIMPLE_METHOD(get_info)
 #if PYOPENCL_CL_VERSION < 0x1010
       .DEF_SIMPLE_METHOD(set_property)
diff --git a/test/test_wrapper.py b/test/test_wrapper.py
index 9ec065f7..ff4ee0cd 100644
--- a/test/test_wrapper.py
+++ b/test/test_wrapper.py
@@ -1247,6 +1247,15 @@ def test_empty_ndrange(ctx_factory, empty_shape):
     prg.add_two(queue, a.shape, None, a.data, allow_empty_ndrange=True)
 
 
+def test_command_queue_context_manager(ctx_factory):
+    ctx = ctx_factory()
+    with cl.CommandQueue(ctx) as queue:
+        q = queue
+
+    with pytest.warns(cl.CommandQueueUsedAfterExit):
+        q.finish()
+
+
 if __name__ == "__main__":
     # make sure that import failures get reported, instead of skipping the tests.
     import pyopencl  # noqa
-- 
GitLab