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

Use synthetic functions of kotlin jvm inline class #4066

Closed
wants to merge 5 commits into from

Conversation

wplong11
Copy link

@wplong11 wplong11 commented Aug 3, 2023

Background

To support kotlin value class,
Synthetic Kotlin constructors should be available

revert: #1005

@pjfanning
Copy link
Member

pjfanning commented Aug 3, 2023

That code has been in jackson for years. Many users have used it knowingly or unknowingly. We can't just revert it because 1 user doesn't like it. No use cases have been provided. No meaningful description has been provided.

@wplong11 wplong11 marked this pull request as draft August 3, 2023 15:51
@wplong11
Copy link
Author

wplong11 commented Aug 3, 2023

@pjfanning Below is the use case. Before discussing the detailed design, I created a PoC.
FasterXML/jackson-module-kotlin#693

@wplong11
Copy link
Author

wplong11 commented Aug 3, 2023

@pjfanning

Since it's a Kotlin-related problem, I wonder if it's correct to support it in a Kotlin module...

Many users have used it knowingly or unknowingly

But what you said worries me too. By the way, can you reproduce the original issue? Deleting it now doesn't seem to be a problem. Can you help me reproduce the problem?

@pjfanning
Copy link
Member

It would be better to get consensus among the jackson-module-kotlin maintainers (eg @k163377) before looking for a jackson-databind change.

It seems like this 1005 change is affecting FasterXML/jackson-module-kotlin#689 too.

@k163377
Copy link
Contributor

k163377 commented Aug 3, 2023

First, you can get a callable constructor without making this change.
Synthetic constructors have a pair of private, non-synthetic constructors that reside in a scope that can be parsed by Jackson.
The code is quite old, but the following may be helpful.
FasterXML/jackson-module-kotlin@2.16...k163377:jackson-module-kotlin:github-413/get_constructor_backup
FasterXML/jackson-module-kotlin#199 (comment)

This approach has the problem that annotations given to the constructor do not work, but Kogera has found a temporary solution.
https://github.com/ProjectMapK/jackson-module-kogera/blob/0ffefda68d5caa17b149d488e4e0054909e84319/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinFeature.kt#L76

As long as there is a workaround, it does not seem so reasonable to change the treatment of composite constructors.


However, I too would like to target composite constructors for processing.

For example, what about adding a process to AnnotationIntrospector that would extract the synthetic constructors that I want to process from the class?
I am not familiar with the databind implementation, but I suspect such a feature would minimize the impact.

If this is also required in modules or libraries other than Kotlin, I think it would be worth considering implementation.

@kkurczewski
Copy link

Hi

As my PR was mentioned I will allow myself highlight problem which I have as well, assume I have class like this:

data class Something(val x: String, val y: String)

And I want to run code like this (it mimics Jackson ctor lookup):

Something::class.java.constructors.filter { !it.isSynthetic }.onEach { println("#1: $it") }.first()

And this will work. However, when you add an inline class to POJO it isn't gonna work:

data class SomethingWithInlineClass(val x: String, val y: kotlin.time.Duration)

SomethingWithInlineClass::class.java.constructors.filter { !it.isSynthetic }.onEach { println("2: $it") }.first()

Result is:

Exception in thread "main" java.util.NoSuchElementException: List is empty.
	at kotlin.collections.CollectionsKt___CollectionsKt.first(_Collections.kt:212)
	at com.fasterxml.jackson.module.kotlin.test.MainKt.main(main.kt:10)
	at com.fasterxml.jackson.module.kotlin.test.MainKt.main(main.kt)

In case of Jackson we get a more verbose message about missing creators. Sadly, there is no way to configure that behavior, there is no class equivalent like NopAnnotationIntrospector, StdValueInstantiator etc. that allow to customize that (there is a lot of final classes in chain) hence I don't have a place where I could use trick @k163377 propose.

@cowtowncoder
Copy link
Member

Ok, I am worried too, although all unit tests do pass. But then again we don't have tests for auto-generated classes frameworks produce etc.
There is also no test showing this works (but I assume Kotlin module would have something).

I do think that improved handling should be added (but not via AnnotationIntrospector): ideally as part of full rewrite of property introspection. I still plan to tackle that before 2.16.0.
This probably wouldn't be added as part of rewrite BUT it:

  1. could/should be tackled immediately after rewritten code works and
  2. possibly should not be made before rewrite (as it might complicate)

So I won't be merging this before going on vacation, but it may be left open.

@cowtowncoder
Copy link
Member

One more thing: allowing per-class inclusion of synthetic methods would be something I'd consider, but the question is how to access this without needing to sub-class is or have global configuration.
If code in question could access AnnotationIntrospector, that might be an option (although not necessarily great one). I may be forgetting some obvious choice, but pluggable "constructor classifier" could do.

The key, at any rate, would be to limit scope of changed visibility to Kotlin (-generated) classes; allow Kotlin module indicate changed rules.
If so, then other modules (often Scala module benefits from additions like this; but also some frameworks?) would benefit from this too, and risk would be limited.

@pjfanning
Copy link
Member

@cowtowncoder I doubt whether this affects Scala module.

@wplong11
Copy link
Author

wplong11 commented Aug 3, 2023

How about this approach? #4067
Ideally, all the logic for Kotlin support would be improved to be in the Kotlin module, but I wanted to start small first.

@wplong11
Copy link
Author

wplong11 commented Aug 3, 2023

I will also check #4066 (comment)

@wplong11
Copy link
Author

wplong11 commented Aug 3, 2023

@k163377

#4066 (comment) For example, what about adding a process to AnnotationIntrospector that would extract the synthetic constructors that I want to process from the class?
I am not familiar with the databind implementation, but I suspect such a feature would minimize the impact.

You mean you want to be able to set the constructor extraction strategy using Module interface (SimpleModule / KotlinModule)?

#4066 (comment) all the logic for Kotlin support would be improved to be in the Kotlin module

If so, this is also possible and is a satisfactory direction.

@k163377
Copy link
Contributor

k163377 commented Aug 3, 2023

@kkurczewski

Something::class.java.constructors.filter { !it.isSynthetic }.onEach { println("#1: $it") }.first()

I don't think I'm reading your argument well, but does not using getDeclaredConstructors have anything to do with the issue?
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Class.html#getDeclaredConstructors--


@wplong11

How about this approach? #4067

Could you please check the Kogera implementation and test cases first?
Since Kogera provides deserialization of value class, I would be happy if you could make a suggestion based on that.

At the very least, the approach you suggested is not sufficient, since the instantiation of the value class requires calling box-impl after calling constructor-impl.

You mean you want to be able to set the constructor extraction strategy using Module interface (SimpleModule / KotlinModule)?

Yes, it should be customizable from Module.

@kkurczewski
Copy link

@k163377

Something::class.java.constructors.filter { !it.isSynthetic }.onEach { println("#1: $it") }.first()

I don't think I'm reading your argument well, but does not using getDeclaredConstructors have anything to do with the issue? https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Class.html#getDeclaredConstructors--

Yes, my mistake - Jackson uses declared constructors. I had too many attempts and my memory starts to plays tricks on me. But I'm pretty sure my current impl still has problem with missing creator, but will take a second look on my PR and will try to use this method.

@wplong11 wplong11 force-pushed the revert-1005 branch 2 times, most recently from 887e79c to a39e938 Compare August 4, 2023 13:24
@wplong11 wplong11 changed the title Support Synthetic Kotlin constructors Use kotlin jvm inline class synthetic functions Aug 4, 2023
@wplong11 wplong11 changed the title Use kotlin jvm inline class synthetic functions Use synthetic functions of kotlin jvm inline class Aug 4, 2023
@wplong11
Copy link
Author

wplong11 commented Aug 4, 2023

since the instantiation of the value class requires calling box-impl after calling constructor-impl.

@k163377 Sorry, I didn't understand.
The Value Class seems to be initialized correctly even if I called box-impl without calling constructor-impl.(FasterXML/jackson-module-kotlin#694 - I added more test cases)
Is there any black magic I don't know about?

@k163377
Copy link
Contributor

k163377 commented Aug 4, 2023

The value class may have a check process in its constructor.
If only box-impl is called, the check is actually skipped.
https://github.com/ProjectMapK/jackson-module-kogera/blob/81c6497eaa5609ede547567bc8ebdc961399e77d/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/_integration/deser/value_class/NoSpecifiedTest.kt#L43-L48

@cowtowncoder
Copy link
Member

Ok, this looks doable -- but I'll need to think about it a bit; I hope to be able to give better feedback (or approve) soon but have a big backlog to go through after vacation.

@JooHyukKim
Copy link
Member

JooHyukKim commented Sep 2, 2023

For example, what about adding a process to AnnotationIntrospector that would extract the synthetic constructors that I want to process from the class?
I am not familiar with the databind implementation, but I suspect such a feature would minimize the impact.

+1 on what @k163377 suggests, adding "extensible point on for modules that can override property discovery behavior". But few things to consider...

  • Method : isIncludableConstructor method is private static --is current change, so we need higher level method for extension. Check comment
  • Class : AnnotationIntrospector name itself already suggests it's not for property-discovery

@@ -406,6 +411,6 @@ private final AnnotationMap collectAnnotations(AnnotatedElement main, AnnotatedE

// for [databind#1005]: do not use or expose synthetic constructors
private static boolean isIncludableConstructor(Constructor<?> c) {
Copy link
Member

Choose a reason for hiding this comment

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

/cc

Copy link
Member

Choose a reason for hiding this comment

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

I don't think current AnnotatedXxxCollectors are optimal extension points, so suggested changes here are probably best we can do on short term. Before bigger rewrite,

Copy link
Member

Choose a reason for hiding this comment

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

so suggested changes here are probably best we can do on short term. Before bigger rewrite,

Agreed 👍🏻

cowtowncoder added a commit that referenced this pull request Sep 2, 2023
@cowtowncoder
Copy link
Member

Ok, I'd be happy to merge this, if we can change this to non-draft PR?

@k163377
Copy link
Contributor

k163377 commented Sep 2, 2023

Personally, I am against merging this PR.

It contains at least some undesirable content from a Kotlin perspective.
It also looks like this could affect many projects that use Kotlin.
I don't think we should make changes with such a large impact when workarounds exist.

Comment on lines +18 to +22
public static boolean isJvmInlineClassSyntheticBoxingFunction(Method method) {
return Modifier.isStatic(method.getModifiers())
&& method.isSynthetic()
&& method.getName().equals("box-impl");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As commented here, using only box-impl to deserialize value class may cause unexpected bugs.
#4066 (comment)

Therefore, this change should be removed.

Comment on lines +8 to +16
public static boolean isJvmInlineClassSyntheticConstructor(Constructor<?> ctor) {
Class<?>[] params = ctor.getParameterTypes();
if (params.length == 0) {
return false;
}

Class<?> lastParam = params[params.length - 1];
return ctor.isSynthetic() && lastParam.getName().equals("kotlin.jvm.internal.DefaultConstructorMarker");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This code also covers constructors that do not actually contain value class as an argument.

The following is a class definition in Kotlin and its constructors taken from the decompiled result.
You can see that the value class is not used, but the argument of the synthetic constructor ends with DefaultConstructorMarker.

data class Data(val v: Int = 0)
// decompiled

   public Data(int v) {
      this.v = v;
   }

   // $FF: synthetic method
   public Data(int var1, int var2, DefaultConstructorMarker var3) {
      if ((var2 & 1) != 0) {
         var1 = 0;
      }

      this(var1);
   }

I can't think of any other way to write it myself, but I think it should at least be supplemented with a comment.

@k163377
Copy link
Contributor

k163377 commented Sep 2, 2023

I thought about it again, but it seems that simply including the synthetic constructor in the processing target does not solve the problem, since the deserialization of the value class requires two functions to be called in the correct order.
It seems to me that we need a function to make externally defined functions and processes available as Creator.

@cowtowncoder
Copy link
Member

cowtowncoder commented Sep 3, 2023

@k163377 I trust your judgment here -- I was under (wrong) impression that this alone would help solve the issue by not pruning certain synthetic constructors/factory methods. But if this alone won't help, yeah, no point in doing that.

That said, I am perfectly willing to add a little bit of code (like here) where necessary to support extensions such as Kotlin or Scala module, and have appropriate scope not to change behavior of types on "other" platforms.
But it sounds, unfortunately, that the code here isn't quite discriminating enough?

Ideally we would have something workable here -- but I need help to known when/if this is the case.

@cowtowncoder
Copy link
Member

Looks like this PR isn't going anywhere and is out-of-date. Will close.

@JooHyukKim
Copy link
Member

JooHyukKim commented Nov 30, 2023

Looks like this PR isn't going anywhere and is out-of-date. Will close.

WRT out-dated, stale issues, there is automated labeler as Github workflow for stale-ness we might be interested in?
I recently been thinking we need one cause you manage loads of PRs and issues 🥲

@cowtowncoder
Copy link
Member

@JooHyukKim thank you for suggestion!

My immediate reaction is that I am -1 for auto-close-issues Bots. Projects that use them seem to have very aggressive garbage collection, leading to (IMHO) hostile experience for submitters. So while I have sympathy for maintainers, personally I much prefer actual human beings closing issues over automated processes.

But this is only for Bots that actually close issues -- I would not be against auto-labeller?
As long as that has long enough period before adding label(s).

@JooHyukKim
Copy link
Member

Np, @cowtowncoder 👍🏼 I agree, closing would be too agressive. And yes it will be just labeler. Okay, will get into it.

@cowtowncoder
Copy link
Member

Sounds good -- and we could/should probably only enable it (if we do) for some of highest-traffic repos.

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.

6 participants