Skip to content

Commit 7cba87d

Browse files
committed
PyCLIF-C-API feature parity with this pybind11 base.__init__() must be called when overriding __init__ error condition:
* google3/third_party/pybind11/include/pybind11/detail/class.h;l=195-204;rcl=555209523 Motivation: Guard against backsliding (b/296065655). Situation: `CppBase` is a PyCLIF-C-API-wrapped C++ object. What happens when the Python interpreter processes the following code (usually at import time)? ``` class PC(CppBase): pass ``` When the native Python `PC` class is built: * `PC` `tp_new` is set to use `CppBase` `tp_new`, but * `PC` `tp_init` does NOT in any way involve `CppBase` `tp_init`. It is the responsibility of `PC.__init__` to call `CppBase.__init__`, but this is not checked. **This CL adds the missing check.** The approach is: * `PC` `tp_init` is replaced with an "intercept" function. * The intercept function calls the original `PC` `tp_init`. * After that call finishes (and if it was successful), the intercept function checks if the `CppBase` wrapped C++ object was initialized. This approach makes the assumption that `PC` `tp_init` is not also modified elsewhere, validated via TGP testing. The practical benefit of guarding against backsliding (e.g. cl/558247087) is assumed to far outweight the very theoretical risk of that assumption not being true, and even if it is not true, the consequences are unlikely to be harmful. An additional consideration is that the switch to PyCLIF-pybind11 will eliminate this risk entirely. For easy reviewing, this is the generated code implementing the new mechanism: ``` // Intentionally leak the unordered_map: // https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables static auto* derived_tp_init_registry = new std::unordered_map<PyTypeObject*, int(*)(PyObject*, PyObject*, PyObject*)>; static int tp_init_intercepted(PyObject* self, PyObject* args, PyObject* kw) { DCHECK(PyType_Check(self) == 0); const auto derived_tp_init = derived_tp_init_registry->find(Py_TYPE(self)); CHECK(derived_tp_init != derived_tp_init_registry->end()); int status = (*derived_tp_init->second)(self, args, kw); if (status == 0 && reinterpret_cast<wrapper*>(self)->cpp.get() == nullptr) { Py_DECREF(self); PyErr_Format(PyExc_TypeError, "%s.__init__() must be called when" " overriding __init__", wrapper_Type->tp_name); return -1; } return status; } static PyObject* tp_new_impl(PyTypeObject* type, PyObject* args, PyObject* kwds) { if (type->tp_init != tp_init_impl && derived_tp_init_registry->count(type) == 0) { (*derived_tp_init_registry)[type] = type->tp_init; type->tp_init = tp_init_intercepted; } return PyType_GenericNew(type, args, kwds); } ``` Background technical pointers: What happens when a `PC` object is constructed (i.e. `PC()`) in Python? * `_PyObject_MakeTpCall()`: google3/third_party/python_runtime/v3_10/Objects/call.c;l=215;rcl=491965220 * `type_call` `type->tp_new()`: google3/third_party/python_runtime/v3_10/Objects/typeobject.c;l=1123;rcl=491965220 * `type_call` `type->tp_init()`: google3/third_party/python_runtime/v3_10/Objects/typeobject.c;l=1135;rcl=491965220 Side note: * `CppBase` `tp_alloc` is NOT called. Instead, `PyObject* self` is allocated and `\0`-initialized in `PyType_GenericAlloc()`. For the wrapped `clif::Instance<CppBase>` this is Undefined Behavior. We are just getting lucky that it works: * google3/third_party/python_runtime/v3_10/Objects/typeobject.c;l=1166;rcl=491965220 This was noted already here: * google3/third_party/clif/python/gen.py;l=504-506;rcl=551297023 Additional manual testing (go/py311-upgrade): ``` blaze test --python3_version=3.11 --python_mode=unstable third_party/clif/... devtools/clif/... -k ``` * http://sponge2/7f8c0ecd-48c4-4269-8d71-e2e08802d40a The only failure is known and unrelated: b/288436695 PiperOrigin-RevId: 559501787
1 parent 2884d42 commit 7cba87d

File tree

2 files changed

+94
-3
lines changed

2 files changed

+94
-3
lines changed

clif/python/gen.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,14 +398,24 @@ def TypeObject(ht_qualname, tracked_slot_groups,
398398
yield (
399399
'static int tp_init_impl(PyObject* self, PyObject* args, PyObject* kw);'
400400
)
401+
yield (
402+
'static int tp_init_intercepted('
403+
'PyObject* self, PyObject* args, PyObject* kw);'
404+
)
401405
if not iterator:
402406
yield ''
403-
yield '// %s tp_alloc' % pyname
407+
yield '// %s tp_alloc_impl' % pyname
404408
yield (
405409
'static PyObject* tp_alloc_impl(PyTypeObject* type, Py_ssize_t nitems);'
406410
)
407411
tp_slots['tp_alloc'] = 'tp_alloc_impl'
408-
tp_slots['tp_new'] = 'PyType_GenericNew'
412+
yield ''
413+
yield '// %s tp_new_impl' % pyname
414+
yield (
415+
'static PyObject* tp_new_impl(PyTypeObject* type, PyObject* args,'
416+
' PyObject* kwds);'
417+
)
418+
tp_slots['tp_new'] = 'tp_new_impl'
409419
yield ''
410420
# Use dtor for dynamic types (derived) to wind down malloc'ed C++ obj, so
411421
# the C++ dtors are run.
@@ -433,6 +443,7 @@ def TypeObject(ht_qualname, tracked_slot_groups,
433443
# Use delete for static types (not derived), allocated with tp_alloc_impl.
434444
tp_slots['tp_free'] = 'tp_free_impl'
435445
yield ''
446+
yield '// %s tp_free_impl' % pyname
436447
yield 'static void tp_free_impl(void* self) {'
437448
yield I+'delete %s(self);' % _Cast(wname)
438449
yield '}'
@@ -479,6 +490,16 @@ def TypeObject(ht_qualname, tracked_slot_groups,
479490
yield I+'return ty;'
480491
yield '}'
481492
if ctor:
493+
yield ''
494+
yield '// Intentionally leak the unordered_map:'
495+
yield (
496+
'// https://google.github.io/styleguide/cppguide.html'
497+
'#Static_and_Global_Variables'
498+
)
499+
yield (
500+
'static auto* derived_tp_init_registry = new std::unordered_map<'
501+
'PyTypeObject*, int(*)(PyObject*, PyObject*, PyObject*)>;'
502+
)
482503
yield ''
483504
yield (
484505
'static int tp_init_impl('
@@ -530,6 +551,28 @@ def TypeObject(ht_qualname, tracked_slot_groups,
530551
yield I+'Py_XDECREF(init);'
531552
yield I+'return init? 0: -1;'
532553
yield '}'
554+
yield ''
555+
yield (
556+
'static int tp_init_intercepted('
557+
'PyObject* self, PyObject* args, PyObject* kw) {'
558+
)
559+
yield I+'DCHECK(PyType_Check(self) == 0);'
560+
yield (
561+
I+'const auto derived_tp_init = '
562+
'derived_tp_init_registry->find(Py_TYPE(self));'
563+
)
564+
yield I+'CHECK(derived_tp_init != derived_tp_init_registry->end());'
565+
yield I+'int status = (*derived_tp_init->second)(self, args, kw);'
566+
yield I+'if (status == 0 &&'
567+
yield I+' reinterpret_cast<wrapper*>(self)->cpp.get() == nullptr) {'
568+
yield I+' Py_DECREF(self);'
569+
yield I+' PyErr_Format(PyExc_TypeError,'
570+
yield I+' "%s.__init__() must be called when"'
571+
yield I+' " overriding __init__", wrapper_Type->tp_name);'
572+
yield I+' return -1;'
573+
yield I+'}'
574+
yield I+'return status;'
575+
yield '}'
533576
if not iterator:
534577
yield ''
535578
yield (
@@ -543,6 +586,19 @@ def TypeObject(ht_qualname, tracked_slot_groups,
543586
yield I+'PyObject* self = %s(wobj);' % _Cast()
544587
yield I+'return PyObject_Init(self, %s);' % wtype
545588
yield '}'
589+
yield ''
590+
yield (
591+
'static PyObject* tp_new_impl(PyTypeObject* type, PyObject* args,'
592+
' PyObject* kwds) {'
593+
)
594+
if ctor:
595+
yield I+'if (type->tp_init != tp_init_impl &&'
596+
yield I+' derived_tp_init_registry->count(type) == 0) {'
597+
yield I+I+'(*derived_tp_init_registry)[type] = type->tp_init;'
598+
yield I+I+'type->tp_init = tp_init_intercepted;'
599+
yield I+'}'
600+
yield I+'return PyType_GenericNew(type, args, kwds);'
601+
yield '}'
546602

547603

548604
def CreateInputParameter(func_name, ast_param, arg, args):

clif/testing/python/python_multiple_inheritance_test.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from absl.testing import absltest
16+
from absl.testing import parameterized
1617

1718
from clif.testing.python import python_multiple_inheritance as tm
1819

@@ -32,7 +33,25 @@ def __init__(self, value):
3233
tm.CppDrvd.__init__(self, value + 1)
3334

3435

35-
class PythonMultipleInheritanceTest(absltest.TestCase):
36+
class PCExplicitInitWithSuper(tm.CppBase):
37+
38+
def __init__(self, value):
39+
super().__init__(value + 1)
40+
41+
42+
class PCExplicitInitMissingSuper(tm.CppBase):
43+
44+
def __init__(self, value):
45+
del value
46+
47+
48+
class PCExplicitInitMissingSuper2(tm.CppBase):
49+
50+
def __init__(self, value):
51+
del value
52+
53+
54+
class PythonMultipleInheritanceTest(parameterized.TestCase):
3655

3756
def testPC(self):
3857
d = PC(11)
@@ -80,6 +99,22 @@ def testPPCCInit(self):
8099
self.assertEqual(d.get_base_value(), (30, 20)[i])
81100
self.assertEqual(d.get_base_value_from_drvd(), 30)
82101

102+
def testPCExplicitInitWithSuper(self):
103+
d = PCExplicitInitWithSuper(14)
104+
self.assertEqual(d.get_base_value(), 15)
105+
106+
@parameterized.parameters(
107+
PCExplicitInitMissingSuper, PCExplicitInitMissingSuper2
108+
)
109+
def testPCExplicitInitMissingSuper(self, derived_type):
110+
with self.assertRaises(TypeError) as ctx:
111+
derived_type(0)
112+
self.assertEndsWith(
113+
str(ctx.exception),
114+
"python_multiple_inheritance.CppBase.__init__() must be called when"
115+
" overriding __init__",
116+
)
117+
83118

84119
if __name__ == "__main__":
85120
absltest.main()

0 commit comments

Comments
 (0)