-
Notifications
You must be signed in to change notification settings - Fork 20
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
Turn JNINativeInterface + JNIInvokeInterface_ into unions #28
Conversation
hi @nikomatsakis - considering your current work on Duchess I'd be interested to hear your feedback on this |
@argv-minus-one and @jrose-signal it would also be good to get your feedback here too |
So from a compiler perspective this isn't 100% correct either: a union has the size of its largest member, but on a system that only has JRE 1.2 the JNI struct will be much smaller than the full set of callbacks. What you really want here is a union of pointers, not a pointer to a union, but that's certainly harder to work with. In terms of improving correctness, well, the compiler can't enforce that you've actually used the right version here, so it's mostly about making things easier for humans. Which isn't bad, but my personal preference would be to do something better:
struct JNINativeInterface<Table>(*const Table);
impl<Table> Deref for JNINativeInterface<Table> {
type target = Table;
// …
}
// Unsafe because implementing this trait means pointers will be cast to this type and unilaterally dereferenced under certain conditions.
unsafe trait JNIGetVersion {
const VERSION: u32;
fn get_version(&self);
}
impl<Table> JNINativeInterface<Table>
where Table: JNIGetVersion {
#[inline]
fn downcast<NewTable: JNIGetVersion>(&self) -> Option<JNINativeInterface<NewTable>> {
if Table::VERSION >= NewTable::VERSION || self.get_version() >= NewTable::VERSION {
Some(JNINativeInterface(self.0.cast()))
} else {
None
}
}
}
fn do_something(interface: &JNINativeInterface<JNINativeInterface_1_1>) {
if let Some(newer_interface) = interface.downcast::<JNINativeInterface_1_2>() {
// do something with newer_interface
} else {
// fallback
}
} Again, the downside here is potentially calling GetVersion a lot. |
Note: since, we're talking about the We could potentially generate a wrapper API with runtime version checking but for this crate we'd still want to also expose the un-abstracted lower-level vtable for use by higher-level layers that might come up with their own way of dealing with version checks more efficiently. (It's quite likely that a higher level abstraction wouldn't need to repeat versions checks for every call so we can't impose that at this level)
yep, we still want a static Rust type that is ABI compatible with This is baked into the design of JNI and it really pretty horrific but I think the scope of this So I think for now we should be assuming higher-level layers like the My more-constrained aim with this change is to iterate the raw The first issue is that each function in the struct is currently wrapped in an Secondly I think we're missing an opportunity to divide the functions up by version within a union in a way that remains ABI compatible but at least requires developers to have to type out their intent to access a specific JNI version. Even though the |
Yes, it's a fairly common C idiom for an extensible "vtable" of sorts. I think having a pointer-to-union rather than union-of-pointers or pointer-to-minimal-struct will make it more difficult to do "safe" wrappers like what I showed above, because Rust doesn't provide good tools for going from |
yep
Since this macro gives us these additional structs like Code can also practically bypass the union and jump straight to the largest version as a way of getting back to the previous monolithic vtable if they maybe find that more convenient in some case (e.g. for defining certain macros perhaps). I think the union type would still be useful to have as a general ABI-compatible binding for |
Although github linked it above I just wanted to also highlight here that I have done a port of the The namespacing of the functions by version helped highlight several places in the |
@rib: Seems like a good idea to me. I don't have anything to add. Edit: Well, I used to have nothing to add. 😅 |
I noticed that this doesn't build in Rust 1.69, evidently because raw function pointers didn't implement
So, this PR will bump the jni-sys MSRV. I have no objection to that (I just forgot to |
Or the Debug implementation could be manually written. After all, it's not like it'll be a good debug experience to see a pile of function addresses. |
Ah, okey. I suppose it'd be nice to be reasonably conservative with the MSRV for a I haven't really looked at the ergonomics for the automatically derived Debug trait here, but yeah maybe we can generate something with the procmacro that's slightly better tailored. If you were intentionally using the Debug trait with these function tables though I guess you might want those function addresses though which are gonna be pretty verbose. Could maybe elide addresses unless pretty printing with Or could just have a stub Debug implementation so we don't block other composite types that embed these types from deriving the Debug trait - and figure that it's anyway not particularly practical to print out a full JNI function table. We should be ok to expand that later without affecting the semver, but would be a lot simpler to start with. |
e1b2090
to
88cc4ae
Compare
5fb8742
to
8139e9f
Compare
Although the project doesn't currently have an MSRV policy, this picks a conservative (10 month old) minimum rust-version that will now be tested via CI.
88cc4ae
to
70a0081
Compare
For reference here, I realized in the end that the Even when later looking at enabling warnings for Since this PR did unintentionally, temporarily require Rust 1.70 to build, it now includes a patch that sets the rust-version to 1.65.0 (10 months old) and tests this via CI. I also renamed the proc macro crate to |
This implements a `#[jni_to_union]` procmacro that lets us declare what version of the JNI spec each function was added and then declare a union that only exposes functions for the specific version being referenced. So instead of a struct like: ```rust struct JNIInvokeInterface_ { pub reserved0: *mut c_void, .. pub GetVersion: unsafe extern "system" fn(env: *mut JNIEnv) -> jint, .. pub NewLocalRef: unsafe extern "system" fn(env: *mut JNIEnv, ref_: jobject) -> jobject, } ``` we have a union like: ``` union JNIInvokeInterface_ { v1_1: JNIInvokeInterface__1_1, v1_2: JNIInvokeInterface__1_2, reserved: JNIInvokeInterface__reserved, } ``` And would access `GetVersion` like: `env.v1_1.GetVersion` and access `NewLocalRef` like: `env.v1_2.NewLocalRef`. Each version struct includes all functions for that version and lower, so it's also possible to access GetVersion like `env.v1_2.GetVersion`. This way it's more explicit when you're accessing functions that aren't part of JNI 1.1 which require you to have checked the version of JNI the JVM supports.
70a0081
to
98565d6
Compare
This implements the idea discussed here: #25
In particular one concrete motivation for this comes from having seen that the
jni
crate currently incorrectly accesses JNI 1.2+ functions without checking that the JNI version is >= 1.2 (e.g. jni-rs/jni-rs#463) and I think that kind of mistake would be much less likely if you had explicitly write out the version of the API that you are dereferencing.This implements a
#[jni_to_union]
procmacro that lets us declare what version of the JNI spec each function was added (or mark them as "reserved") and the macro will replace the struct with a union that only exposes functions for the specific version being referenced.So instead of a struct like:
we have a union like:
And would access
GetVersion
like:env.v1_1.GetVersion
and accessNewLocalRef
like:env.v1_2.NewLocalRef
.Each version struct includes all functions for that version and lower, so it's also possible to access GetVersion like
env.v1_2.GetVersion
.This way it's more explicit when you're accessing functions that aren't part of JNI 1.1 which require you to have checked the version of JNI the JVM supports.