phase 3(android): wire CallEngine::start to wzp-native audio FFI
Some checks failed
Mirror to GitHub / mirror (push) Failing after 39s
Build Release Binaries / build-amd64 (push) Failing after 3m57s

Replaces the Android-side CallEngine::start() stub with a real implementation
that mirrors the desktop start() body but routes all PCM through the
standalone wzp-native cdylib loaded at startup via libloading instead of
using CPAL.

- desktop/src-tauri/src/wzp_native.rs: new module with a static
  OnceLock<libloading::Library> + cached raw fn pointers for every symbol
  we need (version, hello, audio_start/stop, read_capture, write_playout,
  is_running, capture/playout_latency_ms). init() resolves everything once
  at startup; accessors return default values if init() never ran.

- desktop/src-tauri/src/lib.rs: drop the inline dlopen smoke test, add
  `mod wzp_native;` behind target_os="android", and invoke
  wzp_native::init() from the Tauri setup() callback so the library is
  loaded + all symbols cached before any CallEngine can touch audio.

- desktop/src-tauri/src/engine.rs: the Android #[cfg] branch of
  CallEngine::start() now does the full QUIC handshake + signal loop +
  Opus send/recv tasks, calling wzp_native::audio_start() /
  audio_read_capture() / audio_write_playout() instead of the desktop
  CPAL rings. SyncWrapper now holds a placeholder Box<()> on Android
  because the audio backend lives in a process-global singleton inside
  libwzp_native.so rather than being owned per-engine.

Next step: build #39 on the remote docker builder and smoke-test on
Pixel 6 that the Connect button in the UI successfully brings up Oboe
and streams audio through the dlopen boundary.
This commit is contained in:
Siavash Sameni
2026-04-09 18:42:27 +04:00
parent c769a476a2
commit fdbe502524
4 changed files with 3413 additions and 129 deletions

View File

@@ -7,10 +7,16 @@
)]
// Call engine — now compiled on every platform. On desktop it runs the real
// CPAL/VPIO audio pipeline; on Android `CallEngine::start()` currently returns
// an error stub (see engine.rs — that's step C of the Oboe integration).
// CPAL/VPIO audio pipeline; on Android the engine calls into the standalone
// wzp-native cdylib (via the wzp_native module) for Oboe-backed audio.
mod engine;
// Android runtime binding to libwzp_native.so (Oboe audio backend, built as
// a standalone cdylib with cargo-ndk to avoid the Tauri staticlib symbol
// leak — see docs/incident-tauri-android-init-tcb.md).
#[cfg(target_os = "android")]
mod wzp_native;
// CallEngine is only referenced from the non-Android connect/disconnect/etc
// commands; the Android stubs return errors directly.
#[cfg(not(target_os = "android"))]
@@ -536,40 +542,24 @@ 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).
// Load the standalone wzp-native cdylib (Oboe audio bridge) and
// cache its exported function pointers. The library handle is
// kept alive in a 'static OnceLock for the lifetime of the
// process, so CallEngine::start() can invoke its audio FFI
// from anywhere. See src/wzp_native.rs and the incident report
// in docs/incident-tauri-android-init-tcb.md.
#[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}");
}
}
}
match wzp_native::init() {
Ok(()) => {
tracing::info!(
"wzp-native loaded: version={} msg=\"{}\"",
wzp_native::version(),
wzp_native::hello()
);
}
Err(e) => {
tracing::warn!("wzp-native dlopen failed: {e}");
tracing::warn!("wzp-native init failed: {e}");
}
}
}