summaryrefslogtreecommitdiff
path: root/vcl/README.lifecycle
blob: a309b65ef9ead82c7d2533710a08adce6f5b5745 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
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