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

Code generation for callback support in Kotlin #629

Merged
merged 17 commits into from
Aug 20, 2024

Conversation

emarteca
Copy link
Contributor

@emarteca emarteca commented Aug 14, 2024

Code generation following the pattern described in this section of the design doc for callback support.

@emarteca emarteca force-pushed the callback_gen_kotlin branch from 077497f to 9b32ab3 Compare August 14, 2024 18:17
@emarteca emarteca marked this pull request as ready for review August 14, 2024 18:18
@emarteca
Copy link
Contributor Author

@Manishearth
Copy link
Contributor

I plan to review this soonish, but @jcrist1 should let me know what their expectations are for their own review: do you want us to wait on landing this until you've also reviewed, or okay with landing with just my review, or some other thing?

(keeping in mind i understand very little about Kotlin)

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 14, 2024

Skimming over it quickly it doesn't look like it will take long to review. I'd like to try and do a review tonight still. But if I don't get one in by tomorrow feel free to move forward with it.

@Manishearth
Copy link
Contributor

sounds good, seems like we have roughly the same timeline for reviewing this. No need to rush if you can't get to it, I mostly wanted to make sure we communicated review needs and timelines

Copy link
Contributor

@jcrist1 jcrist1 left a comment

Choose a reason for hiding this comment

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

First it looks like the code gen is broken but that has nothing to do with your PR. I'll look into it in a bit to see how much effort it would be to fix.

Otherwise it looks super cool! I manually cleaned up the generated kotlin code to get it compiling, and created a little test:

package dev.diplomattest.somelib

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class CallbackTest {
    @Test
    fun testCallback() {
        val cb: (Int ) -> Int = { i -> i + 2 }
        val calledBack = CallbackWrapper.testMultiArgCallback(
            DiplomatCallback_CallbackWrapper_test_multi_arg_callback_diplomatCallback_f.fromCallback(cb),
            10
        )
        assertEquals(22, calledBack)
    }
}

which passed. It would be nice to add it to the main test code so in principle it would be tested with cargo make test-kotlin-feature.

I think, however, it highlights how it is a bit unergonomic in its current form. It would be nice if the CallbackWrapper.testMultiArgCallback method could directly take the cb without having to wrap it. WDYT? I don't think this would be that difficult to change as you could just move the kotlin code gen around a little bit.

I'll try to have a more careful look at the generated jna code tomorrow, but would also be happy if we just test out a bunch of the generated callback consuming methods.

Copy link
Contributor

@jcrist1 jcrist1 left a comment

Choose a reason for hiding this comment

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

Actually had a couple of code comments that I thought I had saved, but hadn't. So quickly added them.

tool/templates/kotlin/Struct.kt.jinja Outdated Show resolved Hide resolved
tool/templates/kotlin/Struct.kt.jinja Outdated Show resolved Hide resolved
tool/templates/kotlin/Struct.kt.jinja Show resolved Hide resolved
tool/src/kotlin/mod.rs Outdated Show resolved Hide resolved
}
}
val cb_wrap = DiplomatCallback_CallbackWrapper_test_multiple_cb_args_diplomatCallback_g_Native()
cb_wrap.run_callback = callback;
Copy link
Contributor

Choose a reason for hiding this comment

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

question: where is the data/dtor getting set here?

Copy link
Contributor Author

@emarteca emarteca Aug 15, 2024

Choose a reason for hiding this comment

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

In the previous line it's calling the internal constructor on the Native callback wrapper, which has the default value for run_callback, and a 0 pointer for dataa. The destructor is also set to a 0 pointer -- it won't do anything on the Rust side, since I think we decided Kotlin will do all its memory management for the callback structs. The direct assignment to run_callback here is overwriting the default value for this field.

Copy link
Contributor

Choose a reason for hiding this comment

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

so wait, how does Rust tell Kotlin that it's no longer holding on to the callback?

I might be missing something in the design here.

Copy link
Contributor

Choose a reason for hiding this comment

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

So I wasn't paying that much attention to the other PR but the callback is expanded into

        #[no_mangle]
        extern "C" fn CallbackWrapper_test_multiple_cb_args(
            f: DiplomatCallback<i32>,
            g: DiplomatCallback<i32>,
        ) -> i32 {
            let f = move || unsafe {
                std::mem::transmute::<
                    unsafe extern "C" fn(*const c_void, ...) -> i32,
                    unsafe extern "C" fn(*const c_void) -> i32,
                >(f.run_callback)(f.data)
            };
            let g = move |arg0: i32| unsafe {
                std::mem::transmute::<
                    unsafe extern "C" fn(*const c_void, ...) -> i32,
                    unsafe extern "C" fn(*const c_void, i32) -> i32,
                >(g.run_callback)(g.data, arg0)
            };
            CallbackWrapper::test_multiple_cb_args(f, g)
        }

So the impl is only ever monomorphised into the closure g. As I understand it g.data and g.run_callback are copied into the closure and the closure is dropped after the call. But because they aren't owned pointers nothing is done to the backing data. And Kotlin will always wait until the method CallbackWrapper.testMultipleCbArgs is finished before it can GC the callback.

I expect there would be a problem if a struct can have a callback as a field, or if we could wrap a callback in an opaque but this at least fails to compile because of no lifetime bounds on the impl Fn:

    #[diplomat::opaque]
    struct BadThing(Box<dyn Fn(i32) -> i32>);
    impl CallbackWrapper {
        ...
        pub fn test_opaque_callback(f: impl Fn(i32) -> i32) -> Box<BadThing> {
            Box::new(BadThing(Box::new(f)))
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

But if we want callbacks as params or in opaques, then we need lifetime bounds afaics, and to add the callback struct to the edges of the the object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good points -- indeed (as discussed offline too) we need to have Rust store a GC root to the callback wrapper object so that the JVM doesn't get rid of it until it's dropped on the Rust side too. We'll do this in a subsequent PR, but basically the mechanism will be to have the data field store a boxed JNI GlobalRef that references the callback wrapper object. Then in the Drop, Rust will drop the GlobalRef.

@jcrist1 jcrist1 mentioned this pull request Aug 14, 2024
@emarteca
Copy link
Contributor Author

Thanks for the comments! I've pushed some fixes, the main one being refactoring so that a user can directly pass the callback in and the .fromCallback is called internally. I still need to fix the tests to reflect this change, and to add support for callbacks in methods on other custom types (enums, opaques).

@emarteca
Copy link
Contributor Author

emarteca commented Aug 15, 2024

@jcrist1 to run the tests, should it just be cargo make gen-kotlin-feature && cargo make test-kotlin-feature ? This is still failing for me. The first error is because the duckscript is trying to copy a dylib file which is not generated, and when I change dylib to so (since this is generated), the cargo make test-kotlin-feature fails with many Kotlin compilation errors.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 15, 2024

@jcrist1 to run the tests, should it just be cargo make gen-kotlin-feature && cargo make test-kotlin-feature ? This is still failing for me. The first error is because the duckscript is trying to copy a dylib file which is not generated, and when I change dylib to so (since this is generated), the cargo make test-kotlin-feature fails with many Kotlin compilation errors.

dylib to so makes sense. I think the cargo make test-kotlin-feature doesn't run on CI so that wouldn't have been caught, and I only wrote it thinking about my mac.

As for the compile error did you merge the latest main? I had to fix some of the kotlin code gen that fell out of sync in #631 If not, then give that a try. Regardless copy the output of the error, and I'll see if I can figure out why it's failing

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 15, 2024

@emarteca pinging, cause I neglected to ping in the actual message ☝️

@Manishearth
Copy link
Contributor

@jcrist1 yeah, we really should get that CI setup

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 15, 2024

@Manishearth I can look into adding a test-kotlin tomorrow to the ci step. Would need to install java and gradle, but it looks pretty straightforward. https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-gradle.

@emarteca
Copy link
Contributor Author

emarteca commented Aug 15, 2024

@jcrist1 thanks for taking a look! Here's the errors: this is after running cargo make gen-kotlin-feature && cargo make test-kotlin-feature: https://pastebin.com/EUEAegEu

I did merge the newest updates from main first! Also FWIW I am using linux, so maybe there is an OS difference causing these issues

@emarteca
Copy link
Contributor Author

I just pushed callback param support for opaques and enums -- the only thing left (I think) is to fix the tests!
I'm going to make a separate PR with the proper drop setup.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 16, 2024

@jcrist1 thanks for taking a look! Here's the errors: this is after running cargo make gen-kotlin-feature && cargo make test-kotlin-feature: https://pastebin.com/EUEAegEu

I did merge the newest updates from main first! Also FWIW I am using linux, so maybe there is an OS difference causing these issues

@emarteca can you paste the output of gradle --version? I'm expecting an incorrect java version. I tested on java 19.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 16, 2024

I've got a linux backed ci pipeline up here: #638

@emarteca
Copy link
Contributor Author

@jcrist1 thanks for taking a look! Here's the errors: this is after running cargo make gen-kotlin-feature && cargo make test-kotlin-feature: https://pastebin.com/EUEAegEu
I did merge the newest updates from main first! Also FWIW I am using linux, so maybe there is an OS difference causing these issues

@emarteca can you paste the output of gradle --version? I'm expecting an incorrect java version. I tested on java 19.

Thanks! Yes, you're right -- I'm using Java 17, and gradle 8.3. The output of gradle --version is below:

------------------------------------------------------------
Gradle 8.3
------------------------------------------------------------

Build time:   2023-08-17 07:06:47 UTC
Revision:     8afbf24b469158b714b36e84c6f4d4976c86fcd5

Kotlin:       1.9.0
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.10 (Debian 17.0.10+7-Debian-1)
OS:           Linux 6.7.12-1rodete1-amd64 amd64

I'll get back to you on whether we need to get this working on Java 17 or if we can upgrade.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 19, 2024

@jcrist1 thanks for taking a look! Here's the errors: this is after running cargo make gen-kotlin-feature && cargo make test-kotlin-feature: https://pastebin.com/EUEAegEu
I did merge the newest updates from main first! Also FWIW I am using linux, so maybe there is an OS difference causing these issues

@emarteca can you paste the output of gradle --version? I'm expecting an incorrect java version. I tested on java 19.

Thanks! Yes, you're right -- I'm using Java 17, and gradle 8.3. The output of gradle --version is below:

------------------------------------------------------------
Gradle 8.3
------------------------------------------------------------

Build time:   2023-08-17 07:06:47 UTC
Revision:     8afbf24b469158b714b36e84c6f4d4976c86fcd5

Kotlin:       1.9.0
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.10 (Debian 17.0.10+7-Debian-1)
OS:           Linux 6.7.12-1rodete1-amd64 amd64

I'll get back to you on whether we need to get this working on Java 17 or if we can upgrade.

I managed to test with Java 17 in IntelliJ, and there were a couple of panama imports that I apparently have in one one test, that broke the compile. But it managed to compile once I deleted the imports. I can have a look at downgrading the supported java version.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 19, 2024

and once again cause I never remember to ping: @emarteca ☝️

@emarteca
Copy link
Contributor Author

@jcrist1 thanks!! It turns out I do need to keep this compatible with Java 17. Does the test work in newer Java without the imports?
I think ideally, we'd be able to support various versions of Java; but if we're going to pick one it probably makes sense to stick to the newer version. If you give me the hack to fix the build for old Java then I can test it manually, and just make sure the callback tests work on your end with the newer Java?

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 19, 2024

@emarteca there should be no issue with JVM 19 support if this works with JVM 17. It was just some accidental code that was I think I accidentally added when I started working on #499.

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 19, 2024

I pushed some changes to #638 that should make it compatible with JVM 17

Copy link
Contributor

@Manishearth Manishearth left a comment

Choose a reason for hiding this comment

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

overall lgtm, please let me know when this is in a mergeable state

I believe dtors still need to be fixed but that is going to be done later

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 19, 2024

@emarteca the kotlin ci changes that should support jvm 17 were added. Can you try merging main and see if it works?

@emarteca
Copy link
Contributor Author

Thanks @jcrist1 , it does compile now!! 🙏 I have to fix a bug that cropped up (the generated callback code is showing up in all of the Kotlin files, not just the file for the struct that includes the callbacks), and then I'll ping you!

@emarteca emarteca force-pushed the callback_gen_kotlin branch from fe7bb46 to 6cc037b Compare August 19, 2024 22:47
Comment on lines +23 to +24
val cb: (CallbackTestingStruct) -> Int = { s -> s.x + s.y}
val calledBack = CallbackWrapper.testCbWithStruct(cb)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jcrist1 this test is broken with the error java.lang.IllegalArgumentException: Callback argument class dev.diplomattest.somelib.CallbackTestingStruct requires custom type conversion -- do you know how to fix this? Is this a bug, or is this a Kotlin feature?

@emarteca
Copy link
Contributor Author

emarteca commented Aug 19, 2024

@jcrist1 my code gen is working again, and I made a feature_test test for it! The only thing is that the test for the callback that takes a custom struct is broken -- could you maybe take a look, I am not well versed in Kotlin! (this comment points to the broken test: linked)

@jcrist1
Copy link
Contributor

jcrist1 commented Aug 20, 2024

I am not well versed in Kotlin!

😂 sure I can try, but it's actually the same story for me (my background is scala).
image
Will have a look tonight CEST.

@emarteca
Copy link
Contributor Author

emarteca commented Aug 20, 2024

I am not well versed in Kotlin!

😂 sure I can try, but it's actually the same story for me (my background is scala).

ahah i had thought you were an expert! But no worries, I'll look at it again today too :-)

@Manishearth
Copy link
Contributor

For passing the failing tests, rerun tests in that crate and then run cargo insta accept (you'll need to install insta)

You may need to do this multiple times

@Manishearth
Copy link
Contributor

(I forget if you'd figured out how to do this already, but in case this is the first time you're seeing this, worth explaining)

@Manishearth
Copy link
Contributor

I think this is more or less ready to land.

@emarteca
Copy link
Contributor Author

For passing the failing tests, rerun tests in that crate and then run cargo insta accept (you'll need to install insta)

You may need to do this multiple times

oh thanks, I didn't know about this

@emarteca
Copy link
Contributor Author

I think this is more or less ready to land.

sweet! that sounds good -- i think there might be an issue with the struct argument callbacks (that's the type conversion message i was debugging with @jcrist1 ), but I can also comment out that test for now and figure that out in a subsequent PR. The other types of arguments work.

@emarteca
Copy link
Contributor Author

Ok I figured out the issue with struct arguments -- just going to push a fix, and then we should be good to go with this PR

@Manishearth
Copy link
Contributor

Still needs a regen

Copy link
Contributor

@jcrist1 jcrist1 left a comment

Choose a reason for hiding this comment

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

@emarteca I assume your comment

Ok I figured out the issue with struct arguments -- just going to push a fix, and then we should be good to go with this PR

means you don't need any help debugging and all the tests are passing so I'm approving

means that no more help is needed

@Manishearth Manishearth merged commit 6290ef6 into rust-diplomat:main Aug 20, 2024
16 checks passed
@Manishearth
Copy link
Contributor

Merged. The dtors / GlobalRef stuff are still a todo

@Manishearth
Copy link
Contributor

Followup: #648

@emarteca
Copy link
Contributor Author

Thank you!! 🙏

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.

3 participants