diff --git a/Cargo.toml b/Cargo.toml index e3a095b..aadfca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/wzp-client", "crates/wzp-web", "crates/wzp-android", + "crates/wzp-native", "desktop/src-tauri", ] diff --git a/crates/wzp-native/Cargo.toml b/crates/wzp-native/Cargo.toml new file mode 100644 index 0000000..893d3ea --- /dev/null +++ b/crates/wzp-native/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wzp-native" +version = "0.1.0" +edition = "2024" +description = "WarzonePhone native audio library — standalone Android cdylib that eventually owns all C++ (Oboe bridge) and exposes a pure-C FFI. Built with cargo-ndk, loaded at runtime by the Tauri desktop cdylib via libloading." + +# Crate-type is DELIBERATELY only cdylib (no rlib, no staticlib). This crate +# is built with `cargo ndk -t arm64-v8a build --release -p wzp-native` as a +# standalone .so, which is the same path the legacy wzp-android crate uses +# successfully on the same phone / same NDK. Keeping the crate-type single +# avoids the rust-lang/rust#104707 symbol leak that bit us when Tauri's +# desktop crate had ["staticlib", "cdylib", "rlib"] and any C++ static +# archive pulled bionic's internal pthread_create into the final .so. +[lib] +name = "wzp_native" +crate-type = ["cdylib"] + +[dependencies] +# Phase 1 scaffold: no dependencies at all. Pure Rust FFI smoke test so we +# can validate the standalone-cdylib + libloading runtime pipeline before +# bringing Oboe / wzp-codec / etc. back in. Phase 2 will add the Oboe cc +# build and the actual audio pipeline. diff --git a/crates/wzp-native/src/lib.rs b/crates/wzp-native/src/lib.rs new file mode 100644 index 0000000..8d65790 --- /dev/null +++ b/crates/wzp-native/src/lib.rs @@ -0,0 +1,44 @@ +//! wzp-native — standalone Android cdylib for all the C++ audio code. +//! +//! This crate is built with `cargo ndk`, NOT `cargo tauri android build`, +//! because the latter mispipes link flags in a way that causes bionic's +//! private `pthread_create` / `__init_tcb` symbols to land LOCALLY inside +//! any cdylib that also has a `cc::Build::new().cpp(true)` step. See +//! `docs/incident-tauri-android-init-tcb.md` for the full post-mortem. +//! +//! The Tauri desktop crate (`wzp-desktop`) has **no C++ at all**. At +//! runtime on Android, it `libloading::Library::new("libwzp_native.so")`'s +//! this crate's .so and calls the `wzp_native_*` functions below. +//! +//! Phase 1 (this file): a tiny smoke-test FFI surface so we can validate +//! that (a) cargo-ndk happily builds this crate standalone, (b) gradle +//! picks up the resulting .so from jniLibs, (c) the Tauri cdylib can +//! dlopen us at runtime and call exported functions. No C++, no Oboe, no +//! external deps. Phase 2 will add the Oboe cc::Build + audio FFI. + +/// Smoke-test export #1 — returns a fixed magic number so the Tauri cdylib +/// can assert that `dlopen + dlsym` worked end-to-end. Always returns 42. +#[unsafe(no_mangle)] +pub extern "C" fn wzp_native_version() -> i32 { + 42 +} + +/// Smoke-test export #2 — writes a fixed message into the caller's buffer +/// (NUL-terminated, capped at `cap`) and returns the number of bytes +/// written (not counting the NUL). Lets us verify we can move non-trivial +/// data across the FFI boundary without fighting ownership. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wzp_native_hello(out: *mut u8, cap: usize) -> usize { + const MSG: &[u8] = b"hello from wzp-native\0"; + if out.is_null() || cap == 0 { + return 0; + } + let n = MSG.len().min(cap); + // SAFETY: caller provided a writable buffer of at least `cap` bytes. + unsafe { + core::ptr::copy_nonoverlapping(MSG.as_ptr(), out, n); + // ensure last byte is a NUL even if we had to truncate + *out.add(n - 1) = 0; + } + n - 1 // bytes written excluding the NUL +} diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 61f60e6..13d3040 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -29,10 +29,9 @@ path = "src/main.rs" [build-dependencies] tauri-build = { version = "2", features = [] } -# Step A: minimal cc addition — lets build.rs compile cpp/hello.c on Android. -# build-dependencies are resolved against the HOST, not the target, so this -# line is unconditional (the actual cc::Build call is gated on TARGET). -cc = "1" +# cc is no longer needed — all C++ moved to crates/wzp-native (built with +# cargo-ndk and loaded via libloading at runtime). wzp-desktop's .so on +# Android is now pure Rust. [dependencies] tauri = { version = "2", features = [] } @@ -64,6 +63,11 @@ wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] } # doesn't regress the working build. [target.'cfg(target_os = "android")'.dependencies] wzp-client = { path = "../../crates/wzp-client", default-features = false } +# libloading: runtime dlopen of libwzp_native.so — the standalone cdylib +# crate that owns all C++ (Oboe bridge). Keeps wzp-desktop's .so free of +# any C/C++ static archives that would otherwise leak bionic's internal +# pthread_create into our cdylib and trigger the __init_tcb crash. +libloading = "0.8" # Platform-specific [target.'cfg(target_os = "macos")'.dependencies] diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index 7ebe7fe..cae76a6 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -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() } diff --git a/desktop/src-tauri/cpp/cpp_smoke.cpp b/desktop/src-tauri/cpp/cpp_smoke.cpp deleted file mode 100644 index 092c7ee..0000000 --- a/desktop/src-tauri/cpp/cpp_smoke.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// cpp_smoke.cpp — original Step E.1 minimal crashing C++ file. -// Compiled with cc::Build::new().cpp(true).cpp_link_stdlib("c++_shared"). -// Never called from Rust (dead-stripped at link time). Used to validate -// the tauri.conf.json minSdkVersion=26 fix against the smallest variant -// that was reliably crashing with __init_tcb+4. - -extern "C" int wzp_cpp_hello(void) { - return 42; -} diff --git a/desktop/src-tauri/cpp/getauxval_fix.c b/desktop/src-tauri/cpp/getauxval_fix.c deleted file mode 100644 index 13b6ad2..0000000 --- a/desktop/src-tauri/cpp/getauxval_fix.c +++ /dev/null @@ -1,26 +0,0 @@ -/* Override the broken static getauxval from compiler-rt/CRT. - * - * The static version reads from __libc_auxv which is NULL in shared libs - * loaded via dlopen, causing SIGSEGV in init_have_lse_atomics at load time. - * This version calls the real bionic getauxval via dlsym. - * - * Copied verbatim from crates/wzp-android/cpp/getauxval_fix.c — the legacy - * wzp-android crate has been using this shim successfully for months. - */ -#ifdef __ANDROID__ -#include -#include - -typedef unsigned long (*getauxval_fn)(unsigned long); - -unsigned long getauxval(unsigned long type) { - static getauxval_fn real_getauxval = (getauxval_fn)0; - if (!real_getauxval) { - real_getauxval = (getauxval_fn)dlsym((void*)-1L /* RTLD_DEFAULT */, "getauxval"); - if (!real_getauxval) { - return 0; - } - } - return real_getauxval(type); -} -#endif diff --git a/desktop/src-tauri/cpp/hello.c b/desktop/src-tauri/cpp/hello.c deleted file mode 100644 index e218c9b..0000000 --- a/desktop/src-tauri/cpp/hello.c +++ /dev/null @@ -1,14 +0,0 @@ -/* hello.c — minimal C file compiled via cc::Build on Android. - * - * Step A of the incremental Oboe integration: this file exists only to - * exercise the cc::Build → static lib → rustc-link pipeline and prove - * that introducing any C static library into our .so doesn't by itself - * trigger the tao::ndk_glue pthread_create crash we hit on earlier - * attempts. The function is deliberately never called from Rust. - */ - -#include - -int32_t wzp_hello_stub(void) { - return 42; -} diff --git a/desktop/src-tauri/cpp/hello2.c b/desktop/src-tauri/cpp/hello2.c deleted file mode 100644 index 3636934..0000000 --- a/desktop/src-tauri/cpp/hello2.c +++ /dev/null @@ -1,8 +0,0 @@ -/* hello2.c — identical content to hello.c, different file name + symbol. - * Purpose: test if adding a THIRD trivial C static lib via cc::Build - * regresses Step D regardless of what's in the file. Never called from Rust. */ -#include - -int32_t wzp_hello2_stub(void) { - return 43; -} diff --git a/desktop/src-tauri/cpp/oboe_bridge.cpp b/desktop/src-tauri/cpp/oboe_bridge.cpp deleted file mode 100644 index 066bb61..0000000 --- a/desktop/src-tauri/cpp/oboe_bridge.cpp +++ /dev/null @@ -1,278 +0,0 @@ -// Full Oboe implementation for Android -// This file is compiled only when targeting Android - -#include "oboe_bridge.h" - -#ifdef __ANDROID__ -#include -#include -#include -#include - -#define LOG_TAG "wzp-oboe" -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) - -// --------------------------------------------------------------------------- -// Ring buffer helpers (SPSC, lock-free) -// --------------------------------------------------------------------------- - -static inline int32_t ring_available_read(const wzp_atomic_int* write_idx, - const wzp_atomic_int* read_idx, - int32_t capacity) { - int32_t w = std::atomic_load_explicit(write_idx, std::memory_order_acquire); - int32_t r = std::atomic_load_explicit(read_idx, std::memory_order_relaxed); - int32_t avail = w - r; - if (avail < 0) avail += capacity; - return avail; -} - -static inline int32_t ring_available_write(const wzp_atomic_int* write_idx, - const wzp_atomic_int* read_idx, - int32_t capacity) { - return capacity - 1 - ring_available_read(write_idx, read_idx, capacity); -} - -static inline void ring_write(int16_t* buf, int32_t capacity, - wzp_atomic_int* write_idx, const wzp_atomic_int* read_idx, - const int16_t* src, int32_t count) { - int32_t w = std::atomic_load_explicit(write_idx, std::memory_order_relaxed); - for (int32_t i = 0; i < count; i++) { - buf[w] = src[i]; - w++; - if (w >= capacity) w = 0; - } - std::atomic_store_explicit(write_idx, w, std::memory_order_release); -} - -static inline void ring_read(int16_t* buf, int32_t capacity, - const wzp_atomic_int* write_idx, wzp_atomic_int* read_idx, - int16_t* dst, int32_t count) { - int32_t r = std::atomic_load_explicit(read_idx, std::memory_order_relaxed); - for (int32_t i = 0; i < count; i++) { - dst[i] = buf[r]; - r++; - if (r >= capacity) r = 0; - } - std::atomic_store_explicit(read_idx, r, std::memory_order_release); -} - -// --------------------------------------------------------------------------- -// Global state -// --------------------------------------------------------------------------- - -static std::shared_ptr g_capture_stream; -static std::shared_ptr g_playout_stream; -static const WzpOboeRings* g_rings = nullptr; -static std::atomic g_running{false}; -static std::atomic g_capture_latency_ms{0.0f}; -static std::atomic g_playout_latency_ms{0.0f}; - -// --------------------------------------------------------------------------- -// Capture callback -// --------------------------------------------------------------------------- - -class CaptureCallback : public oboe::AudioStreamDataCallback { -public: - oboe::DataCallbackResult onAudioReady( - oboe::AudioStream* stream, - void* audioData, - int32_t numFrames) override { - if (!g_running.load(std::memory_order_relaxed) || !g_rings) { - return oboe::DataCallbackResult::Stop; - } - - const int16_t* src = static_cast(audioData); - int32_t avail = ring_available_write(g_rings->capture_write_idx, - g_rings->capture_read_idx, - g_rings->capture_capacity); - int32_t to_write = (numFrames < avail) ? numFrames : avail; - if (to_write > 0) { - ring_write(g_rings->capture_buf, g_rings->capture_capacity, - g_rings->capture_write_idx, g_rings->capture_read_idx, - src, to_write); - } - - // Update latency estimate - auto result = stream->calculateLatencyMillis(); - if (result) { - g_capture_latency_ms.store(static_cast(result.value()), - std::memory_order_relaxed); - } - - return oboe::DataCallbackResult::Continue; - } -}; - -// --------------------------------------------------------------------------- -// Playout callback -// --------------------------------------------------------------------------- - -class PlayoutCallback : public oboe::AudioStreamDataCallback { -public: - oboe::DataCallbackResult onAudioReady( - oboe::AudioStream* stream, - void* audioData, - int32_t numFrames) override { - if (!g_running.load(std::memory_order_relaxed) || !g_rings) { - memset(audioData, 0, numFrames * sizeof(int16_t)); - return oboe::DataCallbackResult::Stop; - } - - int16_t* dst = static_cast(audioData); - int32_t avail = ring_available_read(g_rings->playout_write_idx, - g_rings->playout_read_idx, - g_rings->playout_capacity); - int32_t to_read = (numFrames < avail) ? numFrames : avail; - - if (to_read > 0) { - ring_read(g_rings->playout_buf, g_rings->playout_capacity, - g_rings->playout_write_idx, g_rings->playout_read_idx, - dst, to_read); - } - // Fill remainder with silence on underrun - if (to_read < numFrames) { - memset(dst + to_read, 0, (numFrames - to_read) * sizeof(int16_t)); - } - - // Update latency estimate - auto result = stream->calculateLatencyMillis(); - if (result) { - g_playout_latency_ms.store(static_cast(result.value()), - std::memory_order_relaxed); - } - - return oboe::DataCallbackResult::Continue; - } -}; - -static CaptureCallback g_capture_cb; -static PlayoutCallback g_playout_cb; - -// --------------------------------------------------------------------------- -// Public C API -// --------------------------------------------------------------------------- - -int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { - if (g_running.load(std::memory_order_relaxed)) { - LOGW("wzp_oboe_start: already running"); - return -1; - } - - g_rings = rings; - - // Build capture stream - oboe::AudioStreamBuilder captureBuilder; - captureBuilder.setDirection(oboe::Direction::Input) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) - ->setFormat(oboe::AudioFormat::I16) - ->setChannelCount(config->channel_count) - ->setSampleRate(config->sample_rate) - ->setFramesPerDataCallback(config->frames_per_burst) - ->setInputPreset(oboe::InputPreset::VoiceCommunication) - ->setDataCallback(&g_capture_cb); - - oboe::Result result = captureBuilder.openStream(g_capture_stream); - if (result != oboe::Result::OK) { - LOGE("Failed to open capture stream: %s", oboe::convertToText(result)); - return -2; - } - - // Build playout stream - oboe::AudioStreamBuilder playoutBuilder; - playoutBuilder.setDirection(oboe::Direction::Output) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) - ->setFormat(oboe::AudioFormat::I16) - ->setChannelCount(config->channel_count) - ->setSampleRate(config->sample_rate) - ->setFramesPerDataCallback(config->frames_per_burst) - ->setUsage(oboe::Usage::VoiceCommunication) - ->setDataCallback(&g_playout_cb); - - result = playoutBuilder.openStream(g_playout_stream); - if (result != oboe::Result::OK) { - LOGE("Failed to open playout stream: %s", oboe::convertToText(result)); - g_capture_stream->close(); - g_capture_stream.reset(); - return -3; - } - - g_running.store(true, std::memory_order_release); - - // Start both streams - result = g_capture_stream->requestStart(); - if (result != oboe::Result::OK) { - LOGE("Failed to start capture: %s", oboe::convertToText(result)); - g_running.store(false, std::memory_order_release); - g_capture_stream->close(); - g_playout_stream->close(); - g_capture_stream.reset(); - g_playout_stream.reset(); - return -4; - } - - result = g_playout_stream->requestStart(); - if (result != oboe::Result::OK) { - LOGE("Failed to start playout: %s", oboe::convertToText(result)); - g_running.store(false, std::memory_order_release); - g_capture_stream->requestStop(); - g_capture_stream->close(); - g_playout_stream->close(); - g_capture_stream.reset(); - g_playout_stream.reset(); - return -5; - } - - LOGI("Oboe started: sr=%d burst=%d ch=%d", - config->sample_rate, config->frames_per_burst, config->channel_count); - return 0; -} - -void wzp_oboe_stop(void) { - g_running.store(false, std::memory_order_release); - - if (g_capture_stream) { - g_capture_stream->requestStop(); - g_capture_stream->close(); - g_capture_stream.reset(); - } - if (g_playout_stream) { - g_playout_stream->requestStop(); - g_playout_stream->close(); - g_playout_stream.reset(); - } - - g_rings = nullptr; - LOGI("Oboe stopped"); -} - -float wzp_oboe_capture_latency_ms(void) { - return g_capture_latency_ms.load(std::memory_order_relaxed); -} - -float wzp_oboe_playout_latency_ms(void) { - return g_playout_latency_ms.load(std::memory_order_relaxed); -} - -int wzp_oboe_is_running(void) { - return g_running.load(std::memory_order_relaxed) ? 1 : 0; -} - -#else -// Non-Android fallback — should not be reached; oboe_stub.cpp is used instead. -// Provide empty implementations just in case. - -int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { - (void)config; (void)rings; - return -99; -} - -void wzp_oboe_stop(void) {} -float wzp_oboe_capture_latency_ms(void) { return 0.0f; } -float wzp_oboe_playout_latency_ms(void) { return 0.0f; } -int wzp_oboe_is_running(void) { return 0; } - -#endif // __ANDROID__ diff --git a/desktop/src-tauri/cpp/oboe_bridge.h b/desktop/src-tauri/cpp/oboe_bridge.h deleted file mode 100644 index 8c2f143..0000000 --- a/desktop/src-tauri/cpp/oboe_bridge.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef WZP_OBOE_BRIDGE_H -#define WZP_OBOE_BRIDGE_H - -#include - -#ifdef __cplusplus -#include -typedef std::atomic wzp_atomic_int; -extern "C" { -#else -#include -typedef atomic_int wzp_atomic_int; -#endif - -typedef struct { - int32_t sample_rate; - int32_t frames_per_burst; - int32_t channel_count; -} WzpOboeConfig; - -typedef struct { - int16_t* capture_buf; - int32_t capture_capacity; - wzp_atomic_int* capture_write_idx; - wzp_atomic_int* capture_read_idx; - - int16_t* playout_buf; - int32_t playout_capacity; - wzp_atomic_int* playout_write_idx; - wzp_atomic_int* playout_read_idx; -} WzpOboeRings; - -int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings); -void wzp_oboe_stop(void); -float wzp_oboe_capture_latency_ms(void); -float wzp_oboe_playout_latency_ms(void); -int wzp_oboe_is_running(void); - -#ifdef __cplusplus -} -#endif - -#endif // WZP_OBOE_BRIDGE_H diff --git a/desktop/src-tauri/cpp/oboe_stub.cpp b/desktop/src-tauri/cpp/oboe_stub.cpp deleted file mode 100644 index 6792259..0000000 --- a/desktop/src-tauri/cpp/oboe_stub.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// Stub implementation for non-Android host builds (testing, cargo check, etc.) - -#include "oboe_bridge.h" -#include - -int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { - (void)config; - (void)rings; - fprintf(stderr, "wzp_oboe_start: stub (not on Android)\n"); - return 0; -} - -void wzp_oboe_stop(void) { - fprintf(stderr, "wzp_oboe_stop: stub (not on Android)\n"); -} - -float wzp_oboe_capture_latency_ms(void) { - return 0.0f; -} - -float wzp_oboe_playout_latency_ms(void) { - return 0.0f; -} - -int wzp_oboe_is_running(void) { - return 0; -} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 3125c25..35e491e 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -535,6 +535,45 @@ pub fn run() { } tracing::info!("app data dir: {data_dir:?}"); let _ = APP_DATA_DIR.set(data_dir); + + // Phase 1 smoke test of the separate-cdylib approach to the + // __init_tcb crash (see docs/incident-tauri-android-init-tcb.md): + // dlopen the sibling libwzp_native.so that gradle dropped into + // our jniLibs directory and call its exported wzp_native_version() + // + wzp_native_hello() functions. If this logs + // "wzp-native dlopen OK: version=42 msg=...", + // we've validated the whole cdylib-split pipeline and can move + // to Phase 2 (port the Oboe bridge into wzp-native). + #[cfg(target_os = "android")] + { + match unsafe { libloading::Library::new("libwzp_native.so") } { + Ok(lib) => { + unsafe { + match lib.get:: i32>(b"wzp_native_version") { + Ok(version_fn) => { + let v = version_fn(); + let mut buf = [0u8; 64]; + let msg = match lib.get:: usize>(b"wzp_native_hello") { + Ok(hello_fn) => { + let n = hello_fn(buf.as_mut_ptr(), buf.len()); + String::from_utf8_lossy(&buf[..n]).into_owned() + } + Err(e) => format!(""), + }; + tracing::info!("wzp-native dlopen OK: version={v} msg=\"{msg}\""); + } + Err(e) => { + tracing::warn!("wzp-native loaded but dlsym(wzp_native_version) failed: {e}"); + } + } + } + } + Err(e) => { + tracing::warn!("wzp-native dlopen failed: {e}"); + } + } + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/scripts/build-tauri-android.sh b/scripts/build-tauri-android.sh index a54960f..178081b 100755 --- a/scripts/build-tauri-android.sh +++ b/scripts/build-tauri-android.sh @@ -179,6 +179,26 @@ if [ "${DO_INIT}" = "1" ] || [ ! -x gen/android/gradlew ]; then cargo tauri android init 2>&1 | tail -20 fi +# ─── wzp-native standalone cdylib (built with cargo-ndk, not cargo-tauri) ── +# Produces libwzp_native.so which wzp-desktop dlopens at runtime via +# libloading. Split exists because cargo-tauri`s linker wiring pulls +# bionic private symbols into any cdylib with cc::Build C++, causing +# __init_tcb+4 SIGSEGV. cargo-ndk uses the same linker path as the +# legacy wzp-android crate which works. +echo ">>> cargo ndk build -p wzp-native --release" +JNI_ABI_DIR=gen/android/app/src/main/jniLibs/arm64-v8a +mkdir -p "$JNI_ABI_DIR" +( + cd /build/source + cargo ndk -t arm64-v8a -o desktop/src-tauri/gen/android/app/src/main/jniLibs \ + build --release -p wzp-native 2>&1 | tail -10 +) +if [ -f "$JNI_ABI_DIR/libwzp_native.so" ]; then + ls -lh "$JNI_ABI_DIR/libwzp_native.so" +else + echo ">>> WARNING: libwzp_native.so not produced" +fi + echo ">>> cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk" cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk