Skip to content

Python submodules initialization fixes backport to 4.x#21489

Merged
opencv-pushbot merged 1 commit intoopencv:4.xfrom
VadimLevin:dev/vlevin/pysubmodules-initialization-fix-backport
Jan 27, 2022
Merged

Python submodules initialization fixes backport to 4.x#21489
opencv-pushbot merged 1 commit intoopencv:4.xfrom
VadimLevin:dev/vlevin/pysubmodules-initialization-fix-backport

Conversation

@VadimLevin
Copy link
Copy Markdown
Contributor

  • Add special case handling when submodule has the same name as parent
  • PyDict_SetItemString doesn't steal reference, so reference count should be explicitly decremented to transfer object life-time ownership
  • Add sanity checks for module registration input

Backport: #21478

Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

msg="Module is not generated for nested namespace")
self.assertTrue(hasattr(cv.utils.nested, "testEchoBooleanFunction"),
msg="Function in nested module is not available")
self.assertEqual(sys.getrefcount(cv.utils.nested), 3,
Copy link
Copy Markdown
Contributor Author

@VadimLevin VadimLevin Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In contrast with 3.4 branch - reference count should be 3, due to changes in how submodules are exported to package.

@VadimLevin
Copy link
Copy Markdown
Contributor Author

Investigation is required why Python 2 and Python 3 reports different reference counting for the nested submodule.
For version Python 2: nested submodule has reference count 2.
For version Python 3: nested submodule has reference count 3.

@alalek
Copy link
Copy Markdown
Member

alalek commented Jan 21, 2022

Could we add detection of "loader" instead of Python version?

Update: Currently both Python2/3 using loader.

@VadimLevin
Copy link
Copy Markdown
Contributor Author

VadimLevin commented Jan 21, 2022

Could we add detection of "loader" instead of Python version?

I want to understand why it happens.
The logic why reference counter should be 3 is the following:

  • cv2.utils.nested
  • cv2._native.utils.nested
  • 1 extra increment from the getrefcount.

Apart from that, the logic for 2 is also valid: cv2.utils and cv2._native.utils should refer to the same module, so its reference count should be 3 when calling getrefcount. In this case nested is accessible only from utils, so no extra increments happen on this level. But this is very confusing....

import cv2
import sys
print(sys.getrefcount(cv2.utils))  # Python 3 outputs: 3 | Python 2 outputs: 4
print(sys.getrefcount(cv2.utils) == sys.getrefcount(cv2._native.utils)) # Both versions output: True

@alalek
Copy link
Copy Markdown
Member

alalek commented Jan 26, 2022

@VadimLevin Could you please to finalize this PR? We need this port into 4.x branch for regular "Merge 3.4".

Main difference in Python2/3 is supporting of "Python code" in bindings.

Python 2:

  • cv2.utils is fully "native" (<module 'cv2.utils' (built-in)>), refcount 2
  • cv2.utils.nested is fully "native" too, refcount 2

Python 3:

  • cv2 - loader package
  • cv2.utils - loader's cv2/utils python code (<module 'cv2.utils' from '...python_loader/cv2/utils/__init__.py'>)
  • entries from "native" cv2.utils are copied into the module above
  • cv2.utils.nested refcount is 3 (from native code, from Python extra "module", +1 from the call)

I believe we could add "if" with Python version check for this check.

@VadimLevin VadimLevin force-pushed the dev/vlevin/pysubmodules-initialization-fix-backport branch 2 times, most recently from 4dece69 to 3364d53 Compare January 26, 2022 12:59
@alalek
Copy link
Copy Markdown
Member

alalek commented Jan 26, 2022

OSX crashes with Python2 during the module cleanup.

Some debug logs (not really useful):

Details
# ./setup_vars.sh lldb -- /usr/local/bin/python2 -c 'import cv2'
Setting vars for OpenCV 4.5.5-dev
Append PYTHONPATH: /build/precommit_macosx/build/python_loader
(lldb) target create "/usr/local/bin/python2"
Current executable set to '/usr/local/bin/python2' (x86_64).
(lldb) settings set -- target.run-args  "-c" "import cv2"
(lldb) r
Process 95807 launched: '/usr/local/bin/python2' (x86_64)
Process 95807 stopped
* thread #2, stop reason = exec
    frame #0: 0x000000010000e000 dyld`_dyld_start
dyld`_dyld_start:
->  0x10000e000 <+0>: popq   %rdi
    0x10000e001 <+1>: pushq  $0x0
    0x10000e003 <+3>: movq   %rsp, %rbp
    0x10000e006 <+6>: andq   $-0x10, %rsp
Target 0: (Python) stopped.
(lldb) c
Process 95807 resuming
Process 95807 stopped
* thread #2, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000000010015e4e6 Python`_PyModule_Clear + 94
Python`_PyModule_Clear:
->  0x10015e4e6 <+94>:  movq   0x8(%rdi), %rax
    0x10015e4ea <+98>:  testb  $0x8, 0xab(%rax)
    0x10015e4f1 <+105>: je     0x10015e538               ; <+176>
    0x10015e4f3 <+107>: callq  0x100166370               ; PyString_AsString
Target 0: (Python) stopped.
(lldb) bt
* thread #2, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
  * frame #0: 0x000000010015e4e6 Python`_PyModule_Clear + 94
    frame #1: 0x00000001001bdb03 Python`PyImport_Cleanup + 1027
    frame #2: 0x00000001001c7eff Python`Py_Finalize + 295
    frame #3: 0x00000001001d9bc6 Python`Py_Main + 2342
    frame #4: 0x00007fff20328631 libdyld.dylib`start + 1
(lldb) register read
General Purpose Registers:
       rax = 0x0000000000000001
       rbx = 0x000000010957d5c8
       rcx = 0x00007ffeefbff768
       rdx = 0x0000000000000090
       rdi = 0xf000000010036e86
       rsi = 0x00007ffeefbff760
       rbp = 0x00007ffeefbff7a0
       rsp = 0x00007ffeefbff760
        r8 = 0x0000000000000001
        r9 = 0x000000000000001f
       r10 = 0x00000001003b81a0
       r11 = 0x0000000000003fff
       r12 = 0x00007ffeefbff770
       r13 = 0x00007ffeefbff768
       r14 = 0x0000000100231268  _Py_NoneStruct
       r15 = 0x00007ffeefbff760
       rip = 0x000000010015e4e6  Python`_PyModule_Clear + 94
    rflags = 0x0000000000010297
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

@VadimLevin
Copy link
Copy Markdown
Contributor Author

VadimLevin commented Jan 26, 2022

OSX crashes with Python2 during the module cleanup.

I don't have an access to OSX machine to troubleshoot this crash.
Is it possible to run Python 2 test with PYTHONVERBOSE environment variable set to 2 and redirected stderr to file (python 2> out.txt)?
Or at least a file with a single import if the issue is reproducible in such simple case:

import cv2
For example, on my Ubuntu 20.04 with Python 2.7.18 I've got the following filtered initialization/clean-up output
# trying cv2.x86_64-linux-gnu.so
# trying cv2.so
# trying cv2module.so
# trying cv2.py
# trying cv2.pyc
import cv2 # directory /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__module.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.py
# /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.pyc matches /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.py
import cv2 # precompiled from /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/__init__.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/os.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/os.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/osmodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/os.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/os.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/importlib.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/importlib.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/importlibmodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/importlib.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/importlib.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/sys.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/sys.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/sysmodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/sys.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/sys.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/numpy.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/numpy.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/numpymodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/numpy.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/numpy.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/copy.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/copy.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/copymodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/copy.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/copy.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/platform.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/platform.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/platformmodule.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/platform.py
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/platform.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2module.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.py
# /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.pyc matches /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.py
import cv2.load_config_py2 # precompiled from /home/vlevin/Projects/OpenCV/opencv/build/python_loader/cv2/load_config_py2.pyc
# trying cv2.x86_64-linux-gnu.so
# trying cv2.so
# trying cv2module.so
# trying cv2.py
# trying cv2.pyc
# trying /home/vlevin/Projects/OpenCV/opencv/build/lib/cv2.x86_64-linux-gnu.so
# trying /home/vlevin/Projects/OpenCV/opencv/build/lib/cv2.so
dlopen("/home/vlevin/Projects/OpenCV/opencv/build/lib/cv2.so", 2);
import cv2 # dynamically loaded from /home/vlevin/Projects/OpenCV/opencv/build/lib/cv2.so
#   clear[2] cv2
# cleanup[1] cv2.gapi.streaming
# cleanup[1] cv2.gapi.core.fluid
# cleanup[1] cv2.gapi.onnx
# cleanup[1] cv2.gapi.wip.gst
# cleanup[1] cv2.gapi.core
# cleanup[1] cv2.gapi.render
# cleanup[1] cv2.gapi.oak
# cleanup[1] cv2.gapi.wip.draw
# cleanup[1] cv2.utils.nested
# cleanup[1] cv2
# cleanup[1] cv2.gapi.ie
# cleanup[1] cv2.samples
# cleanup[1] cv2.gapi.wip
# cleanup[1] cv2.gapi.video
# cleanup[1] cv2.ml
# cleanup[1] cv2.utils
# cleanup[1] cv2.dnn
# cleanup[1] cv2.ipp
# cleanup[1] cv2.gapi.own
# cleanup[1] cv2.ogl
# cleanup[1] cv2.cuda
# cleanup[1] cv2.load_config_py2
# cleanup[1] cv2.segmentation
# cleanup[1] cv2.parallel
# cleanup[1] cv2.detail
# cleanup[1] cv2.fisheye
# cleanup[1] cv2.Error
# cleanup[1] cv2.gapi
# cleanup[1] cv2.ocl
# cleanup[1] cv2.flann
# cleanup[1] cv2.videoio_registry
# cleanup[2] cv2.gapi.core.cpu
# cleanup[2] cv2.gapi.render.ocv
# cleanup[2] cv2.gapi.core.ocl
# cleanup[2] cv2.gapi.ie.detail
# cleanup[2] cv2.utils.fs
# cleanup[2] cv2.gapi.wip.onevpl
# cleanup[2] cv2.gapi.own.detail

/// PyDict_SetItemString doesn't steal a reference so the reference counter
/// of the submodule should be decremented to bind submodule lifetime to the
/// parent module
Py_DECREF(submodule);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are OSX logs before and after removal of this line:

Copy link
Copy Markdown
Member

@alalek alalek Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyImport_AddModule() docs:

Return value: Borrowed reference

and description:

Return the module object corresponding to a module name. The name argument
may be of the form package.module. First check the modules dictionary if
there's one there, and if not, create a new one and insert it in the modules
dictionary.

Likely "the modules dictionary" is a global sys.modules. So reference counter should be 2, not 1:

  • one in sys.modules
  • one after PyDict_SetItemString

Py_CLEAR(submodule); above should be dropped too.

Copy link
Copy Markdown
Contributor Author

@VadimLevin VadimLevin Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thank you. I see the logic behind PyImport_AddModule and then PyDict_SetItemString now.

Should I fix the patch to 3.4 first, and then update this PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These patches are independent (as we do manual port to 4.x for this patch). Update this and create separate PR to 3.4.

- Add special case handling when submodule has the same name as parent
- `PyDict_SetItemString` doesn't steal reference, so reference count
  should be explicitly decremented to transfer object life-time
  ownership
- Add sanity checks for module registration input
- Add Python 2 and Python 3 reference counting handling
@VadimLevin VadimLevin force-pushed the dev/vlevin/pysubmodules-initialization-fix-backport branch from 3364d53 to d887306 Compare January 27, 2022 08:06
Copy link
Copy Markdown
Member

@alalek alalek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you 👍

@opencv-pushbot opencv-pushbot merged commit d41fcf7 into opencv:4.x Jan 27, 2022
@alalek alalek mentioned this pull request Feb 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug category: python bindings port/backport done Label for maintainers. Authors of PR can ignore this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants