Skip to content

Commit 1d356e2

Browse files
committed
add gc support to Rust owned objects
1 parent fd0665d commit 1d356e2

25 files changed

Lines changed: 2996 additions & 710 deletions

build/wd_rust_crate.bzl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,13 @@ def wd_rust_crate(
154154
rust_unpretty(
155155
name = name + "@expand",
156156
deps = [":" + name],
157-
tags = ["manual", "off-by-default"],
157+
tags = ["manual"],
158+
)
159+
160+
if len(test_proc_macro_deps) > 0:
161+
rust_unpretty(
162+
name = name + "_test@expand",
163+
deps = [":" + name + "_test"],
164+
tags = ["manual"],
165+
testonly = True,
158166
)

src/rust/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
- **CXX bridge**: `#[cxx::bridge(namespace = "workerd::rust::<crate>")]` with companion `ffi.c++`/`ffi.h` files
2626
- **Namespace**: always `workerd::rust::*` except `python-parser``edgeworker::rust::python_parser`
2727
- **Errors**: `thiserror` for library crates; `jsg::Error` with `ExceptionType` for JSG-facing crates
28-
- **JSG resources**: must include `_state: jsg::ResourceState` field; `#[jsg_method]` auto-converts `snake_case``camelCase`
28+
- **JSG resources**: `#[jsg_resource]` on struct + impl block; `#[jsg_method]` auto-converts `snake_case``camelCase`; resources integrate with GC via the `GarbageCollected` trait (auto-derived for `Ref<T>` / `WeakRef<T>` fields)
2929
- **Formatting**: `rustfmt.toml``group_imports = "StdExternalCrate"`, `imports_granularity = "Item"` (one `use` per import)
3030
- **Linting**: `just clippy <crate>` — pedantic+nursery; `allow-unwrap-in-tests`
3131
- **Tests**: inline `#[cfg(test)]` modules; JSG tests use `jsg_test::Harness::run_in_context()`

src/rust/api/dns.rs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use jsg::ResourceState;
21
use jsg_macros::jsg_method;
32
use jsg_macros::jsg_resource;
43
use jsg_macros::jsg_struct;
@@ -109,11 +108,7 @@ pub fn parse_replacement(input: &[&str]) -> jsg::Result<String, DnsParserError>
109108
}
110109

111110
#[jsg_resource]
112-
pub struct DnsUtil {
113-
// TODO(soon): Generated code. Move this to jsg-macros.
114-
#[expect(clippy::pub_underscore_fields)]
115-
pub _state: ResourceState,
116-
}
111+
pub struct DnsUtil;
117112

118113
#[jsg_resource]
119114
impl DnsUtil {
@@ -264,9 +259,7 @@ mod tests {
264259

265260
#[test]
266261
fn test_parse_caa_record_issue() {
267-
let dns_util = DnsUtil {
268-
_state: ResourceState::default(),
269-
};
262+
let dns_util = DnsUtil {};
270263
let record = dns_util
271264
.parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67".to_owned())
272265
.unwrap();
@@ -278,9 +271,7 @@ mod tests {
278271

279272
#[test]
280273
fn test_parse_caa_record_issuewild() {
281-
let dns_util = DnsUtil {
282-
_state: ResourceState::default(),
283-
};
274+
let dns_util = DnsUtil {};
284275
let record = dns_util
285276
.parse_caa_record(
286277
"\\# 21 00 09 69 73 73 75 65 77 69 6c 64 6c 65 74 73 65 6e 63 72 79 70 74"
@@ -295,9 +286,7 @@ mod tests {
295286

296287
#[test]
297288
fn test_parse_caa_record_invalid_field() {
298-
let dns_util = DnsUtil {
299-
_state: ResourceState::default(),
300-
};
289+
let dns_util = DnsUtil {};
301290
let result = dns_util.parse_caa_record(
302291
"\\# 15 00 05 69 6e 76 61 6c 69 64 70 6b 69 2e 67 6f 6f 67".to_owned(),
303292
);
@@ -307,9 +296,7 @@ mod tests {
307296

308297
#[test]
309298
fn test_parse_naptr_record() {
310-
let dns_util = DnsUtil {
311-
_state: ResourceState::default(),
312-
};
299+
let dns_util = DnsUtil {};
313300
let record = dns_util
314301
.parse_naptr_record("\\# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00".to_owned())
315302
.unwrap();

src/rust/api/lib.rs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
use std::pin::Pin;
22

3-
use jsg::ResourceState;
4-
use jsg::ResourceTemplate;
3+
use jsg::Resource;
54

65
use crate::dns::DnsUtil;
7-
use crate::dns::DnsUtilTemplate;
86

97
pub mod dns;
108

@@ -27,35 +25,27 @@ pub fn register_nodejs_modules(registry: Pin<&mut ffi::ModuleRegistry>) {
2725
"node-internal:dns",
2826
|isolate| unsafe {
2927
let mut lock = jsg::Lock::from_isolate_ptr(isolate);
30-
let dns_util = jsg::Ref::new(DnsUtil {
31-
_state: ResourceState::default(),
32-
});
33-
let mut dns_util_template = DnsUtilTemplate::new(&mut lock);
34-
35-
jsg::wrap_resource(&mut lock, dns_util, &mut dns_util_template).into_ffi()
28+
let dns_util = DnsUtil::alloc(&lock, DnsUtil {});
29+
DnsUtil::wrap(dns_util, &mut lock).into_ffi()
3630
},
37-
jsg::modules::ModuleType::INTERNAL,
31+
jsg::modules::ModuleType::Internal,
3832
);
3933
}
4034

4135
#[cfg(test)]
4236
mod tests {
43-
use jsg::ResourceTemplate;
4437
use jsg_test::Harness;
4538

4639
use super::*;
4740

4841
#[test]
4942
fn test_wrap_resource_equality() {
5043
let harness = Harness::new();
51-
harness.run_in_context(|lock, _ctx| unsafe {
52-
let dns_util = jsg::Ref::new(DnsUtil {
53-
_state: ResourceState::default(),
54-
});
55-
let mut dns_util_template = DnsUtilTemplate::new(lock);
44+
harness.run_in_context(|lock, _ctx| {
45+
let dns_util = DnsUtil::alloc(lock, DnsUtil {});
5646

57-
let lhs = jsg::wrap_resource(lock, dns_util.clone(), &mut dns_util_template);
58-
let rhs = jsg::wrap_resource(lock, dns_util, &mut dns_util_template);
47+
let lhs = DnsUtil::wrap(dns_util.clone(), lock);
48+
let rhs = DnsUtil::wrap(dns_util, lock);
5949

6050
assert_eq!(lhs, rhs);
6151
Ok(())

src/rust/jsg-macros/README.md

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,13 @@ impl DnsUtil {
5353

5454
Generates boilerplate for JSG resources. Applied to both struct definitions and impl blocks. Automatically implements `jsg::Type::class_name()` using the struct name, or a custom name if provided via the `name` parameter.
5555

56-
**Important:** Resource structs must include a `_state: jsg::ResourceState` field for internal JSG state management.
57-
5856
```rust
5957
#[jsg_resource]
60-
pub struct DnsUtil {
61-
pub _state: jsg::ResourceState,
62-
}
58+
pub struct DnsUtil {}
6359

6460
#[jsg_resource(name = "CustomUtil")]
6561
pub struct MyUtil {
66-
pub _state: jsg::ResourceState,
62+
pub value: u32,
6763
}
6864

6965
#[jsg_resource]
@@ -75,7 +71,12 @@ impl DnsUtil {
7571
}
7672
```
7773

78-
On struct definitions, generates `jsg::Type`, wrapper struct, and `ResourceTemplate` implementations. On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation.
74+
On struct definitions, generates:
75+
- `jsg::Type` implementation
76+
- `jsg::GarbageCollected` implementation (default, no-op trace)
77+
- Wrapper struct and `ResourceTemplate` implementations
78+
79+
On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation.
7980

8081
## `#[jsg_oneof]`
8182

@@ -105,3 +106,45 @@ impl MyResource {
105106
```
106107

107108
The macro generates type-checking code that matches JavaScript values to enum variants without coercion. If no variant matches, a `TypeError` is thrown listing all expected types.
109+
110+
### Garbage Collection
111+
112+
Resources are automatically integrated with V8's garbage collector through the C++ `Wrappable` base class. The macro automatically generates a `GarbageCollected` implementation that traces fields requiring GC integration:
113+
114+
- `Ref<T>` fields - traces the underlying resource
115+
- `TracedReference<T>` fields - traces the JavaScript handle
116+
- `Option<Ref<T>>` and `Option<TracedReference<T>>` - conditionally traces
117+
- `RefCell<Option<Ref<T>>>` - supports cyclic references through interior mutability
118+
119+
```rust
120+
#[jsg_resource]
121+
pub struct MyResource {
122+
// Automatically traced
123+
js_callback: Option<TracedReference<Object>>,
124+
child_resource: Option<Ref<ChildResource>>,
125+
126+
// Not traced (plain data)
127+
name: String,
128+
}
129+
130+
// Cyclic references using RefCell
131+
#[jsg_resource]
132+
pub struct Node {
133+
name: String,
134+
next: RefCell<Option<Ref<Node>>>,
135+
}
136+
```
137+
138+
For complex cases or custom tracing logic, you can manually implement `GarbageCollected` without using the jsg_resource macro:
139+
140+
```rust
141+
pub struct CustomResource {
142+
data: String,
143+
}
144+
145+
impl jsg::GarbageCollected for CustomResource {
146+
fn trace(&self, visitor: &mut jsg::GcVisitor) {
147+
// Custom tracing logic
148+
}
149+
}
150+
```

0 commit comments

Comments
 (0)