Android build fails due to duplicate symbols (start_app) for Dioxus template
Hi,
I have initiated the template app with dioxus for the front end. When i build for ios, no probs. When i build for android i get a linker error seemingly due to the ffi fn 'start_app' clashing with a fn with the same name as part of dioxus launch process. I am unsure as to why this isn't an issue on ios... or if this something I'm doing wrong? Any hints appreciated! I am building for android on a Windows system... although one thing to note was my successful build for ios was performed on a mac system... i haven't currently tested building for android on a mac, but will update if that works when i get a chance to do that test. I built simply with 'cargo android build'
= note: ld.lld: error: duplicate symbol: start_app␍ >>> defined at lib.rs:54 (src\lib/lib.rs:54)␍ >>> C:\Users\ts286220\Documents\rust\mobile_test\target\aarch64-linux-android\debug\deps\mobile_test.693bwtemwm68zbn4smxiqgq1u.rcgu.o:(start_app)␍ >>> defined at lib.rs:109 (src/lib.rs:109)␍ >>> dioxus_mobile-9a2493ea720cf900.dioxus_mobile.e7c67f304d5edf1f-cgu.0.rcgu.o:(.text.start_app+0x0) in archive C:\Users\ts286220\Documents\rust\mobile_test\target\aarch64-linux-android\debug\deps\libdioxus_mobile-9a2493ea720cf900.rlib␍ clang: error: linker command failed with exit code 1 (use -v to see invocation)␍
Same issue, but I have found a solution, although it may be a bit cumbersome.
the problem is that start_app is defined in the newest version in dioxus-mobile, so we need to patch this crate.
first create a lib name dioxus-mobile in crates dir
Cargo.toml:
[dependencies.dioxus-cli-config]
version = "0.6.2"
[dependencies.dioxus-desktop]
default-features = false
version = "0.6.2"
[dependencies.dioxus-lib]
version = "0.6.2"
[dependencies.libc]
version = "0.2.159"
[dependencies.once_cell]
version = "1.17.1"
[features]
default = ["jnibindings", "tokio_runtime", "devtools", "exception"]
devtools = ["dioxus-desktop/devtools"]
exception = ["dioxus-desktop/exception"]
fullscreen = ["dioxus-desktop/fullscreen"]
gnu = ["dioxus-desktop/gnu"]
jnibindings = []
tokio_runtime = ["dioxus-desktop/tokio_runtime"]
transparent = ["dioxus-desktop/transparent"]
[lib]
doctest = false
name = "dioxus_mobile"
path = "src/lib.rs"
test = false
[package]
authors = ["Jonathan Kelley"]
autobenches = false
autobins = false
autoexamples = false
autolib = false
autotests = false
build = false
description = "Mobile-compatible renderer for Dioxus"
edition = "2021"
homepage = "https://dioxuslabs.com/learn/0.6/getting_started"
keywords = ["dom", "ui", "gui", "react"]
license = "MIT OR Apache-2.0"
name = "dioxus-mobile"
readme = "README.md"
repository = "https://github.com/DioxusLabs/dioxus/"
version = "0.6.2"
[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
[target.'cfg(target_os = "android")'.dependencies.jni]
version = "0.21.1"
and lib.rs:
pub use dioxus_desktop::*;
use dioxus_lib::prelude::*;
use std::any::Any;
use std::sync::Mutex;
pub mod launch_bindings {
use super::*;
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
super::launch_cfg(root, contexts, platform_config);
}
pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
todo!()
}
}
/// Launch via the binding API
pub fn launch(root: fn() -> Element) {
launch_cfg(root, vec![], vec![]);
}
pub fn launch_cfg(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
#[cfg(target_os = "android")]
{
*APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
root,
contexts,
platform_config,
});
}
#[cfg(not(target_os = "android"))]
{
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
}
/// We need to store the root function and contexts in a static so that when the tao bindings call
/// "start_app", that the original function arguments are still around.
///
/// If you look closely, you'll notice that we impl Send for this struct. This would normally be
/// unsound. However, we know that the thread that created these objects ("main()" - see JNI_OnLoad)
/// is finished once `start_app` is called. This is similar to how an Rc<T> is technically safe
/// to move between threads if you can prove that no other thread is using the Rc<T> at the same time.
/// Crates like https://crates.io/crates/sendable exist that build on this idea but with runtimk,
/// validation that the current thread is the one that created the object.
///
/// Since `main()` completes, the only reader of this data will be `start_app`, so it's okay to
/// impl this as Send/Sync.
///
/// Todo(jon): the visibility of functions in this module is too public. Make sure to hide them before
/// releasing 0.7.
struct BoundLaunchObjects {
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
}
unsafe impl Send for BoundLaunchObjects {}
unsafe impl Sync for BoundLaunchObjects {}
static APP_OBJECTS: Mutex<Option<BoundLaunchObjects>> = Mutex::new(None);
#[doc(hidden)]
pub fn root() {
let app = APP_OBJECTS
.lock()
.expect("APP_FN_PTR lock failed")
.take()
.expect("Android to have set the app trampoline");
let BoundLaunchObjects {
root,
contexts,
platform_config,
} = app;
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
This is very similar to the original, except that have removed JNI_OnLoad and start_app
Change Project add these into project Cargo.toml
[patch.crates-io]
dioxus-mobile = { path = "./crates/dioxus-mobile" }
dioxus = { version = "0.6.3", features = ["mobile"] }
libc = "0.2"
dioxus-mobile = "0.6.2"
[dependencies.tao]
features = ["rwh_05", "rwh_05"]
version = "0.30.8"
[dependencies.wry]
default-features = false
features = ["os-webview", "protocol", "drag-drop"]
version = "0.45.0"
and change lib.rs like:
use dioxus::prelude::*;
use dioxus_mobile::root;
#[cfg(target_os = "android")]
fn init_logging() {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Trace)
.with_tag("blpl-music"),
);
}
#[cfg(not(target_os = "android"))]
fn init_logging() {
env_logger::init();
}
#[cfg(any(target_os = "android", target_os = "ios"))]
fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(t) => t,
Err(err) => {
eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
std::process::abort()
}
}
}
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn start_app() {
tao::android_binding!(
com_example, // replace this
xxx, // replace this
WryActivity,
wry::android_setup,
root,
tao
);
wry::android_binding!(com_example, blpl_music, wry);
}
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn JNI_OnLoad(
_vm: *mut libc::c_void,
_reserved: *mut libc::c_void,
) -> jni::sys::jint {
stop_unwind(|| main());
jni::sys::JNI_VERSION_1_6
}
pub fn main() {
init_logging();
dioxus::launch(app);
}
fn app() -> Element {
let mut items = use_signal(|| vec![1, 2, 3]);
log::debug!("Hello from the app");
rsx! {
div {
h1 { "Hello, Mobile"}
div { margin_left: "auto", margin_right: "auto", width: "200px", padding: "10px", border: "1px solid black",
button {
onclick: move|_| {
println!("Clicked!");
let mut items_mut = items.write();
let new_item = items_mut.len() + 1;
items_mut.push(new_item);
println!("Requested update");
},
"Add item"
}
for item in items.read().iter() {
div { "- {item}" }
}
}
}
}
}
remove _start_app, change start_app and add a new function JNI_OnLoad
It can run normally on android. but im not sure it can run on ios...
Will try it out, thanks. I think it is far more straightforward in ios, there's just a external c function start_app in the generated xcode project that is called in swift's launch code - which calls the no mangle start_app fn exposed in rust. The names can be changed as long as done in both swift and rust, and there's no clashes anyway there. Dioxus 0.7 is just about to be released... don't know if this has been changed or not.