-
Notifications
You must be signed in to change notification settings - Fork 52
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
Code generation for callback support in Kotlin #629
Conversation
077497f
to
9b32ab3
Compare
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) |
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. |
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 |
There was a problem hiding this 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.
There was a problem hiding this 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/src/kotlin/snapshots/diplomat_tool__kotlin__test__struct.snap
Outdated
Show resolved
Hide resolved
feature_tests/kotlin/somelib/src/main/kotlin/dev/diplomattest/somelib/CallbackWrapper.kt
Outdated
Show resolved
Hide resolved
feature_tests/kotlin/somelib/src/main/kotlin/dev/diplomattest/somelib/CallbackWrapper.kt
Outdated
Show resolved
Hide resolved
feature_tests/kotlin/somelib/src/main/kotlin/dev/diplomattest/somelib/CallbackWrapper.kt
Outdated
Show resolved
Hide resolved
} | ||
} | ||
val cb_wrap = DiplomatCallback_CallbackWrapper_test_multiple_cb_args_diplomatCallback_g_Native() | ||
cb_wrap.run_callback = callback; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)))
}
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
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 |
@jcrist1 to run the tests, should it just be |
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 |
@emarteca pinging, cause I neglected to ping in the actual message ☝️ |
@jcrist1 yeah, we really should get that CI setup |
@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. |
@jcrist1 thanks for taking a look! Here's the errors: this is after running I did merge the newest updates from |
I just pushed callback param support for opaques and enums -- the only thing left (I think) is to fix the tests! |
@emarteca can you paste the output of |
I've got a linux backed ci pipeline up here: #638 |
Thanks! Yes, you're right -- I'm using Java 17, and gradle 8.3. The output of
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. |
and once again cause I never remember to ping: @emarteca ☝️ |
@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 pushed some changes to #638 that should make it compatible with JVM 17 |
There was a problem hiding this 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
@emarteca the kotlin ci changes that should support jvm 17 were added. Can you try merging main and see if it works? |
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! |
fe7bb46
to
6cc037b
Compare
val cb: (CallbackTestingStruct) -> Int = { s -> s.x + s.y} | ||
val calledBack = CallbackWrapper.testCbWithStruct(cb) |
There was a problem hiding this comment.
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?
😂 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 :-) |
For passing the failing tests, rerun tests in that crate and then run You may need to do this multiple times |
(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) |
I think this is more or less ready to land. |
oh thanks, I didn't know about this |
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. |
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 |
Still needs a regen |
There was a problem hiding this 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
Merged. The dtors / GlobalRef stuff are still a todo |
Followup: #648 |
Thank you!! 🙏 |
Code generation following the pattern described in this section of the design doc for callback support.