uniffi-rs
uniffi-rs copied to clipboard
Support passing platform-specific native object
I'm using UniFFI with the third-party Kotlin Multiplatform Binding generation on a Compose Multiplatform app. Since I'm accessing native APIs on the Rust side as well, I encounter cases where I want to pass native objects from the Kotlin side to the Rust side. For example, I may want to pass an Android Activity
object to the Rust side using JNI. I also may want to pass UIViewController*
on iOS.
However, since UniFFI passes all the values via serialization, there is no consistent way to pass such objects to the Rust side. I suggest adding a built-in pobject
type that represents such types. For example, consider the following UDL file and the Rust code:
namespace foo {
pobject multiply_by_two(pobject arg);
};
use uniffi::PlatformObject;
#[uniffi::export]
fn multiply_by_two(arg: PlatformObject) -> PlatformObject {
/* arg.object() is *mut c_void here */
if arg.is_objc() {
let arg = arg.object() as *mut objc::Object;
let NSInteger = objc::class!(NSINteger);
...
PlatformObject::new_objc(result)
} else if arg.is_java() {
let arg = arg.object() as jni_sys::object;
env.find_class("java/lang/Integer");
...
PlatformObject::new_java(result)
} else if arg.is_python() {
let arg = arg.object() as *mut pyo3::ffi::PyObject;
pyo3::ffi::PyLong_Type
...
PlatformObject::new_python(result)
} else {
panic!("Unsupported platform {}!", arg.platform());
}
}
The PlatformObject
type is a pair of a value representing the platform type of object and a *mut c_void
(or NonNull<c_void>
) representing the object. PlatformObject
is represented by the built-in pobject
type in UDL. In the bindings, this will be exposed as id
in Objective-C, kotlin.Any
in Kotlin, and typing.Any
in Python. I suggest the platform type value be a UUID encoded as a 128-bit integer (like COM interfaces).
When passing the object to the Rust side, the generated bindings will serialize the platform value and a pointer to the object, and when retrieving the object from the Rust side, the bindings will check the platform value and convert the pointer to a real object.
For third-party bindings, they may be able to provide an extension function checking whether the given PlatformObject is for that platform.
const UNIFFI_PLATFORM_NODE: /* some value type */ = /* some constant */;
trait PlatformObjectExt {
fn is_node(&self) -> bool;
}
impl PlatformObjectExt for uniffi::PlatformObject {
fn is_node(&self) -> bool {
self.platform() == UNIFFI_PLATFORM_NODE
}
}
use uniffi_node_bindings::PlatformObjectExt as _;
fn multiply_by_two(arg: PlatformObject) -> PlatformObject {
if arg.is_node() {
...
}
...
}
~~However, these features cannot be implemented for the moment on some bindings. For example, Kotlin bindings, as well as the JVM version of the third-party Kotlin multiplatform bindings, use JNA, and AFAIK there is no way to send Java objects as JNI's jobject using JNA. Also, for the third-party C#, Go, and Dart bindings, since there are no counterparts for something like JNI, this pobject
is useless to them.~~
~~Nevertheless, I believe pobject
will increase the utility of UniFFI, and bindings that does not support pobject
can just ignore functions and records that use pobject
.~~
(Edit 1: For the third-party bindings for C#, Go, and Dart, where there are no run-time native reflection features like JNI, they may choose to project pobject
as IntPtr
, uintptr
, and Pointer<Void>
. Consider the case where UniFFI is used with a WinForm C# project. The user may want to pass a HWND handle to the Rust side, which typically is represented as IntPtr
in C# projects)
(Edit 2: On Swift projects, there may be cases where the user wants to pass both UnsafeRawPointer
or Any
to some function. In that case, the binding may generate an enum corresponding to pobject
like the following and allow the user to choose one side)
enum PlatformObject {
case objc(Any)
case raw(UnsafeRawPointer)
}
(Edit 3: It seems JNA supports passing objects via JNI; it is just that JNI interoperability is unstable, i.e., there are many breaking changes even between minor versions. I think these features can be implemented even for now.)
I believe pobject
will increase the utility of UniFFI, as I can think of many use cases for it.
BTW, I'm using JNA and JNI in the same dynamic library; at first, I modified the template so System.loadLibrary is invoked right after Native.loadLibrary is called, but later, I kept the template as original and directly accessed to JNI_GetCreatedJavaVMs to get the current process's JavaVM.
Hi @paxbun, I also would like to pass JNI objects from Kotlin to Rust. Do you have a fork of uniffi that implements this?
@coolbluewater No, I didn't have any time to work on this... If you want features like this but have to pass few objects, you can just System.loadLibrary
the same library and pass the JNI objects. I have an example for you: https://github.com/paxbun/GraphicsTest/blob/6ab7e5d8cdc2864653d91f6d7fa77924c27815d5/rust/src/lib.rs#L16-L39
This example passes android.view.Surface
via JNI and UIView
via a C function, and does the other things using UniFFI. Feel free to ask me about the example :)
Note that this example also uses the KMM UniFFI bindgen mentioned in my first comment, so you have to build the plugin manually, which is not merged to main
yet. Clone the branch of the following PR: https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings/-/merge_requests/39 You can build it with ./gradlew :build-logic:gradle-plugin:publishToMavenLocal
.
This plugin configures almost every environment variable you'd need, but I haven't tested it on Windows or Linux. Please let me know if you have any trouble building that plugin or the example!
I'm going to make a POC of this at least by July.
I've been continuously investigating safer API design for this, but the existence of JNIEnv
always brings me to something ugly or unsafe, which is thread-local but always required for practical use. This is the least ugly design I've come up with:
We need to put JNIEnv
into a polymorphic object, which returns the underlying JNIEnv
when called from a JVM and does nothing when called from other environments, such as Swift or Python. Since JNA does not support putting JNIEnv
in a structure and modifying JNA to support it also does not make sense in the Java world, if we need to do this, the polymorphic object creation must be done on the Rust side, specifically in the scaffold.
For example, if a UniFFI function has a PlatformObject
parameter, the scaffold for that function may look like this:
#[uniffi::export]
fn foo<'a>(env: uniffi::Env<'a>, platform_object: uniffi::PlatformObject) { ... }
pub unsafe extern "C" fn uniffi_my_package_fn_foo(
env_identifier: u128,
env: *const ::std::ffi::c_void,
platform_object: *const ::std::ffi::c_void,
call_status: &mut ::uniffi::RustCallStatus,
) {
uniffi::rust_call(
call_status,
|| {
let env = uniffi::Env::new_unchecked(env_identifier, env);
let platform_object = uniffi::PlatformObject::new_unchecked(platform_object);
my_package::foo(env, platform_object);
},
);
}
and the Kotlin binding can call this function like uniffi_my_package_fn_foo(UNIFFI_IDENTIFIER_JAVA, JNIEnv.current, platformObject)
and the others like uniffi_my_package_fn_foo(OTHER_IDENTIFIER, nullptr, platformObject)
.
Since env
now has the identifier, functions like is_objc
or is_java
are all in uniffi::Env
, not uniffi::PlatformObject
.
#[uniffi::export]
fn foo<'a>(env: uniffi::Env<'a>, platform_object: uniffi::PlatformObject) {
if env.is_java() {
let platform_object = platform_object.into_inner() as jni_sys::object;
...
}
}
But as you can see, this design forces the user to add an additional parameter when using a specific type of parameter, a new constraint that has never been in previous versions.