Skip to content

Commit

Permalink
Added the uniffi_reexport_scaffolding macro
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bendk committed Mar 31, 2022
1 parent 12fb0cd commit 52c38d3
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions docs/manual/src/tutorial/Rust_scaffolding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion fixtures/callbacks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2018"
publish = false

[lib]
crate-type = ["staticlib", "cdylib"]
crate-type = ["lib", "cdylib"]
name = "uniffi_callbacks"

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion fixtures/coverall/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2018"
publish = false

[lib]
crate-type = ["staticlib", "cdylib"]
crate-type = ["lib", "cdylib"]
name = "uniffi_coverall"

[dependencies]
Expand Down
21 changes: 21 additions & 0 deletions fixtures/reexport-scaffolding-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "uniffi-fixture-reexport-scaffolding-macro"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "reexport_scaffolding_macro"
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" }
180 changes: 180 additions & 0 deletions fixtures/reexport-scaffolding-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 == "reexport_scaffolding_macro"
&& artifact.target.kind.iter().any(|item| item == "cdylib")
{
Some(artifact)
} else {
None
}
}
_ => None,
})
.collect::<Vec<_>>();
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<T>(library: &Library, name: &str) -> bool {
unsafe {
library
.get::<T>(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::<T>(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::<unsafe extern "C" fn(&mut RustCallStatus) -> u64>(
&library,
coveralls_ci
.get_function_definition("get_num_alive")
.unwrap()
.ffi_func()
.name()
)
);

// Object method
assert!(
has_symbol::<unsafe extern "C" fn(&mut RustCallStatus) -> 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<unsafe extern "C" fn(&mut RustCallStatus) -> 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<unsafe extern "C" fn(*const c_void, &mut RustCallStatus) -> ()> =
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);
}
}
9 changes: 9 additions & 0 deletions uniffi/src/ffi/rustcalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions uniffi_bindgen/src/interface/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion uniffi_bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}

0 comments on commit 52c38d3

Please sign in to comment.