Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion build/wd_rust_crate.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,13 @@ def wd_rust_crate(
rust_unpretty(
name = name + "@expand",
deps = [":" + name],
tags = ["manual", "off-by-default"],
tags = ["manual"],
)

if len(test_proc_macro_deps) > 0:
rust_unpretty(
name = name + "_test@expand",
deps = [":" + name + "_test"],
tags = ["manual"],
testonly = True,
)
22 changes: 6 additions & 16 deletions src/rust/api/dns.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use jsg::ResourceState;
use jsg_macros::jsg_method;
use jsg_macros::jsg_resource;
use jsg_macros::jsg_struct;
Expand Down Expand Up @@ -110,9 +109,8 @@ pub fn parse_replacement(input: &[&str]) -> jsg::Result<String, DnsParserError>

#[jsg_resource]
pub struct DnsUtil {
// TODO(soon): Generated code. Move this to jsg-macros.
#[expect(clippy::pub_underscore_fields)]
pub _state: ResourceState,
#[expect(clippy::pub_underscore_fields)] // Placeholder until real fields are added
pub _unused: u32,
}

#[jsg_resource]
Expand Down Expand Up @@ -264,9 +262,7 @@ mod tests {

#[test]
fn test_parse_caa_record_issue() {
let dns_util = DnsUtil {
_state: ResourceState::default(),
};
let dns_util = DnsUtil { _unused: 0 };
let record = dns_util
.parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67".to_owned())
.unwrap();
Expand All @@ -278,9 +274,7 @@ mod tests {

#[test]
fn test_parse_caa_record_issuewild() {
let dns_util = DnsUtil {
_state: ResourceState::default(),
};
let dns_util = DnsUtil { _unused: 0 };
let record = dns_util
.parse_caa_record(
"\\# 21 00 09 69 73 73 75 65 77 69 6c 64 6c 65 74 73 65 6e 63 72 79 70 74"
Expand All @@ -295,9 +289,7 @@ mod tests {

#[test]
fn test_parse_caa_record_invalid_field() {
let dns_util = DnsUtil {
_state: ResourceState::default(),
};
let dns_util = DnsUtil { _unused: 0 };
let result = dns_util.parse_caa_record(
"\\# 15 00 05 69 6e 76 61 6c 69 64 70 6b 69 2e 67 6f 6f 67".to_owned(),
);
Expand All @@ -307,9 +299,7 @@ mod tests {

#[test]
fn test_parse_naptr_record() {
let dns_util = DnsUtil {
_state: ResourceState::default(),
};
let dns_util = DnsUtil { _unused: 0 };
let record = dns_util
.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())
.unwrap();
Expand Down
26 changes: 8 additions & 18 deletions src/rust/api/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use std::pin::Pin;

use jsg::ResourceState;
use jsg::ResourceTemplate;
use jsg::Resource;

use crate::dns::DnsUtil;
use crate::dns::DnsUtilTemplate;

pub mod dns;

Expand All @@ -27,35 +25,27 @@ pub fn register_nodejs_modules(registry: Pin<&mut ffi::ModuleRegistry>) {
"node-internal:dns",
|isolate| unsafe {
let mut lock = jsg::Lock::from_isolate_ptr(isolate);
let dns_util = jsg::Ref::new(DnsUtil {
_state: ResourceState::default(),
});
let mut dns_util_template = DnsUtilTemplate::new(&mut lock);

jsg::wrap_resource(&mut lock, dns_util, &mut dns_util_template).into_ffi()
let dns_util = DnsUtil::alloc(&mut lock, DnsUtil { _unused: 0 });
DnsUtil::wrap(dns_util, &mut lock).into_ffi()
},
jsg::modules::ModuleType::INTERNAL,
jsg::modules::ModuleType::Internal,
);
}

#[cfg(test)]
mod tests {
use jsg::ResourceTemplate;
use jsg_test::Harness;

use super::*;

#[test]
fn test_wrap_resource_equality() {
let harness = Harness::new();
harness.run_in_context(|lock, _ctx| unsafe {
let dns_util = jsg::Ref::new(DnsUtil {
_state: ResourceState::default(),
});
let mut dns_util_template = DnsUtilTemplate::new(lock);
harness.run_in_context(|lock, _ctx| {
let dns_util = DnsUtil::alloc(lock, DnsUtil { _unused: 0 });

let lhs = jsg::wrap_resource(lock, dns_util.clone(), &mut dns_util_template);
let rhs = jsg::wrap_resource(lock, dns_util, &mut dns_util_template);
let lhs = DnsUtil::wrap(dns_util.clone(), lock);
let rhs = DnsUtil::wrap(dns_util, lock);

assert_eq!(lhs, rhs);
Ok(())
Expand Down
55 changes: 50 additions & 5 deletions src/rust/jsg-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,15 @@ impl DnsUtil {

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.

**Important:** Resource structs must include a `_state: jsg::ResourceState` field for internal JSG state management.

```rust
#[jsg_resource]
pub struct DnsUtil {
pub _state: jsg::ResourceState,
pub _unused: u8,
}

#[jsg_resource(name = "CustomUtil")]
pub struct MyUtil {
pub _state: jsg::ResourceState,
pub value: u32,
}

#[jsg_resource]
Expand All @@ -75,7 +73,12 @@ impl DnsUtil {
}
```

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.
On struct definitions, generates:
- `jsg::Type` implementation
- `jsg::GarbageCollected` implementation (default, no-op trace)
- Wrapper struct and `ResourceTemplate` implementations

On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation.

## `#[jsg_oneof]`

Expand Down Expand Up @@ -105,3 +108,45 @@ impl MyResource {
```

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.

### Garbage Collection

Resources are automatically integrated with V8's cppgc garbage collector. The macro automatically generates a `GarbageCollected` implementation that traces fields requiring GC integration:

- `Ref<T>` fields - traces the underlying resource
- `TracedReference<T>` fields - traces the JavaScript handle
- `Option<Ref<T>>` and `Option<TracedReference<T>>` - conditionally traces
- `RefCell<Option<Ref<T>>>` - supports cyclic references through interior mutability

```rust
#[jsg_resource]
pub struct MyResource {
// Automatically traced
js_callback: Option<TracedReference<Object>>,
child_resource: Option<Ref<ChildResource>>,

// Not traced (plain data)
name: String,
}

// Cyclic references using RefCell
#[jsg_resource]
pub struct Node {
name: String,
next: RefCell<Option<Ref<Node>>>,
}
```

For complex cases or custom tracing logic, you can manually implement `GarbageCollected` without using the jsg_resource macro:

```rust
pub struct CustomResource {
data: String,
}

impl jsg::GarbageCollected for CustomResource {
fn trace(&self, visitor: &mut jsg::GcVisitor) {
// Custom tracing logic
}
}
```
Loading
Loading