Understanding wxPyBeginBlockThreads…

Some background, when loading a .py file with my AutoCAD plugin, the file is parsed and register commansd in AutoCAD
Users can call newly created commands at the command line.

as sample .py might look like:

import wx
from wx import xrc

import PyRxApp# = all the global methods like acutPrintf,
import PyRx# = Runtime runtime
import PyGe# = Geometry
import PyGi# = Graphics interface
import PyDb# = database
import PyAp# = application, document classes services
import PyEd# = editor

def OnPyInitApp():
    print("\nOnPyInitApp")

def OnPyUnloadApp():
    print("\nOnPyUnloadApp")

def OnPyLoadDwg():
    print("\nOnPyLoadDwg")

def OnPyUnloadDwg():
   print("\nOnPyUnloadDwg")

#command to launch the dialog
def PyRxCmd_wxpyxrc():
    try: 
        dlg = TestDialog(None, -1, "")
        if dlg.ShowModal() == wx.ID_OK:
            print('woohoo')
    except Exception as err:
        print(err)
    finally:
        # explicitly cause the dialog to destroy itself
        dlg.Destroy()

class TestDialog(wx.Dialog):
    def __init__(
            self, parent, id, title, size=wx.DefaultSize, pos=wx.DefaultPosition,
            style=wx.DEFAULT_DIALOG_STYLE, name='DlgTable'):
        wx.Dialog.__init__(self)   
        self.res = xrc.XmlResource('C:/Users/Dan/Documents/DialogBlocks Projects/table dlg/wxg_dlg_inserttable.xrc')
        self.res.LoadDialog(self, parent, "DLG_INSERTTABLE")

where ever my plugin calls into python, I’ve added a class WxPyAutoLock, just matching wxPyBeginBlockThreads / wxPyEndBlockThreads

the C++ side looks like:

static void AcRxPyApp_pyfunc(void)
    {
        if (curDoc() != nullptr)
        {
            const AcString cmdName = commandForCurDocument();
            if (PyRxApp::instance().commands.contains(cmdName))
            {
                try
                {
                    PyObject* method = PyRxApp::instance().commands.at(cmdName);
                    if (method != nullptr)
                    {
                        WxPyAutoLock lock;
                        PyErr_Clear();
                        if (PyCallable_Check(method))
                        {
                            PyObjectPtr rslt(PyObject_CallFunction(method, NULL));
                            if (rslt != nullptr)
                                return;
                        }
                    }
                }
                catch (...)
                {
                   acutPrintf(_T("\npyfunc failed with exception: "));
                }
            }
        }
        acutPrintf(_T("\npyfunc failed: "));
    }

it works fine so far.
I’m just trying to wrap my head around what’s going on here, is this correct? What happens if I launch a modeless dialog where the lock is free and the user might call back into C++? Am I setting myself up for disaster?

well, what does your XRC source for the dialog looks like :roll_eyes:

just a sample dialog I had from widgets, interacts with AutoCAD
I guess I’ll just make tests and see if something goes wonky.

I’m using boost::python for my C++ CAD wrappers, hopefully its aware of the locking mechanism

wxPython2

I had expected an XML file, not a picture
well, if

I would definitely fully trust the magic, be it AutoCAD, C++, Python, XRC or whatever boosted :rofl:

Here it is, created in dialogblocks, which is a wonderful program BTW
but I’m not sure how it’s relevant in helping me understand the locks.

AcRxPyApp_pyfunc applies a lock, then calls the python function PyRxCmd_wxpyxrc, that shows a dialog.

1, If the dialog is modal, AcRxPyApp_pyfunc waits for the dialog and releases the lock.
2, if the dialog is modeless, AcRxPyApp_pyfunc finishes and the lock is released, leaving an open dialog with no lock.

So now, I have two possible lock states, where the dialog may call back into my C++ wrappers (boost::python)

3, does wxPython actually use threads? Or is this for protection from something wonky going on.
4, if so, where does wxPython use them, so I create a unit tests.
wxg_dlg_inserttable.zip (3.1 KB)

sorry, was just to make sure where we are :confounded:
generally, I’m pleased to see in what applications this very excellent PyApp (and with it wx of course) is used and if this Ac… is properly done put the docu under your pillow & enjoy the magic cooperation of AutoCAD and wxPython :sunglasses:

In general, the GIL rules go like this:

  1. When Python calls into C/C++ code, it should release the GIL so other Python threads can be run while the current thread is executing the C++.
  2. Before that C++ code returns to Python, the thread will need to reacquire the GIL.
  3. If that C++ code needs to call any Python APIs, or do any Py_DECREFs, then it needs to first acquire the GIL to ensure that no other threads are running Python code, and then release the GIL again when it is done with the Python APIs.

in wxPython #1 and #2 are handled by the wrapper code generated by sip using the stock Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS macros provided by Python.

For #3 in hand-written C/C++ code in wxPython we have the wxPyBeginBlockThreads and wxPyEndBlockThreads functions to help make this a little simpler. And later the wxPyThreadBlocker RAII class was added to make it even easier. This sounds like what your
WxPyAutoLock is intended to do. You can see its implementation in wxpy_api.h.

If you follow the same rules in your C++ code then everything should play together nicely.

There is one exception that comes to mind, and that is if Python is embedded in a C++ application. Then you need to release the GIL after you’ve initialized Python, so it can then follow the regular pattern after things start up. See samples/embedded/embeded.cpp.

1 Like

Thanks for the detailed explanation! I’ve made unit tests for the cases I was worried about, and everything seems to be working great.

The only thing I need to work through is shutting down cleanly
There’s heap corruption in wxUninitialize() , if I comment that out Py_FinalizeEx() reports the same error.
My guess is that since I’m wrapping AutoCAD’s main frame in a wxFrame, I’m not able to easily control the shutdown order

It’s about 20 years since I did some experiments with embedding and extending python code with C code. I don’t remember any of the details but I do remember that when I experienced core dumps while terminating the application it was often due to not managing python reference counts correctly - either decrementing a reference count that shouldn’t have been decremented, or not decrementing a reference count that should have been decremented. I seem to remember that some python api calls would automatically increment/decrement reference counts for you while other you had to manage yourself. Unfortunately I no longer have any of the code from that work.

1 Like