refactor(android): split C++ into wzp-native cdylib, loaded at runtime
Phase 1 of the big refactor. Escape the Tauri Android __init_tcb+4 symbol leak (rust-lang/rust#104707) by making wzp-desktop's Android .so pure Rust — ZERO cc::Build, no cpp/ files, no C++ in the rustc link step. All future C++ (Oboe audio bridge) lives in a new standalone cdylib crate `wzp-native` which is built with cargo-ndk (the same path the legacy wzp-android crate uses successfully on the same phone + same NDK), copied into Tauri's gen/android/app/src/main/jniLibs at build time, and dlopened by wzp-desktop at runtime via libloading. Changes in this commit: - NEW crate crates/wzp-native/ with crate-type = ["cdylib"] only (no staticlib, no rlib — rust#104707 shows mixing staticlib with cdylib leaks non-exported symbols, which is the original bug source). Phase 1 scaffold has TWO extern "C" functions: wzp_native_version() -> i32 (returns 42) wzp_native_hello(buf, cap) -> usize (writes a string) So we can verify dlopen + dlsym + cross-.so FFI end-to-end before adding any real C++. - desktop/src-tauri/cpp/ directory DELETED (7 files gone). - desktop/src-tauri/build.rs reduced to just the git hash capture + tauri_build::build(). No more cc::Build of any kind. - desktop/src-tauri/Cargo.toml: drop cc from build-dependencies, add libloading = "0.8" as an Android-only runtime dep. - desktop/src-tauri/src/lib.rs Builder::setup() now (on Android only) dlopens libwzp_native.so, calls wzp_native_version() and wzp_native_hello(), and logs the result: "wzp-native dlopen OK: version=42 msg=\"hello from wzp-native\"" If this log appears in logcat when the app launches and the home screen still renders, the split-cdylib pipeline is validated and Phase 2 (port the Oboe bridge into wzp-native) can proceed. - scripts/build-tauri-android.sh: insert a `cargo ndk -t arm64-v8a build --release -p wzp-native` step before `cargo tauri android build`, with `-o desktop/src-tauri/gen/android/app/src/main/jniLibs` so the resulting libwzp_native.so lands in the place gradle will package into the final APK. - Workspace Cargo.toml: add crates/wzp-native to [workspace] members. Phase 2 (separate commit, only if Phase 1 works): - Copy cpp/oboe_bridge.{h,cpp} + getauxval_fix.c from the legacy wzp-android crate into crates/wzp-native/cpp/. - Add cc = "1" as a build-dependency on wzp-native (safe: it's a single-cdylib crate with no staticlib, so no symbol leak). - Add build.rs that compiles the Oboe C++ and the wzp-native Rust FFI exposes the audio start/stop/read/write functions. - wzp-desktop::engine.rs dlopens wzp-native at CallEngine::start, uses its audio functions instead of CPAL on Android. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// ─── Embedded git hash ─────────────────────────────────────────────────
|
||||
// Capture short git hash so the running app can prove which build it is.
|
||||
// Falls back to "unknown" if git isn't available (e.g. when building from
|
||||
// a tarball without a .git dir).
|
||||
let git_hash = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
@@ -15,75 +17,10 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed=../../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../../.git/refs/heads");
|
||||
|
||||
// ─── Step A: single trivial cpp/hello.c compiled via cc::Build ─────────
|
||||
// ─── Step D: also compile getauxval_fix.c (legacy wzp-android shim) ────
|
||||
// getauxval_fix.c overrides the broken static getauxval stub that
|
||||
// compiler-rt pulls in for Android targets. It's been shipping in the
|
||||
// legacy wzp-android .so for months without issue, so including it here
|
||||
// is low-risk — but it's an incremental variable we want to isolate.
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
if target.contains("android") {
|
||||
println!("cargo:rerun-if-changed=cpp/hello.c");
|
||||
cc::Build::new()
|
||||
.file("cpp/hello.c")
|
||||
.compile("wzp_hello");
|
||||
|
||||
println!("cargo:rerun-if-changed=cpp/getauxval_fix.c");
|
||||
cc::Build::new()
|
||||
.file("cpp/getauxval_fix.c")
|
||||
.compile("getauxval_fix");
|
||||
|
||||
// Step D+1: identical-content clone of hello.c as a third cc::Build
|
||||
// static library. Kept around as a sanity check: if this C compile
|
||||
// suddenly started crashing, we'd know the environment regressed.
|
||||
println!("cargo:rerun-if-changed=cpp/hello2.c");
|
||||
cc::Build::new()
|
||||
.file("cpp/hello2.c")
|
||||
.compile("wzp_hello2");
|
||||
|
||||
// ─── minSdkVersion theory test: the original E.1 crashing cpp ──────
|
||||
// Re-add the smallest crashing variant (cpp_smoke.cpp with cpp(true)
|
||||
// + cpp_link_stdlib("c++_shared")) on top of the working Step D+1
|
||||
// baseline. The only additional variable compared to the previous
|
||||
// crashing runs is tauri.conf.json bundle.android.minSdkVersion=26,
|
||||
// which may make tauri-cli stop hardcoding API 24 in its rustc
|
||||
// invocation. If THIS build launches, the minSdkVersion fix is
|
||||
// validated and we can proceed with Oboe integration.
|
||||
println!("cargo:rerun-if-changed=cpp/cpp_smoke.cpp");
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.std("c++17")
|
||||
.cpp_link_stdlib(Some("c++_shared"))
|
||||
.file("cpp/cpp_smoke.cpp")
|
||||
.compile("wzp_cpp_smoke");
|
||||
|
||||
// Per rust-lang/rust#104707 + the android-ndk advice: force the
|
||||
// linker to keep bionic symbols (pthread_create, __init_tcb) as
|
||||
// UND dynamic references resolved against libc.so at runtime,
|
||||
// not bound locally from libc.a that cc-rs + cpp(true) drag in.
|
||||
// llvm-nm confirmed these symbols were landing in our .so as
|
||||
// LOCAL (lowercase t), which is exactly the bug.
|
||||
println!("cargo:rustc-link-arg=-Wl,--exclude-libs,ALL");
|
||||
println!("cargo:rustc-link-arg=-Wl,--no-whole-archive");
|
||||
|
||||
// Copy libc++_shared.so from the NDK sysroot to gen/android jniLibs
|
||||
// so the runtime linker can find it at dlopen time (it's now in the
|
||||
// .so's NEEDED list thanks to cpp_link_stdlib("c++_shared") above).
|
||||
if let Ok(ndk) = std::env::var("ANDROID_NDK_HOME").or_else(|_| std::env::var("NDK_HOME")) {
|
||||
let lib_dir = format!(
|
||||
"{ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android"
|
||||
);
|
||||
println!("cargo:rustc-link-search=native={lib_dir}");
|
||||
let shared_so = format!("{lib_dir}/libc++_shared.so");
|
||||
if std::path::Path::new(&shared_so).exists() {
|
||||
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
|
||||
let jni_dir = format!("{manifest}/gen/android/app/src/main/jniLibs/arm64-v8a");
|
||||
if std::fs::create_dir_all(&jni_dir).is_ok() {
|
||||
let _ = std::fs::copy(&shared_so, format!("{jni_dir}/libc++_shared.so"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No cc::Build of ANY kind on Android — all C++ lives in the standalone
|
||||
// `wzp-native` crate which is built separately with cargo-ndk and loaded
|
||||
// via libloading at runtime. See docs/incident-tauri-android-init-tcb.md
|
||||
// for why this split exists.
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user