jni-rs / jni-sys

Apache License 2.0
55 stars 20 forks source link

Alternative implementation #7

Closed docbrown closed 7 years ago

docbrown commented 7 years ago

I was toying with my own bindings to jni.h for an Android project, but I'm probably not going to finish it. Maybe it will be useful to you. Everything related to the exported JNI_* functions is missing, because Android doesn't provide them. But they shouldn't be any different than what's in this crate already.

https://gist.github.com/docbrown/2b3ca251cc706975750e9d69a2724328

The main differences are:

  1. The binding follows the C++ API more than the C API, just because it's safer type-wise. In the current jni_sys crate, reference types are just aliases of jobject. This means you can accidentally pass a jstring to a function expecting a jarray, for example, and the compiler won't complain. In my implementation, each reference type gets its own distinct underlying type and conversions must be done explicitly. There's probably a way to get some of the types to coerce to their base types (jarray to jobject, for example) without a cast, as in C++, but I didn't get that far.

  2. #[repr(C)] enum is not exactly C-compatible and it can lead to undefined behavior. So, jobjectRefType is just a C-compatible type alias and its values are module-level consts. This is what the winapi crate does and it more closely matches the original API anyway (you don't have to qualify each value with jobjectRefType::).

  3. A macro is used to generate JNIEnv and JavaVM. It also generates wrapper methods like those in the C++ API. It skips generating wrapper methods for variadic functions, although they are still defined in the v-table. I left out the *V variants because the definition of va_list varies by platform and I didn't think it was worth implementing. Unfortunately, the macro requires quite a bit of recursion and is rather slow. I'm sure there are ways to make it more efficient, though.

  4. The function pointers aren't wrapped in Option<T>. Since this is a -sys crate, favoring zero-overhead versus safety is better, in my opinion. A higher-level API can always check each pointer if it wishes, but otherwise it should be assumed that anyone using a -sys crate already knows what they're doing and won't, for example, call GetObjectRefType in a JNI 1.2 environment.

sfackler commented 7 years ago

There's probably a way to get some of the types to coerce to their base types (jarray to jobject, for example) without a cast, as in C++, but I didn't get that far.

I don't know of any way to model C++-style type hierarchies in Rust in a manner that will allow coercion.

So, jobjectRefType is just a C-compatible type alias and its values are module-level consts.

Enum representation in C is implementation defined. How is the size of the type checked in all targets? This is much easier in winapi since there's only one ABI to deal with.

A higher-level API can always check each pointer if it wishes,

There is no way to do that. Function pointers are non-null by definition and any check against null can and will be constant folded.

sfackler commented 7 years ago

Hey, I think my previous comment was dismissive and kind of mean, sorry 😞.

The general philosophy around -sys crates (at least in my opinion) is that they are an as direct as possible transcription of a C interface into Rust. There's always some amount of impedance mismatch between languages, and C is something of a lowest common denominator. Even here, though, you run into troubles with things like C enums.

The C++ JNI interface is nicer to work with than the C one, but the right approach IMO is to avoid using the raw interface entirely in favor of a Rustic wrapper library. The jni crate is one example. There is a lot to deal with when talking to a C library, and it's good to only have to worry about all of that once. JNI is particularly weird here with respect to a the lifetimes of the objects since they're garbage collected and weird encoding of strings, for example (UTF8 doesn't mean UTF8 in Java-land, but actually a weird variant of CESU8).

Function pointers are another weird case. Having to wrap them in options is kind of awful in my opinion (I tried to get that changed before 1.0!), but it's the only way to deal with nullability. In cases where you know that the pointers are never null you can go without it, but some of the pointers will be null (or at least left uninitialized) when you're talking to an older version of Java. You can always look at what version Java reports to be and know what functions to avoid, but having the nullability be explicit is another level of safety.

docbrown commented 7 years ago

I see what you're saying about enums and nullable function pointers. I guess I'm not familiar with all of the nuances of Rust->C FFI yet, so thanks for the explanations.