webidl icon indicating copy to clipboard operation
webidl copied to clipboard

Propagate active script to callbacks

Open domenic opened this issue 5 years ago • 18 comments

See discussion in https://github.com/tc39/ecma262/pull/2086#issuecomment-656257061.

This ensures that in cases like

<!-- index.html -->
<script src="sub/path/foo.js"></script>
// sub/path/foo.js
setTimeout(eval, 0, 'import(`./example.mjs`)');

the resulting imported module is sub/path/example.mjs, and not example.mjs. I.e. it ensures that import() resolves relative to the active script, even when it is being called via a Web IDL callback.

Tests: https://github.com/web-platform-tests/wpt/pull/24535

/cc @hiroshige-g as he was involved in very similar discussions for cases like setTimeout("import(`./example.mjs`)") and @yuki3 as the Chromium binding person working on incumbents.

/cc @jonco3 as he worked on fixing the tests for very similar issues.


Preview | Diff

domenic avatar Jul 09 '20 18:07 domenic

Quite interesting. Thanks for letting me know. :)

yuki3 avatar Jul 09 '20 18:07 yuki3

Based on the test results at https://github.com/web-platform-tests/wpt/pull/24535, nobody seems to implement this yet. Even Firefox, who does great on the existing tests.

However, I think this is definitely the right thing to do. Can you agree on Chrome's behalf, @yuki3 and/or @hiroshige-g? It's fine if it's lower priority, but I think we should make sure this happens...

domenic avatar Jul 10 '20 19:07 domenic

I don't fully understand about 'import', but I totally agree that 'eval's incumbent must be the realm of the caller of 'setTimeout'. Thus, I agree with the test.

AFAIK, in Chromium, setTimeout + IDL callback function already supports the backup incumbent stack, so probably this is @hiroshige-g's area, I think?

yuki3 avatar Jul 11 '20 13:07 yuki3

Basically, this PR is adding another "incumbent-like thing" that gets propagated in the same way as incumbent: namely, the active script. In particular, the active script's base URL is used for import().

So the example setTimeout(eval, 0, 'import(`./example.mjs`)'); is about propagating the active script, not about propagating the incumbent. (The incumbent would come into play with an example like setTimeout(eval, 0, 'postMessage(...)').)

So indeed, this is a bit more in @hiroshige-g's area. But, it will likely impact the bindings code, because it will need propagation through Web IDL callback conversion + calling in the same way incumbent currently does.

domenic avatar Jul 13 '20 15:07 domenic

Oh my. I'm sorry. Now I read this PR a little more carefully. IIUC, active script is not always the script or module of the incumbent realm, so we have to track active script in addition to incumbent. It means another runtime cost. Is this correct?

If so, it will be 25% object size increase of all IDL callback functions and IDL callback interfaces (in Blink). Probably it's okay, but if there is a guarantee that, in case of IDL callbacks, the incumbent settings object's execution context's ScriptOrModule never be null, that's great. (Anyway, I think IDL callbacks will be okay.)

I think it's better to notify those who will implement Promise callbacks and WeakRef callbacks of Chromium. They will need to implement not only the incumbent but also the active script.

Summary:

  1. I think we can support active script in IDL callback functions/interfaces. But not sure about Promise and WeakRef callbacks.
  2. Q: Is there a case that IDL callback's incumbent's execution context's ScriptOrModule is null?

yuki3 avatar Jul 13 '20 15:07 yuki3

Yes, I was a bit worried about the runtime costs. I'm glad you think that cost will be OK, but don't hesitate to push back if you think it is too expensive. Although I think this change is a good one, I don't think it's infinitely good; if it has high costs then we should reevaluate.

I think it's better to notify those who will implement Promise callbacks and WeakRef callbacks of Chromium. They will need to implement not only the incumbent but also the active script.

Yes, @syg is helping with that, and that is actually what prompted this discussion. Currently promises are specced to propagate active script, and they do in Chromium; we were working on generalizing that to WeakRef, but noticed that there was an inconsistency with Web IDL callbacks.

Q: Is there a case that IDL callback's incumbent's execution context's ScriptOrModule is null?

I think it is never null. By "incumbent's execution context" you probably mean the execution context in step 1 of https://html.spec.whatwg.org/#incumbent-settings-object? In that case then it is definitely never null.

This is very interesting. I guess your idea is that Chromium would store the "topmost script-having execution context" (i.e. "incumbent's execution context") instead of storing the (incumbent settings object, active script) tuple? Then it could infer active settings object using the algorithm in https://html.spec.whatwg.org/#incumbent-settings-object, and active script using the ScriptOrModule component.

I think that works, and means no extra runtime cost. (At least, in spec terms; I am not sure how it maps to Chromium implementation primitives.)

domenic avatar Jul 13 '20 16:07 domenic

I think it is never null. By "incumbent's execution context" you probably mean the execution context in step 1 of https://html.spec.whatwg.org/#incumbent-settings-object? In that case then it is definitely never null.

Yes, I meant it, and I had the same idea with yours. That's a good news that we can avoid increase of memory consumption about IDL callbacks. :) Then, I have no concern on this PR.

yuki3 avatar Jul 13 '20 16:07 yuki3

Excellent. I will probably update this PR to use that technique, then, to make it more obvious for implementers.

domenic avatar Jul 13 '20 16:07 domenic

I'll update https://github.com/whatwg/html/pull/5722 to follow suit so we have a common mechanism.

syg avatar Jul 13 '20 16:07 syg

Hmm, now I am worried about the case where the topmost script-having execution context is null. In that case we still need to capture the incumbent settings object as per the backup stack.

I think this could still be done, but we would have to convert the backup incumbent settings object stack into a backup incumbent execution context stack.

That is a larger change. So I would prefer to land this plus whatwg/html#5722 first using the mechanism from this PR, i.e. an (incumbent settings object, active script) tuple. Then I can follow up with a refactoring to consolidate everything to use execution contexts.

domenic avatar Jul 13 '20 16:07 domenic

+1 for making active script the ScriptOrModule component of the incumbent. Active script and incumbent have similar intention, and the concept of incumbent is already complicated (e.g. https://github.com/whatwg/html/issues/1430). It's better to merge active script into incumbent and maintain the incumbent spec/impl as the single source of truth.

I also suspect the current browsers implementation is more like referring the incumbent rather than the current specification of active script, according to cases where a script triggers event firing synchronously. WPT: https://github.com/web-platform-tests/wpt/pull/24305 In such cases, https://html.spec.whatwg.org/multipage/webappapis.html#skip-when-determining-incumbent-counter makes the incumbent's script and the active script different.

(Thanks @yuki3, the chat we had today was quite helpful for me!)

hiroshige-g avatar Jul 14 '20 08:07 hiroshige-g

+1 for making active script the ScriptOrModule component of the incumbent.

This change will require more 262-side changes, @syg. In particular https://tc39.es/ecma262/#sec-getactivescriptormodule would not longer be the correct algorithm; we'd need instead to skip execution contexts whose skip-when-determining-incumbent counter is non-zero.

domenic avatar Jul 22 '20 19:07 domenic

Oh boy. That makes me very wary to support such a unification...

syg avatar Jul 22 '20 20:07 syg

Yeah, I can understand that. Here is my proposal:

  1. Land https://github.com/whatwg/html/pull/5722 (tracking active script/incumbent for promises correctly)
  2. Land https://github.com/whatwg/html/pull/4571 on top of that (weak references, with proper active script/incumbent tracking)
  3. Land this PR and its associated tests, in basically this form. (Tracking active script for Web IDL callbacks)
  4. Investigate unification, i.e. no longer tracking active script and incumbent separately, but instead tracking incumbent execution context, and deriving active script from that.
    • This could involve tests like @hiroshige-g's above. Or even simplifications of incumbent to make it more like active script.

I will set aside my (local on-disk) unification pull request, so we can proceed incrementally. So at this point I think the ball is in other folks' courts. In particular, @syg is working on those HTML pull requests (and their 262 dependencies), and it would be good to get Mozilla or Apple input on the question posed by this PR and its associated tests about

// sub/path/foo.js
setTimeout(eval, 0, 'import(`./example.mjs`)');

domenic avatar Jul 22 '20 20:07 domenic

I discussed it briefly with @jonco3 and neither of us feel strongly.

annevk avatar Jul 23 '20 09:07 annevk

This change feels "correct" to me, but I'm curious, does it affect any cases besides when eval (or possibly other kinds of functions implemented by the platform which parse JavaScript) is used directly as a WebIDL callback? This feels like a relatively narrow case; it doesn't seem so bad to fall back to the document's base URL in this edge case.

I'm wondering how much harm the current semantics of falling back to import() being relative to the document's base URL is causing for web developers. Have we seen any bug reports from web developers confused about these semantics, for example?

littledan avatar Aug 06 '20 20:08 littledan

I'm starting to come around to @littledan's point of view, that while this is the correct thing to do, but is not worth the implementation effort.

does it affect any cases besides when eval (or possibly other kinds of functions implemented by the platform which parse JavaScript) is used directly as a WebIDL callback?

It affects any case where the platform function is dependent on the active script. I think in practice that's only going to be things like eval and setTimeout, because the only thing that currently depends on the active script is import() path resolution, and the only thing that triggers import() is author code.

(maybe it could affect exception stack traces??)

We may want to remove active script tracking from job callbacks as well, since @hiroshige-g is discovering that no browsers seem to do that either?

domenic avatar Aug 30 '21 21:08 domenic

FYI The test that confirms there seem no active scripts for Promise.resolve('import(...)').then(eval) on browsers is https://github.com/web-platform-tests/wpt/pull/30241.

hiroshige-g avatar Sep 03 '21 22:09 hiroshige-g