From 175c3b32ea3d9d9bf2ea6965d3e94eb93792d0a8 Mon Sep 17 00:00:00 2001
From: thorstenhater <24411438+thorstenhater@users.noreply.github.com>
Date: Mon, 14 Dec 2020 09:32:14 +0100
Subject: [PATCH] Fix GIL related error in pybind11 interface. (#1272)

Fix a latent bug in the Python wrapper that was triggered in pybind11 v2.6.1

These changes ensure that the GIL is acquired before calling functions in C++ that
may consume a Python object with reference count 1 as an argument, in which case
the object's destructor is called at the end of the function, after any GIL acquired inside
the function would have been released.

Fixes #1271.
---
 python/cells.cpp  | 30 ------------------------
 python/cells.hpp  |  1 -
 python/recipe.cpp | 59 +++++++++++++++++++++++++++++++++++++----------
 3 files changed, 47 insertions(+), 43 deletions(-)

diff --git a/python/cells.cpp b/python/cells.cpp
index 62ac4011..7953593e 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -30,36 +30,6 @@
 #include "strprintf.hpp"
 
 namespace pyarb {
-
-// Convert a cell description inside a Python object to a cell
-// description in a unique_any, as required by the recipe interface.
-//
-// Warning: requires that the GIL has been acquired before calling,
-// if there is a segmentation fault in the cast or isinstance calls,
-// check that the caller has the GIL.
-arb::util::unique_any convert_cell(pybind11::object o) {
-    using pybind11::isinstance;
-    using pybind11::cast;
-
-    pybind11::gil_scoped_acquire guard;
-    if (isinstance<arb::spike_source_cell>(o)) {
-        return arb::util::unique_any(cast<arb::spike_source_cell>(o));
-    }
-    if (isinstance<arb::benchmark_cell>(o)) {
-        return arb::util::unique_any(cast<arb::benchmark_cell>(o));
-    }
-    if (isinstance<arb::lif_cell>(o)) {
-        return arb::util::unique_any(cast<arb::lif_cell>(o));
-    }
-    if (isinstance<arb::cable_cell>(o)) {
-        return arb::util::unique_any(cast<arb::cable_cell>(o));
-    }
-
-    throw pyarb_error("recipe.cell_description returned \""
-                      + std::string(pybind11::str(o))
-                      + "\" which does not describe a known Arbor cell type");
-}
-
 //
 //  proxies
 //
diff --git a/python/cells.hpp b/python/cells.hpp
index f4fd3175..8560f16a 100644
--- a/python/cells.hpp
+++ b/python/cells.hpp
@@ -7,7 +7,6 @@
 #include <arbor/util/unique_any.hpp>
 
 namespace pyarb {
-arb::util::unique_any convert_cell(pybind11::object o);
 
 struct global_props_shim {
     arb::mechanism_catalogue cat;
diff --git a/python/recipe.cpp b/python/recipe.cpp
index e1c67da5..d543fe6d 100644
--- a/python/recipe.cpp
+++ b/python/recipe.cpp
@@ -6,11 +6,13 @@
 #include <pybind11/pytypes.h>
 #include <pybind11/stl.h>
 
+#include <arbor/benchmark_cell.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/lif_cell.hpp>
+#include <arbor/spike_source_cell.hpp>
 #include <arbor/event_generator.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/recipe.hpp>
-#include <arbor/spike_source_cell.hpp>
 
 #include "cells.hpp"
 #include "conversion.hpp"
@@ -21,22 +23,53 @@
 
 namespace pyarb {
 
+// Convert a cell description inside a Python object to a cell description in a
+// unique_any, as required by the recipe interface.
+// This helper is only to be called while holding the GIL. We require this guard
+// across the lifetime of the description object `o`, since this function can be
+// called without holding the GIL, ie from `simulation::init`, and `o` is a
+// python object that can only be destroyed while holding the GIL. The fact that
+// `cell_description` has a scoped GIL does not help with destruction as it
+// happens outside that scope. `Description` needs to be extended in Python,
+// inheriting from the pybind11 class.
+static arb::util::unique_any convert_cell(pybind11::object o) {
+    using pybind11::isinstance;
+    using pybind11::cast;
+
+    if (isinstance<arb::spike_source_cell>(o)) {
+        return arb::util::unique_any(cast<arb::spike_source_cell>(o));
+    }
+    if (isinstance<arb::benchmark_cell>(o)) {
+        return arb::util::unique_any(cast<arb::benchmark_cell>(o));
+    }
+    if (isinstance<arb::lif_cell>(o)) {
+        return arb::util::unique_any(cast<arb::lif_cell>(o));
+    }
+    if (isinstance<arb::cable_cell>(o)) {
+        return arb::util::unique_any(cast<arb::cable_cell>(o));
+    }
+
+    throw pyarb_error("recipe.cell_description returned \""
+                      + std::string(pybind11::str(o))
+                      + "\" which does not describe a known Arbor cell type");
+}
+
 // The py::recipe::cell_decription returns a pybind11::object, that is
 // unwrapped and copied into a arb::util::unique_any.
-arb::util::unique_any py_recipe_shim::get_cell_description(arb::cell_gid_type gid) const {
-    return try_catch_pyexception(
-                [&](){ return convert_cell(impl_->cell_description(gid)); },
-                "Python error already thrown");
+ arb::util::unique_any py_recipe_shim::get_cell_description(arb::cell_gid_type gid) const {
+    return try_catch_pyexception([&](){
+        pybind11::gil_scoped_acquire guard;
+        return convert_cell(impl_->cell_description(gid));
+    },
+        "Python error already thrown");
 }
 
-std::vector<arb::event_generator> convert_gen(std::vector<pybind11::object> pygens, arb::cell_gid_type gid) {
+// This helper is only to called while holding the GIL, see above.
+static std::vector<arb::event_generator> convert_gen(std::vector<pybind11::object> pygens, arb::cell_gid_type gid) {
     using namespace std::string_literals;
     using pybind11::isinstance;
     using pybind11::cast;
 
-    // Aquire the GIL because it must be held when calling isinstance and cast.
-    pybind11::gil_scoped_acquire guard;
-
     std::vector<arb::event_generator> gens;
     gens.reserve(pygens.size());
 
@@ -58,9 +91,11 @@ std::vector<arb::event_generator> convert_gen(std::vector<pybind11::object> pyge
 }
 
 std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid_type gid) const {
-    return try_catch_pyexception(
-                [&](){ return convert_gen(impl_->event_generators(gid), gid); },
-                "Python error already thrown");
+    return try_catch_pyexception([&](){
+        pybind11::gil_scoped_acquire guard;
+        return convert_gen(impl_->event_generators(gid), gid);
+    },
+        "Python error already thrown");
 }
 
 std::string con_to_string(const arb::cell_connection& c) {
-- 
GitLab