summaryrefslogtreecommitdiffstats
path: root/vcl/README.lifecycle.md
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /vcl/README.lifecycle.md
parentInitial commit. (diff)
downloadlibreoffice-upstream/4%7.4.7.tar.xz
libreoffice-upstream/4%7.4.7.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--vcl/README.lifecycle.md357
1 files changed, 357 insertions, 0 deletions
diff --git a/vcl/README.lifecycle.md b/vcl/README.lifecycle.md
new file mode 100644
index 000000000..8b0e5de2d
--- /dev/null
+++ b/vcl/README.lifecycle.md
@@ -0,0 +1,357 @@
+# 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?
+
+* 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.
+
+## 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 grep`ing 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`