-
Notifications
You must be signed in to change notification settings - Fork 0
/
rustgc_paper.ltx
2529 lines (2119 loc) · 118 KB
/
rustgc_paper.ltx
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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
%&rustgc_paper_preamble
\endofdump
\begin{document}
\begin{abstract}
\noindent Rust is a non-Garbage Collected (GCed) language, but the lack of GC
makes expressing data-structures whose values have multiple owners awkward, inefficient, or both.
In this paper we explore a new design for, and implementation of GC in Rust, called
\ourgc, identifying the key challenge as finalisation. Unlike previous
approaches to GC in Rust, \ourgc maps existing Rust destructors to
finalisers: this makes GC in Rust natural to use but introduces surprising
soundness, performance, and ergonomic problems. \ourgc provides solutions for
each of these problems.
\end{abstract}
\maketitle
\section{Introduction}
\begin{figure}[t]
\lstinputlisting[language=Rust, firstline=6]{listings/first_example.rs}
\captionof{lstlisting}{An \ourgc example, showing use of the \lstinline{Gc<T>}
type and destructors as finalisers. We create a type \lstinline{GcNode} which
models a graph: it stores an 8 bit integer value and a reference (possibly
null, via Rust's standard \lstinline{Option} type) to a neighbouring node
(line 1). We add a normal Rust destructor which \ourgc is able to use as a
finaliser when \lstinline{GcNode} is used inside \lstinline{Gc<T>} (line 2).
Inside \lstinline{main} we create the first GCed node in the graph (line 5).
We use Rust's normal \lstinline{RefCell} type to allow the node to be mutated
(using the \lstinline{RefCell::borrow\_mut} method to dynamically detect mutation that
would undermine Rust's static borrow checker rules)
and pointed to itself (line 6): i.e.~we create a cycle using
\lstinline{Gc<T>}. Cycles such as this cannot be created in the base Rust
language without \lstinline{unsafe} code. One can create cycles with standard
library types such as the reference counting \lstinline{Rc<T>}, though these
require careful, laborious programming to avoid memory leaks
(see~\cref{fig:rc_example}). We then create a second cyclic graph (lines 7
and 8), immediately assigning it to the \lstinline{gc1} variable (line 9).
Doing so means that the first cyclic graph \lstinline{GcNode\{value: 1, ..\}}
is no longer reachable, so when we force a collection (line 10) that node
will be recognised as collectable; its finaliser is scheduled to be run (causing
\lstinline{drop 1} to be printed out at a later point), and when done so the backing
memory can be reclaimed. The print statement outputs \lstinline{2 2} (line 11).}
\label{fig:first_example}
\end{figure}
Amongst the ways one can classify programming languages are whether they
are Garbage Collected (GCed) or not: GCed languages enable implicit memory management;
non-GCed languages require explicit memory management (e.g~\lstinline{C}'s \lstinline{malloc} /
\lstinline{free} functions). Rust's use of affine types and ownership does not
fit within this classification: it is not GCed but it has implicit memory management.
Most portions of most Rust programs are as
succinct as a GCed equivalent, but ownership is too inflexible to express
data-structures that require multiple owners (e.g.~doubly linked lists).
Workarounds (e.g.~reference counting) impose an extra burden on the programmer,
make mistakes more likely, and often come with a performance penalty.
In an attempt to avoid such problems, there are now a number of GCs for Rust
(e.g.~\cite{manish15rustgc, coblenz21bronze, gcarena, boa, shifgrethor}). Most
introduce a user-visible type \lstinline{Gc<T>} which takes a value
of type \lstinline{T}, moves that value to the GC heap, and returns a wrapper around
a pointer to the moved value. \lstinline{Gc<T>} can be \emph{cloned} (i.e.~duplicated) and
\emph{dereferenced} to \lstinline{T} at will by the user. When no
\lstinline{Gc<T>} values can be found, indirectly or directly, from the
program's \emph{roots} (e.g.~variables on the stack, etc.),
then the underlying memory can be reclaimed.
It has proven hard to find a satisfying design and implementation for a GC for
Rust, as perhaps suggested by the number of different attempts to do so.
We identify two fundamental challenges
for GC for Rust: how to give \lstinline{Gc<T>} a familiar, idiomatic, complete
API; and how to make \emph{finalisation} (i.e.~the code that is run just before a
values collected by the GC) safe, performant, and ergonomic. We show that
using conservative GC is necessary and sufficient to solve the API challenge,
but the finalisation challenge is more difficult.
In existing GCs for Rust, if a user needs values of type \lstinline{T} to cause a
finaliser to run, then the user must manually implement a finaliser.
Not only do most finalisers end up duplicating
existing \emph{destructors} (i.e.~code which is run just before a value is
reclaimed by Rust's implicit memory management) but this makes it impossible to
provide finalisers for types in external libraries.
The `obvious' solution to this problem is to allow existing Rust destructors to
be automatically used as finalisers. However, this introduces a number of
problems. In the nearest context to our work, GC for C++, this solution was
explicitly ruled out as the problems were thought
insoluble~\cite[p.~32]{boehm09garbage}. We break these problems down into three
categories:
(1) some safe destructors are not safe finalisers;
(2) finalisers can be run too early;
and (3) finalisers are prohibitively slower than destructors. All are,
at least to some degree, classical GC problems; all are exacerbated
in some way by Rust; and none, with the partial exception of (2), has
existing solutions.
We introduce novel solutions, relying at least in part on Rust's unusual static
guarantees, to each of these problems. We thus gain not just a better GC for
Rust, but also solutions to open GC problems. Our solutions to the problems,
in order, are:
(1) \emph{finaliser safety analysis}
extends Rust's type system to reject programs whose destructors are not
provably safe to be used as finalisers;
(2) \emph{early finaliser prevention} automatically inserts barriers to prevent
optimisations or register allocation from `tricking'
the GC into collecting values before they are dead;
and (3) \emph{finaliser elision} statically optimises away finalisers if the
underlying destructor duplicates work the GC does anyway.
These solutions are implemented as part of \ourgc, a new GC for Rust.
Although \ourgc is not production ready, it has good enough performance
(across a number of benchmarks, its performance is \laurie{XYZ}) and
other polish (e.g.~good quality error messages) that we believe it shows
a plausible path forwards for those who may wish to follow it. Furthermore,
although \ourgc is necessarily tied to Rust, we believe that most of the
techniques in this paper, particularly those related to finalisation, are
likely to generalise to other ownership-based languages.
This paper's high-level structure is: background (\cref{sec:background});
\ourgc's design (\cref{sec:alloy_design}); destructor and finaliser challenges
and solutions (\cref{sec:destructor challenges}); and evaluation
(\laurie{XYZ}). The first three of these parts have the challenge that our work
straddles two areas that seem almost mutually exclusive: GC and Rust. We have
tried to provide sufficient material for readers expert in one of these
areas to gain adequate familiarity with the other, without undue prolixity, but perfection is beyond our
meagre grasp: we encourage readers to skip material they are already
comfortable with.
\section{Background}
\label{sec:background}
\subsection{Does Rust need a GC?}
\begin{figure}[t]
\lstinputlisting[language=Rust, firstline=5]{listings/rc_example.rs}
\captionof{lstlisting}{
A version of~\cref{fig:first_example} using Rust's standard reference
counting type \lstinline{Rc<T>}. To avoid memory leaks we use \emph{weak}
references between nodes (line 1). We again create two cyclic graphs (lines
6--9) using \lstinline{Rc::downgrade} to create weak references (lines 7 and
0). Since \lstinline{Rc<T>} is not copyable, we must use a manual
\lstinline{clone} call to have both the \lstinline{rc1} and \lstinline{rc2}
variables point to the same cyclic graph (line 10). Accessing a neighbour
node becomes a delicate dance requiring upgrading the weak reference (line 11).
The need to downgrade \lstinline{Rc<T>} to \lstinline{Weak<T>} and upgrade
(which may fail, hence the \lstinline{unwrap}) back to \lstinline{Rc<T>}
creates significant extra complexity relative to~\cref{fig:first_example}: compare
line 11 in \cref{fig:first_example} to (the much more complex) lines 10-12
in this \lstinline{Rc} example.
}
\label{fig:rc_example}
\end{figure}
Rust uses affine types~\citep{pierce04advanced} and \emph{ownership}
to statically guarantee that: a \emph{value} (i.e.~an instance of a type) has a
single owner (e.g.~a variable); an owner can \emph{move} (i.e.~permanently
transfer the ownership of) a value to another owner; and
when a value's owner goes out of scope, the value's destructor
is run and its backing memory reclaimed. An owner can pass \emph{references} to a value
to other code, subject to these static restrictions: there can be
multiple read-only references (`\lstinline{&}') to a value or a single
read-write reference (`\lstinline{&mut}'); and references cannot outlast the owner.
These basic rules mean that many Rust programs are as succinct as their equivalents
in GCed languages. This suggests that the search for a good GC for Rust may be Quixotic:
intellectually stimulating, but of no practical value.
However, there are many programs which need to express data structures which
are sound but which do not fit into the restrictions of affine types and
ownerships. These are often described as `cyclic data-structures', though we
prefer the more general wording of `values which may have more than one owner'
(e.g.~interpreters for dynamically typed languages are examples of programs
which are best thought of in the latter category). Rust cannot directly express
such programs, forcing programmers to use various workarounds.
Probably the most common -- or, at least, the most easily recognised in other's
code -- workaround is the reference counting type \lstinline{Rc<T>} in Rust's
standard library. For many data-structures, reference counting is a reasonable
solution, but using it for values which may have multiple owners requires
juggling strong and weak counts. This complicates the program (see~\cref{fig:rc_example}) and makes
it easy for values to live for shorter or longer than intended.
Another common workaround is to store values in a vector and use
integer indices into that vector. Such indices are then morally closer to
machine level pointers than normal Rust references: the indices can become
stale, dangle, or may never have been valid in the first place. The programmer
must also manually deal with issues such as detecting unused values,
compaction, and so on. In other words, such workarounds force the programmer
to write a partial GC themselves. A variant on this idea are
arenas, which gradually accumulate multiple values but free all of them in one go: values
can no longer be reclaimed too early, but, equally, individual values can not be
reclaimed until all values are determined to be unneeded.
A type-based approach is \lstinline{GhostCell}s~\cite{yanovski21ghostcell},
which allow data-structures that have multiple owners to be built
by statically guaranteeing that
there is a single owner for the entire data-structure at any point in the
program. However, this implicitly prevents multiple owners (e.g.~in different threads)
from reading or mutating different parts of the structure.
Although it is easily overlooked, some workarounds (e.g.~\lstinline{Rc<T>})
rely on using \emph{unsafe} Rust (i.e.~parts of the language, often involving
pointers, that are not fully statically checked by the compiler). It is reasonable
to assume that widely used code, even if technically unsafe, has been pored
over sufficiently that it is mostly, perhaps even wholly, reliable in practise. It
is less reasonable to assume that `new' solutions that a programmer implements using
unsafe Rust will immediately reach the same level of reliability. Our experience
is that most Rust programmers will avoid writing new unsafe code if they can, even
if doing so might in the long-term lead to a better system.
While we do not believe that every Rust program would be improved by GC, the
variety of workarounds already present in Rust code suggests that a substantial
subset might benefit from GC.
\subsection{GC terminology}
GC is a venerable field and has accumulated terminology that can seem
unfamiliar or unintuitive. We mostly use the same terminology
as~\cite{jones16garbage}, the major parts of which we define here.
A program which uses GC is split between the \emph{mutator} (the user's program) and
the \emph{collector} (the GC itself). At any given point in time, a particular thread is either
running as a mutator or a collector. In our context, all threads
run as a collector at least sometimes, with some threads always running as a collector.
Tracing and reclamation is performed during a \emph{collection} phase. In the context of
this paper, collections are \emph{stop-the-world}, where all mutator threads
are paused while collection occurs.
A \emph{tracing} GC is one that scans memory looking
for reachable objects from a program's roots: objects that are not reachable
from the roots can then be \emph{reclaimed}. In contrast, a reference counting GC does
not scan memory, and thus cannot free objects that form a cycle. As is common
in most GC literature, henceforth we use `GC' to mean `tracing GC' unless
explicitly stated otherwise.
We refer to memory which is solely under
the control of the GC as the \emph{GC heap}, though this may in practise be
intermingled with non-GCed memory. We use the term `GC value' to refer both to the pointer wrapped in a
\lstinline{Gc<T>} and the underlying value on the GC heap, even though multiple
pointers / wrappers can refer to a single value on the heap, unless doing so
would lead to ambiguity.
We use ``\ourgc'' to refer to the combination of: our extension to the Rust
language; our modifications to the \lstinline{rustc} compiler; and our
integration of the Boehm-Demers-Weiser (BDW) GC into the runtime of programs
compiled with our modified \lstinline{rustc}.
\section{\ourgc: Design and Implementation}
\label{sec:alloy_design}
In this section we outline \ourgc's basic design and implementation choices --
the rest of the paper then goes into detail on the more advanced aspects.
\subsection{Basic Design}
\ourgc provides a \lstinline{Gc<T>} type that exposes an API modelled on the
reference counting type \lstinline{Rc<T>} from Rust's standard library, because
\lstinline{Rc<T>}: is conceptually
similar to \lstinline{Gc<T>}; widely used in Rust code, and its API
familiar; and that API reflects long-term experience about what Rust programmers
need from such a type.
When a user calls \lstinline{Gc::new(v)}, the value \lstinline{v} is
moved to the GC heap: the \lstinline{Gc<T>} value returned to the user is a
simple wrapper around a pointer to \lstinline{v}'s new address. Since the
purpose of a \lstinline{Gc<T>} is to allow a value to have multiple owners,
it must not only be possible to dereference it arbitrarily many times, but
the lifetimes of those references must be allowed to overlap. To avoid undermining
Rust's ownership system, this means that dereferencing a \lstinline{Gc<T>} must
produce an immutable (i.e.~`\lstinline{&}') reference to the underlying value.
If the user wishes to mutate the underlying value, they must use other Rust
types that enable \emph{interior mutability} (e.g.~\lstinline{RefCell<T>} or
\lstinline{Mutex<T>}).
One feature that \ourgc explicitly adopts is \lstinline{Rc<T>}'s
ability to be transformed into a raw pointer (\lstinline{into_raw}) and
back (\lstinline{from_raw}). Though many programmers do not directly
encounter these functions, they are a crucial link between Rust's high and
low-level features (e.g.~being used for the C Foreign Function Interface
(FFI)). We believe that a viable GC for Rust must include this same
ability, but doing so has a profound impact: Rust allows raw pointers to be converted to
the integer type \lstinline{usize} and back\footnote{Although it is outside the
scope of this paper, it would arguably
be preferable for Rust to have different integer types for `data width' and
`address'. Modern C, for example, does this with the \lstinline{size_t} and
\lstinline{uintptr_t} types respectively. Rust now has a provenance lint to
nudge users in this general direction, but the \lstinline{as}
keyword still allows arbitrary conversions.}.
\label{conservative_gc}
Having acknowledged that pointers may end up disguised as integers, it is then
inevitable that \ourgc must be a \emph{conservative} GC, which treats each
reachable machine word as a possible pointer: if,
when considered as a pointer, a word's address falls within a GCed block of memory,
then that block itself is considered reachable (and thus transitively scanned).
Since a conservative GC cannot know if a word is really a pointer, or just happens to be a sequence of
bits that also happens to make it a valid pointer, this over-approximates the
\emph{live set} (i.e.~the blocks that the GC will not reclaim). However, the
most extensive study we know of suggests the false detection rate in Java
programs is under 0.01\% of live objects~\cite{shahriyar14fast}, so it is
rarely a problem in practise.
Conservative GC occupies a grey zone in programming language semantics. In most
languages -- including the semantics of most compiler Intermediate
Representations (IRs) -- conservative GC relies on undefined behaviour, and
some languages allow arbitrary `bit fiddling' on pointers that temporarily
obscures the address they are referring to. \ourgc's use of conservative GC means that it
is, formally speaking, unsound. However, conservative GC is widely used,
including in the two most widespread web browsers: Chrome uses it in its Blink rendering
engine~\citep{ager13oilpan} and Safari uses it in its JavaScript VM JavaScriptCore~\citep{pizlo17riptide}.
Even in 2024, we lack good alternatives to conservative GC: there is no cross-platform API for precise GC; and while some
compilers such as LLVM provide some support for GC features, we have found them
incomplete and buggy. Despite the potential soundness
worries, conservative GC thus remains a widely used technique.
Conservative GC enables \ourgc to make a useful ergonomic improvement over
most other GCs for Rust whose \lstinline{Gc<T>} is only \emph{cloneable}. Such types can be duplicated, but doing
so requires executing arbitrary user code. To make the possible run-time cost of this clear, Rust has
no direct syntax for cloning: users must explicitly call \lstinline{Rc::clone(&x)}
to duplicate a value \lstinline{x}. In contrast, since \ourgc's \lstinline{Gc<T>} is just a wrapper around a pointer it
is not just cloneable but also \emph{copyable}: duplication only requires copying
bytes (i.e.~no arbitrary user code need be executed). Copying is implied by assignment,
reducing the need for a function call entirely\footnote{The lengthier
syntax \lstinline{y = Gc::clone(&x)} is available, since every copyable type is
also cloneable.}. This is not just a syntactic convenience but also reflects an underlying
semantic difference: duplicating a \lstinline{Gc<T>} in \ourgc is is a cheaper and simpler operation
than in other GCs for Rust (or, indeed, an \lstinline{Rc<T>}).
\subsection{Basic Implementation}
The most visible aspect of \ourgc is its fork, and extension of, the standard
Rust compiler \rustc. We forked \rustc~\rustcversion and have
added or changed approximately 3,150 Lines Of Code (LOC).
\ourgc uses the conservative Boehm-Demers-Weiser GC (\boehm)~\citep{boehm88garbage} as its collector.
Although we use some uncommon, and extend other, parts of \boehm, there is
nothing inherently unique about \boehm from \ourgc's perspective.
We made the following changes to \boehm. First, we disabled \boehm's parallel collector threads
because, for reasons we don't fully understand, it worsens \ourgc's
performance. Second, \boehm cannot scan pointers stored in thread locals
because these are platform dependent. Fortunately, \rustc uses LLVM's
thread local storage implementation, which stores such pointers in the
\lstinline{PT_TLS} segment of the ELF binary: we modified \boehm to scan
this ELF segment during each collection. Third,
\boehm normally dynamically intercepts thread creation calls so that it can
then can scan their stacks, but (for bootstrapping
reasons) it is unable to do so in our context: we explicitly changed \ourgc
to register new threads with \boehm. Fourth, we modified \boehm to run finalisers
on a separate thread (see \laurie{Section XYZ}).
\section{Destructors and Finalisers}
\label{sec:destructor challenges}
When a value in Rust is \emph{dropped} (e.g.~the value's owner went out of lexical
scope) its destructor is automatically run. Rust destructors are formed of two
parts, run in the following order: a user-defined \emph{drop method}; and
automatically inserted \emph{drop glue}. Drop methods are optional; users
can provide one for a type by implementing the \lstinline{Drop} trait's \lstinline{drop}
method. Drop glue recursively calls destructors of contained types (e.g.~fields
in a \lstinline{struct}). Although it is common usage to conflate `destructor' in
Rust with drop methods, drop glue is an integral part of a Rust destructor:
we therefore use `destructor' as the umbrella term for both drop methods and drop glue.
Rust's destructors enable a style of programming that originated in C++ called RAII (Resource
Acquisition Is Initialization)~\cite[Section~14.4]{stroustrup97c++}: when a
value is dropped, the underlying resources (e.g.~file handles or mutexes) it
acquired when created are released. Whether one considers all such uses
to be RAII or not, a brief perusal of Rust code will quickly show both that
types that have drop methods are used frequently, and that users fairly often
implement their own drop methods.
Existing GCs for Rust have very separate notions of destructors and finalisers.
Where the former have the \lstinline{Drop} trait, the later typically have
a \lstinline{Finalise} trait. If a user type needs to be finalised, then
the user must provide an implementation of the \lstinline{Finalise} trait for that type.
However, doing so introduces a number of problems:
(1) external libraries are
unlikely to provide finalisers; (2) Rust's \emph{orphan rule} \laurie{jake: is
https://rust-lang.github.io/chalk/book/clauses/coherence.html the best
reference for this?} prevents one implementing traits for types defined in
external libraries (i.e.~unless a library's types were designed to support
\lstinline{Gc<T>}, those types cannot be directly GCed); (3) one cannot
automatically replicate drop glue for finalisers; and (4) one cannot replicate
\rustc's refusal to allow calls to the equivalent of \lstinline{Drop::drop}.
Programmers can workaround problems (1) and (2) in various ways. For example,
they can wrap external library types in \emph{newtypes} (zero-cost wrappers)
and implement finalisers on those instead. Doing so involves drudgery, but
little conceptually difficulty.
Problem (3) has partial solutions: for example, ~\cite{manish15rustgc} uses the
\lstinline{Trace} macro to generate `finaliser glue' (our term) for
\lstinline{struct} fields. This runs into an unsolvable variant of problem (2):
types in external libraries will not implement this trait and cannot be
recursively scanned for finaliser glue.
Problem (4) is impossible to solve in Rust as-is. One cannot define a function
that can never be called --- what use would such a function have? It might seem
tempting to have the \lstinline{finalize} method take ownership of the value,
but \lstinline{Drop::drop} does not do so because that would not allow drop
glue to be run afterwards.
In summary: destruction is a core part of Rust; a GC for Rust ideally needs to
maintain most if not all of those properties for finalisation; but some parts
cannot be replicated in normal user code.
\subsection{General Challenges When Using Destructors as Finalisers}
Even if there were no Rust-specific challenges to using destructors as
finalisers, we would still face the problem that finalisers and destructors
have different, and sometimes incompatible, properties. The best overall
guide to these differences, and the resulting problems, is~\cite{boehm03destructors},
supplemented by later work by some of the same authors on support
for GC in the C++ specification~\cite{boehm09garbage}\footnote{These features
were added to the C+11 specification, but do not seem to have been implemented by
compilers. C++23 removed these features.}.
An obvious difference between destructors and finalisers is when both
are run. Where C++ and Rust define
precisely when a destructor will be run\footnote{Mostly. Rust's `temporary
lifetime extension' delays destruction, but for how long is currently
unspecified.}, finalisers run at an unspecified point in time
after the last reference to a GCed value is removed. However, in some situations
finalisers can be run too early: valid optimisations (e.g~scalar replacement)
can cause a value to appear to be unused, and thus ready for finalisation,
even though some of its constituent parts are still being used.
A less obvious difference relates to where destructors and finalisers are run.
Destructors are run in the thread that the last owner of the value ran in.
However, running finalisers in the same thread as the last owner of the value
ran in can cause race conditions and deadlocks if one or more finalisers try to access a resource that the mutator
expects to have exclusive access too~\cite[section~3.3]{boehm03destructors}.
Although such problems can
affect destructors, it is a clear case of programmer error, since they should
have taken into account the predictable execution point of destructors. Since
finalisers have no such predictable execution point, there is no way for finalisers
to safely access shared resources if they are run on the same thread. Such
problems can only be avoided by running
finalisers on a non-mutator thread: however, not all Rust destructors
are safe to run on another thread.
Perhaps surprisingly, finalisation and cycles are problematic: finalisers
can reference other GCed values that are partly, or wholly, `finalised' and may
even have had their backing memory freed or reused. A related, but
distinct, problem is the ability of finalisers to `resurrect' values by
copying the reference passed to the finaliser and storing it somewhere.
\subsection{The Challenge of Finalisers for \ourgc}
At this point we hope to have convinced the reader of two general points: a
viable GC for Rust needs to be able to use existing destructors as finalisers
whenever possible; and that finalisers, even in existing GCs, cause
various problems.
Over time, finalisers have come to be viewed with increasing suspicion. Java,
for example, has deprecated, and intends eventually removing, per-type
finalisers: instead it has introduced deliberately less flexible per-object `cleaners', whose API
prevents problems such as object resurrection~\cite{goetz21deprecated}. It
is important to differentiate such mechanisms from the \lstinline{Finalize} trait that many existing GCs for
Rust force users to manually implement: cleaners impose restrictions
to make finalisers less problematic; existing \lstinline{Finalize} traits
do not impose any such restrictions.
Our desire that \ourgc should use existing Rust destructors as finalisers whenever
possible may thus seem out of reach. Indeed, in the nearest
context to our work GC for C++ this solution was explicitly ruled out for GC for C++ as the
problems were thought insoluble~\cite[p.~32]{boehm09garbage}. We break these
problems down into four:
(1) finalisers are prohibitively slower than destructors;
(2) finalisers can be run too early;
(3) running finalisers on the same thread as a paused collector can cause race conditions and deadlocks;
(4) some safe destructors are not safe finalisers;
Fortunately for us, Rust's unusual static guarantees, suitably expanded by
\ourgc, allow us to address each problem in a satisfying way. In the following
section, we tackle these problems in the order above, noting that we tackle problems
(1) and (2) separately, and (3) and (4) together.
\section{Finaliser Elision}
Finalisers are slower to run than destructors --- running every Rust destructor
as a GC finaliser in \ourgc is prohibitively slow (\laurie{give rough figures
e.g.~a range}). In this section we show how to sidestep this problem in many
practical situations.
A variety of factors contribute to the performance overhead of finalisers, such as:
a queue of finalisers
must be maintained, whereas destructors can be run immediately; since finalisers run
some time after the last access of a value, they are more likely to cause
cache misses; and so on. Most of these factors are inherent to any GC and
our experience of using and working on \boehm -- a mature, widely used GC -- does
not suggest that major optimisation potential remains. In other words, it is unlikely that \boehm could be
sufficiently optimised to overcome the performance penalty of finalisers we observe.
Instead, whenever possible, \ourgc \emph{elides} finalisers so that they do not need to be run at all.
We are able to do this because many Rust destructors
solely do work that a conservative GC does anyway --- when used as finalisers,
such destructors are thus unnecessary and can be elided.
Consider the type \lstinline{Box<T>} which heap allocates space for a value;
when a \lstinline{Box<T>} value is dropped, the heap allocation will be freed
by \lstinline{Box}'s drop method. In a naive implementation,
\lstinline{Gc<Box<T>>} will, when no GC values remain, run a finaliser which
then runs \lstinline{Box}'s destructor.
Finaliser elision rests on two observations. First,
\lstinline{Box<T>}'s drop method solely consists of a call to \lstinline{free}\footnote{The
function in the Rust API is actually called \lstinline{deallocate}, but that
jars with the terminology we use elsewhere.}. Second, while we informally say
that \lstinline{Box<T>} allocates on the `heap' and \lstinline{Gc<T>} allocates
on the `GC heap', all allocations are made through \boehm and stored in
a single heap. Thus, when used as a finaliser,
\lstinline{Box<T>}'s drop method is unneeded\footnote{There is a subtle asymmetry here: we
cannot elide the destructors themselves, as they are still necessary to ensure
predictable destruction in non-\lstinline{Gc<T>} Rust code.}, as the underlying memory will
naturally be freed by \boehm anyway\footnote{Indeed, since the drop method only
calls \lstinline{free}, any resulting finaliser would cause the underlying
allocation to live longer than if the finaliser was not present!}.
This means that there is no need to run a finaliser for a type such as
\lstinline{Gc<Box<u8>>} and we can statically elide it. However, we can not
elide every \lstinline{Gc<Box<T>>} finaliser: for example, \lstinline{Gc<Box<Rc<u8>>}
requires a finaliser because \lstinline{Rc<T>} needs to decrement a reference
count. This may seem confusing, because in both cases \lstinline{Box<T>}'s drop
method is the same: however, the drop glue added for \lstinline{Box<Rc<u8>}
causes \lstinline{Rc<T>}'s destructor to be run.
\subsection{Implementing finaliser elision}
\begin{algorithm}
\caption{Finaliser Elision}
\label{alg:elision}
\begin{algorithmic}
\Function{RequiresFinalizer}{$T$}
\If {\Call{Impls}{$T$, $Drop$} \AND \NOT \Call{Impls}{$T$, $DropMethodFinalizerElidable$}}
\State \Return{true}
\EndIf
\ForEach {$field \in type$}
\If{\Call{RequiresFinaliser}{$field$}}
\State \Return{true}
\EndIf
\EndFor
\State \Return{false}
\EndFunction
\end{algorithmic}
\end{algorithm}
The aim of finaliser elision is to statically determine which type's destructors do
not require corresponding finalisers and elide them. Notably, finaliser elision
must deal correctly with nested data types.
The high-level algorithm is shown in~\cref{alg:elision}.
In essence, the algorithm has to consider three things. First, types that do not
implement the \lstinline{Drop} trait are clearly candidates for elision. Second,
some types that implement the \lstinline{Drop} trait are also candidates
for elision (e.g.~\lstinline{Box<T>}), but these cannot be automatically determined,
as it would require fully understanding the logic inside the type's drop method.
The user must manually signify to \ourgc by the user implementing the new
unsafe \emph{marker trait} (i.e.~a trait without methods that \ourgc can recognise)
\lstinline{DropMethodFinalizerElidable}:
\begin{lstrustsmall}
unsafe impl<T> DropMethodFinalizerElidable for Box<T> {}
\end{lstrustsmall}
\lstinline{DropMethodFinalizerElidable} requires a careful understanding of a
type's drop method: incorrectly implementing this trait will change a program's
semantics.
Third, we must recursively check a type's fields to see if any require
finalisation --- in other words, we check what the drop glue for a type will do.
A type \lstinline{T<U>} might implement the
\lstinline{DropMethodFinalizerElidable} trait, but if its type parameter \lstinline{U} does
not, and \lstinline{U} implements the \lstinline{Drop} trait, then
\lstinline{T<U>} must have its finaliser run.
\ourgc modifies the standard Rust library to implement
\lstinline{DropMethodFinalizerElidable} on the following types: \lstinline{Box<T>},
\lstinline{Vec<T>}, \lstinline{RawVec<T>}, and \lstinline{HashMap<T>}. As these
types are widely used in real Rust code, this often leads to significant
numbers of finalisers being elided.
\begin{figure}[t]
\begin{lstlisting}[language=Rust, numbers=none,
label={listing:elision_in_rustc}, caption={
Finaliser elision inside \ourgc itself. A new \lstinline{const}
(i.e.~compile-time evaluated) compiler intrinsic
\lstinline{requires_finalizer} returns true if a finaliser is required for a
type. The \lstinline{Gc<T>} type uses this intrinsic to ensure that the
value is registered as requiring a finaliser.
}]
impl<T> Gc<T> {
pub fn new(value: T) -> Self {
if requires_finalizer::<T>() { Gc<T>::new_with_finalizer(value) }
else { Gc<T>::new_without_finalizer(value) }
...
}
}
\end{lstlisting}
\end{figure}
\cref{listing:elision_in_rustc} shows how finaliser elision is used inside the compiler. \ourgc
introduces a new \lstinline{const} compiler intrinsinc
\lstinline{requires_finalizer} (based on~\cref{alg:elision}) which returns true if a finaliser is required for a
type. \lstinline{Gc<T>}'s constructor uses this intrinsic to decide whether a
finaliser must be registered or not. Because \lstinline{requires_finalizer} is
evaluated at compile-time, it can be inlined into \lstinline{Gc::new},
allowing the associated conditional to be removed too. In other words --
compiler optimisations allowing -- the `does this specific type require a
finaliser' checks have no run-time cost at all.
\section{Finaliser Safety Analysis}
In this section we address two problems: running finalisers on the same thread
as a paused collector can cause deadlocks; and some safe destructors are not
safe finalisers. Addressing the first problem is conceptually simple --
finalisers must be run on a separate thread -- but we must ensure that doing so
is sound. We therefore consider this a specific instance of the second problem.
Our solution is the novel technique of
\emph{finaliser safety analysis}. We extend Rust's static analyses, and alter the relevant parts
of \rustc, to reject a type \lstinline{T}'s destructor if it
is not provably safe to be used as a finaliser in a \lstinline{Gc<T>}.
To the best of our knowledge, no other GCed language has an
equivalent of finaliser safety analysis: indeed, we rely on some of Rust's less
common static guarantees.
In this section we first define the ways that reusing destructors as finalisers
would undermine safety, and the general rules for identifying the troublesome
cases. We then explain how we changed \rustc to implement these rules in \ourgc.
\subsection{Rust references}
\begin{figure}[t]
\centering
\begin{minipage}{0.50\textwidth}
\lstinputlisting[language=Rust,firstline=9,lastline=18]{listings/dangling_reference.rs}
\subcaption{}
\end{minipage}%
\begin{minipage}{0.50\textwidth}
\begin{lstlisting}[language=Rust, numbers=none]
error: `Node{value: &*b, nbr: None}` cannot be safely
constructed.
| let gc1 = Gc::new(Node{value: &*b, nbr: None});
| --------^^^^^^^^^^^^^^^^^^^^^^^^^^^-
| | |
| | contains a reference (&) which
| | may no longer be valid when it
| | is finalized.
| `Gc::new` requires that a type is
| reference free.
\end{lstlisting}
\subcaption{}
\end{minipage}
\label{fig:dangling_reference}
\captionof{lstlisting}{A \lstinline{u64} is
placed on the heap using a \lstinline{Box}, and an immutable reference to it
is stored inside the \lstinline{Gc<Node>}. Without finalisation, this is
valid as long as \lstinline{gc1} does not outlive \lstinline{b}. However, the
use of the references from the \lstinline{drop} method is unsound. This is
because the collector is likely to schedule the finaliser to run after the
\lstinline{Box<u64>} is freed when \lstinline{b} goes out of scope. The
dereference on line 3 would thus access a dangling reference causing a
use-after-free.\laurie{this example slightly contradicts the text: is the
problem the existence of reference + finaliser OR the use of the reference in
drop? I'm pretty sure it's the first of these two possibilities, but
I'd like to check!}}
\end{figure}
A \lstinline{Gc<T>} can store (indirectly or indirectly) normal Rust references
(i.e.~\lstinline{&} and \lstinline{&mut}), at which point the \lstinline{Gc<T>}
is subject to Rust's normal borrow checker rules and cannot outlive the
reference. Finalisers are fundamentally incompatible with this, because the
finaliser will be run after the \lstinline{Gc<T>}'s owner has gone out of scope:
if the finaliser were to make use (even indirectly) of a reference, we would
have subverted the borrow checker.
\ourgc's solution to this is simple: \lstinline{Gc<T>} can only include a
reference iff \lstinline{T} does not need finalising. For example,
\lstinline{Gc<&u8>} is permitted, because integers do not need finalising. However,
\lstinline{Gc<Node<&Box<T>>>} is not permitted, because \lstinline{Box} needs
its finaliser to be run in order to free its backing memory. \cref{fig:dangling_reference}
shows the error that \ourgc raises in such a case.
While we could somewhat relax this rule (e.g.~finalisers which provably do not,
directly or indirectly, use a reference could be let through), there are few
benefits to doing so: after all, \lstinline{Gc<T>} is explicitly designed to
help users avoid ownership restrictions. That said, finaliser elision
(see~\cref{XYZ}) does reduce the number of types which need finalisation, thus
enabling more types to be embedded as references, for those who really want to.
\laurie{this paragraph again suggests that we look into the finaliser, but
elsewhere we suggest we don't look into the finaliser.}
As with references, this same unsoundness can be caused by storing raw pointers
inside a \lstinline{Gc} (either immutable \lstinline{*const} or mutable
\lstinline{*mut}) and then dereferencing them in a finaliser. However, \ourgc
is sound even without enforcing this rule for raw pointers because dereferencing raw pointers
is already not possible in safe Rust code, and programmers must ensure the
reference is valid for each dereference anyway.
To check whether a type passed to \lstinline{Gc} contains a reference, \ourgc
defines a \emph{marker} (i.e.~without methods) \emph{auto trait}
(i.e.~automatically implemented for types which adhere to its restrictions)
\lstinline{ReferenceFree}. \ourgc automatically implements
\lstinline{ReferenceFree} for each type that does not itself contain a
type that is not \lstinline{ReferenceFree}. Primitive types such as integers
are \lstinline{ReferenceFree} by default; crucially (and at the risk of stating
the obvious) references themselves are not \lstinline{ReferenceFree}. Thus a
non-\lstinline{ReferenceFree} type `pollutes' its container type(s) in the
manner we desire\footnote{\laurie{``As with all auto traits?``}, users can
force a type to implement \lstinline{ReferenceFree}, but only in unsafe Rust.}.
\laurie{``this is then checked in the \lstinline{CheckCallSite} phase`` is that part of FSA or normal \rustc?}
\subsection{Finalisation Order}
% \subsubsection{Need to introduce concurrency}
% \label{sendsync}
%
% The Rust type system is able to guarantee statically that values are being used
% in a thread-safe way. It does this with the use of two special "marker" traits:
% \lstinline{Send} and \lstinline{Sync}.
%
% A value whose type implements the \lstinline{Send} trait can be transferred to
% other threads. Almost all types in Rust implement the \lstinline{Send} trait
% save for a few exceptions. One such exception is the \lstinline{Rc<T>}
% (reference counting) type. This does \emph{not} implement \lstinline{Send}
% because it does not perform count operations on its underlying contents
% atomically. If it were sent to another thread, it could race if both threads
% tried to update the count simultaneously. In contrast, the \lstinline{Arc<T>}
% type (an atomic implementation of \lstinline{Rc<T>}) does implement
% \lstinline{Send} provided its inner type \lstinline{T} is also \lstinline{Send}.
%
% The corollary to \lstinline{Send} is the \lstinline{Sync} trait, which can be
% implemented on types whose values perform mutation in a thread-safe manner. For
% example, a \lstinline{Mutex<T>} implements \lstinline{Sync} because it provides
% exclusive access to its underlying data atomically. On the other hand, a
% \lstinline{RefCell} (discussed in \autoref{intmut}) does not implement
% \lstinline{Sync} because it only provides single-threaded interior
% mutability. This is because its underlying mechanism to increment and decrement
% borrow counts are not performed atomically.
%
% The \lstinline{Send} and \lstinline{Sync} marker traits are a special kind of
% trait known as \emph{auto traits}. This means that they are automatically
% implemented for every type, unless the type, or a type it contains, has
% explicitly opted out. Types can be opted out via a \emph{negative impl}:
% \begin{lstrustsmall}
% impl !Send for T {}
% \end{lstrustsmall}
% If, for example, a struct \lstinline{S} contains a field of type \lstinline{T}
% from the example above, then the entire struct \lstinline{S} will also not
% implement \lstinline{Send}.
%
% \lstinline{Send} and \lstinline{Sync} can be manually implemented on types, but
% doing so requires \lstinline{unsafe} Rust code since the user must guarantee it
% is safe to use in a multi-threaded context.
\subsection{Finalisation order}
Some GCs implementations guarantee a finalisation order, because for some
applications, this is important if one resource must be cleaned up before
another.
However, this guaranteed finalisation order has two disadvantages. First,
finalising all the objects in a chain of floating garbage happens over multiple
finalisation cycles because an object can only be finalised if it is not
reachable from other unreachable objects. Such a delay in the eventual
reclamation of objects can cause \emph{heap drag}, where unreachable objects
are kept alive longer than necessary.
Second, and most significantly, this approach is not able to finalise cycles of
objects where more than one object needs finalising, which can lead to resource
leaks. This is because the collector cannot know which (if any) object is safe
to finalise first: if an object references another object which has already
been finalised, this is unsound. Boehm proposes a workaround for this where
programmers can refactor the objects in order to break the finalisation
cycle~\citep{boehm03destructors}. Another workaround is to allow users to use
weak references to break cycles (similar to breaking reference count cycles).
Unfortunately this is often difficult to implement
correctly~\citep{jones16garbage} \laurie{we need a page reference for this}.
\ourgc uses an alternative approach where it does not specify any finalisation
order. This allows objects with cycles to be finalised but with a heavy
constraint: they must not reference other objects from inside their finalisers.
However, this restriction is not so bad for us because our requirements for GC
are unique in this respect: \ourgc is not intended to replace Rust's RAII
approach to memory management, instead, it provides optional GC for objects
where the RAII approach is difficult or impossible. It is not uncommon to see
Rust programs which use \ourgc with a mix of GC'd and non-GC'd objects. In such
cases, it is safe for a finaliser to access a field of a non-GC'd object
because there is no danger of them being finalised already.
In addition, one of \ourgc's main goals is to make it easier to work with data
structures that have cycles in Rust. It is suggested that finalisation cycles
are rare in GC'd languages~\citep{jones16garbage}. However, this is different for
\ourgc, since destructors in Rust are common, and mapping them to finalisers
means that it is not uncommon to see finalisation cycles in Rust programs using
\ourgc.
The problem with finalising \lstinline{Gc}s in an unspecified order is that any
other \lstinline{Gc} object which they reference may have already been freed.
Consider the example in \autoref{fig:unsound_finalisation_cycle}, which
allocates \lstinline{Gc}s which reference each other in a cycle. A cycle such as
this one will always lead to unsoundess with finalisers which access other
\lstinline{Gc} objects. This is also a problem for non-cyclic data structures in
\ourgc's non-ordered configuration as there is no guarantee that non-cyclic data
structures will be finalised `outside-in'.
\autoref{fig:unsound_cycle} shows an example of how dereferencing another
\lstinline{Gc} can be unsound.
\begin{figure}[t]
\centering
\begin{minipage}{0.50\textwidth}
\lstinputlisting[language=Rust,firstline=5]{listings/finalisation_cycle.rs}
\subcaption{}
\end{minipage}%
\begin{minipage}{0.50\textwidth}
\begin{lstlisting}[language=Rust, numbers=none]
error: `n1` cannot be safely finalized.
|
4 | let nbr = self.nbr.unwrap();
| --------
| |
| caused by the expression in `fn drop(&mut)`
| here because it uses a type which is not
| safe to use in a finalizer.
...
11 | let gc1 = Gc::new(n1);
| ^^ has a drop method which cannot
| be safely finalized.
|
= help: `Gc` runs finalizers on a separate thread,
so drop methods must only use values whose
types implement `Send + Sync + FinalizerSafe`.
\end{lstlisting}
\subcaption{}
\end{minipage}
\label{fig:finalisation_cycle}
\captionof{lstlisting}{An example of finaliser safety analysis preventing an
unsound Rust program. \textbf{(b)} showing the compile-time error message
shown when attempting to compile the example in \textbf{(a)}. FSA identifies
that another \lstinline{Gc} is being dereferenced inside a finaliser. This is
unsound when \ourgc does not use ordered finalisation as it may have already
been collected.}
\end{figure}
\subsubsection{Destructors Must Be \lstinline{Send}able To Another Thread}
It is common in many languages for finalisers to access fields from other
objects or even global state. Since an object's finaliser is run at some unknown
point in time once it is considered unreachable by the collector, it must be
able to safely access such state without racing with the mutator.
One possible solution might seem to be to prohibit finalisers from acquiring
locks, however this can still cause race-like bugs because of how finalisers can
interleave asynchronously with the mutator~\citep{niko13destructors}.
In addition, \ourgc schedules all finalisers to run on a separate thread to the
mutator. As with any GC, \ourgc can finalise objects at its leisure, with no
way for the programmer to know when this will happen. Rust does not prevent
users from writing code which deadlocks, but it does not make the situation
worse. However, if \ourgc were to perform on-thread finalisation, it would open
the possibility of previously non-deadlocking code deadlocking.
\begin{figure}[t]
\lstinputlisting[language=Rust, firstline=10]{listings/finaliser_deadlock.rs}
\captionof{lstlisting}{An example showing how a potential deadlock can be
caused if finalisers are run on the mutator thread. A shared counter is
created using a reference counted container, a reference to this is then
placed inside a garbage collected container. This is potentially short-lived,
as it is only reachable by the \lstinline{gc} variable until the end of the
inner scope. If \ourgc decides to schedule a collection after this then the
\lstinline{Rc} could be considered garbage, where a finaliser would run its
drop method (line 10). If this happens while the main mutator thread already
holds \lstinline{counter.lock()} (line 32-36) then this program can deadlock.
When finalisers are run on the same thread, there is nothing the programmer
can do in this situation to guarantee that this kind of deadlock does not
occur.}
\label{fig:finaliser_deadlock}
\end{figure}
Unfortunately, finalising objects off-thread now means that shared data, or other
objects accessed from a finaliser must be done in a thread-safe way. The problem
with this is that in \ourgc, a finaliser calls a type's existing drop methods.
Since \lstinline{Drop} was not originally defined in expectation of being
called on a separate thread, it does not guarantee thread safety.
\subsection{The Analysis}
\label{sec:fsa}
Extends Rust's type system to reject programs whose destructors are not
provably safe to be used as finalisers. At compile-time, a static analysis is
performed over the drop methods of each value used in a \lstinline{Gc}.
\subsubsection{Ensuring thread-safety}
The basic idea is that whenever a type is used in a \lstinline{Gc}, that type's
drop method needs to be checked to ensure it only accesses fields which are
safe to be used inside a finaliser. Performing this check only when such types
are used in \lstinline{Gc} is important as it prevents FSA from breaking
existing Rust programs: drop methods with unsound finalisation behaviour are
not a problem if they are never used in a \lstinline{Gc}.
\begin{figure}[t]
\centering
\begin{minipage}{0.50\textwidth}
\lstinputlisting[language=Rust,firstline=9,lastline=18]{listings/thread_unsafety.rs}
\subcaption{}
\end{minipage}%
\begin{minipage}{0.50\textwidth}
\begin{lstlisting}[language=Rust, numbers=none]
error: `n1` cannot be safely finalized.
|
11 | let gc1 = Gc::new(n1);
| ^^ has a drop method which cannot
| be safely finalized.
|
::: ../rc.rs:368:18
|
368 | unsafe { self.ptr.as_ref() }
| --------
| |
| caused by the expression in `fn drop(&mut)` here because
| it uses a type which is not safe to use in a finalizer.
|
= help: `Gc` runs finalizers on a separate thread, so drop methods
must only use values whose types implement `Send + Sync + FinalizerSafe`.
\end{lstlisting}
\subcaption{}
\end{minipage}
\label{fig:dangling_reference}
\captionof{lstlisting}{A \lstinline{u64} is
placed on the heap using a \lstinline{Box}, and an immutable reference to it
is stored inside the \lstinline{Gc<Node>}. Without finalisation, this is
valid as long as \lstinline{gc1} does not outlive \lstinline{b}. However, the
use of the references from the \lstinline{drop} method is unsound. This is
because the collector is likely to schedule the finaliser to run after the
\lstinline{Box<u64>} is freed when \lstinline{b} goes out of scope. The
dereference on line 3 would thus access a dangling reference causing a
use-after-free.}
\end{figure}
The \lstinline{Wrapper} uses a \lstinline{RefCell} to swap the value of
underlying string (line 11). A \lstinline{RefCell} provides a form of interior
mutability which is not thread-safe (because its \lstinline{borrow()} /
\lstinline{borrow_mut()} methods are non-atomic). In this example, a
\lstinline{Wrapper} can safely be placed inside a \lstinline{Gc}, because the
\lstinline{RefCell} is not used in \lstinline{Wrapper}'s finaliser (line 4).
This is checked by FSA at compile-time.
As with any static analysis, FSA is inherently conservative: some drop methods
are impossible to analyse at compile-time so in these cases \ourgc will err on
the side of caution and reject potentially safe programs. This is most likely
to happen in two situations.
First, a drop method may contain a call to an opaque (i.e. externally linked)
function for which the compiler does not have the MIR. If a reference to a field
which would be unsafe to use in a finaliser is passed to this function, then FSA
would reject the program. This function call \emph{could} be safe, but FSA has
no way of knowing, and will reject it.
Second, if a finaliser is used on a trait object which is called using dynamic
dispatch in Rust. At compile-time, it is not possible to know the concrete type
of a trait object, so FSA does not know which drop method to check.
In both cases, the user has the option of explicitly informing the compiler a
particular drop method is safe to use as a finaliser. We observe that this is
rare in practice.
\subsubsection{Making unordered finalisation sound}
\label{sound_unordered_finalisation}
When \ourgc is compiled with unordered finalisation, it prevents drop methods
from dereferencing fields which point to other \lstinline{Gc} objects.