From 5d8cfec7ccf144261dbaccf65b3ef0acb7fb6a43 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Mon, 28 Mar 2022 16:20:04 -0400 Subject: [PATCH] Added the uniffi_reexport_scaffolding macro This can be used to work around [rust-lang#50007](rust-lang/rust#50007), which is getting to be more and more of an issue on the desktop JS project. Updated `uniffi::interface` and some of the fixtures so that we can test this. --- Cargo.toml | 1 + docs/manual/src/tutorial/Rust_scaffolding.md | 18 ++ fixtures/callbacks/Cargo.toml | 2 +- fixtures/coverall/Cargo.toml | 2 +- .../reexport-scaffolding-macro/Cargo.toml | 21 ++ .../reexport-scaffolding-macro/src/lib.rs | 180 ++++++++++++++++++ uniffi/src/ffi/rustcalls.rs | 9 + uniffi_bindgen/src/interface/object.rs | 8 + uniffi_bindgen/src/lib.rs | 2 +- .../templates/ReexportUniFFIScaffolding.rs | 27 +++ .../templates/scaffolding_template.rs | 3 + 11 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 fixtures/reexport-scaffolding-macro/Cargo.toml create mode 100644 fixtures/reexport-scaffolding-macro/src/lib.rs create mode 100644 uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs diff --git a/Cargo.toml b/Cargo.toml index d117c4fcd0..26dd14093e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "fixtures/external-types/crate-two", "fixtures/external-types/lib", + "fixtures/reexport-scaffolding-macro", "fixtures/regressions/enum-without-i32-helpers", "fixtures/regressions/fully-qualified-types", "fixtures/regressions/kotlin-experimental-unsigned-types", diff --git a/docs/manual/src/tutorial/Rust_scaffolding.md b/docs/manual/src/tutorial/Rust_scaffolding.md index 4d0d1fbc54..ef8766b747 100644 --- a/docs/manual/src/tutorial/Rust_scaffolding.md +++ b/docs/manual/src/tutorial/Rust_scaffolding.md @@ -108,3 +108,21 @@ uniffi_build = { path = "path/to/uniffi-rs/uniffi_build, features=["builtin-bind Note that `path/to/uniffi-rs` should be the path to the root of the `uniffi` source tree - ie, the 2 path specs above point to different sub-directories under the `uniffi` root. + +### Libraries that depend on UniFFI components + +Suppose you want to create a shared library that includes one or more +components using UniFFI. The typical way to achieve this is to create a new +crate that depends on the component crates. However, this can run into +[rust-lang#50007](https://github.com/rust-lang/rust/issues/50007). Under +certain circumstances, the scaffolding functions that the component crates +export do not get re-exported by the dependent crate. + +Use the `uniffi_reexport_scaffolding!` macro to work around this issue. If your +library depends on `foo_component`, then add +`foo_component::uniffi_reexport_scaffolding!();` to your `lib.rs` file and +UniFFI will add workaround code that forces the functions to be re-exported. + +Each scaffolding function contains a hash that's derived from the UDL file. +This avoids name collisions when combining multiple UniFFI components into +one library. diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml index c9261d9922..548f94e574 100644 --- a/fixtures/callbacks/Cargo.toml +++ b/fixtures/callbacks/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [lib] -crate-type = ["staticlib", "cdylib"] +crate-type = ["lib", "cdylib"] name = "uniffi_callbacks" [dependencies] diff --git a/fixtures/coverall/Cargo.toml b/fixtures/coverall/Cargo.toml index b7ed46dfcb..652cc1bfdb 100644 --- a/fixtures/coverall/Cargo.toml +++ b/fixtures/coverall/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [lib] -crate-type = ["staticlib", "cdylib"] +crate-type = ["lib", "cdylib"] name = "uniffi_coverall" [dependencies] diff --git a/fixtures/reexport-scaffolding-macro/Cargo.toml b/fixtures/reexport-scaffolding-macro/Cargo.toml new file mode 100644 index 0000000000..c7220eda71 --- /dev/null +++ b/fixtures/reexport-scaffolding-macro/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "library" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "library" +crate-type = ["cdylib"] + +[dependencies] +callbacks = { path = "../callbacks" } +coverall = { path = "../coverall" } +uniffi = { path = "../../uniffi", features=["builtin-bindgen"] } + +[dev-dependencies] +cargo_metadata = "0.13" +lazy_static = "1.4" +libloading = "0.7" +uniffi_bindgen = { path = "../../uniffi_bindgen" } diff --git a/fixtures/reexport-scaffolding-macro/src/lib.rs b/fixtures/reexport-scaffolding-macro/src/lib.rs new file mode 100644 index 0000000000..b65b19b43c --- /dev/null +++ b/fixtures/reexport-scaffolding-macro/src/lib.rs @@ -0,0 +1,180 @@ +uniffi_callbacks::uniffi_reexport_scaffolding!(); +uniffi_coverall::uniffi_reexport_scaffolding!(); + +#[cfg(test)] +mod tests { + use cargo_metadata::Message; + use libloading::{Library, Symbol}; + use std::ffi::CString; + use std::os::raw::c_void; + use std::process::{Command, Stdio}; + use std::str::FromStr; + use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus}; + use uniffi_bindgen::ComponentInterface; + + // Load the dynamic library that was built for this crate. The external functions from + // `uniffi_callbacks' and `uniffi_coverall` should be present. + pub fn load_library() -> Library { + let mut cmd = Command::new("cargo"); + cmd.arg("build").arg("--message-format=json").arg("--lib"); + cmd.stdout(Stdio::piped()); + let mut child = cmd.spawn().unwrap(); + let output = std::io::BufReader::new(child.stdout.take().unwrap()); + let artifacts = Message::parse_stream(output) + .filter_map(|message| match message { + Err(e) => panic!("{}", e), + Ok(Message::CompilerArtifact(artifact)) => { + if artifact.target.name == "library" + && artifact.target.kind.iter().any(|item| item == "cdylib") + { + Some(artifact) + } else { + None + } + } + _ => None, + }) + .collect::>(); + if !child.wait().unwrap().success() { + panic!("Failed to execute `cargo build`"); + } + let artifact = match artifacts.len() { + 1 => &artifacts[0], + n => panic!("Found {} artfiacts from cargo build", n), + }; + let cdylib_files: Vec<_> = artifact + .filenames + .iter() + .filter(|nm| matches!(nm.extension(), Some(std::env::consts::DLL_EXTENSION))) + .collect(); + let library_path = match cdylib_files.len() { + 1 => cdylib_files[0].to_string(), + _ => panic!("Failed to build exactly one cdylib file"), + }; + unsafe { Library::new(library_path).unwrap() } + } + + pub fn has_symbol(library: &Library, name: &str) -> bool { + unsafe { + library + .get::(CString::new(name).unwrap().as_bytes_with_nul()) + .is_ok() + } + } + + pub fn get_symbol<'lib, T>(library: &'lib Library, name: &str) -> Symbol<'lib, T> { + unsafe { + library + .get::(CString::new(name).unwrap().as_bytes_with_nul()) + .unwrap() + } + } + + #[test] + fn test_symbols_present() { + let library = load_library(); + let coveralls_ci = + ComponentInterface::from_str(include_str!("../../coverall/src/coverall.udl")).unwrap(); + let callbacks_ci = + ComponentInterface::from_str(include_str!("../../callbacks/src/callbacks.udl")) + .unwrap(); + + // UniFFI internal function + assert!(has_symbol::< + unsafe extern "C" fn(i32, &mut RustCallStatus) -> RustBuffer, + >( + &library, coveralls_ci.ffi_rustbuffer_alloc().name() + )); + + // Top-level function + assert!( + has_symbol:: u64>( + &library, + coveralls_ci + .get_function_definition("get_num_alive") + .unwrap() + .ffi_func() + .name() + ) + ); + + // Object method + assert!( + has_symbol:: u64>( + &library, + coveralls_ci + .get_object_definition("Coveralls") + .unwrap() + .get_method("get_name") + .ffi_func() + .name() + ) + ); + + // Callback init func + assert!(has_symbol::< + unsafe extern "C" fn(ForeignCallback, &mut RustCallStatus) -> (), + >( + &library, + callbacks_ci + .get_callback_interface_definition("ForeignGetters") + .unwrap() + .ffi_init_callback() + .name() + )); + } + + #[test] + fn test_calls() { + let mut call_status = RustCallStatus::default(); + let library = load_library(); + let coveralls_ci = + ComponentInterface::from_str(include_str!("../../coverall/src/coverall.udl")).unwrap(); + let object_def = coveralls_ci.get_object_definition("Coveralls").unwrap(); + + let get_num_alive: Symbol u64> = get_symbol( + &library, + coveralls_ci + .get_function_definition("get_num_alive") + .unwrap() + .ffi_func() + .name(), + ); + let coveralls_new: Symbol< + unsafe extern "C" fn(RustBuffer, &mut RustCallStatus) -> *const c_void, + > = get_symbol( + &library, + object_def.primary_constructor().unwrap().ffi_func().name(), + ); + let coveralls_get_name: Symbol< + unsafe extern "C" fn(*const c_void, &mut RustCallStatus) -> RustBuffer, + > = get_symbol( + &library, + object_def.get_method("get_name").ffi_func().name(), + ); + let coveralls_free: Symbol ()> = + get_symbol(&library, object_def.ffi_object_free().name()); + + let num_alive = unsafe { get_num_alive(&mut call_status) }; + assert_eq!(call_status.code, 0); + assert_eq!(num_alive, 0); + + let obj_id = unsafe { coveralls_new(String::lower("TestName".into()), &mut call_status) }; + assert_eq!(call_status.code, 0); + + let name_buf = unsafe { coveralls_get_name(obj_id, &mut call_status) }; + assert_eq!(call_status.code, 0); + assert_eq!(String::try_lift(name_buf).unwrap(), "TestName"); + + let num_alive = unsafe { get_num_alive(&mut call_status) }; + assert_eq!(call_status.code, 0); + assert_eq!(num_alive, 1); + + unsafe { coveralls_free(obj_id, &mut call_status) }; + assert_eq!(call_status.code, 0); + + let num_alive = unsafe { get_num_alive(&mut call_status) }; + assert_eq!(call_status.code, 0); + assert_eq!(num_alive, 0); + } +} diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 391fd54dd2..308dd7d6ce 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -66,6 +66,15 @@ pub struct RustCallStatus { // leak the first `RustBuffer`. } +impl Default for RustCallStatus { + fn default() -> Self { + Self { + code: 0, + error_buf: MaybeUninit::uninit(), + } + } +} + #[allow(dead_code)] const CALL_SUCCESS: i8 = 0; // CALL_SUCCESS is set by the calling code const CALL_ERROR: i8 = 1; diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index cca2ba544a..5d5298edf5 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -132,6 +132,14 @@ impl Object { self.methods.iter().collect() } + pub fn get_method(&self, name: &str) -> Method { + let matches: Vec<_> = self.methods.iter().filter(|m| m.name() == name).collect(); + match matches.len() { + 1 => matches[0].clone(), + n => panic!("{} methods named {}", n, name), + } + } + pub fn ffi_object_free(&self) -> &FFIFunction { &self.ffi_func_free } diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 0a4fcbc89f..5c529c3e07 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -112,7 +112,7 @@ pub mod interface; pub mod scaffolding; use bindings::TargetLanguage; -use interface::ComponentInterface; +pub use interface::ComponentInterface; use scaffolding::RustScaffolding; // Generate the infrastructural Rust code for implementing the UDL interface, diff --git a/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs b/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs new file mode 100644 index 0000000000..668f1a2e63 --- /dev/null +++ b/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs @@ -0,0 +1,27 @@ +// Code to re-export the UniFFI scaffolding functions. +// +// Rust won't always re-export the functions from dependencies +// ([rust-lang#50007](https://github.com/rust-lang/rust/issues/50007)) +// +// A workaround for this is to have the dependent crate reference a function from its dependency in +// an extern "C" function. This is clearly hacky and brittle, but at least we have some unittests +// that check if this works (fixtures/reexport-scaffolding-macro). +// +// The main way we use this macro is for that contain multiple UniFFI components (libxul, +// megazord). The combined library has a cargo dependency for each component and calls +// uniffi_reexport_scaffolding!() for each one. + +#[doc(hidden)] +pub fn uniffi_reexport_hack() { +} + +#[macro_export] +macro_rules! uniffi_reexport_scaffolding { + () => { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn {{ ci.namespace() }}_uniffi_reexport_hack() { + $crate::uniffi_reexport_hack() + } + }; +} diff --git a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs index d99d685b76..1d709c1e70 100644 --- a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs +++ b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs @@ -43,4 +43,7 @@ uniffi::assert_compatible_version!("{{ uniffi_version }}"); // Please check that // External and Wrapped types {% include "ExternalTypesTemplate.rs" %} +// The `reexport_uniffi_scaffolding` macro +{% include "ReexportUniFFIScaffolding.rs" %} + {%- import "macros.rs" as rs -%}