diff options
Diffstat (limited to 'vcl/README.lifecycle')
-rw-r--r-- | vcl/README.lifecycle | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/vcl/README.lifecycle b/vcl/README.lifecycle new file mode 100644 index 000000000..a309b65ef --- /dev/null +++ b/vcl/README.lifecycle @@ -0,0 +1,325 @@ +** Understanding transitional VCL lifecycle ** + +---------- How it used to look ---------- + + All VCL classes were explicitly lifecycle managed; so you would +do: + Dialog aDialog(...); // old - on stack allocation + aDialog.Execute(...); +or: + Dialog *pDialog = new Dialog(...); // old - manual heap allocation + pDialog->Execute(...); + delete pDialog; +or: + std::shared_ptr<Dialog> xDialog(new pDialog()); // old + xDialog->Execute(...); + // depending who shared the ptr this would be freed sometime + + In several cases this lead to rather unpleasant code, when +various shared_ptr wrappers were used, the lifecycle was far less than +obvious. Where controls were wrapped by other ref-counted classes - +such as UNO interfaces, which were also used by native Window +pointers, the lifecycle became extremely opaque. In addition VCL had +significant issues with re-enterancy and event emission - adding +various means such as DogTags to try to detect destruction of a window +between calls: + + ImplDelData aDogTag( this ); // 'orrible old code + Show( true, ShowFlags::NoActivate ); + if( !aDogTag.IsDead() ) // did 'this' go invalid yet ? + Update(); + + Unfortunately use of such protection is/was ad-hoc, and far +from uniform, despite the prevalence of such potential problems. + + When a lifecycle problem was hit, typically it would take the +form of accessing memory that had been freed, and contained garbage due +to lingering pointers to freed objects. + + +---------- Where we are now: ---------- + + To fix this situation we now have a VclPtr - which is a smart + reference-counting pointer (include/vcl/vclptr.hxx) which is + designed to look and behave -very- much like a normal pointer + to reduce code-thrash. VclPtr is used to wrap all OutputDevice + derived classes thus: + + VclPtr<Dialog> pDialog( new Dialog( ... ), SAL_NO_ACQUIRE ); + ... + pDialog.disposeAndClear(); + + However - while the VclPtr reference count controls the + lifecycle of the Dialog object, it is necessary to be able to + break reference count cycles. These are extremely common in + widget hierarchies as each widget holds (smart) pointers to + its parents and also its children. + + Thus - all previous 'delete' calls are replaced with 'dispose' + method calls: + +** What is dispose ? + + Dispose is defined to be a method that releases all references + that an object holds - thus allowing their underlying + resources to be released. However - in this specific case it + also releases all backing graphical resources. In practical + terms, all destructor functionality has been moved into + 'dispose' methods, in order to provide a minimal initial + behavioral change. + + As such a VclPtr can have three states: + + VclPtr<PushButton> pButton; + ... + assert (pButton == nullptr || !pButton); // null + assert (pButton && !pButton->IsDisposed()); // alive + assert (pButton && pButton->IsDisposed()); // disposed + +** ScopedVclPtr - making disposes easier + + While replacing existing code with new, it can be a bit + tiresome to have to manually add 'disposeAndClear()' + calls to VclPtr<> instances. + + Luckily it is easy to avoid that with a ScopedVclPtr which + does this for you when it goes out of scope. + +** One extra gotcha - an initial reference-count of 1 + + In the normal world of love and sanity, eg. creating UNO + objects, the objects start with a ref-count of zero. Thus + the first reference is always taken after construction by + the surrounding smart pointer. + + Unfortunately, the existing VCL code is somewhat tortured, + and does a lot of reference and de-reference action on the + class -during- construction. This forces us to construct with + a reference of 1 - and to hand that into the initial smart + pointer with a SAL_NO_ACQUIRE. + + To make this easier, we have 'Instance' template wrappers + that make this apparently easier, by constructing the + pointer for you. + +** How does my familiar code change ? + + Lets tweak the exemplary code above to fit the new model: + +- Dialog aDialog(... dialog params ... ); +- aDialog.Execute(...); ++ ScopedVclPtrInstance<Dialog> pDialog(... dialog params ... ); ++ pDialog->Execute(...); // VclPtr behaves much like a pointer + +or: +- Dialog *pDialog = new Dialog(... dialog params ...); ++ VclPtrInstance<Dialog> pDialog(... dialog params ...); + pDialog->Execute(...); +- delete pDialog; ++ pDialog.disposeAndClear(); // done manually - replaces a delete +or: +- std::shared_ptr<Dialog> xDialog(new Dialog(...)); ++ ScopedVclPtrInstance<Dialog> xDialog(...); + xDialog->Execute(...); ++ // depending how shared_ptr was shared perhaps ++ // someone else gets a VclPtr to xDialog +or: +- VirtualDevice aDev; ++ ScopedVclPtrInstance<VirtualDevice> pDev; + + Other things that are changed are these: + +- pButton = new PushButton(NULL); ++ pButton = VclPtr<PushButton>::Create(nullptr); +... +- vcl::Window *pWindow = new PushButton(NULL); ++ VclPtr<vcl::Window> pWindow; ++ pWindow.reset(VclPtr<PushButton>::Create(nullptr)); + +** Why are these 'disposeOnce' calls in destructors ? + + This is an interim measure while we are migrating, such that + it is possible to delete an object conventionally and ensure + that its dispose method gets called. In the 'end' we would + instead assert that a Window has been disposed in its + destructor, and elide these calls. + + As the object's vtable is altered as we go down the + destruction process, and we want to call the correct dispose + methods we need this disposeOnce(); call for the interim in + every destructor. This is enforced by a clang plugin. + + The plus side of disposeOnce is that the mechanics behind it + ensure that a dispose() method is only called a single time, + simplifying their implementation. + + +---------- Who owns & disposes what ? ---------- + + Window sub-classes tend to create their widgets in one of two +ways and often both. + + 1. Derive from VclBuilderContainer. The VclBuilder then owns + many of the sub-windows, which are fetched by a 'get' + method into local variables often in constructors eg. + + VclPtr<PushButton> mpButton; // in the class + , get(mpButton, "buttonName") // in the constructor + mpButton.clear(); // in dispose. + + We only clear, not disposeAndClear() in our dispose method + for this case, since the VclBuilder / Container truly owns + this Window, and needs to dispose its hierarchy in the + right order - first children then parents. + + 2. Explicitly allocated Windows. These are often created and + managed by custom widgets: + + VclPtr<ComplexWidget> mpComplex; // in the class + , mpComplex( VclPtr<ComplexWidget>::Create( this ) ) // constructor + mpComplex.disposeAndClear(); // in dispose + + ie. an owner has to dispose things they explicitly allocate. + + In order to ensure that the VclBuilderConstructor + sub-classes have their Windows disposed at the correct time + there is a disposeBuilder(); method - that should be added + -only- to the class immediately deriving from + VclBuilderContainer's dispose. + +---------- What remains to be done ? ---------- + + * Cleanup DogTags + + * Expand the VclPtr pattern to many other less + than safe VCL types. + + * create factory functions for VclPtr<> types and privatize + their constructors. + + * Pass 'const VclPtr<> &' instead of pointers everywhere + + add 'explicit' keywords to VclPtr constructors to + accelerate compilation etc. + + * Cleanup common existing methods such that they continue to + work post-dispose. + + * Dispose functions should be audited to: + + not leave dangling pointsr + + shrink them - some work should incrementally + migrate back to destructors. + + * VclBuilder + + ideally should keep a reference to pointers assigned + in 'get()' calls - to avoid needing explicit 'clear' + code in destructors. + + * VclBuilder 'makeFoo' methods + + these should return VclPtr<> types and have their + signatures adjusted en-masse. + + currently we use a VclPtr<> constructor with + SAL_NO_ACQUIRE inside the builder. + +---------- FAQ / debugging hints ---------- + +** Compile with dbgutil + + This is by far the best way to turn on debugging and + assertions that help you find problems. In particular + there are a few that are really helpful: + + vcl/source/window/window.cxx (Window::dispose) + "Window ( N4sfx27sidebar20SidebarDockingWindowE (Properties)) + ^^^ class name window title ^^^ + with live children destroyed: N4sfx27sidebar6TabBarE () + N4sfx27sidebar4DeckE () 10FixedImage ()" + + You can de-mangle these names if you can't read them thus: + + $ c++filt -t N4sfx27sidebar20SidebarDockingWindowE + sfx2::sidebar::SidebarDockingWindow + + In the above case - it is clear that the children have not been + disposed before their parents. As an aside, having a dispose chain + separate from destructors allows us to emit real type names for + parents here. + + To fix this, we will need to get the dispose ordering right, + occasionally in the conversion we re-ordered destruction, or + omitted a disposeAndClear() in a ::dispose() method. + + => If you see this, check the order of disposeAndClear() in + the sfx2::Sidebar::SidebarDockingWindow::dispose() method + + => also worth git grepping for 'new sfx::sidebar::TabBar' to + see where those children were added. + +** Check what it used to do + + While a ton of effort has been put into ensuring that the new + lifecycle code is the functional equivalent of the old code, + the code was created by humans. If you identify an area where + something asserts or crashes here are a few helpful heuristics: + + * Read the git log -u -- path/to/file.cxx + + => Is the order of destruction different ? + + in the past many things were destructed (in reverse order of + declaration in the class) without explicit code. Some of these + may be important to do explicitly at the end of the destructor. + + eg. having a 'Idle' or 'Timer' as a member, may now need an + explicit .Stop() and/or protection from running on a + disposed Window in its callback. + + => Is it 'clear' not 'disposeAndClear' ? + + sometimes we get this wrong. If the code previously used to + use 'delete pFoo;' it should now read pFoo->disposeAndClear(); + Conversely if it didn't delete it, it should be 'clear()' it + is by far the best to leave disposing to the VclBuilder where + possible. + + In simple cases, if we allocate the widget with VclPtrInstance + or VclPtr<Foo>::Create - then we need to disposeAndClear it too. + +** Event / focus / notification ordering + + In the old world, a large amount of work was done in the + ~Window destructor that is now done in Window::dispose. + + Since those Windows were in the process of being destroyed + themselves, their vtables were adjusted to only invoke Window + methods. In the new world, sub-classed methods such as + PreNotify, GetFocus, LoseFocus and others are invoked all down + the inheritance chain from children to parent, during dispose. + + The easiest way to fix these is to just ensure that these + cleanup methods, especially LoseFocus, continue to work even + on disposed Window sub-class instances. + +** It crashes with some invalid memory... + + Assuming that the invalid memory is a Window sub-class itself, + then almost certainly there is some cockup in the + reference-counting; eg. if you hit an OutputDevice::release + assert on mnRefCount - then almost certainly you have a + Window that has already been destroyed. This can easily + happen via this sort of pattern: + + Dialog *pDlg = VclPtr<Dialog>(nullptr /* parent */); + // by here the pDlg quite probably points to free'd memory... + + It is necessary in these cases to ensure that the *pDlg is + a VclPtr<Dialog> instead. + +** It crashes with some invalid memory #2... + + Often a ::dispose method will free some pImpl member, but + not NULL it; and (cf. above) we can now get various virtual + methods called post-dispose; so: + + a) delete pImpl; pImpl = NULL; // in the destructor + b) if (pImpl && ...) // in the subsequently called method + |