Skip to content

Commit 5d8cfec

Browse files
committed
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.
1 parent 12fb0cd commit 5d8cfec

File tree

11 files changed

+270
-3
lines changed

11 files changed

+270
-3
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ members = [
2424
"fixtures/external-types/crate-two",
2525
"fixtures/external-types/lib",
2626

27+
"fixtures/reexport-scaffolding-macro",
2728
"fixtures/regressions/enum-without-i32-helpers",
2829
"fixtures/regressions/fully-qualified-types",
2930
"fixtures/regressions/kotlin-experimental-unsigned-types",

docs/manual/src/tutorial/Rust_scaffolding.md

+18
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,21 @@ uniffi_build = { path = "path/to/uniffi-rs/uniffi_build, features=["builtin-bind
108108
Note that `path/to/uniffi-rs` should be the path to the root of the `uniffi`
109109
source tree - ie, the 2 path specs above point to different sub-directories
110110
under the `uniffi` root.
111+
112+
### Libraries that depend on UniFFI components
113+
114+
Suppose you want to create a shared library that includes one or more
115+
components using UniFFI. The typical way to achieve this is to create a new
116+
crate that depends on the component crates. However, this can run into
117+
[rust-lang#50007](https://github.com/rust-lang/rust/issues/50007). Under
118+
certain circumstances, the scaffolding functions that the component crates
119+
export do not get re-exported by the dependent crate.
120+
121+
Use the `uniffi_reexport_scaffolding!` macro to work around this issue. If your
122+
library depends on `foo_component`, then add
123+
`foo_component::uniffi_reexport_scaffolding!();` to your `lib.rs` file and
124+
UniFFI will add workaround code that forces the functions to be re-exported.
125+
126+
Each scaffolding function contains a hash that's derived from the UDL file.
127+
This avoids name collisions when combining multiple UniFFI components into
128+
one library.

fixtures/callbacks/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2018"
66
publish = false
77

88
[lib]
9-
crate-type = ["staticlib", "cdylib"]
9+
crate-type = ["lib", "cdylib"]
1010
name = "uniffi_callbacks"
1111

1212
[dependencies]

fixtures/coverall/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2018"
66
publish = false
77

88
[lib]
9-
crate-type = ["staticlib", "cdylib"]
9+
crate-type = ["lib", "cdylib"]
1010
name = "uniffi_coverall"
1111

1212
[dependencies]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "library"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[lib]
9+
name = "library"
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
callbacks = { path = "../callbacks" }
14+
coverall = { path = "../coverall" }
15+
uniffi = { path = "../../uniffi", features=["builtin-bindgen"] }
16+
17+
[dev-dependencies]
18+
cargo_metadata = "0.13"
19+
lazy_static = "1.4"
20+
libloading = "0.7"
21+
uniffi_bindgen = { path = "../../uniffi_bindgen" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
uniffi_callbacks::uniffi_reexport_scaffolding!();
2+
uniffi_coverall::uniffi_reexport_scaffolding!();
3+
4+
#[cfg(test)]
5+
mod tests {
6+
use cargo_metadata::Message;
7+
use libloading::{Library, Symbol};
8+
use std::ffi::CString;
9+
use std::os::raw::c_void;
10+
use std::process::{Command, Stdio};
11+
use std::str::FromStr;
12+
use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus};
13+
use uniffi_bindgen::ComponentInterface;
14+
15+
// Load the dynamic library that was built for this crate. The external functions from
16+
// `uniffi_callbacks' and `uniffi_coverall` should be present.
17+
pub fn load_library() -> Library {
18+
let mut cmd = Command::new("cargo");
19+
cmd.arg("build").arg("--message-format=json").arg("--lib");
20+
cmd.stdout(Stdio::piped());
21+
let mut child = cmd.spawn().unwrap();
22+
let output = std::io::BufReader::new(child.stdout.take().unwrap());
23+
let artifacts = Message::parse_stream(output)
24+
.filter_map(|message| match message {
25+
Err(e) => panic!("{}", e),
26+
Ok(Message::CompilerArtifact(artifact)) => {
27+
if artifact.target.name == "library"
28+
&& artifact.target.kind.iter().any(|item| item == "cdylib")
29+
{
30+
Some(artifact)
31+
} else {
32+
None
33+
}
34+
}
35+
_ => None,
36+
})
37+
.collect::<Vec<_>>();
38+
if !child.wait().unwrap().success() {
39+
panic!("Failed to execute `cargo build`");
40+
}
41+
let artifact = match artifacts.len() {
42+
1 => &artifacts[0],
43+
n => panic!("Found {} artfiacts from cargo build", n),
44+
};
45+
let cdylib_files: Vec<_> = artifact
46+
.filenames
47+
.iter()
48+
.filter(|nm| matches!(nm.extension(), Some(std::env::consts::DLL_EXTENSION)))
49+
.collect();
50+
let library_path = match cdylib_files.len() {
51+
1 => cdylib_files[0].to_string(),
52+
_ => panic!("Failed to build exactly one cdylib file"),
53+
};
54+
unsafe { Library::new(library_path).unwrap() }
55+
}
56+
57+
pub fn has_symbol<T>(library: &Library, name: &str) -> bool {
58+
unsafe {
59+
library
60+
.get::<T>(CString::new(name).unwrap().as_bytes_with_nul())
61+
.is_ok()
62+
}
63+
}
64+
65+
pub fn get_symbol<'lib, T>(library: &'lib Library, name: &str) -> Symbol<'lib, T> {
66+
unsafe {
67+
library
68+
.get::<T>(CString::new(name).unwrap().as_bytes_with_nul())
69+
.unwrap()
70+
}
71+
}
72+
73+
#[test]
74+
fn test_symbols_present() {
75+
let library = load_library();
76+
let coveralls_ci =
77+
ComponentInterface::from_str(include_str!("../../coverall/src/coverall.udl")).unwrap();
78+
let callbacks_ci =
79+
ComponentInterface::from_str(include_str!("../../callbacks/src/callbacks.udl"))
80+
.unwrap();
81+
82+
// UniFFI internal function
83+
assert!(has_symbol::<
84+
unsafe extern "C" fn(i32, &mut RustCallStatus) -> RustBuffer,
85+
>(
86+
&library, coveralls_ci.ffi_rustbuffer_alloc().name()
87+
));
88+
89+
// Top-level function
90+
assert!(
91+
has_symbol::<unsafe extern "C" fn(&mut RustCallStatus) -> u64>(
92+
&library,
93+
coveralls_ci
94+
.get_function_definition("get_num_alive")
95+
.unwrap()
96+
.ffi_func()
97+
.name()
98+
)
99+
);
100+
101+
// Object method
102+
assert!(
103+
has_symbol::<unsafe extern "C" fn(&mut RustCallStatus) -> u64>(
104+
&library,
105+
coveralls_ci
106+
.get_object_definition("Coveralls")
107+
.unwrap()
108+
.get_method("get_name")
109+
.ffi_func()
110+
.name()
111+
)
112+
);
113+
114+
// Callback init func
115+
assert!(has_symbol::<
116+
unsafe extern "C" fn(ForeignCallback, &mut RustCallStatus) -> (),
117+
>(
118+
&library,
119+
callbacks_ci
120+
.get_callback_interface_definition("ForeignGetters")
121+
.unwrap()
122+
.ffi_init_callback()
123+
.name()
124+
));
125+
}
126+
127+
#[test]
128+
fn test_calls() {
129+
let mut call_status = RustCallStatus::default();
130+
let library = load_library();
131+
let coveralls_ci =
132+
ComponentInterface::from_str(include_str!("../../coverall/src/coverall.udl")).unwrap();
133+
let object_def = coveralls_ci.get_object_definition("Coveralls").unwrap();
134+
135+
let get_num_alive: Symbol<unsafe extern "C" fn(&mut RustCallStatus) -> u64> = get_symbol(
136+
&library,
137+
coveralls_ci
138+
.get_function_definition("get_num_alive")
139+
.unwrap()
140+
.ffi_func()
141+
.name(),
142+
);
143+
let coveralls_new: Symbol<
144+
unsafe extern "C" fn(RustBuffer, &mut RustCallStatus) -> *const c_void,
145+
> = get_symbol(
146+
&library,
147+
object_def.primary_constructor().unwrap().ffi_func().name(),
148+
);
149+
let coveralls_get_name: Symbol<
150+
unsafe extern "C" fn(*const c_void, &mut RustCallStatus) -> RustBuffer,
151+
> = get_symbol(
152+
&library,
153+
object_def.get_method("get_name").ffi_func().name(),
154+
);
155+
let coveralls_free: Symbol<unsafe extern "C" fn(*const c_void, &mut RustCallStatus) -> ()> =
156+
get_symbol(&library, object_def.ffi_object_free().name());
157+
158+
let num_alive = unsafe { get_num_alive(&mut call_status) };
159+
assert_eq!(call_status.code, 0);
160+
assert_eq!(num_alive, 0);
161+
162+
let obj_id = unsafe { coveralls_new(String::lower("TestName".into()), &mut call_status) };
163+
assert_eq!(call_status.code, 0);
164+
165+
let name_buf = unsafe { coveralls_get_name(obj_id, &mut call_status) };
166+
assert_eq!(call_status.code, 0);
167+
assert_eq!(String::try_lift(name_buf).unwrap(), "TestName");
168+
169+
let num_alive = unsafe { get_num_alive(&mut call_status) };
170+
assert_eq!(call_status.code, 0);
171+
assert_eq!(num_alive, 1);
172+
173+
unsafe { coveralls_free(obj_id, &mut call_status) };
174+
assert_eq!(call_status.code, 0);
175+
176+
let num_alive = unsafe { get_num_alive(&mut call_status) };
177+
assert_eq!(call_status.code, 0);
178+
assert_eq!(num_alive, 0);
179+
}
180+
}

uniffi/src/ffi/rustcalls.rs

+9
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ pub struct RustCallStatus {
6666
// leak the first `RustBuffer`.
6767
}
6868

69+
impl Default for RustCallStatus {
70+
fn default() -> Self {
71+
Self {
72+
code: 0,
73+
error_buf: MaybeUninit::uninit(),
74+
}
75+
}
76+
}
77+
6978
#[allow(dead_code)]
7079
const CALL_SUCCESS: i8 = 0; // CALL_SUCCESS is set by the calling code
7180
const CALL_ERROR: i8 = 1;

uniffi_bindgen/src/interface/object.rs

+8
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ impl Object {
132132
self.methods.iter().collect()
133133
}
134134

135+
pub fn get_method(&self, name: &str) -> Method {
136+
let matches: Vec<_> = self.methods.iter().filter(|m| m.name() == name).collect();
137+
match matches.len() {
138+
1 => matches[0].clone(),
139+
n => panic!("{} methods named {}", n, name),
140+
}
141+
}
142+
135143
pub fn ffi_object_free(&self) -> &FFIFunction {
136144
&self.ffi_func_free
137145
}

uniffi_bindgen/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub mod interface;
112112
pub mod scaffolding;
113113

114114
use bindings::TargetLanguage;
115-
use interface::ComponentInterface;
115+
pub use interface::ComponentInterface;
116116
use scaffolding::RustScaffolding;
117117

118118
// Generate the infrastructural Rust code for implementing the UDL interface,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Code to re-export the UniFFI scaffolding functions.
2+
//
3+
// Rust won't always re-export the functions from dependencies
4+
// ([rust-lang#50007](https://github.com/rust-lang/rust/issues/50007))
5+
//
6+
// A workaround for this is to have the dependent crate reference a function from its dependency in
7+
// an extern "C" function. This is clearly hacky and brittle, but at least we have some unittests
8+
// that check if this works (fixtures/reexport-scaffolding-macro).
9+
//
10+
// The main way we use this macro is for that contain multiple UniFFI components (libxul,
11+
// megazord). The combined library has a cargo dependency for each component and calls
12+
// uniffi_reexport_scaffolding!() for each one.
13+
14+
#[doc(hidden)]
15+
pub fn uniffi_reexport_hack() {
16+
}
17+
18+
#[macro_export]
19+
macro_rules! uniffi_reexport_scaffolding {
20+
() => {
21+
#[doc(hidden)]
22+
#[no_mangle]
23+
pub extern "C" fn {{ ci.namespace() }}_uniffi_reexport_hack() {
24+
$crate::uniffi_reexport_hack()
25+
}
26+
};
27+
}

uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs

+3
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,7 @@ uniffi::assert_compatible_version!("{{ uniffi_version }}"); // Please check that
4343
// External and Wrapped types
4444
{% include "ExternalTypesTemplate.rs" %}
4545

46+
// The `reexport_uniffi_scaffolding` macro
47+
{% include "ReexportUniFFIScaffolding.rs" %}
48+
4649
{%- import "macros.rs" as rs -%}

0 commit comments

Comments
 (0)