Skip to content

script: Reduce usage of Trusted in Node::insert.#37762

Merged
jdm merged 1 commit intoservo:mainfrom
jdm:post-connect-optiona
Jul 15, 2025
Merged

script: Reduce usage of Trusted in Node::insert.#37762
jdm merged 1 commit intoservo:mainfrom
jdm:post-connect-optiona

Conversation

@jdm
Copy link
Copy Markdown
Member

@jdm jdm commented Jun 28, 2025

These changes introduce a new kind of task that uses a variation of the task! syntax. Existing task! usages create task structs that have a Send bound, which requires the use of Trusted<T> to reference a DOM object T inside of the task closure. The new syntax replaces the Send bound with a JSTraceable bound, which requires explicit capture clauses with types. This looks like:

task!(ScriptPrepare: {script: DomRoot<HTMLScriptElement>} |script| {
    script.prepare(CanGc::note());
}),

The capture clauses must list every value that will be referenced from the closure's environment—these values are moved into fields in the generated task structure, which allows them to be traced by the GC as part of a generated JSTraceable implementation. Since the closure itself is not a move closure, any attempts to reference values not explicitly captured will generate a borrow checker error since the closure requires the 'static lifetime.

Testing: Existing WPT tests exercise these code paths. I also attempted to write incorrect tasks that capture references or use values not explicitly captured, and the compiler correctly errors out.
Fixes: part of #35517

@jdm jdm requested a review from gterzian as a code owner June 28, 2025 02:02
@jdm
Copy link
Copy Markdown
Member Author

jdm commented Jun 28, 2025

cc @jschwe

@jdm jdm force-pushed the post-connect-optiona branch from 324d351 to 9c0d4bc Compare June 28, 2025 02:14
@sagudev
Copy link
Copy Markdown
Member

sagudev commented Jun 28, 2025

For the syntax, do we really need to pass idents twice (once as a field, once as an arg)? Can we do:

task!(ScriptPrepare: |script: DomRoot<HTMLScriptElement>| {
    script.prepare(CanGc::note());
}),

As for usability I am only worried about haw to deal with no_trace types, if we even need them in tasks anyway.

@jdm jdm force-pushed the post-connect-optiona branch from 9c0d4bc to 893536b Compare July 11, 2025 14:58
@jdm
Copy link
Copy Markdown
Member Author

jdm commented Jul 11, 2025

For the syntax, do we really need to pass idents twice (once as a field, once as an arg)? Can we do:

task!(ScriptPrepare: |script: DomRoot<HTMLScriptElement>| {
    script.prepare(CanGc::note());
}),

This was a good point! I've fixed the syntax to use this suggestion.

Copy link
Copy Markdown
Member

@gterzian gterzian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A suggestion

macro_rules! task {
($name:ident: |$($field:ident: $field_type:ty$(,)*)*| $body:tt) => {{
#[allow(non_camel_case_types)]
struct $name<F> {
Copy link
Copy Markdown
Member

@gterzian gterzian Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about impl js::gc::Rootable for the struct, and marking it must_root, so that you can store Dom in the struct?

Since those tasks are always stored somewhere, and not kept on the stack, it seems to make sense. The caller would have to root it first before storing it somewhere, but that seems ok.

Copy link
Copy Markdown
Member Author

@jdm jdm Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an interesting idea. It becomes much more complicated to interact with any captured values that are not Dom objects though, so I'm not sure how I feel about it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I would suggest to not use a macro, and just do this by hand, like we do for promise handlers with structs implementing

pub(crate) trait Callback: JSTraceable + MallocSizeOf {

The task macro is a nice shorthand, but I don't think it's worth the convenience at the cost of storing DomRoot on structs(I'm not sure what the cost is, but my understanding is that this is an anti-pattern).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the task macro to be useful for preserving code readability, since it uses a closure that interacts with values directly before it. I'm going to go ahead and merge the current implementation, since the primary use case of this work is code that would not work with your suggestion (a Vec containing DOM objects).

($name:ident: |$($field:ident: $field_type:ty$(,)*)*| $body:tt) => {{
#[allow(non_camel_case_types)]
struct $name<F> {
$($field: $field_type,)*
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per above, this could then be Dom::from_ref(&*$field_type).

F: ::std::ops::FnOnce($($field_type,)*),
{
fn run_once(self) {
(self.task)($(self.$field,)*);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here you could do self.$field.as_rooted().

@jdm jdm added this pull request to the merge queue Jul 12, 2025
@sagudev sagudev removed this pull request from the merge queue due to a manual request Jul 12, 2025
@jdm jdm added this pull request to the merge queue Jul 15, 2025
Merged via the queue into servo:main with commit 9e2ee00 Jul 15, 2025
20 of 21 checks passed
@jdm jdm deleted the post-connect-optiona branch July 15, 2025 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants