refactor(android): split C++ into wzp-native cdylib, loaded at runtime
Some checks failed
Mirror to GitHub / mirror (push) Failing after 38s
Build Release Binaries / build-amd64 (push) Failing after 3m34s

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:
Siavash Sameni
2026-04-09 18:02:53 +04:00
parent 711137da96
commit 7cc53aedc7
14 changed files with 141 additions and 479 deletions

View File

@@ -10,6 +10,7 @@ members = [
"crates/wzp-client",
"crates/wzp-web",
"crates/wzp-android",
"crates/wzp-native",
"desktop/src-tauri",
]

View 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.

View 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
}

View File

@@ -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]

View File

@@ -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()
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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__

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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![

View File

@@ -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