Resolves tdf#133265 - Save state indicator must not be hidden
[LibreOffice.git] / vcl / README.lifecycle
bloba309b65ef9ead82c7d2533710a08adce6f5b5745
1 ** Understanding transitional VCL lifecycle **
3 ---------- How it used to look ----------
5         All VCL classes were explicitly lifecycle managed; so you would
6 do:
7         Dialog aDialog(...);   // old - on stack allocation
8         aDialog.Execute(...);
9 or:
10         Dialog *pDialog = new Dialog(...);  // old - manual heap allocation
11         pDialog->Execute(...);
12         delete pDialog;
13 or:
14         std::shared_ptr<Dialog> xDialog(new pDialog()); // old
15         xDialog->Execute(...);
16         // depending who shared the ptr this would be freed sometime
18         In several cases this lead to rather unpleasant code, when
19 various shared_ptr wrappers were used, the lifecycle was far less than
20 obvious. Where controls were wrapped by other ref-counted classes -
21 such as UNO interfaces, which were also used by native Window
22 pointers, the lifecycle became extremely opaque. In addition VCL had
23 significant issues with re-enterancy and event emission - adding
24 various means such as DogTags to try to detect destruction of a window
25 between calls:
27         ImplDelData aDogTag( this );    // 'orrible old code
28         Show( true, ShowFlags::NoActivate );
29         if( !aDogTag.IsDead() )         // did 'this' go invalid yet ?
30                 Update();
32         Unfortunately use of such protection is/was ad-hoc, and far
33 from uniform, despite the prevalence of such potential problems.
35         When a lifecycle problem was hit, typically it would take the
36 form of accessing memory that had been freed, and contained garbage due
37 to lingering pointers to freed objects.
40 ---------- Where we are now: ----------
42         To fix this situation we now have a VclPtr - which is a smart
43         reference-counting pointer (include/vcl/vclptr.hxx) which is
44         designed to look and behave -very- much like a normal pointer
45         to reduce code-thrash. VclPtr is used to wrap all OutputDevice
46         derived classes thus:
48         VclPtr<Dialog> pDialog( new Dialog( ... ), SAL_NO_ACQUIRE );
49         ...
50         pDialog.disposeAndClear();
52         However - while the VclPtr reference count controls the
53         lifecycle of the Dialog object, it is necessary to be able to
54         break reference count cycles. These are extremely common in
55         widget hierarchies as each widget holds (smart) pointers to
56         its parents and also its children.
58         Thus - all previous 'delete' calls are replaced with 'dispose'
59         method calls:
61 ** What is dispose ?
63         Dispose is defined to be a method that releases all references
64         that an object holds - thus allowing their underlying
65         resources to be released. However - in this specific case it
66         also releases all backing graphical resources. In practical
67         terms, all destructor functionality has been moved into
68         'dispose' methods, in order to provide a minimal initial
69         behavioral change.
71         As such a VclPtr can have three states:
73         VclPtr<PushButton> pButton;
74         ...
75         assert (pButton == nullptr || !pButton);    // null
76         assert (pButton && !pButton->IsDisposed()); // alive
77         assert (pButton &&  pButton->IsDisposed()); // disposed
79 ** ScopedVclPtr - making disposes easier
81         While replacing existing code with new, it can be a bit
82         tiresome to have to manually add 'disposeAndClear()'
83         calls to VclPtr<> instances.
85         Luckily it is easy to avoid that with a ScopedVclPtr which
86         does this for you when it goes out of scope.
88 ** One extra gotcha - an initial reference-count of 1
90         In the normal world of love and sanity, eg. creating UNO
91         objects, the objects start with a ref-count of zero. Thus
92         the first reference is always taken after construction by
93         the surrounding smart pointer.
95         Unfortunately, the existing VCL code is somewhat tortured,
96         and does a lot of reference and de-reference action on the
97         class -during- construction. This forces us to construct with
98         a reference of 1 - and to hand that into the initial smart
99         pointer with a SAL_NO_ACQUIRE.
101         To make this easier, we have 'Instance' template wrappers
102         that make this apparently easier, by constructing the
103         pointer for you.
105 ** How does my familiar code change ?
107         Lets tweak the exemplary code above to fit the new model:
109 -       Dialog aDialog(... dialog params ... );
110 -       aDialog.Execute(...);
111 +       ScopedVclPtrInstance<Dialog> pDialog(... dialog params ... );
112 +       pDialog->Execute(...); // VclPtr behaves much like a pointer
115 -       Dialog *pDialog = new Dialog(... dialog params ...);
116 +       VclPtrInstance<Dialog> pDialog(... dialog params ...);
117         pDialog->Execute(...);
118 -       delete pDialog;
119 +       pDialog.disposeAndClear(); // done manually - replaces a delete
121 -       std::shared_ptr<Dialog> xDialog(new Dialog(...));
122 +       ScopedVclPtrInstance<Dialog> xDialog(...);
123         xDialog->Execute(...);
124 +       // depending how shared_ptr was shared perhaps
125 +       // someone else gets a VclPtr to xDialog
127 -       VirtualDevice aDev;
128 +       ScopedVclPtrInstance<VirtualDevice> pDev;
130         Other things that are changed are these:
132 -       pButton = new PushButton(NULL);
133 +       pButton = VclPtr<PushButton>::Create(nullptr);
135 -       vcl::Window *pWindow = new PushButton(NULL);
136 +       VclPtr<vcl::Window> pWindow;
137 +       pWindow.reset(VclPtr<PushButton>::Create(nullptr));
139 ** Why are these 'disposeOnce' calls in destructors ?
141         This is an interim measure while we are migrating, such that
142         it is possible to delete an object conventionally and ensure
143         that its dispose method gets called. In the 'end' we would
144         instead assert that a Window has been disposed in its
145         destructor, and elide these calls.
147         As the object's vtable is altered as we go down the
148         destruction process, and we want to call the correct dispose
149         methods we need this disposeOnce(); call for the interim in
150         every destructor. This is enforced by a clang plugin.
152         The plus side of disposeOnce is that the mechanics behind it
153         ensure that a dispose() method is only called a single time,
154         simplifying their implementation.
157 ---------- Who owns & disposes what ? ----------
159         Window sub-classes tend to create their widgets in one of two
160 ways and often both.
162         1. Derive from VclBuilderContainer. The VclBuilder then owns
163            many of the sub-windows, which are fetched by a 'get'
164            method into local variables often in constructors eg.
166            VclPtr<PushButton> mpButton;  // in the class
167            , get(mpButton, "buttonName") // in the constructor
168            mpButton.clear();             // in dispose.
170            We only clear, not disposeAndClear() in our dispose method
171            for this case, since the VclBuilder / Container truly owns
172            this Window, and needs to dispose its hierarchy in the
173            right order - first children then parents.
175          2. Explicitly allocated Windows. These are often created and
176             managed by custom widgets:
178             VclPtr<ComplexWidget> mpComplex;                     // in the class
179             , mpComplex( VclPtr<ComplexWidget>::Create( this ) ) // constructor
180             mpComplex.disposeAndClear();                         // in dispose
182             ie. an owner has to dispose things they explicitly allocate.
184           In order to ensure that the VclBuilderConstructor
185           sub-classes have their Windows disposed at the correct time
186           there is a disposeBuilder(); method - that should be added
187           -only- to the class immediately deriving from
188           VclBuilderContainer's dispose.
190 ---------- What remains to be done ? ----------
192         * Cleanup DogTags
194         * Expand the VclPtr pattern to many other less
195           than safe VCL types.
197         * create factory functions for VclPtr<> types and privatize
198           their constructors.
200         * Pass 'const VclPtr<> &' instead of pointers everywhere
201                 + add 'explicit' keywords to VclPtr constructors to
202                   accelerate compilation etc.
204         * Cleanup common existing methods such that they continue to
205           work post-dispose.
207         * Dispose functions should be audited to:
208                 + not leave dangling pointsr
209                 + shrink them - some work should incrementally
210                   migrate back to destructors.
212         * VclBuilder
213                 + ideally should keep a reference to pointers assigned
214                   in 'get()' calls - to avoid needing explicit 'clear'
215                   code in destructors.
217         * VclBuilder 'makeFoo' methods
218                 + these should return VclPtr<> types and have their
219                   signatures adjusted en-masse.
220                 + currently we use a VclPtr<> constructor with
221                   SAL_NO_ACQUIRE inside the builder.
223 ---------- FAQ / debugging hints ----------
225 ** Compile with dbgutil
227         This is by far the best way to turn on debugging and
228         assertions that help you find problems. In particular
229         there are a few that are really helpful:
231         vcl/source/window/window.cxx (Window::dispose)
232                 "Window ( N4sfx27sidebar20SidebarDockingWindowE (Properties))
233                           ^^^ class name                 window title ^^^
234                  with live children destroyed:  N4sfx27sidebar6TabBarE ()
235                  N4sfx27sidebar4DeckE () 10FixedImage ()"
237         You can de-mangle these names if you can't read them thus:
239         $ c++filt -t N4sfx27sidebar20SidebarDockingWindowE
240         sfx2::sidebar::SidebarDockingWindow
242         In the above case - it is clear that the children have not been
243         disposed before their parents. As an aside, having a dispose chain
244         separate from destructors allows us to emit real type names for
245         parents here.
247         To fix this, we will need to get the dispose ordering right,
248         occasionally in the conversion we re-ordered destruction, or
249         omitted a disposeAndClear() in a ::dispose() method.
251         => If you see this, check the order of disposeAndClear() in
252            the sfx2::Sidebar::SidebarDockingWindow::dispose() method
254         => also worth git grepping for 'new sfx::sidebar::TabBar' to
255            see where those children were added.
257 ** Check what it used to do
259         While a ton of effort has been put into ensuring that the new
260         lifecycle code is the functional equivalent of the old code,
261         the code was created by humans. If you identify an area where
262         something asserts or crashes here are a few helpful heuristics:
264         * Read the git log -u -- path/to/file.cxx
266         => Is the order of destruction different ?
268            in the past many things were destructed (in reverse order of
269            declaration in the class) without explicit code. Some of these
270            may be important to do explicitly at the end of the destructor.
272            eg. having a 'Idle' or 'Timer' as a member, may now need an
273                explicit .Stop() and/or protection from running on a
274                disposed Window in its callback.
276         => Is it 'clear' not 'disposeAndClear' ?
278            sometimes we get this wrong. If the code previously used to
279            use 'delete pFoo;' it should now read pFoo->disposeAndClear();
280            Conversely if it didn't delete it, it should be 'clear()' it
281            is by far the best to leave disposing to the VclBuilder where
282            possible.
284            In simple cases, if we allocate the widget with VclPtrInstance
285            or VclPtr<Foo>::Create - then we need to disposeAndClear it too.
287 ** Event / focus / notification ordering
289         In the old world, a large amount of work was done in the
290         ~Window destructor that is now done in Window::dispose.
292         Since those Windows were in the process of being destroyed
293         themselves, their vtables were adjusted to only invoke Window
294         methods. In the new world, sub-classed methods such as
295         PreNotify, GetFocus, LoseFocus and others are invoked all down
296         the inheritance chain from children to parent, during dispose.
298         The easiest way to fix these is to just ensure that these
299         cleanup methods, especially LoseFocus, continue to work even
300         on disposed Window sub-class instances.
302 ** It crashes with some invalid memory...
304     Assuming that the invalid memory is a Window sub-class itself,
305         then almost certainly there is some cockup in the
306         reference-counting; eg. if you hit an OutputDevice::release
307         assert on mnRefCount - then almost certainly you have a
308         Window that has already been destroyed. This can easily
309         happen via this sort of pattern:
311         Dialog *pDlg = VclPtr<Dialog>(nullptr /* parent */);
312         // by here the pDlg quite probably points to free'd memory...
314         It is necessary in these cases to ensure that the *pDlg is
315         a VclPtr<Dialog> instead.
317 ** It crashes with some invalid memory #2...
319         Often a ::dispose method will free some pImpl member, but
320         not NULL it; and (cf. above) we can now get various virtual
321         methods called post-dispose; so:
323         a) delete pImpl; pImpl = NULL; // in the destructor
324         b) if (pImpl && ...)           // in the subsequently called method