Skip to content

Commit 324531d

Browse files
authored
gh-111903: Add @critical_section directive to Argument Clinic. (#111904)
The `@critical_section` directive instructs Argument Clinic to generate calls to `Py_BEGIN_CRITICAL_SECTION()` and `Py_END_CRITICAL_SECTION()` around the bound function. In `--disable-gil` builds, these calls will lock and unlock the `self` object. They are no-ops in the default build. This is used in one place (`_io._Buffered.close`) as a demonstration. Subsequent PRs will use it more widely in the `_io.Buffered` bindings.
1 parent 16055c1 commit 324531d

File tree

5 files changed

+117
-8
lines changed

5 files changed

+117
-8
lines changed

Lib/test/clinic.test.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5467,3 +5467,78 @@ docstr_fallback_to_converter_default(PyObject *module, PyObject *const *args, Py
54675467
static PyObject *
54685468
docstr_fallback_to_converter_default_impl(PyObject *module, str a)
54695469
/*[clinic end generated code: output=ae24a9c6f60ee8a6 input=0cbe6a4d24bc2274]*/
5470+
5471+
5472+
/*[clinic input]
5473+
@critical_section
5474+
test_critical_section
5475+
[clinic start generated code]*/
5476+
5477+
PyDoc_STRVAR(test_critical_section__doc__,
5478+
"test_critical_section($module, /)\n"
5479+
"--\n"
5480+
"\n");
5481+
5482+
#define TEST_CRITICAL_SECTION_METHODDEF \
5483+
{"test_critical_section", (PyCFunction)test_critical_section, METH_NOARGS, test_critical_section__doc__},
5484+
5485+
static PyObject *
5486+
test_critical_section_impl(PyObject *module);
5487+
5488+
static PyObject *
5489+
test_critical_section(PyObject *module, PyObject *Py_UNUSED(ignored))
5490+
{
5491+
PyObject *return_value = NULL;
5492+
5493+
Py_BEGIN_CRITICAL_SECTION(module);
5494+
return_value = test_critical_section_impl(module);
5495+
Py_END_CRITICAL_SECTION();
5496+
5497+
return return_value;
5498+
}
5499+
5500+
static PyObject *
5501+
test_critical_section_impl(PyObject *module)
5502+
/*[clinic end generated code: output=9d5a87bb28aa3f0c input=8c58956d6ff00f80]*/
5503+
5504+
5505+
/*[clinic input]
5506+
@critical_section
5507+
test_critical_section_meth_o
5508+
a: object(subclass_of="&PyUnicode_Type")
5509+
/
5510+
[clinic start generated code]*/
5511+
5512+
PyDoc_STRVAR(test_critical_section_meth_o__doc__,
5513+
"test_critical_section_meth_o($module, a, /)\n"
5514+
"--\n"
5515+
"\n");
5516+
5517+
#define TEST_CRITICAL_SECTION_METH_O_METHODDEF \
5518+
{"test_critical_section_meth_o", (PyCFunction)test_critical_section_meth_o, METH_O, test_critical_section_meth_o__doc__},
5519+
5520+
static PyObject *
5521+
test_critical_section_meth_o_impl(PyObject *module, PyObject *a);
5522+
5523+
static PyObject *
5524+
test_critical_section_meth_o(PyObject *module, PyObject *arg)
5525+
{
5526+
PyObject *return_value = NULL;
5527+
PyObject *a;
5528+
5529+
if (!PyUnicode_Check(arg)) {
5530+
_PyArg_BadArgument("test_critical_section_meth_o", "argument", "str", arg);
5531+
goto exit;
5532+
}
5533+
a = arg;
5534+
Py_BEGIN_CRITICAL_SECTION(module);
5535+
return_value = test_critical_section_meth_o_impl(module, a);
5536+
Py_END_CRITICAL_SECTION();
5537+
5538+
exit:
5539+
return return_value;
5540+
}
5541+
5542+
static PyObject *
5543+
test_critical_section_meth_o_impl(PyObject *module, PyObject *a)
5544+
/*[clinic end generated code: output=7a9d7420802d1202 input=376533f51eceb6c3]*/
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Argument Clinic now supports the ``@critical_section`` directive that
2+
instructs Argument Clinic to generate a critical section around the function
3+
call, which locks the ``self`` object in ``--disable-gil`` builds. Patch by
4+
Sam Gross.

Modules/_io/bufferedio.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "Python.h"
1111
#include "pycore_bytesobject.h" // _PyBytes_Join()
1212
#include "pycore_call.h" // _PyObject_CallNoArgs()
13+
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
1314
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
1415
#include "pycore_pyerrors.h" // _Py_FatalErrorFormat()
1516
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
@@ -521,12 +522,13 @@ buffered_closed_get(buffered *self, void *context)
521522
}
522523

523524
/*[clinic input]
525+
@critical_section
524526
_io._Buffered.close
525527
[clinic start generated code]*/
526528

527529
static PyObject *
528530
_io__Buffered_close_impl(buffered *self)
529-
/*[clinic end generated code: output=7280b7b42033be0c input=d20b83d1ddd7d805]*/
531+
/*[clinic end generated code: output=7280b7b42033be0c input=56d95935b03fd326]*/
530532
{
531533
PyObject *res = NULL;
532534
int r;

Modules/_io/clinic/bufferedio.c.h

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/clinic/clinic.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ def __init__(self) -> None:
470470
# The C statements required to clean up after the impl call.
471471
self.cleanup: list[str] = []
472472

473+
# The C statements to generate critical sections (per-object locking).
474+
self.lock: list[str] = []
475+
self.unlock: list[str] = []
476+
473477

474478
class FormatCounterFormatter(string.Formatter):
475479
"""
@@ -1109,7 +1113,8 @@ def output_templates(
11091113
condition=include.condition)
11101114

11111115
has_option_groups = parameters and (parameters[0].group or parameters[-1].group)
1112-
default_return_converter = f.return_converter.type == 'PyObject *'
1116+
simple_return = (f.return_converter.type == 'PyObject *'
1117+
and not f.critical_section)
11131118
new_or_init = f.kind.new_or_init
11141119

11151120
vararg: int | str = NO_VARARG
@@ -1183,7 +1188,9 @@ def parser_body(
11831188
""") + "\n"
11841189
finale = normalize_snippet("""
11851190
{modifications}
1191+
{lock}
11861192
{return_value} = {c_basename}_impl({impl_arguments});
1193+
{unlock}
11871194
{return_conversion}
11881195
{post_parsing}
11891196
@@ -1219,7 +1226,7 @@ def parser_body(
12191226

12201227
flags = "METH_METHOD|METH_FASTCALL|METH_KEYWORDS"
12211228
parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS
1222-
return_error = ('return NULL;' if default_return_converter
1229+
return_error = ('return NULL;' if simple_return
12231230
else 'goto exit;')
12241231
parser_code = [normalize_snippet("""
12251232
if (nargs) {{
@@ -1228,7 +1235,7 @@ def parser_body(
12281235
}}
12291236
""" % return_error, indent=4)]
12301237

1231-
if default_return_converter:
1238+
if simple_return:
12321239
parser_definition = '\n'.join([
12331240
parser_prototype,
12341241
'{{',
@@ -1245,7 +1252,7 @@ def parser_body(
12451252
converters[0].format_unit == 'O'):
12461253
meth_o_prototype = self.METH_O_PROTOTYPE
12471254

1248-
if default_return_converter:
1255+
if simple_return:
12491256
# maps perfectly to METH_O, doesn't need a return converter.
12501257
# so we skip making a parse function
12511258
# and call directly into the impl function.
@@ -1858,6 +1865,10 @@ def render_function(
18581865
selfless = parameters[1:]
18591866
assert isinstance(f_self.converter, self_converter), "No self parameter in " + repr(f.full_name) + "!"
18601867

1868+
if f.critical_section:
1869+
data.lock.append('Py_BEGIN_CRITICAL_SECTION({self_name});')
1870+
data.unlock.append('Py_END_CRITICAL_SECTION();')
1871+
18611872
last_group = 0
18621873
first_optional = len(selfless)
18631874
positional = selfless and selfless[-1].is_positional_only()
@@ -1937,6 +1948,8 @@ def render_function(
19371948
template_dict['post_parsing'] = format_escape("".join(data.post_parsing).rstrip())
19381949
template_dict['cleanup'] = format_escape("".join(data.cleanup))
19391950
template_dict['return_value'] = data.return_value
1951+
template_dict['lock'] = "\n".join(data.lock)
1952+
template_dict['unlock'] = "\n".join(data.unlock)
19401953

19411954
# used by unpack tuple code generator
19421955
unpack_min = first_optional
@@ -1961,6 +1974,8 @@ def render_function(
19611974
modifications=template_dict['modifications'],
19621975
post_parsing=template_dict['post_parsing'],
19631976
cleanup=template_dict['cleanup'],
1977+
lock=template_dict['lock'],
1978+
unlock=template_dict['unlock'],
19641979
)
19651980

19661981
# Only generate the "exit:" label
@@ -2954,6 +2969,7 @@ class Function:
29542969
# functions with optional groups because we can't represent
29552970
# those accurately with inspect.Signature in 3.4.
29562971
docstring_only: bool = False
2972+
critical_section: bool = False
29572973

29582974
def __post_init__(self) -> None:
29592975
self.parent = self.cls or self.module
@@ -5108,6 +5124,7 @@ class DSLParser:
51085124
coexist: bool
51095125
parameter_continuation: str
51105126
preserve_output: bool
5127+
critical_section: bool
51115128
from_version_re = re.compile(r'([*/]) +\[from +(.+)\]')
51125129

51135130
def __init__(self, clinic: Clinic) -> None:
@@ -5142,6 +5159,7 @@ def reset(self) -> None:
51425159
self.forced_text_signature: str | None = None
51435160
self.parameter_continuation = ''
51445161
self.preserve_output = False
5162+
self.critical_section = False
51455163

51465164
def directive_version(self, required: str) -> None:
51475165
global version
@@ -5270,6 +5288,9 @@ def at_classmethod(self) -> None:
52705288
fail("Can't set @classmethod, function is not a normal callable")
52715289
self.kind = CLASS_METHOD
52725290

5291+
def at_critical_section(self) -> None:
5292+
self.critical_section = True
5293+
52735294
def at_staticmethod(self) -> None:
52745295
if self.kind is not CALLABLE:
52755296
fail("Can't set @staticmethod, function is not a normal callable")
@@ -5492,7 +5513,8 @@ def state_modulename_name(self, line: str) -> None:
54925513
return_converter = CReturnConverter()
54935514

54945515
self.function = Function(name=function_name, full_name=full_name, module=module, cls=cls, c_basename=c_basename,
5495-
return_converter=return_converter, kind=self.kind, coexist=self.coexist)
5516+
return_converter=return_converter, kind=self.kind, coexist=self.coexist,
5517+
critical_section=self.critical_section)
54965518
self.block.signatures.append(self.function)
54975519

54985520
# insert a self converter automatically

0 commit comments

Comments
 (0)