1 /* Proof-of-concept of a -fanalyzer plugin.
2 Detect (some) uses of CPython API outside of the Global Interpreter Lock.
3 https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock
5 /* { dg-options "-g" } */
9 #include "gcc-plugin.h"
12 #include "coretypes.h"
13 #include "make-unique.h"
14 #include "diagnostic.h"
17 #include "gimple-iterator.h"
18 #include "gimple-walk.h"
19 #include "diagnostic-event-id.h"
20 #include "analyzer/analyzer.h"
21 #include "analyzer/analyzer-logging.h"
23 #include "analyzer/sm.h"
24 #include "analyzer/pending-diagnostic.h"
26 int plugin_is_GPL_compatible
;
33 type_based_on_pyobject_p (tree type
)
35 /* Ideally we'd also check for "subclasses" here by iterating up the
36 first field of each struct. */
37 if (TREE_CODE (type
) != RECORD_TYPE
)
39 tree name
= TYPE_IDENTIFIER (type
);
42 return id_equal (name
, "PyObject");
45 /* An experimental state machine, for tracking whether the GIL is held,
48 class gil_state_machine
: public state_machine
51 gil_state_machine (logger
*logger
);
53 bool inherited_state_p () const final override
{ return false; }
55 bool on_stmt (sm_context
&sm_ctxt
,
56 const supernode
*node
,
57 const gimple
*stmt
) const final override
;
59 bool can_purge_p (state_t s
) const final override
;
61 void check_for_pyobject_usage_without_gil (sm_context
&sm_ctxt
,
62 const supernode
*node
,
67 void check_for_pyobject_in_call (sm_context
&sm_ctxt
,
68 const supernode
*node
,
70 tree callee_fndecl
) const;
73 /* These states are "global", rather than per-expression. */
75 /* State for when we've released the GIL. */
76 state_t m_released_gil
;
82 /* Subclass for diagnostics involving the GIL. */
84 class gil_diagnostic
: public pending_diagnostic
87 /* There isn't a warning ID for us to use. */
88 int get_controlling_option () const final override
93 location_t
fixup_location (location_t loc
,
94 bool) const final override
96 /* Ideally we'd check for specific macros here, and only
97 resolve certain macros. */
98 if (linemap_location_from_macro_expansion_p (line_table
, loc
))
99 loc
= linemap_resolve_location (line_table
, loc
,
100 LRK_MACRO_EXPANSION_POINT
, NULL
);
104 label_text
describe_state_change (const evdesc::state_change
&change
)
107 if (change
.is_global_p ()
108 && change
.m_new_state
== m_sm
.m_released_gil
)
109 return change
.formatted_print ("releasing the GIL here");
110 if (change
.is_global_p ()
111 && change
.m_new_state
== m_sm
.get_start_state ())
112 return change
.formatted_print ("acquiring the GIL here");
113 return label_text ();
116 diagnostic_event::meaning
117 get_meaning_for_state_change (const evdesc::state_change
&change
)
120 if (change
.is_global_p ())
122 if (change
.m_new_state
== m_sm
.m_released_gil
)
123 return diagnostic_event::meaning (diagnostic_event::VERB_release
,
124 diagnostic_event::NOUN_lock
);
125 else if (change
.m_new_state
== m_sm
.get_start_state ())
126 return diagnostic_event::meaning (diagnostic_event::VERB_acquire
,
127 diagnostic_event::NOUN_lock
);
129 return diagnostic_event::meaning ();
132 gil_diagnostic (const gil_state_machine
&sm
) : m_sm (sm
)
137 const gil_state_machine
&m_sm
;
140 class double_save_thread
: public gil_diagnostic
143 double_save_thread (const gil_state_machine
&sm
, const gcall
*call
)
144 : gil_diagnostic (sm
), m_call (call
)
147 const char *get_kind () const final override
149 return "double_save_thread";
152 bool subclass_equal_p (const pending_diagnostic
&base_other
) const override
154 const double_save_thread
&sub_other
155 = (const double_save_thread
&)base_other
;
156 return m_call
== sub_other
.m_call
;
159 bool emit (diagnostic_emission_context
&ctxt
) final override
161 return ctxt
.warn ("nested usage of %qs", "Py_BEGIN_ALLOW_THREADS");
164 label_text
describe_final_event (const evdesc::final_event
&ev
) final override
166 return ev
.formatted_print ("nested usage of %qs here",
167 "Py_BEGIN_ALLOW_THREADS");
174 class fncall_without_gil
: public gil_diagnostic
177 fncall_without_gil (const gil_state_machine
&sm
, const gcall
*call
,
178 tree callee_fndecl
, unsigned arg_idx
)
179 : gil_diagnostic (sm
), m_call (call
), m_callee_fndecl (callee_fndecl
),
183 const char *get_kind () const final override
185 return "fncall_without_gil";
188 bool subclass_equal_p (const pending_diagnostic
&base_other
) const override
190 const fncall_without_gil
&sub_other
191 = (const fncall_without_gil
&)base_other
;
192 return (m_call
== sub_other
.m_call
193 && m_callee_fndecl
== sub_other
.m_callee_fndecl
194 && m_arg_idx
== sub_other
.m_arg_idx
);
197 bool emit (diagnostic_emission_context
&ctxt
) final override
200 return ctxt
.warn ("use of PyObject as argument %i of %qE"
202 m_arg_idx
+ 1, m_callee_fndecl
);
204 return ctxt
.warn ("use of PyObject as argument %i of call"
206 m_arg_idx
+ 1, m_callee_fndecl
);
209 label_text
describe_final_event (const evdesc::final_event
&ev
) final override
212 return ev
.formatted_print ("use of PyObject as argument %i of %qE here"
214 m_arg_idx
+ 1, m_callee_fndecl
);
216 return ev
.formatted_print ("use of PyObject as argument %i of call here"
218 m_arg_idx
+ 1, m_callee_fndecl
);
223 tree m_callee_fndecl
;
227 class pyobject_usage_without_gil
: public gil_diagnostic
230 pyobject_usage_without_gil (const gil_state_machine
&sm
, tree expr
)
231 : gil_diagnostic (sm
), m_expr (expr
)
234 const char *get_kind () const final override
236 return "pyobject_usage_without_gil";
239 bool subclass_equal_p (const pending_diagnostic
&base_other
) const override
241 return same_tree_p (m_expr
,
242 ((const pyobject_usage_without_gil
&)base_other
).m_expr
);
245 bool emit (diagnostic_emission_context
&ctxt
) final override
247 return ctxt
.warn ("use of PyObject %qE without the GIL", m_expr
);
250 label_text
describe_final_event (const evdesc::final_event
&ev
) final override
252 return ev
.formatted_print ("PyObject %qE used here without the GIL",
260 /* gil_state_machine's ctor. */
262 gil_state_machine::gil_state_machine (logger
*logger
)
263 : state_machine ("gil", logger
)
265 m_released_gil
= add_state ("released_gil");
266 m_stop
= add_state ("stop");
271 cb_data (const gil_state_machine
&sm
, sm_context
&sm_ctxt
,
272 const supernode
*snode
, const gimple
*stmt
)
273 : m_sm (sm
), m_sm_ctxt (sm_ctxt
), m_snode (snode
), m_stmt (stmt
)
277 const gil_state_machine
&m_sm
;
278 sm_context
&m_sm_ctxt
;
279 const supernode
*m_snode
;
280 const gimple
*m_stmt
;
284 check_for_pyobject (gimple
*, tree op
, tree
, void *data
)
286 cb_data
*d
= (cb_data
*)data
;
287 d
->m_sm
.check_for_pyobject_usage_without_gil (d
->m_sm_ctxt
, d
->m_snode
,
292 /* Assuming that the GIL has been released, complain about any
293 PyObject * arguments passed to CALL. */
296 gil_state_machine::check_for_pyobject_in_call (sm_context
&sm_ctxt
,
297 const supernode
*node
,
299 tree callee_fndecl
) const
301 for (unsigned i
= 0; i
< gimple_call_num_args (call
); i
++)
303 tree arg
= gimple_call_arg (call
, i
);
304 if (TREE_CODE (TREE_TYPE (arg
)) != POINTER_TYPE
)
306 tree type
= TREE_TYPE (TREE_TYPE (arg
));
307 if (type_based_on_pyobject_p (type
))
309 sm_ctxt
.warn (node
, call
, NULL_TREE
,
310 make_unique
<fncall_without_gil
> (*this, call
,
313 sm_ctxt
.set_global_state (m_stop
);
318 /* Implementation of state_machine::on_stmt vfunc for gil_state_machine. */
321 gil_state_machine::on_stmt (sm_context
&sm_ctxt
,
322 const supernode
*node
,
323 const gimple
*stmt
) const
325 const state_t global_state
= sm_ctxt
.get_global_state ();
326 if (const gcall
*call
= dyn_cast
<const gcall
*> (stmt
))
328 if (tree callee_fndecl
= sm_ctxt
.get_fndecl_for_call (call
))
330 if (is_named_call_p (callee_fndecl
, "PyEval_SaveThread", call
, 0))
333 inform (input_location
, "found call to %qs",
334 "PyEval_SaveThread");
335 if (global_state
== m_released_gil
)
337 sm_ctxt
.warn (node
, stmt
, NULL_TREE
,
338 make_unique
<double_save_thread
> (*this, call
));
339 sm_ctxt
.set_global_state (m_stop
);
342 sm_ctxt
.set_global_state (m_released_gil
);
345 else if (is_named_call_p (callee_fndecl
, "PyEval_RestoreThread",
349 inform (input_location
, "found call to %qs",
350 "PyEval_SaveThread");
351 if (global_state
== m_released_gil
)
352 sm_ctxt
.set_global_state (m_start
);
355 else if (global_state
== m_released_gil
)
357 /* Find PyObject * args of calls to fns with unknown bodies. */
358 if (!fndecl_has_gimple_body_p (callee_fndecl
))
359 check_for_pyobject_in_call (sm_ctxt
, node
, call
, callee_fndecl
);
362 else if (global_state
== m_released_gil
)
363 check_for_pyobject_in_call (sm_ctxt
, node
, call
, NULL
);
366 if (global_state
== m_released_gil
)
368 /* Walk the stmt, finding uses of PyObject (or "subclasses"). */
369 cb_data
d (*this, sm_ctxt
, node
, stmt
);
370 walk_stmt_load_store_addr_ops (const_cast <gimple
*> (stmt
), &d
,
379 gil_state_machine::can_purge_p (state_t s ATTRIBUTE_UNUSED
) const
385 gil_state_machine::check_for_pyobject_usage_without_gil (sm_context
&sm_ctxt
,
386 const supernode
*node
,
390 tree type
= TREE_TYPE (op
);
391 if (type_based_on_pyobject_p (type
))
393 sm_ctxt
.warn (node
, stmt
, NULL_TREE
,
394 make_unique
<pyobject_usage_without_gil
> (*this, op
));
395 sm_ctxt
.set_global_state (m_stop
);
399 /* Callback handler for the PLUGIN_ANALYZER_INIT event. */
402 gil_analyzer_init_cb (void *gcc_data
, void */
*user_data*/
)
404 ana::plugin_analyzer_init_iface
*iface
405 = (ana::plugin_analyzer_init_iface
*)gcc_data
;
406 LOG_SCOPE (iface
->get_logger ());
408 inform (input_location
, "got here: gil_analyzer_init_cb");
409 iface
->register_state_machine
410 (make_unique
<gil_state_machine
> (iface
->get_logger ()));
415 #endif /* #if ENABLE_ANALYZER */
418 plugin_init (struct plugin_name_args
*plugin_info
,
419 struct plugin_gcc_version
*version
)
422 const char *plugin_name
= plugin_info
->base_name
;
424 inform (input_location
, "got here; %qs", plugin_name
);
425 register_callback (plugin_info
->base_name
,
426 PLUGIN_ANALYZER_INIT
,
427 ana::gil_analyzer_init_cb
,
428 NULL
); /* void *user_data */
430 sorry_no_analyzer ();