Skip to content

JSScript serialisable#2403

Open
bunkenburg wants to merge 9 commits into
mozilla:masterfrom
bunkenburg:JSScriptSerializable
Open

JSScript serialisable#2403
bunkenburg wants to merge 9 commits into
mozilla:masterfrom
bunkenburg:JSScriptSerializable

Conversation

@bunkenburg

Copy link
Copy Markdown

This is for #2402

It makes JSScript serialisable.

There are two cases to consider:

  1. interpreted mode
  2. compiled mode.

For interpreted mode, there is not much to fix: just JSScript implements Serializable

In compiled mode, the JavaScript is compiled to a class in Java bytecode. For serialization I considered putting the bytes of the class into serialisation. But that became too complicated and unclear. A much simpler solution is: I don't need the compiled class, because all the information is in the JavaScript program text. So at serialisation, I forget the compiled stuff (JSScript is substituted by CompiledScriptProxy), and at deserialisation, from the JavaScript text, I compile it again. The JavaScript text is available in JSDescriptor.

Compiling again is a slow step, I know, but maybe it's done later or in a different place, so anyway it seems a clean solution to me. Anyway, implementing serialisation for compiled mode this way makes something slow that before was impossible, so it's not making any use case worse.

@gbrail gbrail left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for working on this!

*/
public class JSScript implements Script, ScriptOrFn<JSScript> {
public class JSScript implements Script, ScriptOrFn<JSScript>, Serializable {
@Serial private static final long serialVersionUID = 1L;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since have another PR open related to serialVersionUID in general, please assign a real one here, generated by the "serialver" tool or your IDE, not generated by AI ;-)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The value 1 is as good as any other. This class was not serialisable before, so there's no old value that it must be different from. The other PR #2249 does not affect JSScript. Or is there something I'm missing?

}

@Serial
protected Object writeReplace() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Back in the day when I dealt with serialization all the time, we used to implement Externalizable which let us take control over serialization without all this stuff -- might be worth considering that too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Externalizable is good if you want to do it completely by yourself. Here I do less:

  • for interpreted mode: JSScript is serialised and deserialised normally.
  • for compiled mode: JSScript is not serialised; rather it is substituted by a CompiledScriptProxy (that has the JavaScript source). The proxy is serialised in the normal way. At the other end, the proxy is deserialised in the normal way (it's almost just a String), and then CompiledScriptProxy.readResolve() substitutes it back to a JSScript with compiled code (by compiling again).

/** recompiles from source */
@Serial
private Object readResolve() {
Context cx = Context.enter();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I may not be following, but won't this result in all interpreted scripts being deserialized into compiled scripts?

Also, we (perhaps controversially) made Context Closeable so you can just use a try-with-resources here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No, an interpreted JSScript goes in and comes out in the normal way. A compiled JSScript doesn't go in. JSScript.writeReplace() substitutes it by a CompiledScriptProxy on the way in, and CompiledScriptProxy.readResolve() substitutes it back to a compiled JSScript after it came out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, I'll do try-with-resource.

@bunkenburg bunkenburg requested a review from gbrail June 3, 2026 17:54
@gbrail

gbrail commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

It looks like we could use a rebase here, since the test262 directory seems to be at a different place in your branch versus the one that we're testing against.

@aardvark179

Copy link
Copy Markdown
Contributor

I think this is a laudable effort, but there are some things that we should think about if we want to actually support serialisation for continuations, and there are likely some other bugs which would also need to be fixed.

Since continuations only work under interpreted mode I'm not sure if it's worth the complexity of handling compiled functions, it might be wiser to simply throw an error if the script is compiled, but I think there is more we need to consider even among interpreted scripts.

If we round trip a script are we also round tripping the entire scope and context from which it was reachable? I.e. does it matter if cached template literals and symbols do not maintain their identity? I think our template literals may already be utterly broken because I think we're caching Scriptable objects across contexts, but going forward we may need to think about symbols and similar as we add support for classes and private fields (as private field identifiers are very similar to symbols).

In preparation for introducing a new interpreter we've also changed some things around. It might be best to add something to descriptor to return if it is simply serialisable or requires recompilation which would then ask that question of both the code and construct objects. Then compiled code can say it requires recompilation.

If we do want to support serialisation of compiled scripts should we not have similar functionality for functions? I don't know how easy it is to generate a compiled function which would not also cause its script to remain reachable, but I can believe it's possible.

Finally, you might also want to rebase your changes rather than a merge down as this tends to make PRs much easier to manage and understand. I did this for you at https://github.com/aardvark179/rhino/tree/aardvark179-jsscript-rebase, if that helps, and removed the (accidental?) update to test262 commit.

@aardvark179

aardvark179 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

After thinking about it more while on on my lunchtime walk I was going to post some more concerns around the order in which objects might be encountered during serialisation and what the effects of that might be, but I think these problem might be avoidable by moving where we perform the write replacement. If we were to move the problem to OptJSCode then I think we could greatly reduce how this could go wrong. Doing the write replacement there might require that those objects hold on to a little more meta data to be able to reconstitute themselves, but I think make for a firmer foundation. Specifically descriptors for child functions would be naturally preserved so serialising a scope which maintained references to those functions would work, and it would help shift all the questions on templates and symbols as we could move all the storage of those objects to the descriptors themselves.

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