├── .github
└── workflows
│ └── rust.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE.md
├── README.md
├── dumpster
├── .gitignore
├── Cargo.toml
└── src
│ ├── impls.rs
│ ├── lib.rs
│ ├── ptr.rs
│ ├── sync
│ ├── collect.rs
│ ├── mod.rs
│ └── tests.rs
│ └── unsync
│ ├── collect.rs
│ ├── mod.rs
│ └── tests.rs
├── dumpster_bench
├── .gitignore
├── Cargo.toml
├── scripts
│ └── make_plots.py
└── src
│ ├── lib.rs
│ └── main.rs
├── dumpster_derive
├── .gitignore
├── Cargo.toml
└── src
│ └── lib.rs
├── dumpster_test
├── .gitignore
├── Cargo.toml
└── src
│ └── lib.rs
└── rustfmt.toml
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | pull_request:
7 | branches: ["master"]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | test:
14 | runs-on: ${{ matrix.os }}
15 |
16 | strategy:
17 | matrix:
18 | os:
19 | - ubuntu-latest
20 | - windows-latest
21 | - macOS-latest
22 |
23 | steps:
24 | - name: Checkout sources
25 | uses: actions/checkout@v2
26 | - name: Install nightly toolchain
27 | uses: actions-rs/toolchain@v1
28 | with:
29 | profile: minimal
30 | toolchain: nightly
31 | override: true
32 | - name: Build
33 | uses: actions-rs/cargo@v1
34 | with:
35 | command: build
36 | args: --all-features
37 | - name: Run tests
38 | uses: actions-rs/cargo@v1
39 | with:
40 | command: test
41 | args: --all-features
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
4 | *.csv
5 | .vscode
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # `dumpster` Changelog
2 |
3 | ## 1.1.0
4 |
5 | ### New features
6 |
7 | - Added support for [`either`](https://crates.io/crates/either).
8 |
9 | ### Bug fixes
10 |
11 | - Derive implementations no longer erroneously refer to `heapsize`.
12 |
13 | ### Other changes
14 |
15 | - Slight performance and code style improvements.
16 | - Improved internal documentation on safety.
17 | - Remove `strict-provenance` requirement as it is now stabilized.
18 |
19 | ## 1.0.0
20 |
21 | ### Breaking changes
22 |
23 | - Rename `Collectable` to `Trace`.
24 |
25 | ## 0.2.1
26 |
27 | ### New features
28 |
29 | - Implement `Collectable` for `std::any::TypeId`.
30 |
31 | ## 0.2.0
32 |
33 | ### New features
34 |
35 | - Added `Gc::as_ptr`.
36 | - Added `Gc::ptr_eq`.
37 | - Implemented `PartialEq` and `Eq` for garbage collected pointers.
38 |
39 | ### Other
40 |
41 | - Changed license from GNU GPLv3 or later to MPL 2.0.
42 | - Allocations which do not contain `Gc`s will simply be reference counted.
43 |
44 | ## 0.1.2
45 |
46 | ### New features
47 |
48 | - Implement `Collectable` for `OnceCell`, `HashMap`, and `BTreeMap`.
49 | - Add `try_clone` and `try_deref` to `unsync::Gc` and `sync::Gc`.
50 | - Make dereferencing `Gc` only panic on truly-dead `Gc`s.
51 |
52 | ### Bugfixes
53 |
54 | - Prevent dead `Gc`s from escaping their `Drop` implementation, potentially causing UAFs.
55 | - Use fully-qualified name for `Result` in derive macro, preventing some bugs.
56 |
57 | ### Other
58 |
59 | - Improve performance in `unsync` by using `parking_lot` for concurrency primitives.
60 | - Improve documentation of panicking behavior in `Gc`.
61 | - Fix spelling mistakes in documentation.
62 |
63 | ## 0.1.1
64 |
65 | ### Bugfixes
66 |
67 | - Prevent possible UAFs caused by accessing `Gc`s during `Drop` impls by panicking.
68 |
69 | ### Other
70 |
71 | - Fix spelling mistakes in documentation.
72 |
73 | ## 0.1.0
74 |
75 | Initial release.
76 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "dumpster",
4 | "dumpster_derive",
5 | "dumpster_test",
6 | "dumpster_bench",
7 | ]
8 | resolver = "2"
9 |
10 | [patch.crates-io]
11 | dumpster = { path = "dumpster" }
12 |
13 | [profile.release]
14 | lto = true
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | ### 1. Definitions
5 |
6 | **1.1. “Contributor”**
7 | means each individual or legal entity that creates, contributes to
8 | the creation of, or owns Covered Software.
9 |
10 | **1.2. “Contributor Version”**
11 | means the combination of the Contributions of others (if any) used
12 | by a Contributor and that particular Contributor's Contribution.
13 |
14 | **1.3. “Contribution”**
15 | means Covered Software of a particular Contributor.
16 |
17 | **1.4. “Covered Software”**
18 | means Source Code Form to which the initial Contributor has attached
19 | the notice in Exhibit A, the Executable Form of such Source Code
20 | Form, and Modifications of such Source Code Form, in each case
21 | including portions thereof.
22 |
23 | **1.5. “Incompatible With Secondary Licenses”**
24 | means
25 |
26 | * **(a)** that the initial Contributor has attached the notice described
27 | in Exhibit B to the Covered Software; or
28 | * **(b)** that the Covered Software was made available under the terms of
29 | version 1.1 or earlier of the License, but not also under the
30 | terms of a Secondary License.
31 |
32 | **1.6. “Executable Form”**
33 | means any form of the work other than Source Code Form.
34 |
35 | **1.7. “Larger Work”**
36 | means a work that combines Covered Software with other material, in
37 | a separate file or files, that is not Covered Software.
38 |
39 | **1.8. “License”**
40 | means this document.
41 |
42 | **1.9. “Licensable”**
43 | means having the right to grant, to the maximum extent possible,
44 | whether at the time of the initial grant or subsequently, any and
45 | all of the rights conveyed by this License.
46 |
47 | **1.10. “Modifications”**
48 | means any of the following:
49 |
50 | * **(a)** any file in Source Code Form that results from an addition to,
51 | deletion from, or modification of the contents of Covered
52 | Software; or
53 | * **(b)** any new file in Source Code Form that contains any Covered
54 | Software.
55 |
56 | **1.11. “Patent Claims” of a Contributor**
57 | means any patent claim(s), including without limitation, method,
58 | process, and apparatus claims, in any patent Licensable by such
59 | Contributor that would be infringed, but for the grant of the
60 | License, by the making, using, selling, offering for sale, having
61 | made, import, or transfer of either its Contributions or its
62 | Contributor Version.
63 |
64 | **1.12. “Secondary License”**
65 | means either the GNU General Public License, Version 2.0, the GNU
66 | Lesser General Public License, Version 2.1, the GNU Affero General
67 | Public License, Version 3.0, or any later versions of those
68 | licenses.
69 |
70 | **1.13. “Source Code Form”**
71 | means the form of the work preferred for making modifications.
72 |
73 | **1.14. “You” (or “Your”)**
74 | means an individual or a legal entity exercising rights under this
75 | License. For legal entities, “You” includes any entity that
76 | controls, is controlled by, or is under common control with You. For
77 | purposes of this definition, “control” means **(a)** the power, direct
78 | or indirect, to cause the direction or management of such entity,
79 | whether by contract or otherwise, or **(b)** ownership of more than
80 | fifty percent (50%) of the outstanding shares or beneficial
81 | ownership of such entity.
82 |
83 |
84 | ### 2. License Grants and Conditions
85 |
86 | #### 2.1. Grants
87 |
88 | Each Contributor hereby grants You a world-wide, royalty-free,
89 | non-exclusive license:
90 |
91 | * **(a)** under intellectual property rights (other than patent or trademark)
92 | Licensable by such Contributor to use, reproduce, make available,
93 | modify, display, perform, distribute, and otherwise exploit its
94 | Contributions, either on an unmodified basis, with Modifications, or
95 | as part of a Larger Work; and
96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer
97 | for sale, have made, import, and otherwise transfer either its
98 | Contributions or its Contributor Version.
99 |
100 | #### 2.2. Effective Date
101 |
102 | The licenses granted in Section 2.1 with respect to any Contribution
103 | become effective for each Contribution on the date the Contributor first
104 | distributes such Contribution.
105 |
106 | #### 2.3. Limitations on Grant Scope
107 |
108 | The licenses granted in this Section 2 are the only rights granted under
109 | this License. No additional rights or licenses will be implied from the
110 | distribution or licensing of Covered Software under this License.
111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
112 | Contributor:
113 |
114 | * **(a)** for any code that a Contributor has removed from Covered Software;
115 | or
116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's
117 | modifications of Covered Software, or **(ii)** the combination of its
118 | Contributions with other software (except as part of its Contributor
119 | Version); or
120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of
121 | its Contributions.
122 |
123 | This License does not grant any rights in the trademarks, service marks,
124 | or logos of any Contributor (except as may be necessary to comply with
125 | the notice requirements in Section 3.4).
126 |
127 | #### 2.4. Subsequent Licenses
128 |
129 | No Contributor makes additional grants as a result of Your choice to
130 | distribute the Covered Software under a subsequent version of this
131 | License (see Section 10.2) or under the terms of a Secondary License (if
132 | permitted under the terms of Section 3.3).
133 |
134 | #### 2.5. Representation
135 |
136 | Each Contributor represents that the Contributor believes its
137 | Contributions are its original creation(s) or it has sufficient rights
138 | to grant the rights to its Contributions conveyed by this License.
139 |
140 | #### 2.6. Fair Use
141 |
142 | This License is not intended to limit any rights You have under
143 | applicable copyright doctrines of fair use, fair dealing, or other
144 | equivalents.
145 |
146 | #### 2.7. Conditions
147 |
148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
149 | in Section 2.1.
150 |
151 |
152 | ### 3. Responsibilities
153 |
154 | #### 3.1. Distribution of Source Form
155 |
156 | All distribution of Covered Software in Source Code Form, including any
157 | Modifications that You create or to which You contribute, must be under
158 | the terms of this License. You must inform recipients that the Source
159 | Code Form of the Covered Software is governed by the terms of this
160 | License, and how they can obtain a copy of this License. You may not
161 | attempt to alter or restrict the recipients' rights in the Source Code
162 | Form.
163 |
164 | #### 3.2. Distribution of Executable Form
165 |
166 | If You distribute Covered Software in Executable Form then:
167 |
168 | * **(a)** such Covered Software must also be made available in Source Code
169 | Form, as described in Section 3.1, and You must inform recipients of
170 | the Executable Form how they can obtain a copy of such Source Code
171 | Form by reasonable means in a timely manner, at a charge no more
172 | than the cost of distribution to the recipient; and
173 |
174 | * **(b)** You may distribute such Executable Form under the terms of this
175 | License, or sublicense it under different terms, provided that the
176 | license for the Executable Form does not attempt to limit or alter
177 | the recipients' rights in the Source Code Form under this License.
178 |
179 | #### 3.3. Distribution of a Larger Work
180 |
181 | You may create and distribute a Larger Work under terms of Your choice,
182 | provided that You also comply with the requirements of this License for
183 | the Covered Software. If the Larger Work is a combination of Covered
184 | Software with a work governed by one or more Secondary Licenses, and the
185 | Covered Software is not Incompatible With Secondary Licenses, this
186 | License permits You to additionally distribute such Covered Software
187 | under the terms of such Secondary License(s), so that the recipient of
188 | the Larger Work may, at their option, further distribute the Covered
189 | Software under the terms of either this License or such Secondary
190 | License(s).
191 |
192 | #### 3.4. Notices
193 |
194 | You may not remove or alter the substance of any license notices
195 | (including copyright notices, patent notices, disclaimers of warranty,
196 | or limitations of liability) contained within the Source Code Form of
197 | the Covered Software, except that You may alter any license notices to
198 | the extent required to remedy known factual inaccuracies.
199 |
200 | #### 3.5. Application of Additional Terms
201 |
202 | You may choose to offer, and to charge a fee for, warranty, support,
203 | indemnity or liability obligations to one or more recipients of Covered
204 | Software. However, You may do so only on Your own behalf, and not on
205 | behalf of any Contributor. You must make it absolutely clear that any
206 | such warranty, support, indemnity, or liability obligation is offered by
207 | You alone, and You hereby agree to indemnify every Contributor for any
208 | liability incurred by such Contributor as a result of warranty, support,
209 | indemnity or liability terms You offer. You may include additional
210 | disclaimers of warranty and limitations of liability specific to any
211 | jurisdiction.
212 |
213 |
214 | ### 4. Inability to Comply Due to Statute or Regulation
215 |
216 | If it is impossible for You to comply with any of the terms of this
217 | License with respect to some or all of the Covered Software due to
218 | statute, judicial order, or regulation then You must: **(a)** comply with
219 | the terms of this License to the maximum extent possible; and **(b)**
220 | describe the limitations and the code they affect. Such description must
221 | be placed in a text file included with all distributions of the Covered
222 | Software under this License. Except to the extent prohibited by statute
223 | or regulation, such description must be sufficiently detailed for a
224 | recipient of ordinary skill to be able to understand it.
225 |
226 |
227 | ### 5. Termination
228 |
229 | **5.1.** The rights granted under this License will terminate automatically
230 | if You fail to comply with any of its terms. However, if You become
231 | compliant, then the rights granted under this License from a particular
232 | Contributor are reinstated **(a)** provisionally, unless and until such
233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an
234 | ongoing basis, if such Contributor fails to notify You of the
235 | non-compliance by some reasonable means prior to 60 days after You have
236 | come back into compliance. Moreover, Your grants from a particular
237 | Contributor are reinstated on an ongoing basis if such Contributor
238 | notifies You of the non-compliance by some reasonable means, this is the
239 | first time You have received notice of non-compliance with this License
240 | from such Contributor, and You become compliant prior to 30 days after
241 | Your receipt of the notice.
242 |
243 | **5.2.** If You initiate litigation against any entity by asserting a patent
244 | infringement claim (excluding declaratory judgment actions,
245 | counter-claims, and cross-claims) alleging that a Contributor Version
246 | directly or indirectly infringes any patent, then the rights granted to
247 | You by any and all Contributors for the Covered Software under Section
248 | 2.1 of this License shall terminate.
249 |
250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all
251 | end user license agreements (excluding distributors and resellers) which
252 | have been validly granted by You or Your distributors under this License
253 | prior to termination shall survive termination.
254 |
255 |
256 | ### 6. Disclaimer of Warranty
257 |
258 | > Covered Software is provided under this License on an “as is”
259 | > basis, without warranty of any kind, either expressed, implied, or
260 | > statutory, including, without limitation, warranties that the
261 | > Covered Software is free of defects, merchantable, fit for a
262 | > particular purpose or non-infringing. The entire risk as to the
263 | > quality and performance of the Covered Software is with You.
264 | > Should any Covered Software prove defective in any respect, You
265 | > (not any Contributor) assume the cost of any necessary servicing,
266 | > repair, or correction. This disclaimer of warranty constitutes an
267 | > essential part of this License. No use of any Covered Software is
268 | > authorized under this License except under this disclaimer.
269 |
270 | ### 7. Limitation of Liability
271 |
272 | > Under no circumstances and under no legal theory, whether tort
273 | > (including negligence), contract, or otherwise, shall any
274 | > Contributor, or anyone who distributes Covered Software as
275 | > permitted above, be liable to You for any direct, indirect,
276 | > special, incidental, or consequential damages of any character
277 | > including, without limitation, damages for lost profits, loss of
278 | > goodwill, work stoppage, computer failure or malfunction, or any
279 | > and all other commercial damages or losses, even if such party
280 | > shall have been informed of the possibility of such damages. This
281 | > limitation of liability shall not apply to liability for death or
282 | > personal injury resulting from such party's negligence to the
283 | > extent applicable law prohibits such limitation. Some
284 | > jurisdictions do not allow the exclusion or limitation of
285 | > incidental or consequential damages, so this exclusion and
286 | > limitation may not apply to You.
287 |
288 |
289 | ### 8. Litigation
290 |
291 | Any litigation relating to this License may be brought only in the
292 | courts of a jurisdiction where the defendant maintains its principal
293 | place of business and such litigation shall be governed by laws of that
294 | jurisdiction, without reference to its conflict-of-law provisions.
295 | Nothing in this Section shall prevent a party's ability to bring
296 | cross-claims or counter-claims.
297 |
298 |
299 | ### 9. Miscellaneous
300 |
301 | This License represents the complete agreement concerning the subject
302 | matter hereof. If any provision of this License is held to be
303 | unenforceable, such provision shall be reformed only to the extent
304 | necessary to make it enforceable. Any law or regulation which provides
305 | that the language of a contract shall be construed against the drafter
306 | shall not be used to construe this License against a Contributor.
307 |
308 |
309 | ### 10. Versions of the License
310 |
311 | #### 10.1. New Versions
312 |
313 | Mozilla Foundation is the license steward. Except as provided in Section
314 | 10.3, no one other than the license steward has the right to modify or
315 | publish new versions of this License. Each version will be given a
316 | distinguishing version number.
317 |
318 | #### 10.2. Effect of New Versions
319 |
320 | You may distribute the Covered Software under the terms of the version
321 | of the License under which You originally received the Covered Software,
322 | or under the terms of any subsequent version published by the license
323 | steward.
324 |
325 | #### 10.3. Modified Versions
326 |
327 | If you create software not governed by this License, and you want to
328 | create a new license for such software, you may create and use a
329 | modified version of this License if you rename the license and remove
330 | any references to the name of the license steward (except to note that
331 | such modified license differs from this License).
332 |
333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
334 |
335 | If You choose to distribute Source Code Form that is Incompatible With
336 | Secondary Licenses under the terms of this version of the License, the
337 | notice described in Exhibit B of this License must be attached.
338 |
339 | ## Exhibit A - Source Code Form License Notice
340 |
341 | This Source Code Form is subject to the terms of the Mozilla Public
342 | License, v. 2.0. If a copy of the MPL was not distributed with this
343 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
344 |
345 | If it is not possible or desirable to put the notice in a particular
346 | file, then You may include the notice in a location (such as a LICENSE
347 | file in a relevant directory) where a recipient would be likely to look
348 | for such a notice.
349 |
350 | You may add additional accurate notices of copyright ownership.
351 |
352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice
353 |
354 | This Source Code Form is "Incompatible With Secondary Licenses", as
355 | defined by the Mozilla Public License, v. 2.0.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `dumpster`: A cycle-tracking garbage collector for Rust
2 |
3 | `dumpster` is an cycle-detecting garbage collector for Rust.
4 | It detects unreachable allocations and automatically frees them.
5 |
6 | ## Why should you use this crate?
7 |
8 | In short, `dumpster` offers a great mix of usability, performance, and flexibility.
9 |
10 | - `dumpster`'s API is a drop-in replacement for `std`'s reference-counted shared allocations
11 | (`Rc` and `Arc`).
12 | - It's very performant and has builtin implementations of both thread-local and concurrent
13 | garbage collection.
14 | - There are no restrictions on the reference structure within a garbage-collected allocation
15 | (references may point in any way you like).
16 | - It's trivial to make a custom type Trace using the provided derive macros.
17 | - You can even store `?Sized` data in a garbage-collected pointer!
18 |
19 | ## How it works
20 |
21 | `dumpster` is unlike most tracing garbage collectors.
22 | Other GCs keep track of a set of roots, which can then be used to perform a sweep and find out
23 | which allocations are reachable and which are not.
24 | Instead, `dumpster` extends reference-counted garbage collection (such as `std::rc::Rc`) with a
25 | cycle-detection algorithm, enabling it to effectively clean up self-referential data structures.
26 |
27 | For a deeper dive, check out this
28 | [blog post](https://claytonwramsey.github.io/2023/08/14/dumpster.html).
29 |
30 | ## What this library contains
31 |
32 | `dumpster` actually contains two garbage collector implementations: one thread-local, non-`Send`
33 | garbage collector in the module `unsync`, and one thread-safe garbage collector in the module
34 | `sync`.
35 | These garbage collectors can be safely mixed and matched.
36 |
37 | This library also comes with a derive macro for creating custom Trace types.
38 |
39 | ## Examples
40 |
41 | ```rust
42 | use dumpster::{Trace, unsync::Gc};
43 |
44 | #[derive(Trace)]
45 | struct Foo {
46 | ptr: RefCell>>,
47 | }
48 |
49 | // Create a new garbage-collected Foo.
50 | let foo = Gc::new(Foo {
51 | ptr: RefCell::new(None),
52 | });
53 |
54 | // Insert a circular reference inside of the foo.
55 | *foo.ptr.borrow_mut() = Some(foo.clone());
56 |
57 | // Render the foo inaccessible.
58 | // This may trigger a collection, but it's not guaranteed.
59 | // If we had used `Rc` instead of `Gc`, this would have caused a memory leak.
60 | drop(foo);
61 |
62 | // Trigger a collection.
63 | // This isn't necessary, but it guarantees that `foo` will be collected immediately (instead of
64 | // later).
65 | dumpster::unsync::collect();
66 | ```
67 |
68 | ## Installation
69 |
70 | To install, simply add `dumpster` as a dependency to your project.
71 |
72 | ```toml
73 | [dependencies]
74 | dumpster = "1.1.0"
75 | ```
76 |
77 | ## Optional features
78 |
79 | ## `derive`
80 |
81 | `derive` is enabled by default.
82 | It enables the derive macro for `Trace`, which makes it easy for users to implement their
83 | own Trace types.
84 |
85 | ```rust
86 | use dumpster::{unsync::Gc, Trace};
87 | use std::cell::RefCell;
88 |
89 | #[derive(Trace)] // no manual implementation required
90 | struct Foo(RefCell >>);
91 |
92 | let my_foo = Gc::new(Foo(RefCell::new(None)));
93 | *my_foo.0.borrow_mut() = Some(my_foo.clone());
94 |
95 | drop(my_foo); // my_foo will be automatically cleaned up
96 | ```
97 |
98 | ## `either`
99 |
100 | `either` is disabled by default. It adds support for the [`either`](https://crates.io/crates/either) crate,
101 | specifically by implementing `Trace` for [`either::Either`](https://docs.rs/either/1.13.0/either/enum.Either.html).
102 |
103 | ## `coerce-unsized`
104 |
105 | `coerce-unsized` is disabled by default.
106 | This enables the implementation of `CoerceUnsized` for each garbage collector,
107 | making it possible to use `Gc` with `!Sized` types conveniently.
108 |
109 | ```rust
110 | use dumpster::unsync::Gc;
111 |
112 | // this only works with "coerce-unsized" enabled while compiling on nightly Rust
113 | let gc1: Gc<[u8]> = Gc::new([1, 2, 3]);
114 | ```
115 |
116 | To use `coerce-unsized`, edit your installation to `Cargo.toml` to include the feature.
117 |
118 | ```toml
119 | [dependencies]
120 | dumpster = { version = "1.1.0", features = ["coerce-unsized"]}
121 | ```
122 |
123 | ## License
124 |
125 | This code is licensed under the Mozilla Public License, version 2.0.
126 | For more information, refer to [LICENSE.md](LICENSE.md).
127 |
--------------------------------------------------------------------------------
/dumpster/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/dumpster/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dumpster"
3 | version = "1.1.0"
4 | edition = "2021"
5 | license = "MPL-2.0"
6 | authors = ["Clayton Ramsey"]
7 | description = "A concurrent cycle-tracking garbage collector."
8 | repository = "https://github.com/claytonwramsey/dumpster"
9 | readme = "../README.md"
10 | keywords = ["dumpster", "garbage_collector", "gc"]
11 | categories = ["memory-management", "data-structures"]
12 |
13 | [features]
14 | default = ["derive"]
15 | coerce-unsized = []
16 | derive = ["dep:dumpster_derive"]
17 | either = ["dep:either"]
18 |
19 | [dependencies]
20 | parking_lot = "0.12.3"
21 | dumpster_derive = { version = "1.1.0", path = "../dumpster_derive", optional = true }
22 | either = { version = "1.13.0", optional = true }
23 |
24 | [dev-dependencies]
25 | fastrand = "2.0.0"
26 |
27 | [package.metadata.playground]
28 | features = ["derive"]
29 |
30 | [package.metadata.docs.rs]
31 | features = ["derive"]
32 | targets = ["x86_64-unknown-linux-gnu"]
33 | rustdoc-args = ["--generate-link-to-definition"]
34 |
--------------------------------------------------------------------------------
/dumpster/src/impls.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! Implementations of [`Trace`] for common data types.
10 |
11 | #![allow(deprecated)]
12 |
13 | use std::{
14 | any::TypeId,
15 | borrow::Cow,
16 | cell::{Cell, OnceCell, RefCell},
17 | collections::{
18 | hash_map::{DefaultHasher, RandomState},
19 | BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque,
20 | },
21 | convert::Infallible,
22 | ffi::{OsStr, OsString},
23 | hash::{BuildHasher, BuildHasherDefault, SipHasher},
24 | marker::PhantomData,
25 | num::{
26 | NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
27 | NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
28 | },
29 | ops::Deref,
30 | path::{Path, PathBuf},
31 | rc::Rc,
32 | sync::{
33 | atomic::{
34 | AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32,
35 | AtomicU64, AtomicU8, AtomicUsize,
36 | },
37 | Mutex, MutexGuard, OnceLock, RwLock, RwLockReadGuard, TryLockError,
38 | },
39 | };
40 |
41 | use crate::{Trace, Visitor};
42 |
43 | unsafe impl Trace for Infallible {
44 | fn accept(&self, _: &mut V) -> Result<(), ()> {
45 | match *self {}
46 | }
47 | }
48 |
49 | #[cfg(feature = "either")]
50 | unsafe impl Trace for either::Either {
51 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
52 | match self {
53 | either::Either::Left(a) => a.accept(visitor),
54 | either::Either::Right(b) => b.accept(visitor),
55 | }
56 | }
57 | }
58 |
59 | /// Implement `Trace` trivially for some parametric `?Sized` type.
60 | macro_rules! param_trivial_impl_unsized {
61 | ($x: ty) => {
62 | unsafe impl Trace for $x {
63 | #[inline]
64 | fn accept(&self, _: &mut V) -> Result<(), ()> {
65 | Ok(())
66 | }
67 | }
68 | };
69 | }
70 |
71 | param_trivial_impl_unsized!(MutexGuard<'static, T>);
72 | param_trivial_impl_unsized!(RwLockReadGuard<'static, T>);
73 | param_trivial_impl_unsized!(&'static T);
74 | param_trivial_impl_unsized!(PhantomData);
75 |
76 | unsafe impl Trace for Box {
77 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
78 | (**self).accept(visitor)
79 | }
80 | }
81 |
82 | unsafe impl Trace for BuildHasherDefault {
83 | fn accept(&self, _: &mut V) -> Result<(), ()> {
84 | Ok(())
85 | }
86 | }
87 |
88 | unsafe impl<'a, T: ToOwned> Trace for Cow<'a, T>
89 | where
90 | T::Owned: Trace,
91 | {
92 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
93 | if let Cow::Owned(ref v) = self {
94 | v.accept(visitor)?;
95 | }
96 | Ok(())
97 | }
98 | }
99 |
100 | unsafe impl Trace for RefCell {
101 | #[inline]
102 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
103 | self.try_borrow().map_err(|_| ())?.accept(visitor)
104 | }
105 | }
106 |
107 | unsafe impl Trace for Mutex {
108 | #[inline]
109 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
110 | self.try_lock()
111 | .map_err(|e| match e {
112 | TryLockError::Poisoned(_) => panic!(),
113 | TryLockError::WouldBlock => (),
114 | })?
115 | .deref()
116 | .accept(visitor)
117 | }
118 | }
119 |
120 | unsafe impl Trace for RwLock {
121 | #[inline]
122 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
123 | self.try_read()
124 | .map_err(|e| match e {
125 | TryLockError::Poisoned(_) => panic!(),
126 | TryLockError::WouldBlock => (),
127 | })?
128 | .deref()
129 | .accept(visitor)
130 | }
131 | }
132 |
133 | unsafe impl Trace for Option {
134 | #[inline]
135 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
136 | match self {
137 | Some(x) => x.accept(visitor),
138 | None => Ok(()),
139 | }
140 | }
141 | }
142 |
143 | unsafe impl Trace for Result {
144 | #[inline]
145 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
146 | match self {
147 | Ok(t) => t.accept(visitor),
148 | Err(e) => e.accept(visitor),
149 | }
150 | }
151 | }
152 |
153 | unsafe impl Trace for Cell {
154 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
155 | self.get().accept(visitor)
156 | }
157 | }
158 |
159 | unsafe impl Trace for OnceCell {
160 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
161 | self.get().map_or(Ok(()), |x| x.accept(visitor))
162 | }
163 | }
164 |
165 | unsafe impl Trace for OnceLock {
166 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
167 | self.get().map_or(Ok(()), |x| x.accept(visitor))
168 | }
169 | }
170 |
171 | /// Implement [`Trace`] for a collection data structure which has some method `iter()` that
172 | /// iterates over all elements of the data structure and `iter_mut()` which does the same over
173 | /// mutable references.
174 | macro_rules! Trace_collection_impl {
175 | ($x: ty) => {
176 | unsafe impl Trace for $x {
177 | #[inline]
178 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
179 | for elem in self {
180 | elem.accept(visitor)?;
181 | }
182 | Ok(())
183 | }
184 | }
185 | };
186 | }
187 |
188 | Trace_collection_impl!(Vec);
189 | Trace_collection_impl!(VecDeque);
190 | Trace_collection_impl!(LinkedList);
191 | Trace_collection_impl!([T]);
192 | Trace_collection_impl!(HashSet);
193 | Trace_collection_impl!(BinaryHeap);
194 | Trace_collection_impl!(BTreeSet);
195 |
196 | unsafe impl Trace for HashMap {
197 | fn accept(&self, visitor: &mut Z) -> Result<(), ()> {
198 | for (k, v) in self {
199 | k.accept(visitor)?;
200 | v.accept(visitor)?;
201 | }
202 | self.hasher().accept(visitor)
203 | }
204 | }
205 |
206 | unsafe impl Trace for BTreeMap {
207 | fn accept(&self, visitor: &mut Z) -> Result<(), ()> {
208 | for (k, v) in self {
209 | k.accept(visitor)?;
210 | v.accept(visitor)?;
211 | }
212 | Ok(())
213 | }
214 | }
215 |
216 | unsafe impl Trace for [T; N] {
217 | #[inline]
218 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
219 | for elem in self {
220 | elem.accept(visitor)?;
221 | }
222 | Ok(())
223 | }
224 | }
225 |
226 | /// Implement [`Trace`] for a trivially-collected type which contains no [`Gc`]s in its
227 | /// fields.
228 | macro_rules! Trace_trivial_impl {
229 | ($x: ty) => {
230 | unsafe impl Trace for $x {
231 | #[inline]
232 | fn accept(&self, _: &mut V) -> Result<(), ()> {
233 | Ok(())
234 | }
235 | }
236 | };
237 | }
238 |
239 | Trace_trivial_impl!(());
240 |
241 | Trace_trivial_impl!(u8);
242 | Trace_trivial_impl!(u16);
243 | Trace_trivial_impl!(u32);
244 | Trace_trivial_impl!(u64);
245 | Trace_trivial_impl!(u128);
246 | Trace_trivial_impl!(usize);
247 | Trace_trivial_impl!(i8);
248 | Trace_trivial_impl!(i16);
249 | Trace_trivial_impl!(i32);
250 | Trace_trivial_impl!(i64);
251 | Trace_trivial_impl!(i128);
252 | Trace_trivial_impl!(isize);
253 |
254 | Trace_trivial_impl!(bool);
255 | Trace_trivial_impl!(char);
256 |
257 | Trace_trivial_impl!(f32);
258 | Trace_trivial_impl!(f64);
259 |
260 | Trace_trivial_impl!(AtomicU8);
261 | Trace_trivial_impl!(AtomicU16);
262 | Trace_trivial_impl!(AtomicU32);
263 | Trace_trivial_impl!(AtomicU64);
264 | Trace_trivial_impl!(AtomicUsize);
265 | Trace_trivial_impl!(AtomicI8);
266 | Trace_trivial_impl!(AtomicI16);
267 | Trace_trivial_impl!(AtomicI32);
268 | Trace_trivial_impl!(AtomicI64);
269 | Trace_trivial_impl!(AtomicIsize);
270 |
271 | Trace_trivial_impl!(NonZeroU8);
272 | Trace_trivial_impl!(NonZeroU16);
273 | Trace_trivial_impl!(NonZeroU32);
274 | Trace_trivial_impl!(NonZeroU64);
275 | Trace_trivial_impl!(NonZeroU128);
276 | Trace_trivial_impl!(NonZeroUsize);
277 | Trace_trivial_impl!(NonZeroI8);
278 | Trace_trivial_impl!(NonZeroI16);
279 | Trace_trivial_impl!(NonZeroI32);
280 | Trace_trivial_impl!(NonZeroI64);
281 | Trace_trivial_impl!(NonZeroI128);
282 | Trace_trivial_impl!(NonZeroIsize);
283 |
284 | Trace_trivial_impl!(String);
285 | Trace_trivial_impl!(str);
286 | Trace_trivial_impl!(PathBuf);
287 | Trace_trivial_impl!(Path);
288 | Trace_trivial_impl!(OsString);
289 | Trace_trivial_impl!(OsStr);
290 |
291 | Trace_trivial_impl!(DefaultHasher);
292 | Trace_trivial_impl!(RandomState);
293 | Trace_trivial_impl!(Rc);
294 | Trace_trivial_impl!(SipHasher);
295 |
296 | Trace_trivial_impl!(TypeId);
297 |
298 | /// Implement [`Trace`] for a tuple.
299 | macro_rules! Trace_tuple {
300 | () => {}; // This case is handled above by the trivial case
301 | ($($args:ident),*) => {
302 | unsafe impl<$($args: Trace),*> Trace for ($($args,)*) {
303 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
304 | #[allow(non_snake_case)]
305 | let &($(ref $args,)*) = self;
306 | $(($args).accept(visitor)?;)*
307 | Ok(())
308 | }
309 | }
310 | }
311 | }
312 |
313 | Trace_tuple!();
314 | Trace_tuple!(A);
315 | Trace_tuple!(A, B);
316 | Trace_tuple!(A, B, C);
317 | Trace_tuple!(A, B, C, D);
318 | Trace_tuple!(A, B, C, D, E);
319 | Trace_tuple!(A, B, C, D, E, F);
320 | Trace_tuple!(A, B, C, D, E, F, G);
321 | Trace_tuple!(A, B, C, D, E, F, G, H);
322 | Trace_tuple!(A, B, C, D, E, F, G, H, I);
323 | Trace_tuple!(A, B, C, D, E, F, G, H, I, J);
324 |
325 | /// Implement `Trace` for one function type.
326 | macro_rules! Trace_fn {
327 | ($ty:ty $(,$args:ident)*) => {
328 | unsafe impl Trace for $ty {
329 | fn accept(&self, _: &mut V) -> Result<(), ()> { Ok(()) }
330 | }
331 | }
332 | }
333 |
334 | /// Implement `Trace` for all functions with a given set of args.
335 | macro_rules! Trace_fn_group {
336 | () => {
337 | Trace_fn!(extern "Rust" fn () -> Ret);
338 | Trace_fn!(extern "C" fn () -> Ret);
339 | Trace_fn!(unsafe extern "Rust" fn () -> Ret);
340 | Trace_fn!(unsafe extern "C" fn () -> Ret);
341 | };
342 | ($($args:ident),*) => {
343 | Trace_fn!(extern "Rust" fn ($($args),*) -> Ret, $($args),*);
344 | Trace_fn!(extern "C" fn ($($args),*) -> Ret, $($args),*);
345 | Trace_fn!(extern "C" fn ($($args),*, ...) -> Ret, $($args),*);
346 | Trace_fn!(unsafe extern "Rust" fn ($($args),*) -> Ret, $($args),*);
347 | Trace_fn!(unsafe extern "C" fn ($($args),*) -> Ret, $($args),*);
348 | Trace_fn!(unsafe extern "C" fn ($($args),*, ...) -> Ret, $($args),*);
349 | }
350 | }
351 |
352 | Trace_fn_group!();
353 | Trace_fn_group!(A);
354 | Trace_fn_group!(A, B);
355 | Trace_fn_group!(A, B, C);
356 | Trace_fn_group!(A, B, C, D);
357 | Trace_fn_group!(A, B, C, D, E);
358 | Trace_fn_group!(A, B, C, D, E, F);
359 | Trace_fn_group!(A, B, C, D, E, F, G);
360 | Trace_fn_group!(A, B, C, D, E, F, G, H);
361 | Trace_fn_group!(A, B, C, D, E, F, G, H, I);
362 | Trace_fn_group!(A, B, C, D, E, F, G, H, I, J);
363 |
--------------------------------------------------------------------------------
/dumpster/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! A cycle-tracking concurrent garbage collector with an easy-to-use API.
10 | //!
11 | //! Most garbage collectors are _tracing_ garbage collectors, meaning that they keep track of a set
12 | //! of roots which are directly accessible from the stack, and then use those roots to find the set
13 | //! of all accessible allocations.
14 | //! However, because Rust does not allow us to hook into when a value is moved, it's quite difficult
15 | //! to detect when a garbage-collected value stops being a root.
16 | //!
17 | //! `dumpster` takes a different approach.
18 | //! It begins by using simple reference counting, then automatically detects cycles.
19 | //! Allocations are freed when their reference count reaches zero or when they are only accessible
20 | //! via their descendants.
21 | //!
22 | //! Garbage-collected pointers can be created and destroyed in _O(1)_ amortized time, but destroying
23 | //! a garbage-collected pointer may take _O(r)_, where _r_ is the number of existing
24 | //! garbage-collected references, on occasion.
25 | //! However, the cleanups that require _O(r)_ performance are performed once every _O(1/r)_ times
26 | //! a reference is dropped, yielding an amortized _O(1)_ runtime.
27 | //!
28 | //! # Why should you use this crate?
29 | //!
30 | //! In short, `dumpster` offers a great mix of usability, performance, and flexibility.
31 | //!
32 | //! - `dumpster`'s API is a drop-in replacement for `std`'s reference-counted shared allocations
33 | //! (`Rc` and `Arc`).
34 | //! - It's very performant and has builtin implementations of both thread-local and concurrent
35 | //! garbage collection.
36 | //! - There are no restrictions on the reference structure within a garbage-collected allocation
37 | //! (references may point in any way you like).
38 | //! - It's trivial to make a custom type Trace using the provided derive macros.
39 | //! - You can even store `?Sized` data in a garbage-collected pointer!
40 | //!
41 | //! # Module structure
42 | //!
43 | //! `dumpster` contains 3 core modules: the root (this module), as well as [`sync`] and [`unsync`].
44 | //! `sync` contains an implementation of thread-safe garbage-collected pointers, while `unsync`
45 | //! contains an implementation of thread-local garbage-collected pointers which cannot be shared
46 | //! across threads.
47 | //! Thread-safety requires some synchronization overhead, so for a single-threaded application,
48 | //! it is recommended to use `unsync`.
49 | //!
50 | //! The project root contains common definitions across both `sync` and `unsync`.
51 | //! Types which implement [`Trace`] can immediately be used in `unsync`, but in order to use
52 | //! `sync`'s garbage collector, the types must also implement [`Sync`].
53 | //!
54 | //! # Examples
55 | //!
56 | //! If your code is meant to run as a single thread, or if your data doesn't need to be shared
57 | //! across threads, you should use [`unsync::Gc`] to store your allocations.
58 | //!
59 | //! ```
60 | //! use dumpster::unsync::Gc;
61 | //! use std::cell::Cell;
62 | //!
63 | //! let my_gc = Gc::new(Cell::new(0451));
64 | //!
65 | //! let other_gc = my_gc.clone(); // shallow copy
66 | //! other_gc.set(512);
67 | //!
68 | //! assert_eq!(my_gc.get(), 512);
69 | //! ```
70 | //!
71 | //! For data which is shared across threads, you can use [`sync::Gc`] with the exact same API.
72 | //!
73 | //! ```
74 | //! use dumpster::sync::Gc;
75 | //! use std::sync::Mutex;
76 | //!
77 | //! let my_shared_gc = Gc::new(Mutex::new(25));
78 | //! let other_shared_gc = my_shared_gc.clone();
79 | //!
80 | //! std::thread::scope(|s| {
81 | //! s.spawn(move || {
82 | //! *other_shared_gc.lock().unwrap() = 35;
83 | //! });
84 | //! });
85 | //!
86 | //! println!("{}", *my_shared_gc.lock().unwrap());
87 | //! ```
88 | //!
89 | //! It's trivial to use custom data structures with the provided derive macro.
90 | //!
91 | //! ```
92 | //! use dumpster::{unsync::Gc, Trace};
93 | //! use std::cell::RefCell;
94 | //!
95 | //! #[derive(Trace)]
96 | //! struct Foo {
97 | //! refs: RefCell>>,
98 | //! }
99 | //!
100 | //! let foo = Gc::new(Foo {
101 | //! refs: RefCell::new(Vec::new()),
102 | //! });
103 | //!
104 | //! foo.refs.borrow_mut().push(foo.clone());
105 | //!
106 | //! drop(foo);
107 | //!
108 | //! // even though foo had a self reference, it still got collected!
109 | //! ```
110 | //!
111 | //! # Installation
112 | //!
113 | //! To use `dumpster`, add the following lines to your `Cargo.toml`.
114 | //!
115 | //! ```toml
116 | //! [dependencies]
117 | //! dumpster = "1.1.0"
118 | //! ```
119 | //!
120 | //! # Optional features
121 | //!
122 | //! ## `derive`
123 | //!
124 | //! `derive` is enabled by default.
125 | //! It enables the derive macro for `Trace`, which makes it easy for users to implement their
126 | //! own Trace types.
127 | //!
128 | //! ```
129 | //! use dumpster::{unsync::Gc, Trace};
130 | //! use std::cell::RefCell;
131 | //!
132 | //! #[derive(Trace)] // no manual implementation required
133 | //! struct Foo(RefCell>>);
134 | //!
135 | //! let my_foo = Gc::new(Foo(RefCell::new(None)));
136 | //! *my_foo.0.borrow_mut() = Some(my_foo.clone());
137 | //!
138 | //! drop(my_foo); // my_foo will be automatically cleaned up
139 | //! ```
140 | //!
141 | //! ## `either`
142 | //!
143 | //! `either` is disabled by default. It adds support for the [`either`](https://crates.io/crates/either) crate,
144 | //! specifically by implementing [`Trace`] for [`either::Either`](https://docs.rs/either/1.13.0/either/enum.Either.html).
145 | //!
146 | //! ## `coerce-unsized`
147 | //!
148 | //! `coerce-unsized` is disabled by default.
149 | //! This enables the implementation of [`std::ops::CoerceUnsized`] for each garbage collector,
150 | //! making it possible to use `Gc` with `!Sized` types conveniently.
151 | #![cfg_attr(
152 | feature = "coerce-unsized",
153 | doc = r##"
154 | ```
155 | // this only works with "coerce-unsized" enabled while compiling on nightly Rust
156 | use dumpster::unsync::Gc;
157 |
158 | let gc1: Gc<[u8]> = Gc::new([1, 2, 3]);
159 | ```
160 | "##
161 | )]
162 | //! To use `coerce-unsized`, edit your installation to `Cargo.toml` to include the feature.
163 | //!
164 | //! ```toml
165 | //! [dependencies]
166 | //! dumpster = { version = "1.1.0", features = ["coerce-unsized"]}
167 | //! ```
168 | //!
169 | //! # License
170 | //!
171 | //! `dumpster` is licensed under the Mozilla Public License, version 2.0.
172 | //! For more details, refer to
173 | //! [LICENSE.md](https://github.com/claytonwramsey/dumpster/blob/master/LICENSE.md).
174 |
175 | #![warn(clippy::pedantic)]
176 | #![warn(clippy::cargo)]
177 | #![warn(missing_docs)]
178 | #![warn(clippy::missing_docs_in_private_items)]
179 | #![allow(clippy::multiple_crate_versions, clippy::result_unit_err)]
180 | #![cfg_attr(feature = "coerce-unsized", feature(coerce_unsized))]
181 | #![cfg_attr(feature = "coerce-unsized", feature(unsize))]
182 |
183 | mod impls;
184 |
185 | mod ptr;
186 | pub mod sync;
187 | pub mod unsync;
188 |
189 | /// The trait that any garbage-Trace data must implement.
190 | ///
191 | /// This trait should usually be implemented by using `#[derive(Trace)]`, using the provided
192 | /// macro.
193 | /// Only data structures using raw pointers or other magic should manually implement `Trace`.
194 | ///
195 | /// # Safety
196 | ///
197 | /// If the implementation of this trait is incorrect, this will result in undefined behavior,
198 | /// typically double-frees or use-after-frees.
199 | /// This includes [`Trace::accept`], even though it is a safe function, since its correctness
200 | /// is required for safety.
201 | ///
202 | /// # Examples
203 | ///
204 | /// Implementing `Trace` for a scalar type which contains no garbage-collected references
205 | /// is very easy.
206 | /// Accepting a visitor is simply a no-op.
207 | ///
208 | /// ```
209 | /// use dumpster::{Trace, Visitor};
210 | ///
211 | /// struct Foo(u8);
212 | ///
213 | /// unsafe impl Trace for Foo {
214 | /// fn accept(&self, visitor: &mut V) -> Result<(), ()> {
215 | /// Ok(())
216 | /// }
217 | /// }
218 | /// ```
219 | ///
220 | /// However, if a data structure contains a garbage collected pointer, it must delegate to its
221 | /// fields in `accept`.
222 | ///
223 | /// ```
224 | /// use dumpster::{unsync::Gc, Trace, Visitor};
225 | ///
226 | /// struct Bar(Gc);
227 | ///
228 | /// unsafe impl Trace for Bar {
229 | /// fn accept(&self, visitor: &mut V) -> Result<(), ()> {
230 | /// self.0.accept(visitor)
231 | /// }
232 | /// }
233 | /// ```
234 | ///
235 | /// A data structure with two or more fields which could own a garbage-collected pointer should
236 | /// delegate to both fields in a consistent order:
237 | ///
238 | /// ```
239 | /// use dumpster::{unsync::Gc, Trace, Visitor};
240 | ///
241 | /// struct Baz {
242 | /// a: Gc,
243 | /// b: Gc,
244 | /// }
245 | ///
246 | /// unsafe impl Trace for Baz {
247 | /// fn accept(&self, visitor: &mut V) -> Result<(), ()> {
248 | /// self.a.accept(visitor)?;
249 | /// self.b.accept(visitor)?;
250 | /// Ok(())
251 | /// }
252 | /// }
253 | /// ```
254 | pub unsafe trait Trace {
255 | /// Accept a visitor to this garbage-collected value.
256 | ///
257 | /// Implementors of this function need only delegate to all fields owned by this value which
258 | /// may contain a garbage-collected reference (either a [`sync::Gc`] or a [`unsync::Gc`]).
259 | ///
260 | /// For structures which have more than one field, they should return immediately after the
261 | /// first `Err` is returned from one of its fields.
262 | /// To do so efficiently, we recommend using the try operator (`?`) on each field and then
263 | /// returning `Ok(())` after delegating to each field.
264 | ///
265 | /// # Errors
266 | ///
267 | /// Errors are returned from this function whenever a field of this object returns an error
268 | /// after delegating acceptance to it, or if this value's data is inaccessible (such as
269 | /// attempting to borrow from a [`RefCell`](std::cell::RefCell) which has already been
270 | /// mutably borrowed).
271 | fn accept(&self, visitor: &mut V) -> Result<(), ()>;
272 | }
273 |
274 | /// A visitor for a garbage collected value.
275 | ///
276 | /// This visitor allows us to hide details of the implementation of the garbage-collection procedure
277 | /// from implementors of [`Trace`].
278 | ///
279 | /// When accepted by a `Trace`, this visitor will be delegated down until it reaches a
280 | /// garbage-collected pointer.
281 | /// Then, the garbage-collected pointer will call one of `visit_sync` or `visit_unsync`, depending
282 | /// on which type of pointer it is.
283 | ///
284 | /// In general, it's not expected for consumers of this library to write their own visitors.
285 | pub trait Visitor {
286 | /// Visit a synchronized garbage-collected pointer.
287 | ///
288 | /// This function is called for every [`sync::Gc`] owned by the value that accepted this
289 | /// visitor.
290 | fn visit_sync(&mut self, gc: &sync::Gc)
291 | where
292 | T: Trace + Send + Sync + ?Sized;
293 |
294 | /// Visit a thread-local garbage-collected pointer.
295 | ///
296 | /// This function is called for every [`unsync::Gc`] owned by the value that accepted this
297 | /// visitor.
298 | fn visit_unsync(&mut self, gc: &unsync::Gc)
299 | where
300 | T: Trace + ?Sized;
301 | }
302 |
303 | // Re-export #[derive(Trace)].
304 | //
305 | // The reason re-exporting is not enabled by default is that disabling it would
306 | // be annoying for crates that provide handwritten impls or data formats. They
307 | // would need to disable default features and then explicitly re-enable std.
308 | #[cfg(feature = "derive")]
309 | extern crate dumpster_derive;
310 |
311 | #[cfg(feature = "derive")]
312 | /// The derive macro for implementing `Trace`.
313 | ///
314 | /// This enables users of `dumpster` to easily store custom types inside a `Gc`.
315 | /// To do so, simply annotate your type with `#[derive(Trace)]`.
316 | ///
317 | /// # Examples
318 | ///
319 | /// ```
320 | /// use dumpster::Trace;
321 | ///
322 | /// #[derive(Trace)]
323 | /// struct Foo {
324 | /// bar: Option>,
325 | /// }
326 | /// ```
327 | pub use dumpster_derive::Trace;
328 |
329 | /// Determine whether some value contains a garbage-collected pointer.
330 | ///
331 | /// This function will return one of three values:
332 | /// - `Ok(true)`: The data structure contains a garbage-collected pointer.
333 | /// - `Ok(false)`: The data structure contains no garbage-collected pointers.
334 | /// - `Err(())`: The data structure was accessed while we checked it for garbage-collected pointers.
335 | fn contains_gcs(x: &T) -> Result {
336 | /// A visitor structure used for determining whether some garbage-collected pointer contains a
337 | /// `Gc` in its pointed-to value.
338 | struct ContainsGcs(bool);
339 |
340 | impl Visitor for ContainsGcs {
341 | fn visit_sync(&mut self, _: &sync::Gc)
342 | where
343 | T: Trace + Send + Sync + ?Sized,
344 | {
345 | self.0 = true;
346 | }
347 |
348 | fn visit_unsync(&mut self, _: &unsync::Gc)
349 | where
350 | T: Trace + ?Sized,
351 | {
352 | self.0 = true;
353 | }
354 | }
355 |
356 | let mut visit = ContainsGcs(false);
357 | x.accept(&mut visit)?;
358 | Ok(visit.0)
359 | }
360 |
--------------------------------------------------------------------------------
/dumpster/src/ptr.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! Custom pointer types used by this garbage collector.
10 |
11 | use std::{
12 | fmt,
13 | mem::{size_of, MaybeUninit},
14 | ptr::{addr_of, addr_of_mut, copy_nonoverlapping, NonNull},
15 | };
16 |
17 | #[repr(C)]
18 | #[derive(Clone, Copy)]
19 | /// A pointer for an allocation, extracted out as raw data.
20 | /// This contains both the pointer and all the pointer's metadata, but hidden behind an unknown
21 | /// interpretation.
22 | /// We trust that all pointers (even to `?Sized` or `dyn` types) are 2 words or fewer in size.
23 | /// This is a hack! Like, a big hack!
24 | pub(crate) struct Erased([usize; 2]);
25 |
26 | impl Erased {
27 | /// Construct a new erased pointer to some data from a reference
28 | ///
29 | /// # Panics
30 | ///
31 | /// This function will panic if the size of a reference is larger than the size of an
32 | /// `ErasedPtr`.
33 | /// To my knowledge, there are no pointer types with this property.
34 | pub fn new(reference: NonNull) -> Erased {
35 | let mut ptr = Erased([0; 2]);
36 | let ptr_size = size_of::>();
37 | // Extract out the pointer as raw memory
38 | assert!(
39 | ptr_size <= size_of::(),
40 | "pointers to T are too big for storage"
41 | );
42 | unsafe {
43 | // SAFETY: We know that `cleanup` has at least as much space as `ptr_size`, and that
44 | // `box_ref` has size equal to `ptr_size`.
45 | copy_nonoverlapping(
46 | addr_of!(reference).cast::(),
47 | addr_of_mut!(ptr.0).cast::(),
48 | ptr_size,
49 | );
50 | }
51 |
52 | ptr
53 | }
54 |
55 | /// Specify this pointer into a pointer of a particular type.
56 | ///
57 | /// # Safety
58 | ///
59 | /// This function must only be specified to the type that the pointer was constructed with
60 | /// via [`ErasedPtr::new`].
61 | pub unsafe fn specify(self) -> NonNull {
62 | let mut box_ref: MaybeUninit> = MaybeUninit::zeroed();
63 |
64 | // For some reason, switching the ordering of casts causes this to create wacky undefined
65 | // behavior. Why? I don't know. I have better things to do than pontificate on this on a
66 | // Sunday afternoon.
67 | copy_nonoverlapping(
68 | addr_of!(self.0).cast::(),
69 | addr_of_mut!(box_ref).cast::(),
70 | size_of::>(),
71 | );
72 |
73 | box_ref.assume_init()
74 | }
75 | }
76 |
77 | impl fmt::Debug for Erased {
78 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 | write!(f, "ErasedPtr({:x?})", self.0)
80 | }
81 | }
82 |
83 | #[cfg(not(feature = "coerce-unsized"))]
84 | /// A nullable pointer to an `?Sized` type.
85 | ///
86 | /// We need this because it's actually impossible to create a null `*mut T` if `T` is `?Sized`.
87 | pub(crate) struct Nullable(Option>);
88 | #[cfg(feature = "coerce-unsized")]
89 | /// A nullable pointer to an `?Sized` type.
90 | ///
91 | /// We need this because it's actually impossible to create a null `*mut T` if `T` is `?Sized`.
92 | pub(crate) struct Nullable(*mut T);
93 |
94 | impl Nullable {
95 | /// Create a new nullable pointer from a non-null pointer.
96 | pub fn new(ptr: NonNull) -> Nullable {
97 | #[cfg(not(feature = "coerce-unsized"))]
98 | {
99 | Nullable(Some(ptr))
100 | }
101 | #[cfg(feature = "coerce-unsized")]
102 | {
103 | Nullable(ptr.as_ptr())
104 | }
105 | }
106 |
107 | #[allow(clippy::unused_self)]
108 | /// Convert this pointer to a null pointer.
109 | pub fn as_null(self) -> Nullable {
110 | #[cfg(not(feature = "coerce-unsized"))]
111 | {
112 | Nullable(None)
113 | }
114 | #[cfg(feature = "coerce-unsized")]
115 | {
116 | Nullable(self.0.with_addr(0))
117 | }
118 | }
119 |
120 | /// Determine whether this pointer is null.
121 | pub fn is_null(self) -> bool {
122 | self.as_option().is_none()
123 | }
124 |
125 | /// Convert this pointer to an `Option>`.
126 | pub fn as_option(self) -> Option> {
127 | #[cfg(not(feature = "coerce-unsized"))]
128 | {
129 | self.0
130 | }
131 | #[cfg(feature = "coerce-unsized")]
132 | {
133 | NonNull::new(self.0)
134 | }
135 | }
136 |
137 | /// Convert this pointer to a `NonNull`, panicking if this pointer is null with message
138 | /// `msg`.
139 | pub fn expect(self, msg: &str) -> NonNull {
140 | self.as_option().expect(msg)
141 | }
142 |
143 | /// Convert this pointer to a `NonNull`, panicking if this pointer is null.
144 | pub fn unwrap(self) -> NonNull {
145 | self.as_option().unwrap()
146 | }
147 | }
148 |
149 | impl Clone for Nullable {
150 | fn clone(&self) -> Self {
151 | *self
152 | }
153 | }
154 | impl Copy for Nullable {}
155 |
156 | #[cfg(feature = "coerce-unsized")]
157 | impl std::ops::CoerceUnsized> for Nullable
158 | where
159 | T: std::marker::Unsize + ?Sized,
160 | U: ?Sized,
161 | {
162 | }
163 |
164 | impl fmt::Debug for Nullable {
165 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 | write!(f, "Nullable({:x?})", self.0)
167 | }
168 | }
169 |
170 | #[cfg(test)]
171 | mod tests {
172 | use std::alloc::{dealloc, Layout};
173 |
174 | use super::*;
175 |
176 | #[test]
177 | fn erased_alloc() {
178 | let orig_ptr = Box::leak(Box::new(7u8));
179 | let erased_ptr = Erased::new(NonNull::from(orig_ptr));
180 |
181 | unsafe {
182 | let remade_ptr = erased_ptr.specify::();
183 | dealloc(remade_ptr.as_ptr(), Layout::for_value(remade_ptr.as_ref()));
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/dumpster/src/sync/collect.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! A synchronized collection algorithm.
10 |
11 | use std::{
12 | alloc::{dealloc, Layout},
13 | cell::{Cell, RefCell},
14 | collections::{hash_map::Entry, HashMap},
15 | mem::{replace, swap, take, transmute},
16 | ptr::{drop_in_place, NonNull},
17 | sync::{
18 | atomic::{AtomicPtr, AtomicUsize, Ordering},
19 | LazyLock,
20 | },
21 | };
22 |
23 | use parking_lot::{Mutex, RwLock};
24 |
25 | use crate::{ptr::Erased, Trace, Visitor};
26 |
27 | use super::{default_collect_condition, CollectCondition, CollectInfo, Gc, GcBox, CURRENT_TAG};
28 |
29 | /// The garbage truck, which is a global data structure containing information about allocations
30 | /// which might need to be collected.
31 | struct GarbageTruck {
32 | /// The contents of the garbage truck, containing all the allocations which need to be
33 | /// collected and have already been delivered by a [`Dumpster`].
34 | contents: Mutex>,
35 | /// A lock used for synchronizing threads that are awaiting completion of a collection process.
36 | /// This lock should be acquired for reads by threads running a collection and for writes by
37 | /// threads awaiting collection completion.
38 | collecting_lock: RwLock<()>,
39 | /// The number of [`Gc`]s dropped since the last time [`Dumpster::collect_all()`] was called.
40 | n_gcs_dropped: AtomicUsize,
41 | /// The number of [`Gc`]s currently existing (which have not had their internals replaced with
42 | /// `None`).
43 | n_gcs_existing: AtomicUsize,
44 | /// The function which determines whether a collection should be triggered.
45 | /// This pointer value should always be cast to a [`CollectCondition`], but since `AtomicPtr`
46 | /// doesn't handle function pointers correctly, we just cast to `*mut ()`.
47 | collect_condition: AtomicPtr<()>,
48 | }
49 |
50 | /// A structure containing the global information for the garbage collector.
51 | struct Dumpster {
52 | /// A lookup table for the allocations which may need to be cleaned up later.
53 | contents: RefCell>,
54 | /// The number of times an allocation on this thread has been dropped.
55 | n_drops: Cell,
56 | }
57 |
58 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
59 | /// A unique identifier for an allocation.
60 | struct AllocationId(NonNull>);
61 |
62 | #[derive(Debug)]
63 | /// The information which describes an allocation that may need to be cleaned up later.
64 | struct TrashCan {
65 | /// A pointer to the allocation to be cleaned up.
66 | ptr: Erased,
67 | /// The function which can be used to build a reference graph.
68 | /// This function is safe to call on `ptr`.
69 | dfs_fn: unsafe fn(Erased, &mut HashMap),
70 | }
71 |
72 | #[derive(Debug)]
73 | /// A node in the reference graph, which is constructed while searching for unreachable allocations.
74 | struct AllocationInfo {
75 | /// An erased pointer to the allocation.
76 | ptr: Erased,
77 | /// Function for dropping the allocation when its weak and strong count hits zero.
78 | /// Should have the same behavior as dropping a Gc normally to a reference count of zero.
79 | weak_drop_fn: unsafe fn(Erased),
80 | /// Information about this allocation's reachability.
81 | reachability: Reachability,
82 | }
83 |
84 | #[derive(Debug)]
85 | /// The state of whether an allocation is reachable or of unknown reachability.
86 | enum Reachability {
87 | /// The information describing an allocation whose accessibility is unknown.
88 | Unknown {
89 | /// The IDs for the allocations directly accessible from this allocation.
90 | children: Vec,
91 | /// The number of references in the reference count for this allocation which are
92 | /// "unaccounted," which have not been found while constructing the graph.
93 | /// It is the difference between the allocations indegree in the "true" reference graph vs
94 | /// the one we are currently building.
95 | n_unaccounted: usize,
96 | /// A function used to destroy the allocation.
97 | destroy_fn: unsafe fn(Erased, &HashMap),
98 | },
99 | /// The allocation here is reachable.
100 | /// No further information is needed.
101 | Reachable,
102 | }
103 |
104 | /// The global garbage truck.
105 | /// All [`TrashCans`] should eventually end up in here.
106 | static GARBAGE_TRUCK: LazyLock = LazyLock::new(|| GarbageTruck {
107 | contents: Mutex::new(HashMap::new()),
108 | collecting_lock: RwLock::new(()),
109 | n_gcs_dropped: AtomicUsize::new(0),
110 | n_gcs_existing: AtomicUsize::new(0),
111 | collect_condition: AtomicPtr::new(default_collect_condition as *mut ()),
112 | });
113 |
114 | thread_local! {
115 | /// The dumpster for this thread.
116 | /// Allocations which are "dirty" will be transferred to this dumpster before being moved into
117 | /// the garbage truck for final collection.
118 | static DUMPSTER: Dumpster = Dumpster {
119 | contents: RefCell::new(HashMap::new()),
120 | n_drops: Cell::new(0),
121 | };
122 |
123 | /// Whether the currently-running thread is doing a cleanup.
124 | /// This cannot be stored in `DUMPSTER` because otherwise it would cause weird use-after-drop
125 | /// behavior.
126 | static CLEANING: Cell = const { Cell::new(false) };
127 | }
128 |
129 | #[allow(clippy::module_name_repetitions)]
130 | /// Collect all allocations in the garbage truck (but not necessarily the dumpster), then await
131 | /// completion of the collection.
132 | /// Ensures that all allocations dropped on the calling thread are cleaned up
133 | pub fn collect_all_await() {
134 | DUMPSTER.with(|d| d.deliver_to(&GARBAGE_TRUCK));
135 | GARBAGE_TRUCK.collect_all();
136 | drop(GARBAGE_TRUCK.collecting_lock.read());
137 | }
138 |
139 | /// Notify that a `Gc` was destroyed, and update the tracking count for the number of dropped and
140 | /// existing `Gc`s.
141 | ///
142 | /// This may trigger a linear-time cleanup of all allocations, but this will be guaranteed to
143 | /// occur with less-than-linear frequency, so it's always O(1).
144 | pub fn notify_dropped_gc() {
145 | GARBAGE_TRUCK.n_gcs_existing.fetch_sub(1, Ordering::Relaxed);
146 | GARBAGE_TRUCK.n_gcs_dropped.fetch_add(1, Ordering::Relaxed);
147 | DUMPSTER.with(|dumpster| {
148 | dumpster.n_drops.set(dumpster.n_drops.get() + 1);
149 | if dumpster.is_full() {
150 | dumpster.deliver_to(&GARBAGE_TRUCK);
151 | }
152 | });
153 |
154 | let collect_cond = unsafe {
155 | // SAFETY: we only ever store collection conditions in the collect-condition box
156 | transmute::<*mut (), CollectCondition>(
157 | GARBAGE_TRUCK.collect_condition.load(Ordering::Relaxed),
158 | )
159 | };
160 | if collect_cond(&CollectInfo { _private: () }) {
161 | GARBAGE_TRUCK.collect_all();
162 | }
163 | }
164 |
165 | /// Notify that a [`Gc`] was created, and increment the number of total existing `Gc`s.
166 | pub fn notify_created_gc() {
167 | GARBAGE_TRUCK.n_gcs_existing.fetch_add(1, Ordering::Relaxed);
168 | }
169 |
170 | /// Mark an allocation as "dirty," implying that it may or may not be inaccessible and need to
171 | /// be cleaned up.
172 | pub(super) fn mark_dirty(allocation: &GcBox)
173 | where
174 | T: Trace + Send + Sync + ?Sized,
175 | {
176 | DUMPSTER.with(|dumpster| {
177 | if dumpster
178 | .contents
179 | .borrow_mut()
180 | .insert(
181 | AllocationId::from(allocation),
182 | TrashCan {
183 | ptr: Erased::new(NonNull::from(allocation)),
184 | dfs_fn: dfs::,
185 | },
186 | )
187 | .is_none()
188 | {
189 | allocation.weak.fetch_add(1, Ordering::Acquire);
190 | }
191 | });
192 | }
193 |
194 | /// Mark an allocation as "clean," implying that it has already been cleaned up and does not
195 | /// need to be cleaned again.
196 | pub(super) fn mark_clean(allocation: &GcBox)
197 | where
198 | T: Trace + Send + Sync + ?Sized,
199 | {
200 | DUMPSTER.with(|dumpster| {
201 | if dumpster
202 | .contents
203 | .borrow_mut()
204 | .remove(&AllocationId::from(allocation))
205 | .is_some()
206 | {
207 | allocation.weak.fetch_sub(1, Ordering::Release);
208 | }
209 | });
210 | }
211 |
212 | #[allow(clippy::missing_panics_doc)]
213 | /// Set the function which determines whether the garbage collector should be run.
214 | ///
215 | /// `f` will be periodically called by the garbage collector to determine whether it should perform
216 | /// a full traversal of the heap.
217 | /// When `f` returns true, a traversal will begin.
218 | ///
219 | /// # Examples
220 | ///
221 | /// ```
222 | /// use dumpster::sync::{set_collect_condition, CollectInfo};
223 | ///
224 | /// /// This function will make sure a GC traversal never happens unless directly activated.
225 | /// fn never_collect(_: &CollectInfo) -> bool {
226 | /// false
227 | /// }
228 | ///
229 | /// set_collect_condition(never_collect);
230 | /// ```
231 | pub fn set_collect_condition(f: CollectCondition) {
232 | GARBAGE_TRUCK
233 | .collect_condition
234 | .store(f as *mut (), Ordering::Relaxed);
235 | }
236 |
237 | /// Determine whether this thread is currently cleaning.
238 | pub fn currently_cleaning() -> bool {
239 | CLEANING.get()
240 | }
241 |
242 | /// Get the number of `[Gc]`s dropped since the last collection.
243 | pub fn n_gcs_dropped() -> usize {
244 | GARBAGE_TRUCK.n_gcs_dropped.load(Ordering::Relaxed)
245 | }
246 |
247 | /// Get the number of `[Gc]`s currently existing in the entire program.
248 | pub fn n_gcs_existing() -> usize {
249 | GARBAGE_TRUCK.n_gcs_existing.load(Ordering::Relaxed)
250 | }
251 |
252 | impl Dumpster {
253 | /// Deliver all [`TrashCans`] contained by this dumpster to the garbage collect, removing them
254 | /// from the local dumpster storage and adding them to the global truck.
255 | fn deliver_to(&self, garbage_truck: &GarbageTruck) {
256 | self.n_drops.set(0);
257 | let mut guard = garbage_truck.contents.lock();
258 | for (id, can) in self.contents.borrow_mut().drain() {
259 | if guard.insert(id, can).is_some() {
260 | unsafe {
261 | // SAFETY: an allocation can only be in the dumpster if it still exists and its
262 | // header is valid
263 | id.0.as_ref()
264 | }
265 | .weak
266 | .fetch_sub(1, Ordering::Release);
267 | }
268 | }
269 | }
270 |
271 | /// Determine whether this dumpster is full (and therefore should have its contents delivered to
272 | /// the garbage truck).
273 | fn is_full(&self) -> bool {
274 | self.contents.borrow().len() > 100_000 || self.n_drops.get() > 100_000
275 | }
276 | }
277 |
278 | impl GarbageTruck {
279 | #[allow(clippy::module_name_repetitions)]
280 | /// Search through the set of existing allocations which have been marked inaccessible, and see
281 | /// if they are inaccessible.
282 | /// If so, drop those allocations.
283 | fn collect_all(&self) {
284 | let collecting_guard = self.collecting_lock.write();
285 | self.n_gcs_dropped.store(0, Ordering::Relaxed);
286 | let to_collect = take(&mut *self.contents.lock());
287 | let mut ref_graph = HashMap::with_capacity(to_collect.len());
288 |
289 | CURRENT_TAG.fetch_add(1, Ordering::Release);
290 |
291 | for (_, TrashCan { ptr, dfs_fn }) in to_collect {
292 | unsafe {
293 | // SAFETY: `ptr` may only be in `to_collect` if it was a valid pointer
294 | // and `dfs_fn` must have been created with the intent of referring to
295 | // the erased type of `ptr`.
296 | dfs_fn(ptr, &mut ref_graph);
297 | }
298 | }
299 |
300 | let root_ids = ref_graph
301 | .iter()
302 | .filter_map(|(&k, v)| match v.reachability {
303 | Reachability::Reachable => Some(k),
304 | Reachability::Unknown { n_unaccounted, .. } => (n_unaccounted > 0
305 | || unsafe {
306 | // SAFETY: we found `k` in the reference graph,
307 | // so it must still be an extant allocation
308 | k.0.as_ref().weak.load(Ordering::Acquire) > 1
309 | })
310 | .then_some(k),
311 | })
312 | .collect::>();
313 | for root_id in root_ids {
314 | mark(root_id, &mut ref_graph);
315 | }
316 |
317 | CLEANING.set(true);
318 | // set of allocations which must be destroyed because we were the last weak pointer to it
319 | let mut weak_destroys = Vec::new();
320 | for (id, node) in &ref_graph {
321 | let header_ref = unsafe { id.0.as_ref() };
322 | match node.reachability {
323 | Reachability::Unknown { destroy_fn, .. } => unsafe {
324 | // SAFETY: `destroy_fn` must have been created with `node.ptr` in mind,
325 | // and we have proven that no other references to `node.ptr` exist
326 | destroy_fn(node.ptr, &ref_graph);
327 | },
328 | Reachability::Reachable => {
329 | if header_ref.weak.fetch_sub(1, Ordering::Release) == 1
330 | && header_ref.strong.load(Ordering::Acquire) == 0
331 | {
332 | // we are the last reference to the allocation.
333 | // mark to be cleaned up later
334 | // no real synchronization loss to storing the guard because we had the last
335 | // reference anyway
336 | weak_destroys.push((node.weak_drop_fn, node.ptr));
337 | }
338 | }
339 | };
340 | }
341 | CLEANING.set(false);
342 | for (drop_fn, ptr) in weak_destroys {
343 | unsafe {
344 | // SAFETY: we have proven (via header_ref.weak = 1) that the cleaning
345 | // process had the last reference to the allocation.
346 | // `drop_fn` must have been created with the true value of `ptr` in mind.
347 | drop_fn(ptr);
348 | };
349 | }
350 | drop(collecting_guard);
351 | }
352 | }
353 |
354 | /// Build out a part of the reference graph, making note of all allocations which are reachable from
355 | /// the one described in `ptr`.
356 | ///
357 | /// # Inputs
358 | ///
359 | /// - `ptr`: A pointer to the allocation that we should start constructing from.
360 | /// - `ref_graph`: A lookup from allocation IDs to node information about that allocation.
361 | ///
362 | /// # Effects
363 | ///
364 | /// `ref_graph` will be expanded to include all allocations reachable from `ptr`.
365 | ///
366 | /// # Safety
367 | ///
368 | /// `ptr` must have been created as a pointer to a `GcBox`.
369 | unsafe fn dfs(
370 | ptr: Erased,
371 | ref_graph: &mut HashMap,
372 | ) {
373 | let box_ref = unsafe {
374 | // SAFETY: We require `ptr` to be a an erased pointer to `GcBox`.
375 | ptr.specify::>().as_ref()
376 | };
377 | let starting_id = AllocationId::from(box_ref);
378 | let Entry::Vacant(v) = ref_graph.entry(starting_id) else {
379 | // the weak count was incremented by another DFS operation elsewhere.
380 | // Decrement it to have only one from us.
381 | box_ref.weak.fetch_sub(1, Ordering::Release);
382 | return;
383 | };
384 | let strong_count = box_ref.strong.load(Ordering::Acquire);
385 | v.insert(AllocationInfo {
386 | ptr,
387 | weak_drop_fn: drop_weak_zero::,
388 | reachability: Reachability::Unknown {
389 | children: Vec::new(),
390 | n_unaccounted: strong_count,
391 | destroy_fn: destroy_erased::,
392 | },
393 | });
394 |
395 | if box_ref
396 | .value
397 | .accept(&mut Dfs {
398 | ref_graph,
399 | current_id: starting_id,
400 | })
401 | .is_err()
402 | || box_ref.generation.load(Ordering::Acquire) >= CURRENT_TAG.load(Ordering::Relaxed)
403 | {
404 | // box_ref.value was accessed while we worked
405 | // mark this allocation as reachable
406 | mark(starting_id, ref_graph);
407 | }
408 | }
409 |
410 | #[derive(Debug)]
411 | /// The visitor structure used for building the found-reference-graph of allocations.
412 | struct Dfs<'a> {
413 | /// The reference graph.
414 | /// Each allocation is assigned a node.
415 | ref_graph: &'a mut HashMap,
416 | /// The allocation ID currently being visited.
417 | /// Used for knowing which node is the parent of another.
418 | current_id: AllocationId,
419 | }
420 |
421 | impl<'a> Visitor for Dfs<'a> {
422 | fn visit_sync(&mut self, gc: &Gc)
423 | where
424 | T: Trace + Send + Sync + ?Sized,
425 | {
426 | // must not use deref operators since we don't want to update the generation
427 | let ptr = unsafe {
428 | // SAFETY: This is the same as the deref implementation, but avoids
429 | // incrementing the generation count.
430 | (*gc.ptr.get()).unwrap()
431 | };
432 | let box_ref = unsafe {
433 | // SAFETY: same as above.
434 | ptr.as_ref()
435 | };
436 | let current_tag = CURRENT_TAG.load(Ordering::Relaxed);
437 | if gc.tag.swap(current_tag, Ordering::Relaxed) >= current_tag
438 | || box_ref.generation.load(Ordering::Acquire) >= current_tag
439 | {
440 | // This pointer was already tagged by this sweep, so it must have been moved by
441 | mark(self.current_id, self.ref_graph);
442 | return;
443 | }
444 |
445 | let mut new_id = AllocationId::from(box_ref);
446 |
447 | let Reachability::Unknown {
448 | ref mut children, ..
449 | } = self
450 | .ref_graph
451 | .get_mut(&self.current_id)
452 | .unwrap()
453 | .reachability
454 | else {
455 | // this node has been proven reachable by something higher up. No need to keep building
456 | // its ref graph
457 | return;
458 | };
459 | children.push(new_id);
460 |
461 | match self.ref_graph.entry(new_id) {
462 | Entry::Occupied(mut o) => match o.get_mut().reachability {
463 | Reachability::Unknown {
464 | ref mut n_unaccounted,
465 | ..
466 | } => {
467 | *n_unaccounted -= 1;
468 | }
469 | Reachability::Reachable => (),
470 | },
471 | Entry::Vacant(v) => {
472 | // This allocation has never been visited by the reference graph builder
473 | let strong_count = box_ref.strong.load(Ordering::Acquire);
474 | box_ref.weak.fetch_add(1, Ordering::Acquire);
475 | v.insert(AllocationInfo {
476 | ptr: Erased::new(ptr),
477 | weak_drop_fn: drop_weak_zero::,
478 | reachability: Reachability::Unknown {
479 | children: Vec::new(),
480 | n_unaccounted: strong_count - 1,
481 | destroy_fn: destroy_erased::,
482 | },
483 | });
484 |
485 | // Save the previously visited ID, then carry on to the next one
486 | swap(&mut new_id, &mut self.current_id);
487 |
488 | if box_ref.value.accept(self).is_err()
489 | || box_ref.generation.load(Ordering::Acquire) >= current_tag
490 | {
491 | // On failure, this means `**gc` is accessible, and should be marked
492 | // as such
493 | mark(self.current_id, self.ref_graph);
494 | }
495 |
496 | // Restore current_id and carry on
497 | swap(&mut new_id, &mut self.current_id);
498 | }
499 | };
500 | }
501 |
502 | fn visit_unsync(&mut self, _: &crate::unsync::Gc)
503 | where
504 | T: Trace + ?Sized,
505 | {
506 | unreachable!("sync Gc cannot own an unsync Gc");
507 | }
508 | }
509 |
510 | /// Traverse the reference graph, marking `root` and any allocations reachable from `root` as
511 | /// reachable.
512 | fn mark(root: AllocationId, graph: &mut HashMap) {
513 | let node = graph.get_mut(&root).unwrap();
514 | if let Reachability::Unknown { children, .. } =
515 | replace(&mut node.reachability, Reachability::Reachable)
516 | {
517 | for child in children {
518 | mark(child, graph);
519 | }
520 | }
521 | }
522 |
523 | /// Destroy an allocation, obliterating its GCs, dropping it, and deallocating it.
524 | ///
525 | /// # Safety
526 | ///
527 | /// `ptr` must have been created from a pointer to a `GcBox`.
528 | unsafe fn destroy_erased(
529 | ptr: Erased,
530 | graph: &HashMap,
531 | ) {
532 | /// A visitor for decrementing the reference count of pointees.
533 | struct PrepareForDestruction<'a> {
534 | /// The reference graph.
535 | /// Must have been populated with reachability already.
536 | graph: &'a HashMap,
537 | }
538 |
539 | impl Visitor for PrepareForDestruction<'_> {
540 | fn visit_sync(&mut self, gc: &crate::sync::Gc)
541 | where
542 | T: Trace + Send + Sync + ?Sized,
543 | {
544 | let id = AllocationId::from(unsafe {
545 | // SAFETY: This is the same as dereferencing the GC.
546 | (*gc.ptr.get()).unwrap()
547 | });
548 | if matches!(self.graph[&id].reachability, Reachability::Reachable) {
549 | unsafe {
550 | // SAFETY: This is the same as dereferencing the GC.
551 | id.0.as_ref().strong.fetch_sub(1, Ordering::Release);
552 | }
553 | } else {
554 | unsafe {
555 | // SAFETY: The GC is unreachable,
556 | // so the GC will never be dereferenced again.
557 | gc.ptr.get().write((*gc.ptr.get()).as_null());
558 | }
559 | }
560 | }
561 |
562 | fn visit_unsync(&mut self, _: &crate::unsync::Gc)
563 | where
564 | T: Trace + ?Sized,
565 | {
566 | unreachable!("no unsync members of sync Gc possible!");
567 | }
568 | }
569 |
570 | let specified = ptr.specify::>().as_mut();
571 | specified
572 | .value
573 | .accept(&mut PrepareForDestruction { graph })
574 | .expect("allocation assumed to be unreachable but somehow was accessed");
575 | let layout = Layout::for_value(specified);
576 | drop_in_place(specified);
577 | dealloc(std::ptr::from_mut::>(specified).cast(), layout);
578 | }
579 |
580 | /// Function for handling dropping an allocation when its weak and strong reference count reach
581 | /// zero.
582 | ///
583 | /// # Safety
584 | ///
585 | /// `ptr` must have been created as a pointer to a `GcBox`.
586 | unsafe fn drop_weak_zero(ptr: Erased) {
587 | let mut specified = ptr.specify::>();
588 | assert_eq!(specified.as_ref().weak.load(Ordering::Relaxed), 0);
589 | assert_eq!(specified.as_ref().strong.load(Ordering::Relaxed), 0);
590 |
591 | let layout = Layout::for_value(specified.as_ref());
592 | drop_in_place(specified.as_mut());
593 | dealloc(specified.as_ptr().cast(), layout);
594 | }
595 |
596 | unsafe impl Send for AllocationId {}
597 | unsafe impl Sync for AllocationId {}
598 |
599 | impl From<&GcBox> for AllocationId
600 | where
601 | T: Trace + Send + Sync + ?Sized,
602 | {
603 | fn from(value: &GcBox) -> Self {
604 | AllocationId(NonNull::from(value).cast())
605 | }
606 | }
607 |
608 | impl From>> for AllocationId
609 | where
610 | T: Trace + Send + Sync + ?Sized,
611 | {
612 | fn from(value: NonNull>) -> Self {
613 | AllocationId(value.cast())
614 | }
615 | }
616 |
617 | impl Drop for Dumpster {
618 | fn drop(&mut self) {
619 | self.deliver_to(&GARBAGE_TRUCK);
620 | // collect_all();
621 | }
622 | }
623 |
624 | impl Drop for GarbageTruck {
625 | fn drop(&mut self) {
626 | GARBAGE_TRUCK.collect_all();
627 | }
628 | }
629 |
--------------------------------------------------------------------------------
/dumpster/src/sync/mod.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! Thread-safe shared garbage collection.
10 | //!
11 | //! Most users of this module will be interested in using [`Gc`] directly out of the box - this will
12 | //! just work.
13 | //! Those with more particular needs (such as benchmarking) should turn toward
14 | //! [`set_collect_condition`] in order to tune exactly when the garbage collector does cleanups.
15 | //!
16 | //! # Examples
17 | //!
18 | //! ```
19 | //! use dumpster::sync::Gc;
20 | //!
21 | //! let my_gc = Gc::new(100);
22 | //! let other_gc = my_gc.clone();
23 | //!
24 | //! drop(my_gc);
25 | //! drop(other_gc);
26 | //!
27 | //! // contents of the Gc are automatically freed
28 | //! ```
29 |
30 | mod collect;
31 | #[cfg(test)]
32 | mod tests;
33 |
34 | use std::{
35 | alloc::{dealloc, Layout},
36 | borrow::Borrow,
37 | cell::UnsafeCell,
38 | fmt::Debug,
39 | num::NonZeroUsize,
40 | ops::Deref,
41 | ptr::{addr_of, addr_of_mut, drop_in_place, NonNull},
42 | sync::atomic::{fence, AtomicUsize, Ordering},
43 | };
44 |
45 | use crate::{contains_gcs, ptr::Nullable, Trace, Visitor};
46 |
47 | use self::collect::{
48 | collect_all_await, currently_cleaning, mark_clean, mark_dirty, n_gcs_dropped, n_gcs_existing,
49 | notify_created_gc, notify_dropped_gc,
50 | };
51 |
52 | /// A thread-safe garbage-collected pointer.
53 | ///
54 | /// This pointer can be duplicated and then shared across threads.
55 | /// Garbage collection is performed concurrently.
56 | ///
57 | /// # Examples
58 | ///
59 | /// ```
60 | /// use dumpster::sync::Gc;
61 | /// use std::sync::atomic::{AtomicUsize, Ordering};
62 | ///
63 | /// let shared = Gc::new(AtomicUsize::new(0));
64 | ///
65 | /// std::thread::scope(|s| {
66 | /// s.spawn(|| {
67 | /// let other_gc = shared.clone();
68 | /// other_gc.store(1, Ordering::Relaxed);
69 | /// });
70 | ///
71 | /// shared.store(2, Ordering::Relaxed);
72 | /// });
73 | ///
74 | /// println!("{}", shared.load(Ordering::Relaxed));
75 | /// ```
76 | ///
77 | /// # Interaction with `Drop`
78 | ///
79 | /// While collecting cycles, it's possible for a `Gc` to exist that points to some deallocated
80 | /// object.
81 | /// To prevent undefined behavior, these `Gc`s are marked as dead during collection and rendered
82 | /// inaccessible.
83 | /// Dereferencing or cloning a `Gc` during the `Drop` implementation of a `Trace` type could
84 | /// result in the program panicking to keep the program from accessing memory after freeing it.
85 | /// If you're accessing a `Gc` during a `Drop` implementation, make sure to use the fallible
86 | /// operations [`Gc::try_deref`] and [`Gc::try_clone`].
87 | pub struct Gc {
88 | /// The pointer to the allocation.
89 | ptr: UnsafeCell>>,
90 | /// The tag information of this pointer, used for mutation detection when marking.
91 | tag: AtomicUsize,
92 | }
93 |
94 | /// The tag of the current sweep operation.
95 | /// All new allocations are minted with the current tag.
96 | static CURRENT_TAG: AtomicUsize = AtomicUsize::new(0);
97 |
98 | #[repr(C)]
99 | /// The backing allocation for a [`Gc`].
100 | struct GcBox
101 | where
102 | T: Trace + Send + Sync + ?Sized,
103 | {
104 | /// The "strong" count, which is the number of extant `Gc`s to this allocation.
105 | /// If the strong count is zero, a value contained in the allocation may be dropped, but the
106 | /// allocation itself must still be valid.
107 | strong: AtomicUsize,
108 | /// The "weak" count, which is the number of references to this allocation stored in to-collect
109 | /// buffers by the collection algorithm.
110 | /// If the weak count is zero, the allocation may be destroyed.
111 | weak: AtomicUsize,
112 | /// The current generation number of the allocation.
113 | /// The generation number is assigned to the global generation every time a strong reference is
114 | /// created or destroyed or a `Gc` pointing to this allocation is dereferenced.
115 | generation: AtomicUsize,
116 | /// The actual data stored in the allocation.
117 | value: T,
118 | }
119 |
120 | unsafe impl Send for Gc where T: Trace + Send + Sync + ?Sized {}
121 | unsafe impl Sync for Gc where T: Trace + Send + Sync + ?Sized {}
122 |
123 | /// Begin a collection operation of the allocations on the heap.
124 | ///
125 | /// Due to concurrency issues, this might not collect every single unreachable allocation that
126 | /// currently exists, but often calling `collect()` will get allocations made by this thread.
127 | ///
128 | /// # Examples
129 | ///
130 | /// ```
131 | /// use dumpster::sync::{collect, Gc};
132 | ///
133 | /// let gc = Gc::new(vec![1, 2, 3]);
134 | /// drop(gc);
135 | ///
136 | /// collect(); // the vector originally in `gc` _might_ be dropped now, but could be dropped later
137 | /// ```
138 | pub fn collect() {
139 | collect_all_await();
140 | }
141 |
142 | #[derive(Debug)]
143 | /// Information passed to a [`CollectCondition`] used to determine whether the garbage collector
144 | /// should start collecting.
145 | ///
146 | /// A `CollectInfo` is exclusively created by being passed as an argument to the collection
147 | /// condition.
148 | /// To set a custom collection condition, refer to [`set_collect_condition`].
149 | ///
150 | /// # Examples
151 | ///
152 | /// ```
153 | /// use dumpster::sync::{set_collect_condition, CollectInfo};
154 | ///
155 | /// fn my_collect_condition(info: &CollectInfo) -> bool {
156 | /// (info.n_gcs_dropped_since_last_collect() + info.n_gcs_existing()) % 2 == 0
157 | /// }
158 | ///
159 | /// set_collect_condition(my_collect_condition);
160 | /// ```
161 | pub struct CollectInfo {
162 | /// Dummy value so this is a private structure.
163 | _private: (),
164 | }
165 |
166 | /// A function which determines whether the garbage collector should start collecting.
167 | /// This type primarily exists so that it can be used with [`set_collect_condition`].
168 | ///
169 | /// # Examples
170 | ///
171 | /// ```rust
172 | /// use dumpster::sync::{set_collect_condition, CollectInfo};
173 | ///
174 | /// fn always_collect(_: &CollectInfo) -> bool {
175 | /// true
176 | /// }
177 | ///
178 | /// set_collect_condition(always_collect);
179 | /// ```
180 | pub type CollectCondition = fn(&CollectInfo) -> bool;
181 |
182 | #[must_use]
183 | /// The default collection condition used by the garbage collector.
184 | ///
185 | /// There are no guarantees about what this function returns, other than that it will return `true`
186 | /// with sufficient frequency to ensure that all `Gc` operations are amortized _O(1)_ in runtime.
187 | ///
188 | /// This function isn't really meant to be called by users, but rather it's supposed to be handed
189 | /// off to [`set_collect_condition`] to return to the default operating mode of the library.
190 | ///
191 | /// This collection condition applies globally, i.e. to every thread.
192 | ///
193 | /// # Examples
194 | ///
195 | /// ```rust
196 | /// use dumpster::sync::{default_collect_condition, set_collect_condition, CollectInfo};
197 | ///
198 | /// fn other_collect_condition(info: &CollectInfo) -> bool {
199 | /// info.n_gcs_existing() >= 25 || default_collect_condition(info)
200 | /// }
201 | ///
202 | /// // Use my custom collection condition.
203 | /// set_collect_condition(other_collect_condition);
204 | ///
205 | /// // I'm sick of the custom collection condition.
206 | /// // Return to the original.
207 | /// set_collect_condition(default_collect_condition);
208 | /// ```
209 | pub fn default_collect_condition(info: &CollectInfo) -> bool {
210 | info.n_gcs_dropped_since_last_collect() > info.n_gcs_existing()
211 | }
212 |
213 | pub use collect::set_collect_condition;
214 |
215 | impl Gc
216 | where
217 | T: Trace + Send + Sync + ?Sized,
218 | {
219 | /// Construct a new garbage-collected value.
220 | ///
221 | /// # Examples
222 | ///
223 | /// ```
224 | /// use dumpster::sync::Gc;
225 | ///
226 | /// let _ = Gc::new(0);
227 | /// ```
228 | pub fn new(value: T) -> Gc
229 | where
230 | T: Sized,
231 | {
232 | notify_created_gc();
233 | Gc {
234 | ptr: UnsafeCell::new(Nullable::new(NonNull::from(Box::leak(Box::new(GcBox {
235 | strong: AtomicUsize::new(1),
236 | weak: AtomicUsize::new(0),
237 | generation: AtomicUsize::new(CURRENT_TAG.load(Ordering::Acquire)),
238 | value,
239 | }))))),
240 | tag: AtomicUsize::new(0),
241 | }
242 | }
243 |
244 | /// Attempt to dereference this `Gc`.
245 | ///
246 | /// This function will return `None` if `self` is a "dead" `Gc`, which points to an
247 | /// already-deallocated object.
248 | /// This can only occur if a `Gc` is accessed during the `Drop` implementation of a
249 | /// [`Trace`] object.
250 | ///
251 | /// For a version which panics instead of returning `None`, consider using [`Deref`].
252 | ///
253 | /// # Examples
254 | ///
255 | /// For a still-living `Gc`, this always returns `Some`.
256 | ///
257 | /// ```
258 | /// use dumpster::sync::Gc;
259 | ///
260 | /// let gc1 = Gc::new(0);
261 | /// assert!(Gc::try_deref(&gc1).is_some());
262 | /// ```
263 | ///
264 | /// The only way to get a `Gc` which fails on `try_clone` is by accessing a `Gc` during its
265 | /// `Drop` implementation.
266 | ///
267 | /// ```
268 | /// use dumpster::{sync::Gc, Trace};
269 | /// use std::sync::Mutex;
270 | ///
271 | /// #[derive(Trace)]
272 | /// struct Cycle(Mutex>>);
273 | ///
274 | /// impl Drop for Cycle {
275 | /// fn drop(&mut self) {
276 | /// let guard = self.0.lock().unwrap();
277 | /// let maybe_ref = Gc::try_deref(guard.as_ref().unwrap());
278 | /// assert!(maybe_ref.is_none());
279 | /// }
280 | /// }
281 | ///
282 | /// let gc1 = Gc::new(Cycle(Mutex::new(None)));
283 | /// *gc1.0.lock().unwrap() = Some(gc1.clone());
284 | /// # drop(gc1);
285 | /// # dumpster::sync::collect();
286 | /// ```
287 | pub fn try_deref(gc: &Gc) -> Option<&T> {
288 | #[allow(clippy::unnecessary_lazy_evaluations)]
289 | unsafe {
290 | (!(*gc.ptr.get()).is_null()).then(|| &**gc)
291 | }
292 | }
293 |
294 | /// Attempt to clone this `Gc`.
295 | ///
296 | /// This function will return `None` if `self` is a "dead" `Gc`, which points to an
297 | /// already-deallocated object.
298 | /// This can only occur if a `Gc` is accessed during the `Drop` implementation of a
299 | /// [`Trace`] object.
300 | ///
301 | /// For a version which panics instead of returning `None`, consider using [`Clone`].
302 | ///
303 | /// # Examples
304 | ///
305 | /// For a still-living `Gc`, this always returns `Some`.
306 | ///
307 | /// ```
308 | /// use dumpster::sync::Gc;
309 | ///
310 | /// let gc1 = Gc::new(0);
311 | /// let gc2 = Gc::try_clone(&gc1).unwrap();
312 | /// ```
313 | ///
314 | /// The only way to get a `Gc` which fails on `try_clone` is by accessing a `Gc` during its
315 | /// `Drop` implementation.
316 | ///
317 | /// ```
318 | /// use dumpster::{sync::Gc, Trace};
319 | /// use std::sync::Mutex;
320 | ///
321 | /// #[derive(Trace)]
322 | /// struct Cycle(Mutex>>);
323 | ///
324 | /// impl Drop for Cycle {
325 | /// fn drop(&mut self) {
326 | /// let cloned = Gc::try_clone(self.0.lock().unwrap().as_ref().unwrap());
327 | /// assert!(cloned.is_none());
328 | /// }
329 | /// }
330 | ///
331 | /// let gc1 = Gc::new(Cycle(Mutex::new(None)));
332 | /// *gc1.0.lock().unwrap() = Some(gc1.clone());
333 | /// # drop(gc1);
334 | /// # dumpster::sync::collect();
335 | /// ```
336 | pub fn try_clone(gc: &Gc) -> Option> {
337 | unsafe { (!(*gc.ptr.get()).is_null()).then(|| gc.clone()) }
338 | }
339 |
340 | /// Provides a raw pointer to the data.
341 | ///
342 | /// Panics if `self` is a "dead" `Gc`,
343 | /// which points to an already-deallocated object.
344 | /// This can only occur if a `Gc` is accessed during the `Drop` implementation of a
345 | /// [`Trace`] object.
346 | ///
347 | /// # Examples
348 | ///
349 | /// ```
350 | /// use dumpster::sync::Gc;
351 | /// let x = Gc::new("hello".to_owned());
352 | /// let y = Gc::clone(&x);
353 | /// let x_ptr = Gc::as_ptr(&x);
354 | /// assert_eq!(x_ptr, Gc::as_ptr(&x));
355 | /// assert_eq!(unsafe { &*x_ptr }, "hello");
356 | /// ```
357 | pub fn as_ptr(gc: &Gc) -> *const T {
358 | unsafe {
359 | let ptr = NonNull::as_ptr((*gc.ptr.get()).unwrap());
360 | addr_of_mut!((*ptr).value)
361 | }
362 | }
363 |
364 | /// Determine whether two `Gc`s are equivalent by reference.
365 | /// Returns `true` if both `this` and `other` point to the same value, in the same style as
366 | /// [`std::ptr::eq`].
367 | ///
368 | /// # Examples
369 | ///
370 | /// ```
371 | /// use dumpster::sync::Gc;
372 | ///
373 | /// let gc1 = Gc::new(0);
374 | /// let gc2 = Gc::clone(&gc1); // points to same spot as `gc1`
375 | /// let gc3 = Gc::new(0); // same value, but points to a different object than `gc1`
376 | ///
377 | /// assert!(Gc::ptr_eq(&gc1, &gc2));
378 | /// assert!(!Gc::ptr_eq(&gc1, &gc3));
379 | /// ```
380 | pub fn ptr_eq(this: &Gc, other: &Gc) -> bool {
381 | unsafe { *this.ptr.get() }.as_option() == unsafe { *other.ptr.get() }.as_option()
382 | }
383 |
384 | /// Get the number of references to the value pointed to by this `Gc`.
385 | ///
386 | /// This does not include internal references generated by the garbage collector.
387 | ///
388 | /// # Panics
389 | ///
390 | /// This function may panic if the `Gc` whose reference count we are loading is "dead" (i.e.
391 | /// generated through a `Drop` implementation). For further reference, take a look at
392 | /// [`Gc::is_dead`].
393 | ///
394 | /// # Examples
395 | ///
396 | /// ```
397 | /// use dumpster::sync::Gc;
398 | ///
399 | /// let gc = Gc::new(());
400 | /// assert_eq!(gc.ref_count().get(), 1);
401 | /// let gc2 = gc.clone();
402 | /// assert_eq!(gc.ref_count().get(), 2);
403 | /// drop(gc);
404 | /// drop(gc2);
405 | /// ```
406 | pub fn ref_count(&self) -> NonZeroUsize {
407 | let box_ptr = unsafe { *self.ptr.get() }.expect(
408 | "Attempt to dereference Gc to already-collected object. \
409 | This means a Gc escaped from a Drop implementation, likely implying a bug in your code.",
410 | );
411 | let box_ref = unsafe { box_ptr.as_ref() };
412 | NonZeroUsize::new(box_ref.strong.load(Ordering::Relaxed))
413 | .expect("strong count to a GcBox may never be zero while a Gc to it exists")
414 | }
415 |
416 | /// Determine whether this is a dead `Gc`.
417 | ///
418 | /// A `Gc` is dead if it is accessed while the value it points to has been destroyed; this only
419 | /// occurs if one attempts to interact with a `Gc` during a structure's [`Drop`] implementation.
420 | /// However, this is not always guaranteed - sometime the garbage collector will leave `Gc`s
421 | /// alive in differing orders, so users should not rely on the destruction order of `Gc`s to
422 | /// determine whether it is dead.
423 | ///
424 | /// # Examples
425 | ///
426 | /// ```
427 | /// use dumpster::{sync::Gc, Trace};
428 | /// use std::sync::OnceLock;
429 | ///
430 | /// #[derive(Trace)]
431 | /// struct Cycle(OnceLock>);
432 | ///
433 | /// impl Drop for Cycle {
434 | /// fn drop(&mut self) {
435 | /// assert!(self.0.get().unwrap().is_dead());
436 | /// }
437 | /// }
438 | ///
439 | /// let gc1 = Gc::new(Cycle(OnceLock::new()));
440 | /// gc1.0.set(gc1.clone());
441 | /// # drop(gc1);
442 | /// # dumpster::sync::collect();
443 | /// ```
444 | pub fn is_dead(&self) -> bool {
445 | unsafe { *self.ptr.get() }.is_null()
446 | }
447 | }
448 |
449 | impl Clone for Gc
450 | where
451 | T: Trace + Send + Sync + ?Sized,
452 | {
453 | /// Clone a garbage-collected reference.
454 | /// This does not clone the underlying data.
455 | ///
456 | /// # Panics
457 | ///
458 | /// This function will panic if the `Gc` being cloned points to a deallocated object.
459 | /// This is only possible if said `Gc` is accessed during the `Drop` implementation of a
460 | /// `Trace` value.
461 | ///
462 | /// For a fallible version, refer to [`Gc::try_clone`].
463 | ///
464 | /// # Examples
465 | ///
466 | /// ```
467 | /// use dumpster::sync::Gc;
468 | /// use std::sync::atomic::{AtomicU8, Ordering};
469 | ///
470 | /// let gc1 = Gc::new(AtomicU8::new(0));
471 | /// let gc2 = gc1.clone();
472 | ///
473 | /// gc1.store(1, Ordering::Relaxed);
474 | /// assert_eq!(gc2.load(Ordering::Relaxed), 1);
475 | /// ```
476 | ///
477 | /// The following example will fail, because cloning a `Gc` to a deallocated object is wrong.
478 | ///
479 | /// ```should_panic
480 | /// use dumpster::{sync::Gc, Trace};
481 | /// use std::sync::Mutex;
482 | ///
483 | /// #[derive(Trace)]
484 | /// struct Cycle(Mutex>>);
485 | ///
486 | /// impl Drop for Cycle {
487 | /// fn drop(&mut self) {
488 | /// let _ = self.0.lock().unwrap().as_ref().unwrap().clone();
489 | /// }
490 | /// }
491 | ///
492 | /// let gc1 = Gc::new(Cycle(Mutex::new(None)));
493 | /// *gc1.0.lock().unwrap() = Some(gc1.clone());
494 | /// # drop(gc1);
495 | /// # dumpster::sync::collect();
496 | /// ```
497 | fn clone(&self) -> Gc {
498 | let box_ref = unsafe {
499 | (*self.ptr.get()).expect("attempt to clone Gc to already-deallocated object. \
500 | This means a Gc was accessed during a Drop implementation, likely implying a bug in your code.").as_ref()
501 | };
502 | // increment strong count before generation to ensure cleanup never underestimates ref count
503 | box_ref.strong.fetch_add(1, Ordering::Acquire);
504 | box_ref
505 | .generation
506 | .store(CURRENT_TAG.load(Ordering::Acquire), Ordering::Release);
507 | notify_created_gc();
508 | // mark_clean(box_ref); // causes performance drops
509 | Gc {
510 | ptr: UnsafeCell::new(unsafe { *self.ptr.get() }),
511 | tag: AtomicUsize::new(CURRENT_TAG.load(Ordering::Acquire)),
512 | }
513 | }
514 | }
515 |
516 | impl Drop for Gc
517 | where
518 | T: Trace + Send + Sync + ?Sized,
519 | {
520 | fn drop(&mut self) {
521 | if currently_cleaning() {
522 | return;
523 | }
524 | let Some(mut ptr) = unsafe { *self.ptr.get() }.as_option() else {
525 | return;
526 | };
527 | let box_ref = unsafe { ptr.as_ref() };
528 | box_ref.weak.fetch_add(1, Ordering::AcqRel); // ensures that this allocation wasn't freed
529 | // while we weren't looking
530 | box_ref
531 | .generation
532 | .store(CURRENT_TAG.load(Ordering::Relaxed), Ordering::Release);
533 | match box_ref.strong.fetch_sub(1, Ordering::AcqRel) {
534 | 0 => unreachable!("strong cannot reach zero while a Gc to it exists"),
535 | 1 => {
536 | mark_clean(box_ref);
537 | if box_ref.weak.fetch_sub(1, Ordering::Release) == 1 {
538 | // destroyed the last weak reference! we can safely deallocate this
539 | let layout = Layout::for_value(box_ref);
540 | fence(Ordering::Acquire);
541 | unsafe {
542 | drop_in_place(ptr.as_mut());
543 | dealloc(ptr.as_ptr().cast(), layout);
544 | }
545 | }
546 | }
547 | _ => {
548 | if contains_gcs(&box_ref.value).unwrap_or(true) {
549 | mark_dirty(box_ref);
550 | }
551 | box_ref.weak.fetch_sub(1, Ordering::Release);
552 | }
553 | }
554 | notify_dropped_gc();
555 | }
556 | }
557 |
558 | impl CollectInfo {
559 | #[must_use]
560 | /// Get the number of times that a [`Gc`] has been dropped since the last time a collection
561 | /// operation was performed.
562 | ///
563 | /// # Examples
564 | ///
565 | /// ```
566 | /// use dumpster::sync::{set_collect_condition, CollectInfo};
567 | ///
568 | /// // Collection condition for whether many Gc's have been dropped.
569 | /// fn have_many_gcs_dropped(info: &CollectInfo) -> bool {
570 | /// info.n_gcs_dropped_since_last_collect() > 100
571 | /// }
572 | ///
573 | /// set_collect_condition(have_many_gcs_dropped);
574 | /// ```
575 | pub fn n_gcs_dropped_since_last_collect(&self) -> usize {
576 | n_gcs_dropped()
577 | }
578 |
579 | #[must_use]
580 | /// Get the total number of [`Gc`]s which currently exist.
581 | ///
582 | /// # Examples
583 | ///
584 | /// ```
585 | /// use dumpster::sync::{set_collect_condition, CollectInfo};
586 | ///
587 | /// // Collection condition for whether many Gc's currently exist.
588 | /// fn do_many_gcs_exist(info: &CollectInfo) -> bool {
589 | /// info.n_gcs_existing() > 100
590 | /// }
591 | ///
592 | /// set_collect_condition(do_many_gcs_exist);
593 | /// ```
594 | pub fn n_gcs_existing(&self) -> usize {
595 | n_gcs_existing()
596 | }
597 | }
598 |
599 | unsafe impl Trace for Gc {
600 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
601 | visitor.visit_sync(self);
602 | Ok(())
603 | }
604 | }
605 |
606 | impl Deref for Gc {
607 | type Target = T;
608 |
609 | /// Dereference this pointer, creating a reference to the contained value `T`.
610 | ///
611 | /// # Panics
612 | ///
613 | /// This function may panic if it is called from within the implementation of `std::ops::Drop`
614 | /// of its owning value, since returning such a reference could cause a use-after-free.
615 | /// It is not guaranteed to panic.
616 | ///
617 | /// # Examples
618 | ///
619 | /// The following is a correct time to dereference a `Gc`.
620 | ///
621 | /// ```
622 | /// use dumpster::sync::Gc;
623 | ///
624 | /// let my_gc = Gc::new(0u8);
625 | /// let my_ref: &u8 = &my_gc;
626 | /// ```
627 | ///
628 | /// Dereferencing a `Gc` while dropping is not correct.
629 | ///
630 | /// ```should_panic
631 | /// // This is wrong!
632 | /// use dumpster::{sync::Gc, Trace};
633 | /// use std::sync::Mutex;
634 | ///
635 | /// #[derive(Trace)]
636 | /// struct Bad {
637 | /// s: String,
638 | /// cycle: Mutex>>,
639 | /// }
640 | ///
641 | /// impl Drop for Bad {
642 | /// fn drop(&mut self) {
643 | /// println!("{}", self.cycle.lock().unwrap().as_ref().unwrap().s)
644 | /// }
645 | /// }
646 | ///
647 | /// let foo = Gc::new(Bad {
648 | /// s: "foo".to_string(),
649 | /// cycle: Mutex::new(None),
650 | /// });
651 | /// ```
652 | fn deref(&self) -> &Self::Target {
653 | let box_ref = unsafe {
654 | (*self.ptr.get()).expect(
655 | "Attempting to dereference Gc to already-deallocated object.\
656 | This is caused by accessing a Gc during a Drop implementation, likely implying a bug in your code."
657 | ).as_ref()
658 | };
659 | let current_tag = CURRENT_TAG.load(Ordering::Acquire);
660 | self.tag.store(current_tag, Ordering::Release);
661 | box_ref.generation.store(current_tag, Ordering::Release);
662 | &box_ref.value
663 | }
664 | }
665 |
666 | impl PartialEq> for Gc
667 | where
668 | T: Trace + Send + Sync + ?Sized + PartialEq,
669 | {
670 | /// Test for equality on two `Gc`s.
671 | ///
672 | /// Two `Gc`s are equal if their inner values are equal, even if they are stored in different
673 | /// allocations.
674 | /// Because `PartialEq` does not imply reflexivity, and there is no current path for trait
675 | /// specialization, this function does not do a "fast-path" check for reference equality.
676 | /// Therefore, if two `Gc`s point to the same allocation, the implementation of `eq` will still
677 | /// require a direct call to `eq` on the values.
678 | ///
679 | /// # Panics
680 | ///
681 | /// This function may panic if it is called from within the implementation of `std::ops::Drop`
682 | /// of its owning value, since returning such a reference could cause a use-after-free.
683 | /// It is not guaranteed to panic.
684 | /// Additionally, if this `Gc` is moved out of an allocation during a `Drop` implementation, it
685 | /// could later cause a panic.
686 | /// For further details, refer to the main documentation for `Gc`.
687 | ///
688 | /// ```
689 | /// use dumpster::sync::Gc;
690 | ///
691 | /// let gc = Gc::new(6);
692 | /// assert!(gc == Gc::new(6));
693 | /// ```
694 | fn eq(&self, other: &Gc) -> bool {
695 | self.as_ref() == other.as_ref()
696 | }
697 | }
698 |
699 | impl Eq for Gc where T: Trace + Send + Sync + ?Sized + PartialEq {}
700 |
701 | impl AsRef for Gc {
702 | fn as_ref(&self) -> &T {
703 | self
704 | }
705 | }
706 |
707 | impl Borrow for Gc {
708 | fn borrow(&self) -> &T {
709 | self
710 | }
711 | }
712 |
713 | impl std::fmt::Pointer for Gc {
714 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 | std::fmt::Pointer::fmt(&addr_of!(**self), f)
716 | }
717 | }
718 |
719 | #[cfg(feature = "coerce-unsized")]
720 | impl std::ops::CoerceUnsized> for Gc
721 | where
722 | T: std::marker::Unsize + Trace + Send + Sync + ?Sized,
723 | U: Trace + Send + Sync + ?Sized,
724 | {
725 | }
726 |
727 | impl Debug for Gc {
728 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729 | write!(
730 | f,
731 | "Gc({:?}, {})",
732 | self.ptr,
733 | self.tag.load(Ordering::Acquire)
734 | )
735 | }
736 | }
737 |
--------------------------------------------------------------------------------
/dumpster/src/sync/tests.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use std::{
10 | collections::{hash_map::Entry, HashMap},
11 | mem::{swap, take, transmute, MaybeUninit},
12 | ptr::NonNull,
13 | sync::{
14 | atomic::{AtomicUsize, Ordering},
15 | Mutex,
16 | },
17 | };
18 |
19 | use crate::Visitor;
20 |
21 | use super::*;
22 |
23 | struct DropCount<'a>(&'a AtomicUsize);
24 |
25 | impl<'a> Drop for DropCount<'a> {
26 | fn drop(&mut self) {
27 | self.0.fetch_add(1, Ordering::Release);
28 | }
29 | }
30 |
31 | unsafe impl Trace for DropCount<'_> {
32 | fn accept(&self, _: &mut V) -> Result<(), ()> {
33 | Ok(())
34 | }
35 | }
36 |
37 | struct MultiRef {
38 | refs: Mutex>>,
39 | #[allow(unused)]
40 | count: DropCount<'static>,
41 | }
42 |
43 | unsafe impl Trace for MultiRef {
44 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
45 | self.refs.accept(visitor)
46 | }
47 | }
48 |
49 | #[test]
50 | fn single_alloc() {
51 | static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
52 | let gc1 = Gc::new(DropCount(&DROP_COUNT));
53 |
54 | collect();
55 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 0);
56 | drop(gc1);
57 | collect();
58 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 1);
59 | }
60 |
61 | #[test]
62 | fn ref_count() {
63 | static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
64 | let gc1 = Gc::new(DropCount(&DROP_COUNT));
65 | let gc2 = Gc::clone(&gc1);
66 |
67 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 0);
68 | drop(gc1);
69 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 0);
70 | drop(gc2);
71 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 1);
72 | }
73 |
74 | #[test]
75 | fn self_referential() {
76 | struct Foo(Mutex>>);
77 | static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
78 |
79 | unsafe impl Trace for Foo {
80 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
81 | self.0.accept(visitor)
82 | }
83 | }
84 |
85 | impl Drop for Foo {
86 | fn drop(&mut self) {
87 | println!("begin increment of the drop count!");
88 | DROP_COUNT.fetch_add(1, Ordering::Release);
89 | }
90 | }
91 |
92 | let gc1 = Gc::new(Foo(Mutex::new(None)));
93 | *gc1.0.lock().unwrap() = Some(Gc::clone(&gc1));
94 |
95 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 0);
96 | drop(gc1);
97 | collect();
98 | assert_eq!(DROP_COUNT.load(Ordering::Acquire), 1);
99 | }
100 |
101 | #[test]
102 | fn two_cycle() {
103 | static DROP_0: AtomicUsize = AtomicUsize::new(0);
104 | static DROP_1: AtomicUsize = AtomicUsize::new(0);
105 |
106 | let gc0 = Gc::new(MultiRef {
107 | refs: Mutex::new(Vec::new()),
108 | count: DropCount(&DROP_0),
109 | });
110 | let gc1 = Gc::new(MultiRef {
111 | refs: Mutex::new(vec![Gc::clone(&gc0)]),
112 | count: DropCount(&DROP_1),
113 | });
114 | gc0.refs.lock().unwrap().push(Gc::clone(&gc1));
115 |
116 | collect();
117 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
118 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
119 | drop(gc0);
120 | collect();
121 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
122 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
123 | drop(gc1);
124 | collect();
125 | assert_eq!(DROP_0.load(Ordering::Acquire), 1);
126 | assert_eq!(DROP_0.load(Ordering::Acquire), 1);
127 | }
128 |
129 | #[test]
130 | fn self_ref_two_cycle() {
131 | static DROP_0: AtomicUsize = AtomicUsize::new(0);
132 | static DROP_1: AtomicUsize = AtomicUsize::new(0);
133 |
134 | let gc0 = Gc::new(MultiRef {
135 | refs: Mutex::new(Vec::new()),
136 | count: DropCount(&DROP_0),
137 | });
138 | let gc1 = Gc::new(MultiRef {
139 | refs: Mutex::new(vec![Gc::clone(&gc0)]),
140 | count: DropCount(&DROP_1),
141 | });
142 | gc0.refs.lock().unwrap().extend([gc0.clone(), gc1.clone()]);
143 | gc1.refs.lock().unwrap().push(gc1.clone());
144 |
145 | collect();
146 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
147 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
148 | drop(gc0);
149 | collect();
150 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
151 | assert_eq!(DROP_0.load(Ordering::Acquire), 0);
152 | drop(gc1);
153 | collect();
154 | assert_eq!(DROP_0.load(Ordering::Acquire), 1);
155 | assert_eq!(DROP_0.load(Ordering::Acquire), 1);
156 | }
157 |
158 | #[test]
159 | fn parallel_loop() {
160 | static COUNT_1: AtomicUsize = AtomicUsize::new(0);
161 | static COUNT_2: AtomicUsize = AtomicUsize::new(0);
162 | static COUNT_3: AtomicUsize = AtomicUsize::new(0);
163 | static COUNT_4: AtomicUsize = AtomicUsize::new(0);
164 |
165 | let gc1 = Gc::new(MultiRef {
166 | count: DropCount(&COUNT_1),
167 | refs: Mutex::new(Vec::new()),
168 | });
169 | let gc2 = Gc::new(MultiRef {
170 | count: DropCount(&COUNT_2),
171 | refs: Mutex::new(vec![Gc::clone(&gc1)]),
172 | });
173 | let gc3 = Gc::new(MultiRef {
174 | count: DropCount(&COUNT_3),
175 | refs: Mutex::new(vec![Gc::clone(&gc1)]),
176 | });
177 | let gc4 = Gc::new(MultiRef {
178 | count: DropCount(&COUNT_4),
179 | refs: Mutex::new(vec![Gc::clone(&gc2), Gc::clone(&gc3)]),
180 | });
181 | gc1.refs.lock().unwrap().push(Gc::clone(&gc4));
182 |
183 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
184 | assert_eq!(COUNT_2.load(Ordering::Acquire), 0);
185 | assert_eq!(COUNT_3.load(Ordering::Acquire), 0);
186 | assert_eq!(COUNT_4.load(Ordering::Acquire), 0);
187 | drop(gc1);
188 | collect();
189 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
190 | assert_eq!(COUNT_2.load(Ordering::Acquire), 0);
191 | assert_eq!(COUNT_3.load(Ordering::Acquire), 0);
192 | assert_eq!(COUNT_4.load(Ordering::Acquire), 0);
193 | drop(gc2);
194 | collect();
195 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
196 | assert_eq!(COUNT_2.load(Ordering::Acquire), 0);
197 | assert_eq!(COUNT_3.load(Ordering::Acquire), 0);
198 | assert_eq!(COUNT_4.load(Ordering::Acquire), 0);
199 | drop(gc3);
200 | collect();
201 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
202 | assert_eq!(COUNT_2.load(Ordering::Acquire), 0);
203 | assert_eq!(COUNT_3.load(Ordering::Acquire), 0);
204 | assert_eq!(COUNT_4.load(Ordering::Acquire), 0);
205 | drop(gc4);
206 | collect();
207 | assert_eq!(COUNT_1.load(Ordering::Acquire), 1);
208 | assert_eq!(COUNT_2.load(Ordering::Acquire), 1);
209 | assert_eq!(COUNT_3.load(Ordering::Acquire), 1);
210 | assert_eq!(COUNT_4.load(Ordering::Acquire), 1);
211 | }
212 |
213 | #[test]
214 | /// Test that we can drop a Gc which points to some allocation with a locked Mutex inside it
215 | // note: I tried using `ntest::timeout` but for some reason that caused this test to trivially pass.
216 | fn deadlock() {
217 | let gc1 = Gc::new(Mutex::new(()));
218 | let gc2 = gc1.clone();
219 |
220 | let guard = gc1.lock();
221 | drop(gc2);
222 | collect();
223 | drop(guard);
224 | }
225 |
226 | #[test]
227 | fn open_drop() {
228 | static COUNT_1: AtomicUsize = AtomicUsize::new(0);
229 | let gc1 = Gc::new(MultiRef {
230 | refs: Mutex::new(Vec::new()),
231 | count: DropCount(&COUNT_1),
232 | });
233 |
234 | gc1.refs.lock().unwrap().push(gc1.clone());
235 | let guard = gc1.refs.lock();
236 | collect();
237 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
238 | drop(guard);
239 | drop(gc1);
240 | collect();
241 |
242 | assert_eq!(COUNT_1.load(Ordering::Acquire), 1);
243 | }
244 |
245 | #[test]
246 | #[cfg_attr(miri, ignore = "miri is too slow")]
247 | fn eventually_collect() {
248 | static COUNT_1: AtomicUsize = AtomicUsize::new(0);
249 | static COUNT_2: AtomicUsize = AtomicUsize::new(0);
250 |
251 | let gc1 = Gc::new(MultiRef {
252 | refs: Mutex::new(Vec::new()),
253 | count: DropCount(&COUNT_1),
254 | });
255 | let gc2 = Gc::new(MultiRef {
256 | refs: Mutex::new(vec![gc1.clone()]),
257 | count: DropCount(&COUNT_2),
258 | });
259 | gc1.refs.lock().unwrap().push(gc2.clone());
260 |
261 | assert_eq!(COUNT_1.load(Ordering::Acquire), 0);
262 | assert_eq!(COUNT_2.load(Ordering::Acquire), 0);
263 |
264 | drop(gc1);
265 | drop(gc2);
266 |
267 | for _ in 0..200_000 {
268 | let gc = Gc::new(());
269 | drop(gc);
270 | }
271 |
272 | // after enough time, gc1 and gc2 should have been collected
273 | assert_eq!(COUNT_1.load(Ordering::Acquire), 1);
274 | assert_eq!(COUNT_2.load(Ordering::Acquire), 1);
275 | }
276 |
277 | #[test]
278 | #[cfg(feature = "coerce-unsized")]
279 | fn coerce_array() {
280 | let gc1: Gc<[u8; 3]> = Gc::new([0, 0, 0]);
281 | let gc2: Gc<[u8]> = gc1;
282 | assert_eq!(gc2.len(), 3);
283 | assert_eq!(
284 | std::mem::size_of::>(),
285 | 3 * std::mem::size_of::()
286 | );
287 | }
288 |
289 | #[test]
290 | fn malicious() {
291 | static EVIL: AtomicUsize = AtomicUsize::new(0);
292 | static A_DROP_DETECT: AtomicUsize = AtomicUsize::new(0);
293 | struct A {
294 | x: Gc,
295 | y: Gc,
296 | }
297 | struct X {
298 | a: Mutex>>,
299 | y: NonNull,
300 | }
301 | struct Y {
302 | a: Mutex>>,
303 | }
304 |
305 | unsafe impl Send for X {}
306 |
307 | unsafe impl Trace for A {
308 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
309 | self.x.accept(visitor)?;
310 | self.y.accept(visitor)
311 | }
312 | }
313 |
314 | unsafe impl Trace for X {
315 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
316 | self.a.accept(visitor)?;
317 |
318 | if EVIL.fetch_add(1, Ordering::Relaxed) == 1 {
319 | println!("committing evil...");
320 | // simulates a malicious thread
321 | let y = unsafe { self.y.as_ref() };
322 | *y.a.lock().unwrap() = (*self.a.lock().unwrap()).take();
323 | }
324 |
325 | Ok(())
326 | }
327 | }
328 |
329 | unsafe impl Trace for Y {
330 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
331 | self.a.accept(visitor)
332 | }
333 | }
334 |
335 | unsafe impl Sync for X {}
336 |
337 | impl Drop for A {
338 | fn drop(&mut self) {
339 | A_DROP_DETECT.fetch_add(1, Ordering::Relaxed);
340 | }
341 | }
342 |
343 | let y = Gc::new(Y {
344 | a: Mutex::new(None),
345 | });
346 | let x = Gc::new(X {
347 | a: Mutex::new(None),
348 | y: NonNull::from(y.as_ref()),
349 | });
350 | let a = Gc::new(A { x, y });
351 | *a.x.a.lock().unwrap() = Some(a.clone());
352 |
353 | collect();
354 | drop(a.clone());
355 | EVIL.store(1, Ordering::Relaxed);
356 | collect();
357 | assert_eq!(A_DROP_DETECT.load(Ordering::Relaxed), 0);
358 | drop(a);
359 | collect();
360 | assert_eq!(A_DROP_DETECT.load(Ordering::Relaxed), 1);
361 | }
362 |
363 | #[test]
364 | #[cfg_attr(miri, ignore = "miri is too slow")]
365 | #[allow(clippy::too_many_lines)]
366 | fn fuzz() {
367 | const N: usize = 20_000;
368 | static DROP_DETECTORS: [AtomicUsize; N] = {
369 | let mut detectors: [MaybeUninit; N] =
370 | unsafe { transmute(MaybeUninit::<[AtomicUsize; N]>::uninit()) };
371 |
372 | let mut i = 0;
373 | while i < N {
374 | detectors[i] = MaybeUninit::new(AtomicUsize::new(0));
375 | i += 1;
376 | }
377 |
378 | unsafe { transmute(detectors) }
379 | };
380 |
381 | #[derive(Debug)]
382 | struct Alloc {
383 | refs: Mutex>>,
384 | id: usize,
385 | }
386 |
387 | impl Drop for Alloc {
388 | fn drop(&mut self) {
389 | DROP_DETECTORS[self.id].fetch_add(1, Ordering::Relaxed);
390 | }
391 | }
392 |
393 | unsafe impl Trace for Alloc {
394 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
395 | self.refs.accept(visitor)
396 | }
397 | }
398 |
399 | fn dfs(alloc: &Gc, graph: &mut HashMap>) {
400 | if let Entry::Vacant(v) = graph.entry(alloc.id) {
401 | if alloc.id == 2822 || alloc.id == 2814 {
402 | println!("{} - {alloc:?}", alloc.id);
403 | }
404 | v.insert(Vec::new());
405 | alloc.refs.lock().unwrap().iter().for_each(|a| {
406 | graph.get_mut(&alloc.id).unwrap().push(a.id);
407 | dfs(a, graph);
408 | });
409 | }
410 | }
411 |
412 | fastrand::seed(12345);
413 | let mut gcs = (0..50)
414 | .map(|i| {
415 | Gc::new(Alloc {
416 | refs: Mutex::new(Vec::new()),
417 | id: i,
418 | })
419 | })
420 | .collect::>();
421 |
422 | let mut next_detector = 50;
423 | for _ in 0..N {
424 | if gcs.is_empty() {
425 | gcs.push(Gc::new(Alloc {
426 | refs: Mutex::new(Vec::new()),
427 | id: next_detector,
428 | }));
429 | next_detector += 1;
430 | }
431 | match fastrand::u8(0..4) {
432 | 0 => {
433 | println!("add gc {next_detector}");
434 | gcs.push(Gc::new(Alloc {
435 | refs: Mutex::new(Vec::new()),
436 | id: next_detector,
437 | }));
438 | next_detector += 1;
439 | }
440 | 1 => {
441 | if gcs.len() > 1 {
442 | let from = fastrand::usize(0..gcs.len());
443 | let to = fastrand::usize(0..gcs.len());
444 | println!("add ref {} -> {}", gcs[from].id, gcs[to].id);
445 | let new_gc = gcs[to].clone();
446 | let mut guard = gcs[from].refs.lock().unwrap();
447 | guard.push(new_gc);
448 | }
449 | }
450 | 2 => {
451 | let idx = fastrand::usize(0..gcs.len());
452 | println!("remove gc {}", gcs[idx].id);
453 | gcs.swap_remove(idx);
454 | }
455 | 3 => {
456 | let from = fastrand::usize(0..gcs.len());
457 | let mut guard = gcs[from].refs.lock().unwrap();
458 | if !guard.is_empty() {
459 | let to = fastrand::usize(0..guard.len());
460 | println!("drop ref {} -> {}", gcs[from].id, guard[to].id);
461 | guard.swap_remove(to);
462 | }
463 | }
464 | _ => unreachable!(),
465 | }
466 | }
467 |
468 | let mut graph = HashMap::new();
469 | graph.insert(9999, Vec::new());
470 | for alloc in &gcs {
471 | graph.get_mut(&9999).unwrap().push(alloc.id);
472 | dfs(alloc, &mut graph);
473 | }
474 | println!("{graph:#?}");
475 |
476 | drop(gcs);
477 | collect();
478 |
479 | let mut n_missing = 0;
480 | for (id, count) in DROP_DETECTORS[..next_detector].iter().enumerate() {
481 | let num = count.load(Ordering::Relaxed);
482 | if num != 1 {
483 | println!("expected 1 for id {id} but got {num}");
484 | n_missing += 1;
485 | }
486 | }
487 | assert_eq!(n_missing, 0);
488 | }
489 |
490 | #[test]
491 | fn root_canal() {
492 | struct A {
493 | b: Gc,
494 | }
495 |
496 | struct B {
497 | a0: Mutex>>,
498 | a1: Mutex >>,
499 | a2: Mutex >>,
500 | a3: Mutex >>,
501 | }
502 |
503 | unsafe impl Trace for A {
504 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
505 | self.b.accept(visitor)
506 | }
507 | }
508 |
509 | unsafe impl Trace for B {
510 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
511 | let n_prior_visits = B_VISIT_COUNT.fetch_add(1, Ordering::Relaxed);
512 | self.a0.accept(visitor)?;
513 | self.a1.accept(visitor)?;
514 |
515 | // simulate a malicious thread swapping things around
516 | if n_prior_visits == 1 {
517 | println!("committing evil...");
518 | swap(
519 | &mut *SMUGGLED_POINTERS[0].lock().unwrap(),
520 | &mut *SMUGGLED_POINTERS[1]
521 | .lock()
522 | .unwrap()
523 | .as_ref()
524 | .unwrap()
525 | .b
526 | .a0
527 | .lock()
528 | .unwrap(),
529 | );
530 | swap(&mut *self.a0.lock().unwrap(), &mut *self.a2.lock().unwrap());
531 | swap(
532 | &mut *SMUGGLED_POINTERS[0].lock().unwrap(),
533 | &mut *SMUGGLED_POINTERS[1]
534 | .lock()
535 | .unwrap()
536 | .as_ref()
537 | .unwrap()
538 | .b
539 | .a1
540 | .lock()
541 | .unwrap(),
542 | );
543 | swap(&mut *self.a1.lock().unwrap(), &mut *self.a3.lock().unwrap());
544 | }
545 |
546 | self.a2.accept(visitor)?;
547 | self.a3.accept(visitor)?;
548 |
549 | // smuggle out some pointers
550 | if n_prior_visits == 0 {
551 | println!("smuggling...");
552 | *SMUGGLED_POINTERS[0].lock().unwrap() = take(&mut *self.a2.lock().unwrap());
553 | *SMUGGLED_POINTERS[1].lock().unwrap() = take(&mut *self.a3.lock().unwrap());
554 | }
555 |
556 | Ok(())
557 | }
558 | }
559 |
560 | impl Drop for B {
561 | fn drop(&mut self) {
562 | B_DROP_DETECT.fetch_add(1, Ordering::Relaxed);
563 | }
564 | }
565 |
566 | static SMUGGLED_POINTERS: [Mutex>>; 2] = [Mutex::new(None), Mutex::new(None)];
567 | static B_VISIT_COUNT: AtomicUsize = AtomicUsize::new(0);
568 | static B_DROP_DETECT: AtomicUsize = AtomicUsize::new(0);
569 |
570 | let a = Gc::new(A {
571 | b: Gc::new(B {
572 | a0: Mutex::new(None),
573 | a1: Mutex::new(None),
574 | a2: Mutex::new(None),
575 | a3: Mutex::new(None),
576 | }),
577 | });
578 | *a.b.a0.lock().unwrap() = Some(a.clone());
579 | *a.b.a1.lock().unwrap() = Some(a.clone());
580 | *a.b.a2.lock().unwrap() = Some(a.clone());
581 | *a.b.a3.lock().unwrap() = Some(a.clone());
582 |
583 | drop(a.clone());
584 | collect();
585 | println!("{}", CURRENT_TAG.load(Ordering::Relaxed));
586 |
587 | assert!(dbg!(SMUGGLED_POINTERS[0].lock().unwrap().as_ref()).is_some());
588 | assert!(SMUGGLED_POINTERS[1].lock().unwrap().as_ref().is_some());
589 | println!("{}", B_VISIT_COUNT.load(Ordering::Relaxed));
590 |
591 | assert_eq!(B_DROP_DETECT.load(Ordering::Relaxed), 0);
592 | drop(a);
593 | assert_eq!(B_DROP_DETECT.load(Ordering::Relaxed), 0);
594 | collect();
595 | println!("{}", CURRENT_TAG.load(Ordering::Relaxed));
596 |
597 | assert_eq!(B_DROP_DETECT.load(Ordering::Relaxed), 0);
598 |
599 | *SMUGGLED_POINTERS[0].lock().unwrap() = None;
600 | *SMUGGLED_POINTERS[1].lock().unwrap() = None;
601 | collect();
602 |
603 | assert_eq!(B_DROP_DETECT.load(Ordering::Relaxed), 1);
604 | }
605 |
606 | #[test]
607 | #[should_panic = "Attempting to dereference Gc to already-deallocated object.This is caused by accessing a Gc during a Drop implementation, likely implying a bug in your code."]
608 | fn escape_dead_pointer() {
609 | static ESCAPED: Mutex >> = Mutex::new(None);
610 |
611 | struct Escape {
612 | x: u8,
613 | ptr: Mutex >>,
614 | }
615 |
616 | impl Drop for Escape {
617 | fn drop(&mut self) {
618 | let mut escaped_guard = ESCAPED.lock().unwrap();
619 | if escaped_guard.is_none() {
620 | *escaped_guard = self.ptr.lock().unwrap().take();
621 | }
622 | }
623 | }
624 |
625 | unsafe impl Trace for Escape {
626 | fn accept(&self, visitor: &mut V) -> Result<(), ()> {
627 | self.ptr.accept(visitor)
628 | }
629 | }
630 |
631 | let esc = Gc::new(Escape {
632 | x: 0,
633 | ptr: Mutex::new(None),
634 | });
635 |
636 | *(*esc).ptr.lock().unwrap() = Some(esc.clone());
637 | drop(esc);
638 | collect();
639 | println!("{}", ESCAPED.lock().unwrap().as_ref().unwrap().x);
640 | }
641 |
--------------------------------------------------------------------------------
/dumpster/src/unsync/collect.rs:
--------------------------------------------------------------------------------
1 | /*
2 | dumpster, acycle-tracking garbage collector for Rust. Copyright (C) 2023 Clayton Ramsey.
3 |
4 | This Source Code Form is subject to the terms of the Mozilla Public
5 | License, v. 2.0. If a copy of the MPL was not distributed with this
6 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | //! Implementations of the single-threaded garbage-collection logic.
10 |
11 | use std::{
12 | alloc::{dealloc, Layout},
13 | cell::{Cell, RefCell},
14 | collections::{hash_map::Entry, HashMap, HashSet},
15 | num::NonZeroUsize,
16 | ptr::{drop_in_place, NonNull},
17 | };
18 |
19 | use crate::{
20 | ptr::Erased,
21 | unsync::{default_collect_condition, CollectInfo, Gc},
22 | Trace, Visitor,
23 | };
24 |
25 | use super::{CollectCondition, GcBox};
26 |
27 | thread_local! {
28 | /// Whether the current thread is running a cleanup process.
29 | pub(super) static COLLECTING: Cell = const { Cell::new(false) };
30 | /// The global collection of allocation information for this thread.
31 | pub(super) static DUMPSTER: Dumpster = Dumpster {
32 | to_collect: RefCell::new(HashMap::new()),
33 | n_ref_drops: Cell::new(0),
34 | n_refs_living: Cell::new(0),
35 | collect_condition: Cell::new(default_collect_condition),
36 | };
37 | }
38 |
39 | /// A dumpster is a collection of all the garbage that may or may not need to be cleaned up.
40 | /// It also contains information relevant to when a cleanup should be triggered.
41 | pub(super) struct Dumpster {
42 | /// A map from allocation IDs for allocations which may need to be collected to pointers to
43 | /// their allocations.
44 | to_collect: RefCell>,
45 | /// The number of times a reference has been dropped since the last collection was triggered.
46 | pub n_ref_drops: Cell,
47 | /// The number of references that currently exist in the entire heap and stack.
48 | pub n_refs_living: Cell,
49 | /// The function for determining whether a collection should be run.
50 | pub collect_condition: Cell,
51 | }
52 |
53 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54 | /// A unique identifier for an allocated garbage-collected block.
55 | ///
56 | /// It contains a pointer to the reference count of the allocation.
57 | struct AllocationId(pub NonNull>);
58 |
59 | impl From>> for AllocationId
60 | where
61 | T: Trace + ?Sized,
62 | {
63 | /// Get an allocation ID from a pointer to an allocation.
64 | fn from(value: NonNull>) -> Self {
65 | AllocationId(value.cast())
66 | }
67 | }
68 |
69 | #[derive(Debug)]
70 | /// The necessary information required to collect some garbage-collected data.
71 | /// This data is stored in a map from allocation IDs to the necessary cleanup operation.
72 | struct Cleanup {
73 | /// The function which is called to build the reference graph and find all allocations
74 | /// reachable from this allocation.
75 | dfs_fn: unsafe fn(Erased, &mut Dfs),
76 | /// The function which is called to mark descendants of this allocation as reachable.
77 | mark_fn: unsafe fn(Erased, &mut Mark),
78 | /// A function used for dropping the allocation.
79 | drop_fn: unsafe fn(Erased, &mut DropAlloc<'_>),
80 | /// An erased pointer to the allocation.
81 | ptr: Erased,
82 | }
83 |
84 | impl Cleanup {
85 | /// Construct a new cleanup for an allocation.
86 | fn new(box_ptr: NonNull |