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:
@@ -10,6 +10,7 @@ members = [
|
||||
"crates/wzp-client",
|
||||
"crates/wzp-web",
|
||||
"crates/wzp-android",
|
||||
"crates/wzp-native",
|
||||
"desktop/src-tauri",
|
||||
]
|
||||
|
||||
|
||||
22
crates/wzp-native/Cargo.toml
Normal file
22
crates/wzp-native/Cargo.toml
Normal file
@@ -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.
|
||||
44
crates/wzp-native/src/lib.rs
Normal file
44
crates/wzp-native/src/lib.rs
Normal file
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
int32_t wzp_hello_stub(void) {
|
||||
return 42;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
int32_t wzp_hello2_stub(void) {
|
||||
return 43;
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
// Full Oboe implementation for Android
|
||||
// This file is compiled only when targeting Android
|
||||
|
||||
#include "oboe_bridge.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <oboe/Oboe.h>
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
|
||||
#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<oboe::AudioStream> g_capture_stream;
|
||||
static std::shared_ptr<oboe::AudioStream> g_playout_stream;
|
||||
static const WzpOboeRings* g_rings = nullptr;
|
||||
static std::atomic<bool> g_running{false};
|
||||
static std::atomic<float> g_capture_latency_ms{0.0f};
|
||||
static std::atomic<float> 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<const int16_t*>(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<float>(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<int16_t*>(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<float>(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__
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef WZP_OBOE_BRIDGE_H
|
||||
#define WZP_OBOE_BRIDGE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <atomic>
|
||||
typedef std::atomic<int32_t> wzp_atomic_int;
|
||||
extern "C" {
|
||||
#else
|
||||
#include <stdatomic.h>
|
||||
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
|
||||
@@ -1,27 +0,0 @@
|
||||
// Stub implementation for non-Android host builds (testing, cargo check, etc.)
|
||||
|
||||
#include "oboe_bridge.h"
|
||||
#include <stdio.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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::<unsafe extern "C" fn() -> i32>(b"wzp_native_version") {
|
||||
Ok(version_fn) => {
|
||||
let v = version_fn();
|
||||
let mut buf = [0u8; 64];
|
||||
let msg = match lib.get::<unsafe extern "C" fn(*mut u8, usize) -> 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!("<no hello: {e}>"),
|
||||
};
|
||||
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![
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user