Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement java.io.Serializable for some of the classes #373

Closed
wants to merge 8 commits into from

Conversation

dkhalanskyjb
Copy link
Collaborator

Implement java.io.Serializable for

  • Instant
  • LocalDate
  • LocalTime
  • LocalDateTime
  • UtcOffset

TimeZone is not Serializable because its behavior is system-dependent. We can make it java.io.Serializable later if there is demand.

We are using string representations instead of relying on Java's entities being java.io.Serializable so that we have more freedom to change our implementation later.

Fixes #143

@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from e803d80 to 75a14a6 Compare March 22, 2024 14:03
@dkhalanskyjb dkhalanskyjb requested a review from ilya-g March 25, 2024 11:05
@Moozart
Copy link

Moozart commented Apr 9, 2024

Can you please merge this commit if possible? @ilya-g

Copy link
Member

@ilya-g ilya-g left a comment

Choose a reason for hiding this comment

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

For better control over serialized representation I would suggest using writeReplace/readResolve with an Externalizable container.

@@ -111,6 +113,25 @@ public actual class Instant internal constructor(internal val value: jtInstant)

internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)

@JvmStatic
private val serialVersionUID: Long = 1L
Copy link
Member

Choose a reason for hiding this comment

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

We usually use const val for that.

@dkhalanskyjb
Copy link
Collaborator Author

I don't understand the idea. Do you mean that LocalTime should be Externalizable?

From the docs (https://docs.oracle.com/javase%2F7%2Fdocs%2Fapi%2F%2F/java/io/Externalizable.html):

When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor

But we don't have public no-arg constructors. I guess we could do something like class LocalTime { @PublishedApi internal constructor() }, which would make it a public constructor for the JVM, and trick Kotlin somehow to avoid initializing val value: java.time.LocalTime in that constructor, but is it worth it to make such intrusive changes just to support Java serialization?

@akhbulatov
Copy link

A very necessary implementation on Android to be able to pass LocalDate/LocalDateTime objects between screens

@dkhalanskyjb
Copy link
Collaborator Author

@akhbulatov, it's not "necessary", just convenient: see #143 (comment)

@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from 75a14a6 to 9fc52b6 Compare May 13, 2024 12:15
@dkhalanskyjb dkhalanskyjb requested a review from ilya-g May 13, 2024 12:15
@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from 9fc52b6 to 32c9cc0 Compare May 23, 2024 12:21
@wkornewald
Copy link
Contributor

Do you have an estimate when this will get merged? It's a pretty huge inconvenience for us to wrap Instant because we want to export an SDK and the wrapping shouldn't be exposed to our users. It seems to be impossible without writing huge amounts of boilerplate.

Copy link
Member

@ilya-g ilya-g left a comment

Choose a reason for hiding this comment

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

Take a look at how the list and set builders are serialized in stdlib. I believe we'd better have something similar here.
If you want, I can take over this branch and implement it accordingly.

}

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
Copy link
Member

Choose a reason for hiding this comment

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

I believe this would write all object fields first.

@dkhalanskyjb
Copy link
Collaborator Author

Do you mean https://github.com/JetBrains/kotlin/blob/3be0aa15f714726e4872c09b35f789652c46876d/libraries/stdlib/jvm/src/kotlin/collections/builders/ListBuilder.kt#L674? Ok, I think I got the idea, thanks. This does look better than reflection.

It's not important to me who does this.

import kotlinx.datetime.*
import java.io.*

internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, we can place it in kotlinx.datetime package and name it shortly, e.g. Ser, to shave off some bytes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean, shave off some bytes from the serialized representation? I'm okay with that, as long as it still stays in the internal/ directory.

Does it make sense to mark this with @PublishedApi to signal that it's an incompatible change to move/rename this class?

Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to mark this with @PublishedApi to signal that it's an incompatible change to move/rename this class?

Yes, I can do that

@ilya-g ilya-g force-pushed the add-java-serialization branch from ce1dc4a to 42fa2a5 Compare August 15, 2024 15:10
import kotlinx.datetime.*
import java.io.*

internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can these values be private?

Copy link
Member

Choose a reason for hiding this comment

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

They can, but does it makes a difference for an internal class? Or you mean in order not to generate getters/setters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, for the latter.

import kotlinx.datetime.*
import java.io.*

internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean, shave off some bytes from the serialized representation? I'm okay with that, as long as it still stays in the internal/ directory.

Does it make sense to mark this with @PublishedApi to signal that it's an incompatible change to move/rename this class?

@dkhalanskyjb
Copy link
Collaborator Author

I think it's good to go, but can't approve a pull request that I myself opened.

@dkhalanskyjb
Copy link
Collaborator Author

The reason I'm not clicking the "merge" button is that we are going to move kotlinx.datetime.Instant to kotlin.time.Instant (#382), so I don't think it makes sense to make it java.io.Serializable. @ilya-g, WDYT about removing java.io.Serializable for Instant from this PR and merging the implementation for the rest of the classes?

@wkornewald
Copy link
Contributor

Does this mean the stdlib will get a nice solution for adding Serializable support from within KMP code? I've contributed something to Ktor which could possibly serve as a starting point for a simple helper API in the stdlib: ktorio/ktor#4421

@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from d6238ee to e876cfd Compare March 4, 2025 10:20
@dkhalanskyjb dkhalanskyjb requested a review from ilya-g March 4, 2025 10:29
@dkhalanskyjb
Copy link
Collaborator Author

@ilya-g, FYI: only the last commit is new, the rest are only updated because I've had to rebase this PR.

@dkhalanskyjb dkhalanskyjb added this to the 0.7.0 milestone Mar 17, 2025
@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from 34da4d1 to 5df40c3 Compare March 17, 2025 13:12
@dkhalanskyjb
Copy link
Collaborator Author

Now there are two new commits.

Implement java.io.Serializable for
* Instant
* LocalDate
* LocalTime
* LocalDateTime
* UtcOffset

TimeZone is not `Serializable` because its behavior is
system-dependent. We can make it `java.io.Serializable` later
if there is demand.

We are using string representations instead of relying on Java's
entities being `java.io.Serializable` so that we have more freedom
to change our implementation later.

Fixes #143
@dkhalanskyjb dkhalanskyjb force-pushed the add-java-serialization branch from 5df40c3 to 1776d8c Compare March 20, 2025 11:02
dkhalanskyjb pushed a commit that referenced this pull request Mar 20, 2025
Implement java.io.Serializable for
* LocalDate
* LocalTime
* LocalDateTime
* UtcOffset

TimeZone is not `Serializable` because its behavior is
system-dependent. We can make it `java.io.Serializable` later
if there is demand.

Instant is not `Serializable` because it is about to be removed.

We are using string representations instead of relying on Java's
entities being `java.io.Serializable` so that we have more freedom
to change our implementation later.

Fixes #143
@dkhalanskyjb
Copy link
Collaborator Author

Merged manually in 98c3e53 to preserve authorship.

@dkhalanskyjb dkhalanskyjb deleted the add-java-serialization branch March 20, 2025 11:33
@@ -100,6 +102,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
@PublishedApi
@JvmName("toEpochDays")
internal fun toEpochDaysJvm(): Int = value.toEpochDay().clampToInt()

private fun writeReplace(): Any = Ser(Ser.DATE_TAG, this)
Copy link
Contributor

Choose a reason for hiding this comment

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

The java.time classes also have a readObject method that always throws an exception with a comment that this is about defending against malicious streams. I'm not very familiar with Java's serialization, so I don't know if this is needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I found this post about serialization via writeReplace/readResolve. This section is about the always throwing readObject method. Without it, a hand crafted byte stream could cause deserialization of e.g. LocalDate without going through Ser.readResolve. This could violate invariants of the classes. I think it could for example sneak in null to the non-nullable property LocalDate.value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! Yes, deserialize("aced00057372001a6b6f746c696e782e6461746574696d652e4c6f63616c44617465618443f17dae33e70200014c00097472756556616c75657400154c6a6176612f74696d652f4c6f63616c446174653b787070" .hexToByteArray(HexFormat { bytes.byteSeparator = "" })) does indeed create a LocalDate whose value is null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@lukellmann, would you like to make a PR that fixes (and tests!) this problem?

Copy link
Contributor

Choose a reason for hiding this comment

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

sure, but it will probably take a few days

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm on it but it will take a bit (I'm going through the spec).

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.

Implement java.io.Serializable on the JVM
6 participants